const TCRSYNC_SERVER = "https://tt-rss.org/tcrsync/"; function Model() { const self = this; self.fileName = ko.observable(""); self._zip = null; self._zipEntries = ko.observableArray(); self.syncAccount = ko.observable(""); self.errorMessage = ko.observable(""); self._themeName = ko.observable(""); self.themeName = ko.computed({ read: function() { return self._themeName(); }, write: function(theme) { localforage.setItem("TTC:THEME", theme).then(() => { model._themeName(theme); }); } }); self.cacheKey = function(suffix) { const sha1 = require('js-sha1'); return sha1(self.fileName()) + ":" + suffix; }; self.isLoading = ko.observable(false); self.syncClient = { _sha1_compat: function(str) { const sha1 = require('js-sha1'); const hash = sha1.create(); hash.update(str); const chars = hash.array(''); let rv = ""; for (let i = 0; i < chars.length; i++) { rv += Number(chars[i]).toString(16); } return rv; }, set: function(page) { return new Promise((resolve, reject) => { if (!self.syncAccount() || !self.fileName()) { resolve(null); return; } const owner = this._sha1_compat(self.syncAccount()); const hash = this._sha1_compat(self.fileName()); console.log('syncClient, set', owner, hash, page); $.post(TCRSYNC_SERVER, {op: "set", version: 2, owner: owner, hash: hash, position: page}).then((resp) => { console.log('tcrsync_set', resp); resolve(page); }).fail(() => { reject(null); }) }); }, get: function() { return new Promise((resolve, reject) => { if (!self.syncAccount() || !self.fileName()) { resolve(-1); return; } const owner = this._sha1_compat(self.syncAccount()); const hash = this._sha1_compat(self.fileName()); console.log('syncClient, get', owner, hash); $.post(TCRSYNC_SERVER, {op: "get", version: 2, owner: owner, hash: hash}).then((resp) => { console.log('tcrsync_get', resp); const page = isNaN(resp) ? -1 : parseInt(resp); resolve(page); }); }); }, }; self._closeFile = function() { self._zipEntries.removeAll(); self._zip = null; self.fileName(""); self.currentPage(-2); self.isLoading(false); localforage.setItem("TTC:LAST-OPENED-FILE", null); }; self.closeFile = function() { return new Promise((resolve, reject) => { self.syncClient.set(model.currentPage()).then(() => { self._closeFile(); resolve(null); }).catch(() => { self._closeFile(); resolve(null); }); }); }; self.openFile = function (file) { model.closeFile().then(() => { console.log('openFile', file, typeof file); if (typeof file != "string") { self.errorMessage("Can't open file: incorrect type."); return; } self.isLoading(true); window.setTimeout(() => { try { const AdmZip = require('adm-zip'); self._zip = new AdmZip(file); const zipEntries = self._zip.getEntries(); for (let i = 0; i < zipEntries.length; i++) { const ze = zipEntries[i]; if (ze.entryName.match(/\.(jpe?g|gif|bmp|png|webp)$/i)) { // prevent observer events (?) - open faster self._zipEntries().push(ze); } } self._zipEntries.sort(function(a, b) { return a.entryName.localeCompare(b.entryName); }); localforage.setItem("TTC:LAST-OPENED-FILE", file).then(() => { self.mruList(file); self.fileName(file.split(/[\\/]/).pop()); localforage.getItem(model.cacheKey("SINGLE-COLUMN")).then((single) => { model.singleColumn(single); localforage.getItem(model.cacheKey("FLIP-COLUMNS")).then((flip) => { model.flipColumns(flip); }); localforage.getItem(self.cacheKey("POSITION")).then((page) => { if (page) self.currentPage(page); else self.currentPage(0); self.isLoading(false); }); }); }); } catch (e) { console.warn(e); self.errorMessage(e.toString()); self.isLoading(false); } }, 100); }); }; self.documentTitle = ko.computed(function () { if (self.fileName()) return "Pow! Comics Reader: " + self.fileName() + " [" + self.currentPageDisp() + " / " + self.totalPages() + ", " + self.progressPct() + "%]"; else return "Pow! Comics Reader"; }); self.totalPages = ko.computed(function () { return self._zipEntries().length; }); self._currentPage = ko.observable(-2); self.currentPage = ko.computed({ read: function() { return self._currentPage(); }, write: function(page) { self._currentPage(page); localforage.getItem(self.cacheKey("POSITION")).then((stored) => { if (stored < page) { localforage.setItem(self.cacheKey("POSITION"), page).then(() => { console.log('saved position', page); if (page % 10 == 0) { model.syncClient.set(page); } }); } }); }, }); self._mruList = ko.observableArray(); self.mruList = ko.computed({ read: function () { return self._mruList(); }, write: function (file) { self.mruList().splice(4); if (!self._mruList().find((f) => { return file == f })) { self._mruList.unshift(file); require('electron').remote.app.addRecentDocument(file); } localforage.setItem("TTC:MRU-LIST", self._mruList()); } }); self.mruClear = function() { self._mruList.removeAll(); localforage.setItem("TTC:MRU-LIST", self._mruList()); require('electron').remote.app.clearRecentDocuments(); }; self._fitToWidth = ko.observable(false); self.fitToWidth = ko.computed({ read: function() { return self._fitToWidth(); }, write: function(ftw) { localforage.setItem("TTC:FIT-TO-WIDTH", ftw).then(() => { self._fitToWidth(ftw); }); }, }); self._singleColumn = ko.observable(false); self.singleColumn = ko.computed({ read: function() { return self._singleColumn(); }, write: function(single) { localforage.setItem(self.cacheKey("SINGLE-COLUMN"), single).then(() => { self._singleColumn(single); }); }, }); self._flipColumns = ko.observable(false); self.flipColumns = ko.computed({ read: function() { return self._flipColumns(); }, write: function (flip) { localforage.setItem(self.cacheKey("FLIP-COLUMNS"), flip).then(() => { self._flipColumns(flip); }); }, }); self.fitSize = ko.computed(function() { return !self.fitToWidth(); }); self.currentPageDisp = ko.computed(function () { const page = self.currentPage(); if (page < 0) return 0; else return page + 1; }); self.progressPct = ko.computed(function () { if (self.totalPages() > 0) { return parseInt(self.currentPageDisp() / self.totalPages() * 100); } else { return ""; } }); self._updateProgress = ko.computed(function() { const progress = self.progressPct(); const { remote } = require('electron'); remote.getCurrentWindow().setProgressBar(progress/100); }); self.openFileDialog = function() { const { remote } = require('electron'); const { dialog } = remote; dialog.showOpenDialog(remote.getCurrentWindow(), { properties: ['openFile'], filters: [ { name: 'Comic Archives', extensions: ['cbz', 'zip'] } ], }).then((result) => { console.log('openFileDialog result', result); if (result && result.filePaths && result.filePaths[0]) { self.openFile(result.filePaths[0]); } }); }; self._updateMruMenu = ko.computed(function() { const { remote } = require('electron'); const { Menu, MenuItem } = remote; const file = Menu.getApplicationMenu().getMenuItemById('M-FILE'); if (file != null) { const menu = file.submenu; menu.clear(); menu.append(new MenuItem({label: '&Open...', accelerator: 'Ctrl+O', click: () => { self.openFileDialog(); }})); menu.append(new MenuItem({id: 'F-CLOSE', label: '&Close', click: () => { self.closeFile(); }})); menu.append(new MenuItem({type: 'separator'})); const mruList = self.mruList(); if (mruList.length > 0) { for (let i = 0; i < mruList.length; i++) { const file = mruList[i]; menu.append(new MenuItem({label: "&" + (i+1) + ". " + file, click: () => { self.openFile(file); }})); } menu.append(new MenuItem({label: '&Clear recent files', click: () => { self.mruClear(); }})); menu.append(new MenuItem({type: 'separator'})); } menu.append(new MenuItem({label: 'E&xit', role: 'quit'})); } }); self._updateMenu = ko.computed(function() { const enabled = self.fileName() != ""; const { remote } = require('electron'); const menu = remote.Menu.getApplicationMenu(); $.each(menu.items, (i,g) => { if (g.submenu != null) $.each(g.submenu.items, (i,m) => { if (m.id && m.id.indexOf("F-") == 0) { m.enabled = enabled; switch (m.id) { case "F-FIT": m.checked = self.fitToWidth(); break; case "F-SINGLE": m.checked = self.singleColumn(); break; case "F-FLIP": m.checked = self.flipColumns(); break; case "F-SYNC": m.enabled = self.syncAccount() != ""; } } }); }); }); self.getLeftPage = ko.computed(function () { const page = self.currentPage(); if (self._zip && page >= 0 && page < self._zipEntries().length) { const ze = self._zipEntries()[page]; if (ze) { return 'data:image/jpeg;base64,' + ze.getData().toString("base64"); } } }); self.getRightPage = ko.computed(function () { const page = self.currentPage() + 1; if (self._zip && page >= 0 && page < self._zipEntries().length) { const ze = self._zipEntries()[page]; if (ze) { return 'data:image/jpeg;base64,' + ze.getData().toString("base64"); } } }); } const model = new Model(); $(document).ready(function () { const { remote, ipcRenderer } = require('electron'); ipcRenderer.on("open-settings", () => { $('#settings-modal').modal('show'); }); ipcRenderer.on("open-file", (event, args) => { model.openFile(args); }); ipcRenderer.on("mark-as-read", (event, args) => { if (confirm("Mark as read?")) { model.currentPage(model.totalPages()-1); } }); ipcRenderer.on("open-location", (event, args) => { const total = model.totalPages(); if (total == 0) return; const prompt = require('electron-prompt'); prompt({ title: "Go to", label: 'Jump to location [1-' + total + ']:', value: model.currentPageDisp(), useHtmlLabel: true, height: 160, inputAttrs: { type: 'numeric' } }).then((res) => { if (res != null && res <= model.totalPages()) { model.currentPage(res - 1); } }); }); ipcRenderer.on("close-file", (event, args) => { model.syncClient.set(model.currentPage()).then(() => { model.closeFile(); }); }); ipcRenderer.on("fit-to-width", (event, args) => { model.fitToWidth(!model.fitToWidth()); }); ipcRenderer.on("zoom-in", (event, args) => { const reader = $("#reader"); const zoom = parseInt(parseFloat((reader.css("zoom")) || 1) * 100); if (zoom < 200) reader.css("zoom", (zoom + 20) + "%"); }); ipcRenderer.on("zoom-out", (event, args) => { const reader = $("#reader"); const zoom = parseInt(parseFloat((reader.css("zoom")) || 1) * 100); if (zoom > 25) reader.css("zoom", (zoom - 20) + "%"); }); ipcRenderer.on("zoom-reset", (event, args) => { $("#reader").css("zoom", "100%"); }); ipcRenderer.on("single-column", (event, args) => { model.singleColumn(!model.singleColumn()); }); ipcRenderer.on("flip-columns", (event, args) => { model.flipColumns(!model.flipColumns()); }); ipcRenderer.on("clear-last-read", (event, args) => { if (confirm("Clear stored last read position?")) { localforage.setItem(model.cacheKey("POSITION"), 0).then(() => { model.currentPage(0); }); } }); ipcRenderer.on("sync-to-last", (event, args) => { model.syncClient.get().then((page) => { if (page > model.currentPage()) { if (confirm("You are currently on page %p. Furthest read page stored on the server is %r. Open it instead?" .replace("%p", model.currentPageDisp()) .replace("%r", page + 1))) { model.currentPage(page); } } else { alert("No information stored or you are on the furthest read page."); } }); }); function fixColumnWidths() { if ($(".right-page").is(":visible")) { $("#reader img").addClass("limit-width"); } else { $("#reader img").removeClass("limit-width"); } } $(window).on("dragover dragleave dragend", (e) => { return false; }); $(window).on("drop", (e) => { e.preventDefault(); const file = e.originalEvent.dataTransfer.files[0]; if (file != null) model.openFile(file.path); }); $("#reader img").on("load", function(e) { const left = $("#reader .left-page"); const right = $("#reader .right-page"); left.fadeIn(); if (left.width() >= left.height() || right.width() >= right.height()) { right.hide(); } else { right.fadeIn(); } window.setTimeout(() => { fixColumnWidths(); }, 10); }); $(".sync-account").on("change", function() { const acct = $(this).val(); localforage.setItem("TTC:SYNC-ACCOUNT", acct).then(() => { model.syncAccount(acct); }); }); $(".theme-select").on("change", function() { const theme = $(this).val(); model.themeName(theme); }); $(".fit-to-width-cb").on("click", (e) => { model.fitToWidth(e.target.checked); }); $(".single-column-cb").on("click", (e) => { model.singleColumn(e.target.checked); }); $(".flip-columns-cb").on("click", (e) => { model.flipColumns(e.target.checked); }); $("#left .top, #right .top").on("click", (e) => { e.stopPropagation(); let page = model.currentPage(); if ($(".right-page").is(":visible") && page > 1) page = page - 1; if (page > 0) { $("#reader img").hide(); model.currentPage(page - 1); } }); $("#left .bottom, #right .bottom").on("click", (e) => { e.stopPropagation(); let page = model.currentPage(); if ($(".right-page").is(":visible") && page < model.totalPages() - 2) page = page + 1; if (page < model.totalPages() - 1) { $("#reader img").hide(); model.currentPage(page + 1); } }); $(window).on("resize", () => { fixColumnWidths(); }); $(window).on("beforeunload", (e) => { if (model.currentPage() > 0 && !window._readyToQuit) { e.preventDefault(); window._readyToQuit = true; model.syncClient.set(model.currentPage()).then(() => { require('electron').remote.app.quit(); }).catch(() => { require('electron').remote.app.quit(); }); return false; } }); $(window).on("wheel", function(evt) { if ($(".modal").is(":visible")) return; if (model.fitSize()) { if (evt.originalEvent.deltaY > 0) { $("#right").click(); } else if (evt.originalEvent.deltaY < 0) { $("#left").click(); } } }); $(document).on("keydown", (e) => { if ($(".modal").is(":visible")) return; switch (e.which) { case 37: $("#left .top").click(); break; case 32: case 39: $("#left .bottom").click(); break; } }); localforage.getItem("TTC:SYNC-ACCOUNT").then((acct) => { if (acct) model.syncAccount(acct); localforage.getItem("TTC:FIT-TO-WIDTH").then((ftw) => { model.fitToWidth(ftw); }); localforage.getItem("TTC:THEME").then((theme) => { model.themeName(theme); }); ko.applyBindings(model, document.querySelector('html')); localforage.getItem("TTC:MRU-LIST").then((mru) => { if (mru != null) $.each(mru, (i,e) => { model._mruList.push(e); }); localforage.getItem("TTC:LAST-OPENED-FILE").then((last) => { const argv = remote.process.argv; if (argv[1] && argv[1] != ".") { model.openFile(argv[1]); } else if (last != null) { model.openFile(last); } }); }); }); });