@@ -213,6 +214,7 @@ import LockQueue from '../../../share/LockQueue';
import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
+import coversStorage from '../share/coversStorage';
const componentOptions = {
components: {
@@ -240,6 +242,8 @@ class RecentBooksPage {
showSameBook = false;
archive = false;
+ covers = {};
+
created() {
this.commit = this.$store.commit;
@@ -264,6 +268,7 @@ class RecentBooksPage {
this.showBar();
await this.updateTableData();
await this.scrollToActiveBook();
+ //await this.scrollRefresh();
})();
}
@@ -336,6 +341,7 @@ class RecentBooksPage {
active: (activeBook.key == book.key),
activeParent: false,
inGroup: false,
+ coverPageUrl: book.coverPageUrl,
//для сортировки
loadTimeRaw,
@@ -435,8 +441,6 @@ class RecentBooksPage {
//.....
this.tableData = result;
-
- this.$refs.virtualScroll.refresh();
} finally {
this.lock.ret();
}
@@ -569,6 +573,8 @@ class RecentBooksPage {
}
async scrollToActiveBook() {
+ await this.$nextTick();
+
this.lockScroll = true;
try {
let activeIndex = -1;
@@ -614,6 +620,16 @@ class RecentBooksPage {
}
}
+ async scrollRefresh() {
+ this.lockScroll = true;
+ await utils.sleep(100);
+ try {
+ this.$refs.virtualScroll.refresh();
+ } finally {
+ await utils.sleep(100);
+ this.lockScroll = false;
+ }
+ }
get sortMethodOptions() {
return [
@@ -643,6 +659,43 @@ class RecentBooksPage {
}
return true;
}
+
+ makeCoverHtml(data) {
+ return `

`;
+ }
+
+ isLoadedCover(coverPageUrl) {
+ if (!coverPageUrl)
+ return false;
+
+ let loadedCover = this.covers[coverPageUrl];
+ if (!loadedCover) {
+ (async() => {
+ //сначала заглянем в storage
+ let data = await coversStorage.getData(coverPageUrl);
+ if (data) {
+ this.covers[coverPageUrl] = this.makeCoverHtml(data);
+ } else {//иначе идем на сервер
+ try {
+ data = await readerApi.getUploadedFileBuf(coverPageUrl);
+ await coversStorage.setData(coverPageUrl, data);
+ this.covers[coverPageUrl] = this.makeCoverHtml(data);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ })();
+ }
+
+ return (loadedCover != undefined);
+ }
+
+ getCoverHtml(coverPageUrl) {
+ if (coverPageUrl && this.covers[coverPageUrl])
+ return this.covers[coverPageUrl];
+ else
+ return '';
+ }
}
export default vueComponent(RecentBooksPage);
@@ -716,14 +769,14 @@ export default vueComponent(RecentBooksPage);
line-height: 110%;
border-left: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
- height: 12px;
+ height: 14px;
}
.row-info-top {
line-height: 110%;
border: 1px solid #cccccc;
border-right: 0;
- height: 12px;
+ height: 14px;
}
.time-info, .row-info-top {
@@ -731,8 +784,8 @@ export default vueComponent(RecentBooksPage);
}
.read-bar {
- height: 4px;
- background-color: #bbbbbb;
+ height: 6px;
+ background-color: #b8b8b8;
}
.del-button {
diff --git a/client/components/Reader/SettingsPage/SettingsPage.vue b/client/components/Reader/SettingsPage/SettingsPage.vue
index 4982df37..ae158e5d 100644
--- a/client/components/Reader/SettingsPage/SettingsPage.vue
+++ b/client/components/Reader/SettingsPage/SettingsPage.vue
@@ -124,6 +124,7 @@ import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage';
+import readerApi from '../../../api/reader';
import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
@@ -636,8 +637,17 @@ class SettingsPage {
if (index < 0)
newUserWallpapers.push({label, cssClass});
- if (!wallpaperStorage.keyExists(cssClass))
+ if (!wallpaperStorage.keyExists(cssClass)) {
await wallpaperStorage.setData(cssClass, data);
+ //отправим data на сервер в файл `/upload/${key}`
+ try {
+ //const res =
+ await readerApi.uploadFileBuf(data);
+ //console.log(res);
+ } catch (e) {
+ console.error(e);
+ }
+ }
this.userWallpapers = newUserWallpapers;
this.wallpaper = cssClass;
diff --git a/client/components/Reader/share/BookParser.js b/client/components/Reader/share/BookParser.js
index 2ff421fb..9f4a9211 100644
--- a/client/components/Reader/share/BookParser.js
+++ b/client/components/Reader/share/BookParser.js
@@ -85,6 +85,7 @@ export default class BookParser {
let binaryId = '';
let binaryType = '';
let dimPromises = [];
+ this.coverPageId = '';
//оглавление
this.contents = [];
@@ -289,7 +290,7 @@ export default class BookParser {
const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href);
- if (href[0] == '#') {//local
+ if (local) {//local
imageNum++;
if (inPara && !this.sets.showInlineImagesInCenter && !center)
@@ -301,6 +302,11 @@ export default class BookParser {
if (inPara && this.sets.showInlineImagesInCenter)
newParagraph();
+
+ //coverpage
+ if (path == '/fictionbook/description/title-info/coverpage/image') {
+ this.coverPageId = id;
+ }
} else {//external
imageNum++;
diff --git a/client/components/Reader/share/bookManager.js b/client/components/Reader/share/bookManager.js
index cc7d76f9..193f5c4d 100644
--- a/client/components/Reader/share/bookManager.js
+++ b/client/components/Reader/share/bookManager.js
@@ -2,8 +2,10 @@ import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash';
-import * as utils from '../../../share/utils';
import BookParser from './BookParser';
+import readerApi from '../../../api/reader';
+import coversStorage from './coversStorage';
+import * as utils from '../../../share/utils';
const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
@@ -345,9 +347,36 @@ class BookManager {
const parsed = new BookParser(this.settings);
const parsedMeta = await parsed.parse(data, callback);
+
+ //cover page
+ let coverPageUrl = '';
+ if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
+ const bin = parsed.binary[parsed.coverPageId];
+ let dataUrl = `data:${bin.type};base64,${bin.data}`;
+ try {
+ dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
+ } catch (e) {
+ console.error(e);
+ }
+
+ //отправим dataUrl на сервер в /upload
+ try {
+ await readerApi.uploadFileBuf(dataUrl, (url) => {
+ coverPageUrl = url;
+ });
+ } catch (e) {
+ console.error(e);
+ }
+
+ //сохраним в storage
+ if (coverPageUrl)
+ await coversStorage.setData(coverPageUrl, dataUrl);
+ }
+
const result = Object.assign({}, meta, parsedMeta, {
length: data.length,
textLength: parsed.textLength,
+ coverPageUrl,
parsed
});
diff --git a/client/components/Reader/share/coversStorage.js b/client/components/Reader/share/coversStorage.js
new file mode 100644
index 00000000..6908d94b
--- /dev/null
+++ b/client/components/Reader/share/coversStorage.js
@@ -0,0 +1,61 @@
+import localForage from 'localforage';
+//import _ from 'lodash';
+import * as utils from '../../../share/utils';
+
+const maxDataSize = 100*1024*1024;
+
+const coversStore = localForage.createInstance({
+ name: 'coversStorage'
+});
+
+class CoversStorage {
+ constructor() {
+ }
+
+ async init() {
+ this.cleanCovers(); //no await
+ }
+
+ async setData(key, data) {
+ await coversStore.setItem(key, {addTime: Date.now(), data});
+ }
+
+ async getData(key) {
+ const item = await coversStore.getItem(key);
+ return (item ? item.data : undefined);
+ }
+
+ async removeData(key) {
+ await coversStore.removeItem(key);
+ }
+
+ async cleanCovers() {
+ await utils.sleep(10000);
+
+ while (1) {// eslint-disable-line no-constant-condition
+ let size = 0;
+ let min = Date.now();
+ let toDel = null;
+ for (const key of (await coversStore.keys())) {
+ const item = await coversStore.getItem(key);
+
+ size += item.data.length;
+
+ if (item.addTime < min) {
+ toDel = key;
+ min = item.addTime;
+ }
+ }
+
+
+ if (size > maxDataSize && toDel) {
+ await this.removeData(toDel);
+ } else {
+ break;
+ }
+ }
+ }
+
+}
+
+export default new CoversStorage();
\ No newline at end of file
diff --git a/client/components/Reader/share/wallpaperStorage.js b/client/components/Reader/share/wallpaperStorage.js
index 192c4601..9d98603d 100644
--- a/client/components/Reader/share/wallpaperStorage.js
+++ b/client/components/Reader/share/wallpaperStorage.js
@@ -32,6 +32,10 @@ class WallpaperStorage {
this.cachedKeys = await wpStore.keys();
}
+ async getKeys() {
+ return await wpStore.keys();
+ }
+
keyExists(key) {//не асинхронная
return this.cachedKeys.includes(key);
}
diff --git a/client/components/Reader/versionHistory.js b/client/components/Reader/versionHistory.js
index 00b49a3a..334c6368 100644
--- a/client/components/Reader/versionHistory.js
+++ b/client/components/Reader/versionHistory.js
@@ -1,4 +1,18 @@
export const versionHistory = [
+{
+ version: '0.11.8',
+ releaseDate: '2022-07-14',
+ showUntil: '2022-07-13',
+ content:
+`
+
+ - добавлено отображение и синхронизация обложек в окне загруженных книг
+ - добавлена синхронизация обоев
+
+
+`
+},
+
{
version: '0.11.7',
releaseDate: '2022-07-12',
diff --git a/client/share/utils.js b/client/share/utils.js
index 916d94a0..39196212 100644
--- a/client/share/utils.js
+++ b/client/share/utils.js
@@ -363,4 +363,50 @@ export function getBookTitle(fb2) {
]).join(' - ');
return result;
+}
+
+export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
+ return new Promise ((resolve, reject) => { (async() => {
+ const img = new Image();
+
+ let resolved = false;
+ img.onload = () => {
+ try {
+ let width = img.width;
+ let height = img.height;
+
+ if (width > height) {
+ if (width > toWidth) {
+ height = height * (toWidth / width);
+ width = toWidth;
+ }
+ } else {
+ if (height > toHeight) {
+ width = width * (toHeight / height);
+ height = toHeight;
+ }
+ }
+
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext('2d');
+ ctx.drawImage(img, 0, 0, width, height);
+ const result = canvas.toDataURL('image/jpeg', quality);
+ resolved = true;
+ resolve(result);
+ } catch (e) {
+ reject(e);
+ return;
+ }
+ };
+
+ img.onerror = reject;
+
+ img.src = dataUrl;
+
+ await sleep(1000);
+ if (!resolved)
+ reject('Не удалось изменить размер');
+ })().catch(reject); });
}
\ No newline at end of file
diff --git a/client/store/modules/reader.js b/client/store/modules/reader.js
index ab4a8972..dadada1b 100644
--- a/client/store/modules/reader.js
+++ b/client/store/modules/reader.js
@@ -191,6 +191,8 @@ const settingDefaults = {
recentShowSameBook: false,
recentSortMethod: '',
+
+ needUpdateSettingsView: 0,
};
for (const font of fonts)
diff --git a/package-lock.json b/package-lock.json
index 3b35a2c2..3b0544d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "Liberama",
- "version": "0.11.7",
+ "version": "0.11.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "Liberama",
- "version": "0.11.7",
+ "version": "0.11.8",
"hasInstallScript": true,
"license": "CC0-1.0",
"dependencies": {
diff --git a/package.json b/package.json
index 1303b588..f945b657 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Liberama",
- "version": "0.11.7",
+ "version": "0.11.8",
"author": "Book Pauk
",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
diff --git a/server/controllers/WebSocketController.js b/server/controllers/WebSocketController.js
index 229dbcf0..add0a3a8 100644
--- a/server/controllers/WebSocketController.js
+++ b/server/controllers/WebSocketController.js
@@ -25,6 +25,10 @@ class WebSocketController {
ws.on('message', (message) => {
this.onMessage(ws, message.toString());
});
+
+ ws.on('error', (err) => {
+ log(LM_ERR, err);
+ });
});
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
@@ -70,6 +74,10 @@ class WebSocketController {
await this.readerRestoreCachedFile(req, ws); break;
case 'reader-storage':
await this.readerStorageDo(req, ws); break;
+ case 'upload-file-buf':
+ await this.uploadFileBuf(req, ws); break;
+ case 'upload-file-touch':
+ await this.uploadFileTouch(req, ws); break;
default:
throw new Error(`Action not found: ${req.action}`);
@@ -168,6 +176,20 @@ class WebSocketController {
this.send(await this.readerStorage.doAction(req.body), req, ws);
}
+
+ async uploadFileBuf(req, ws) {
+ if (!req.buf)
+ throw new Error(`key 'buf' is empty`);
+
+ this.send({url: await this.readerWorker.saveFileBuf(req.buf)}, req, ws);
+ }
+
+ async uploadFileTouch(req, ws) {
+ if (!req.url)
+ throw new Error(`key 'url' is empty`);
+
+ this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
+ }
}
module.exports = WebSocketController;
diff --git a/server/core/Reader/ReaderWorker.js b/server/core/Reader/ReaderWorker.js
index cebd5cfe..70a6530d 100644
--- a/server/core/Reader/ReaderWorker.js
+++ b/server/core/Reader/ReaderWorker.js
@@ -219,6 +219,27 @@ class ReaderWorker {
return `disk://${hash}`;
}
+ async saveFileBuf(buf) {
+ const hash = await utils.getBufHash(buf, 'sha256', 'hex');
+ const outFilename = `${this.config.uploadDir}/${hash}`;
+
+ if (!await fs.pathExists(outFilename)) {
+ await fs.writeFile(outFilename, buf);
+ } else {
+ await utils.touchFile(outFilename);
+ }
+
+ return `disk://${hash}`;
+ }
+
+ async uploadFileTouch(url) {
+ const outFilename = `${this.config.uploadDir}/${url.replace('disk://', '')}`;
+
+ await utils.touchFile(outFilename);
+
+ return url;
+ }
+
async restoreRemoteFile(filename) {
const basename = path.basename(filename);
const targetName = `${this.config.tempPublicDir}/${basename}`;
diff --git a/server/core/WebSocketConnection.js b/server/core/WebSocketConnection.js
index 3045659e..fcdbcd1e 100644
--- a/server/core/WebSocketConnection.js
+++ b/server/core/WebSocketConnection.js
@@ -94,7 +94,7 @@ class WebSocketConnection {
this.ws = new this.WebSocket(this.url);
}
- const onopen = (e) => {
+ const onopen = () => {
this.connecting = false;
resolve(this.ws);
};
diff --git a/server/core/utils.js b/server/core/utils.js
index 7dc2b636..9d4a6c9b 100644
--- a/server/core/utils.js
+++ b/server/core/utils.js
@@ -34,6 +34,12 @@ function getFileHash(filename, hashName, enc) {
});
}
+function getBufHash(buf, hashName, enc) {
+ const hash = crypto.createHash(hashName);
+ hash.update(buf);
+ return hash.digest(enc);
+}
+
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -129,6 +135,7 @@ module.exports = {
fromBase36,
bufferRemoveZeroes,
getFileHash,
+ getBufHash,
sleep,
toUnixTime,
randomHexString,
diff --git a/server/index.js b/server/index.js
index 633f0f84..848d8c53 100644
--- a/server/index.js
+++ b/server/index.js
@@ -11,6 +11,8 @@ const ayncExit = new (require('./core/AsyncExit'))();
let log = null;
+const maxPayloadSize = 50;//in MB
+
async function init() {
//config
const configManager = new (require('./config'))();//singleton
@@ -63,7 +65,7 @@ async function main() {
if (serverCfg.mode !== 'none') {
const app = express();
const server = http.createServer(app);
- const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
+ const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
const serverConfig = Object.assign({}, config, serverCfg);
@@ -75,7 +77,7 @@ async function main() {
}
app.use(compression({ level: 1 }));
- app.use(express.json({limit: '10mb'}));
+ app.use(express.json({limit: `${maxPayloadSize}mb`}));
if (devModule)
devModule.logQueries(app);