From 586ebf8dc897c794b2a22e393f477ebcea2c7148 Mon Sep 17 00:00:00 2001 From: dudeitssm <36095297+dudeitssm@users.noreply.github.com> Date: Mon, 1 Aug 2022 00:06:31 +0000 Subject: [PATCH 1/9] Fix typo: "Pleae" should be "Please" --- src/util/util.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/util.cr b/src/util/util.cr index 5060694..53d2007 100644 --- a/src/util/util.cr +++ b/src/util/util.cr @@ -184,7 +184,7 @@ def delete_cache_and_exit(path : String) File.delete path Logger.fatal "Invalid library cache deleted. Mango needs to " \ "perform a full reset to recover from this. " \ - "Pleae restart Mango. This is NOT a bug." + "Please restart Mango. This is NOT a bug." Logger.fatal "Exiting" exit 1 end From cd3ee0728c032b0515cd0e0ce1b96dfb25f5f41a Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 19 Aug 2022 11:07:45 +0000 Subject: [PATCH 2/9] Handle the case where only one plugin is installed fixes #329 --- public/js/subscription-manager.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/js/subscription-manager.js b/public/js/subscription-manager.js index fad4e56..3d76d30 100644 --- a/public/js/subscription-manager.js +++ b/public/js/subscription-manager.js @@ -13,12 +13,12 @@ const component = () => { if (!data.success) throw new Error(data.error); this.plugins = data.plugins; - const pid = localStorage.getItem("plugin"); - if (pid && this.plugins.map((p) => p.id).includes(pid)) - this.pid = pid; - else if (this.plugins.length > 0) - this.pid = this.plugins[0].id; + let pid = localStorage.getItem("plugin"); + if (!pid || !this.plugins.find((p) => p.id === pid)) { + pid = this.plugins[0].id; + } + this.pid = pid; this.list(pid); }) .catch((e) => { From 28c098a56e247d3d0a7c1cf3a81e1a45a83280d3 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 19 Aug 2022 11:40:41 +0000 Subject: [PATCH 3/9] Add eslint and style fix --- .eslintrc.js | 11 + .prettierrc | 6 + gulpfile.js | 85 +-- package.json | 9 +- public/js/admin.js | 106 ++-- public/js/alert.js | 8 +- public/js/common.js | 82 +-- public/js/dots.js | 30 +- public/js/download-manager.js | 245 +++++---- public/js/missing-items.js | 128 +++-- public/js/plugin-download.js | 847 +++++++++++++++--------------- public/js/reader.js | 667 +++++++++++------------ public/js/search.js | 54 +- public/js/sort-items.js | 24 +- public/js/subscription-manager.js | 276 +++++----- public/js/subscription.js | 176 ++++--- public/js/title.js | 725 +++++++++++++------------ public/js/user-edit.js | 8 +- public/js/user.js | 29 +- 19 files changed, 1817 insertions(+), 1699 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .prettierrc diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a59e1e9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + parser: '@babel/eslint-parser', + parserOptions: { requireConfigFile: false }, + plugins: ['prettier'], + rules: { + eqeqeq: ['error', 'always'], + 'object-shorthand': ['error', 'always'], + 'prettier/prettier': 'error', + 'no-var': 'error', + }, +}; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..fc42682 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 2 +} diff --git a/gulpfile.js b/gulpfile.js index b1634e6..75b866d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -5,13 +5,17 @@ const minifyCss = require('gulp-minify-css'); const less = require('gulp-less'); gulp.task('copy-img', () => { - return gulp.src('node_modules/uikit/src/images/backgrounds/*.svg') - .pipe(gulp.dest('public/img')); + return gulp + .src('node_modules/uikit/src/images/backgrounds/*.svg') + .pipe(gulp.dest('public/img')); }); gulp.task('copy-font', () => { - return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff**') - .pipe(gulp.dest('public/webfonts')); + return gulp + .src( + 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff**', + ) + .pipe(gulp.dest('public/webfonts')); }); // Copy files from node_modules @@ -19,49 +23,60 @@ gulp.task('node-modules-copy', gulp.parallel('copy-img', 'copy-font')); // Compile less gulp.task('less', () => { - return gulp.src([ - 'public/css/mango.less', - 'public/css/tags.less' - ]) - .pipe(less()) - .pipe(gulp.dest('public/css')); + return gulp + .src(['public/css/mango.less', 'public/css/tags.less']) + .pipe(less()) + .pipe(gulp.dest('public/css')); }); // Transpile and minify JS files and output to dist gulp.task('babel', () => { - return gulp.src(['public/js/*.js', '!public/js/*.min.js']) - .pipe(babel({ - presets: [ - ['@babel/preset-env', { - targets: '>0.25%, not dead, ios>=9' - }] - ], - })) - .pipe(minify({ - removeConsole: true, - builtIns: false - })) - .pipe(gulp.dest('dist/js')); + return gulp + .src(['public/js/*.js', '!public/js/*.min.js']) + .pipe( + babel({ + presets: [ + [ + '@babel/preset-env', + { + targets: '>0.25%, not dead, ios>=9', + }, + ], + ], + }), + ) + .pipe( + minify({ + removeConsole: true, + builtIns: false, + }), + ) + .pipe(gulp.dest('dist/js')); }); // Minify CSS and output to dist gulp.task('minify-css', () => { - return gulp.src('public/css/*.css') - .pipe(minifyCss()) - .pipe(gulp.dest('dist/css')); + return gulp + .src('public/css/*.css') + .pipe(minifyCss()) + .pipe(gulp.dest('dist/css')); }); // Copy static files (includeing images) to dist gulp.task('copy-files', () => { - return gulp.src([ - 'public/*.*', - 'public/img/**', - 'public/webfonts/*', - 'public/js/*.min.js' - ], { - base: 'public' - }) - .pipe(gulp.dest('dist')); + return gulp + .src( + [ + 'public/*.*', + 'public/img/**', + 'public/webfonts/*', + 'public/js/*.min.js', + ], + { + base: 'public', + }, + ) + .pipe(gulp.dest('dist')); }); // Set up the public folder for development diff --git a/package.json b/package.json index a8fa19f..63caa83 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,22 @@ "author": "Alex Ling ", "license": "MIT", "devDependencies": { + "@babel/eslint-parser": "^7.18.9", "@babel/preset-env": "^7.11.5", "all-contributors-cli": "^6.19.0", + "eslint": "^8.22.0", + "eslint-plugin-prettier": "^4.2.1", "gulp": "^4.0.2", "gulp-babel": "^8.0.0", "gulp-babel-minify": "^0.5.1", "gulp-less": "^4.0.1", "gulp-minify-css": "^1.2.4", - "less": "^3.11.3" + "less": "^3.11.3", + "prettier": "^2.7.1" }, "scripts": { - "uglify": "gulp" + "uglify": "gulp", + "lint": "eslint public/js *.js --ext .js" }, "dependencies": { "@fortawesome/fontawesome-free": "^5.14.0", diff --git a/public/js/admin.js b/public/js/admin.js index 57926b1..5a380b0 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1,58 +1,56 @@ const component = () => { - return { - progress: 1.0, - generating: false, - scanning: false, - scanTitles: 0, - scanMs: -1, - themeSetting: '', + return { + progress: 1.0, + generating: false, + scanning: false, + scanTitles: 0, + scanMs: -1, + themeSetting: '', - init() { - this.getProgress(); - setInterval(() => { - this.getProgress(); - }, 5000); + init() { + this.getProgress(); + setInterval(() => { + this.getProgress(); + }, 5000); - const setting = loadThemeSetting(); - this.themeSetting = setting.charAt(0).toUpperCase() + setting.slice(1); - }, - themeChanged(event) { - const newSetting = $(event.currentTarget).val().toLowerCase(); - saveThemeSetting(newSetting); - setTheme(); - }, - scan() { - if (this.scanning) return; - this.scanning = true; - this.scanMs = -1; - this.scanTitles = 0; - $.post(`${base_url}api/admin/scan`) - .then(data => { - this.scanMs = data.milliseconds; - this.scanTitles = data.titles; - }) - .catch(e => { - alert('danger', `Failed to trigger a scan. Error: ${e}`); - }) - .always(() => { - this.scanning = false; - }); - }, - generateThumbnails() { - if (this.generating) return; - this.generating = true; - this.progress = 0.0; - $.post(`${base_url}api/admin/generate_thumbnails`) - .then(() => { - this.getProgress() - }); - }, - getProgress() { - $.get(`${base_url}api/admin/thumbnail_progress`) - .then(data => { - this.progress = data.progress; - this.generating = data.progress > 0; - }); - }, - }; + const setting = loadThemeSetting(); + this.themeSetting = setting.charAt(0).toUpperCase() + setting.slice(1); + }, + themeChanged(event) { + const newSetting = $(event.currentTarget).val().toLowerCase(); + saveThemeSetting(newSetting); + setTheme(); + }, + scan() { + if (this.scanning) return; + this.scanning = true; + this.scanMs = -1; + this.scanTitles = 0; + $.post(`${base_url}api/admin/scan`) + .then((data) => { + this.scanMs = data.milliseconds; + this.scanTitles = data.titles; + }) + .catch((e) => { + alert('danger', `Failed to trigger a scan. Error: ${e}`); + }) + .always(() => { + this.scanning = false; + }); + }, + generateThumbnails() { + if (this.generating) return; + this.generating = true; + this.progress = 0.0; + $.post(`${base_url}api/admin/generate_thumbnails`).then(() => { + this.getProgress(); + }); + }, + getProgress() { + $.get(`${base_url}api/admin/thumbnail_progress`).then((data) => { + this.progress = data.progress; + this.generating = data.progress > 0; + }); + }, + }; }; diff --git a/public/js/alert.js b/public/js/alert.js index babc0b6..1574ac8 100644 --- a/public/js/alert.js +++ b/public/js/alert.js @@ -1,6 +1,6 @@ const alert = (level, text) => { - $('#alert').empty(); - const html = `

${text}

`; - $('#alert').append(html); - $("html, body").animate({ scrollTop: 0 }); + $('#alert').empty(); + const html = `

${text}

`; + $('#alert').append(html); + $('html, body').animate({ scrollTop: 0 }); }; diff --git a/public/js/common.js b/public/js/common.js index d1fd829..e2f950f 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -11,7 +11,7 @@ * @param {string} selector - The jQuery selector to the root element */ const setProp = (key, prop, selector = '#root') => { - $(selector).get(0).__x.$data[key] = prop; + $(selector).get(0).__x.$data[key] = prop; }; /** @@ -23,7 +23,7 @@ const setProp = (key, prop, selector = '#root') => { * @return {*} The data property */ const getProp = (key, selector = '#root') => { - return $(selector).get(0).__x.$data[key]; + return $(selector).get(0).__x.$data[key]; }; /** @@ -41,7 +41,10 @@ const getProp = (key, selector = '#root') => { * @return {bool} */ const preferDarkMode = () => { - return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return ( + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ); }; /** @@ -52,7 +55,7 @@ const preferDarkMode = () => { * @return {bool} */ const validThemeSetting = (theme) => { - return ['dark', 'light', 'system'].indexOf(theme) >= 0; + return ['dark', 'light', 'system'].indexOf(theme) >= 0; }; /** @@ -62,9 +65,9 @@ const validThemeSetting = (theme) => { * @return {string} A theme setting ('dark', 'light', or 'system') */ const loadThemeSetting = () => { - let str = localStorage.getItem('theme'); - if (!str || !validThemeSetting(str)) str = 'system'; - return str; + let str = localStorage.getItem('theme'); + if (!str || !validThemeSetting(str)) str = 'system'; + return str; }; /** @@ -74,11 +77,11 @@ const loadThemeSetting = () => { * @return {string} The current theme to use ('dark' or 'light') */ const loadTheme = () => { - let setting = loadThemeSetting(); - if (setting === 'system') { - setting = preferDarkMode() ? 'dark' : 'light'; - } - return setting; + let setting = loadThemeSetting(); + if (setting === 'system') { + setting = preferDarkMode() ? 'dark' : 'light'; + } + return setting; }; /** @@ -87,9 +90,9 @@ const loadTheme = () => { * @function saveThemeSetting * @param {string} setting - A theme setting */ -const saveThemeSetting = setting => { - if (!validThemeSetting(setting)) setting = 'system'; - localStorage.setItem('theme', setting); +const saveThemeSetting = (setting) => { + if (!validThemeSetting(setting)) setting = 'system'; + localStorage.setItem('theme', setting); }; /** @@ -99,10 +102,10 @@ const saveThemeSetting = setting => { * @function toggleTheme */ const toggleTheme = () => { - const theme = loadTheme(); - const newTheme = theme === 'dark' ? 'light' : 'dark'; - saveThemeSetting(newTheme); - setTheme(newTheme); + const theme = loadTheme(); + const newTheme = theme === 'dark' ? 'light' : 'dark'; + saveThemeSetting(newTheme); + setTheme(newTheme); }; /** @@ -113,31 +116,32 @@ const toggleTheme = () => { * `loadTheme` to get a theme and apply it. */ const setTheme = (theme) => { - if (!theme) theme = loadTheme(); - if (theme === 'dark') { - $('html').css('background', 'rgb(20, 20, 20)'); - $('body').addClass('uk-light'); - $('.ui-widget-content').addClass('dark'); - } else { - $('html').css('background', ''); - $('body').removeClass('uk-light'); - $('.ui-widget-content').removeClass('dark'); - } + if (!theme) theme = loadTheme(); + if (theme === 'dark') { + $('html').css('background', 'rgb(20, 20, 20)'); + $('body').addClass('uk-light'); + $('.ui-widget-content').addClass('dark'); + } else { + $('html').css('background', ''); + $('body').removeClass('uk-light'); + $('.ui-widget-content').removeClass('dark'); + } }; // do it before document is ready to prevent the initial flash of white on // most pages setTheme(); $(() => { - // hack for the reader page - setTheme(); + // hack for the reader page + setTheme(); - // on system dark mode setting change - if (window.matchMedia) { - window.matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', event => { - if (loadThemeSetting() === 'system') - setTheme(event.matches ? 'dark' : 'light'); - }); - } + // on system dark mode setting change + if (window.matchMedia) { + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', (event) => { + if (loadThemeSetting() === 'system') + setTheme(event.matches ? 'dark' : 'light'); + }); + } }); diff --git a/public/js/dots.js b/public/js/dots.js index 266c846..2baa9bf 100644 --- a/public/js/dots.js +++ b/public/js/dots.js @@ -5,22 +5,22 @@ * @param {object} e - The title element to truncate */ const truncate = (e) => { - $(e).dotdotdot({ - truncate: 'letter', - watch: true, - callback: (truncated) => { - if (truncated) { - $(e).attr('uk-tooltip', $(e).attr('data-title')); - } else { - $(e).removeAttr('uk-tooltip'); - } - } - }); + $(e).dotdotdot({ + truncate: 'letter', + watch: true, + callback: (truncated) => { + if (truncated) { + $(e).attr('uk-tooltip', $(e).attr('data-title')); + } else { + $(e).removeAttr('uk-tooltip'); + } + }, + }); }; $('.uk-card-title').each((i, e) => { - // Truncate the title when it first enters the view - $(e).one('inview', () => { - truncate(e); - }); + // Truncate the title when it first enters the view + $(e).one('inview', () => { + truncate(e); + }); }); diff --git a/public/js/download-manager.js b/public/js/download-manager.js index 1183ce5..512bb6d 100644 --- a/public/js/download-manager.js +++ b/public/js/download-manager.js @@ -1,116 +1,135 @@ const component = () => { - return { - jobs: [], - paused: undefined, - loading: false, - toggling: false, - ws: undefined, + return { + jobs: [], + paused: undefined, + loading: false, + toggling: false, + ws: undefined, - wsConnect(secure = true) { - const url = `${secure ? 'wss' : 'ws'}://${location.host}${base_url}api/admin/mangadex/queue`; - console.log(`Connecting to ${url}`); - this.ws = new WebSocket(url); - this.ws.onmessage = event => { - const data = JSON.parse(event.data); - this.jobs = data.jobs; - this.paused = data.paused; - }; - this.ws.onclose = () => { - if (this.ws.failed) - return this.wsConnect(false); - alert('danger', 'Socket connection closed'); - }; - this.ws.onerror = () => { - if (secure) - return this.ws.failed = true; - alert('danger', 'Socket connection failed'); - }; - }, - init() { - this.wsConnect(); - this.load(); - }, - load() { - this.loading = true; - $.ajax({ - type: 'GET', - url: base_url + 'api/admin/mangadex/queue', - dataType: 'json' - }) - .done(data => { - if (!data.success && data.error) { - alert('danger', `Failed to fetch download queue. Error: ${data.error}`); - return; - } - this.jobs = data.jobs; - this.paused = data.paused; - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }) - .always(() => { - this.loading = false; - }); - }, - jobAction(action, event) { - let url = `${base_url}api/admin/mangadex/queue/${action}`; - if (event) { - const id = event.currentTarget.closest('tr').id.split('-').slice(1).join('-'); - url = `${url}?${$.param({ - id: id - })}`; - } - console.log(url); - $.ajax({ - type: 'POST', - url: url, - dataType: 'json' - }) - .done(data => { - if (!data.success && data.error) { - alert('danger', `Failed to ${action} job from download queue. Error: ${data.error}`); - return; - } - this.load(); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); - }, - toggle() { - this.toggling = true; - const action = this.paused ? 'resume' : 'pause'; - const url = `${base_url}api/admin/mangadex/queue/${action}`; - $.ajax({ - type: 'POST', - url: url, - dataType: 'json' - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }) - .always(() => { - this.load(); - this.toggling = false; - }); - }, - statusClass(status) { - let cls = 'label '; - switch (status) { - case 'Pending': - cls += 'label-pending'; - break; - case 'Completed': - cls += 'label-success'; - break; - case 'Error': - cls += 'label-danger'; - break; - case 'MissingPages': - cls += 'label-warning'; - break; - } - return cls; - } - }; + wsConnect(secure = true) { + const url = `${secure ? 'wss' : 'ws'}://${ + location.host + }${base_url}api/admin/mangadex/queue`; + console.log(`Connecting to ${url}`); + this.ws = new WebSocket(url); + this.ws.onmessage = (event) => { + const data = JSON.parse(event.data); + this.jobs = data.jobs; + this.paused = data.paused; + }; + this.ws.onclose = () => { + if (this.ws.failed) return this.wsConnect(false); + alert('danger', 'Socket connection closed'); + }; + this.ws.onerror = () => { + if (secure) return (this.ws.failed = true); + alert('danger', 'Socket connection failed'); + }; + }, + init() { + this.wsConnect(); + this.load(); + }, + load() { + this.loading = true; + $.ajax({ + type: 'GET', + url: base_url + 'api/admin/mangadex/queue', + dataType: 'json', + }) + .done((data) => { + if (!data.success && data.error) { + alert( + 'danger', + `Failed to fetch download queue. Error: ${data.error}`, + ); + return; + } + this.jobs = data.jobs; + this.paused = data.paused; + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }) + .always(() => { + this.loading = false; + }); + }, + jobAction(action, event) { + let url = `${base_url}api/admin/mangadex/queue/${action}`; + if (event) { + const id = event.currentTarget + .closest('tr') + .id.split('-') + .slice(1) + .join('-'); + url = `${url}?${$.param({ + id, + })}`; + } + console.log(url); + $.ajax({ + type: 'POST', + url, + dataType: 'json', + }) + .done((data) => { + if (!data.success && data.error) { + alert( + 'danger', + `Failed to ${action} job from download queue. Error: ${data.error}`, + ); + return; + } + this.load(); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); + }, + toggle() { + this.toggling = true; + const action = this.paused ? 'resume' : 'pause'; + const url = `${base_url}api/admin/mangadex/queue/${action}`; + $.ajax({ + type: 'POST', + url, + dataType: 'json', + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }) + .always(() => { + this.load(); + this.toggling = false; + }); + }, + statusClass(status) { + let cls = 'label '; + switch (status) { + case 'Pending': + cls += 'label-pending'; + break; + case 'Completed': + cls += 'label-success'; + break; + case 'Error': + cls += 'label-danger'; + break; + case 'MissingPages': + cls += 'label-warning'; + break; + } + return cls; + }, + }; }; diff --git a/public/js/missing-items.js b/public/js/missing-items.js index 0babb24..a9758b6 100644 --- a/public/js/missing-items.js +++ b/public/js/missing-items.js @@ -1,60 +1,74 @@ const component = () => { - return { - empty: true, - titles: [], - entries: [], - loading: true, + return { + empty: true, + titles: [], + entries: [], + loading: true, - load() { - this.loading = true; - this.request('GET', `${base_url}api/admin/titles/missing`, data => { - this.titles = data.titles; - this.request('GET', `${base_url}api/admin/entries/missing`, data => { - this.entries = data.entries; - this.loading = false; - this.empty = this.entries.length === 0 && this.titles.length === 0; - }); - }); - }, - rm(event) { - const rawID = event.currentTarget.closest('tr').id; - const [type, id] = rawID.split('-'); - const url = `${base_url}api/admin/${type === 'title' ? 'titles' : 'entries'}/missing/${id}`; - this.request('DELETE', url, () => { - this.load(); - }); - }, - rmAll() { - UIkit.modal.confirm('Are you sure? All metadata associated with these items, including their tags and thumbnails, will be deleted from the database.', { - labels: { - ok: 'Yes, delete them', - cancel: 'Cancel' - } - }).then(() => { - this.request('DELETE', `${base_url}api/admin/titles/missing`, () => { - this.request('DELETE', `${base_url}api/admin/entries/missing`, () => { - this.load(); - }); - }); - }); - }, - request(method, url, cb) { - console.log(url); - $.ajax({ - type: method, - url: url, - contentType: 'application/json' - }) - .done(data => { - if (data.error) { - alert('danger', `Failed to ${method} ${url}. Error: ${data.error}`); - return; - } - if (cb) cb(data); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to ${method} ${url}. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); - } - }; + load() { + this.loading = true; + this.request('GET', `${base_url}api/admin/titles/missing`, (data) => { + this.titles = data.titles; + this.request('GET', `${base_url}api/admin/entries/missing`, (data) => { + this.entries = data.entries; + this.loading = false; + this.empty = this.entries.length === 0 && this.titles.length === 0; + }); + }); + }, + rm(event) { + const rawID = event.currentTarget.closest('tr').id; + const [type, id] = rawID.split('-'); + const url = `${base_url}api/admin/${ + type === 'title' ? 'titles' : 'entries' + }/missing/${id}`; + this.request('DELETE', url, () => { + this.load(); + }); + }, + rmAll() { + UIkit.modal + .confirm( + 'Are you sure? All metadata associated with these items, including their tags and thumbnails, will be deleted from the database.', + { + labels: { + ok: 'Yes, delete them', + cancel: 'Cancel', + }, + }, + ) + .then(() => { + this.request('DELETE', `${base_url}api/admin/titles/missing`, () => { + this.request( + 'DELETE', + `${base_url}api/admin/entries/missing`, + () => { + this.load(); + }, + ); + }); + }); + }, + request(method, url, cb) { + console.log(url); + $.ajax({ + type: method, + url, + contentType: 'application/json', + }) + .done((data) => { + if (data.error) { + alert('danger', `Failed to ${method} ${url}. Error: ${data.error}`); + return; + } + if (cb) cb(data); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to ${method} ${url}. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); + }, + }; }; diff --git a/public/js/plugin-download.js b/public/js/plugin-download.js index 0f19738..18a5be1 100644 --- a/public/js/plugin-download.js +++ b/public/js/plugin-download.js @@ -1,452 +1,435 @@ const component = () => { - return { - plugins: [], - subscribable: false, - info: undefined, - pid: undefined, - chapters: undefined, // undefined: not searched yet, []: empty - manga: undefined, // undefined: not searched yet, []: empty - mid: undefined, // id of the selected manga - allChapters: [], - query: "", - mangaTitle: "", - searching: false, - adding: false, - sortOptions: [], - showFilters: false, - appliedFilters: [], - chaptersLimit: 500, - listManga: false, - subscribing: false, - subscriptionName: "", + return { + plugins: [], + subscribable: false, + info: undefined, + pid: undefined, + chapters: undefined, // undefined: not searched yet, []: empty + manga: undefined, // undefined: not searched yet, []: empty + mid: undefined, // id of the selected manga + allChapters: [], + query: '', + mangaTitle: '', + searching: false, + adding: false, + sortOptions: [], + showFilters: false, + appliedFilters: [], + chaptersLimit: 500, + listManga: false, + subscribing: false, + subscriptionName: '', - init() { - const tableObserver = new MutationObserver(() => { - console.log("table mutated"); - $("#selectable").selectable({ - filter: "tr", - }); - }); - tableObserver.observe($("table").get(0), { - childList: true, - subtree: true, - }); - fetch(`${base_url}api/admin/plugin`) - .then((res) => res.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - this.plugins = data.plugins; + init() { + const tableObserver = new MutationObserver(() => { + console.log('table mutated'); + $('#selectable').selectable({ + filter: 'tr', + }); + }); + tableObserver.observe($('table').get(0), { + childList: true, + subtree: true, + }); + fetch(`${base_url}api/admin/plugin`) + .then((res) => res.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + this.plugins = data.plugins; - const pid = localStorage.getItem("plugin"); - if (pid && this.plugins.map((p) => p.id).includes(pid)) - return this.loadPlugin(pid); + const pid = localStorage.getItem('plugin'); + if (pid && this.plugins.map((p) => p.id).includes(pid)) + return this.loadPlugin(pid); - if (this.plugins.length > 0) - this.loadPlugin(this.plugins[0].id); - }) - .catch((e) => { - alert( - "danger", - `Failed to list the available plugins. Error: ${e}` - ); - }); - }, - loadPlugin(pid) { - fetch( - `${base_url}api/admin/plugin/info?${new URLSearchParams({ - plugin: pid, - })}` - ) - .then((res) => res.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - this.info = data.info; - this.subscribable = data.subscribable; - this.pid = pid; - }) - .catch((e) => { - alert( - "danger", - `Failed to get plugin metadata. Error: ${e}` - ); - }); - }, - pluginChanged() { - this.manga = undefined; - this.chapters = undefined; - this.mid = undefined; - this.loadPlugin(this.pid); - localStorage.setItem("plugin", this.pid); - }, - get chapterKeys() { - if (this.allChapters.length < 1) return []; - return Object.keys(this.allChapters[0]).filter( - (k) => !["manga_title"].includes(k) - ); - }, - searchChapters(query) { - this.searching = true; - this.allChapters = []; - this.sortOptions = []; - this.chapters = undefined; - this.listManga = false; - fetch( - `${base_url}api/admin/plugin/list?${new URLSearchParams({ - plugin: this.pid, - query: query, - })}` - ) - .then((res) => res.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - try { - this.mangaTitle = data.chapters[0].manga_title; - if (!this.mangaTitle) throw new Error(); - } catch (e) { - this.mangaTitle = data.title; - } + if (this.plugins.length > 0) this.loadPlugin(this.plugins[0].id); + }) + .catch((e) => { + alert('danger', `Failed to list the available plugins. Error: ${e}`); + }); + }, + loadPlugin(pid) { + fetch( + `${base_url}api/admin/plugin/info?${new URLSearchParams({ + plugin: pid, + })}`, + ) + .then((res) => res.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + this.info = data.info; + this.subscribable = data.subscribable; + this.pid = pid; + }) + .catch((e) => { + alert('danger', `Failed to get plugin metadata. Error: ${e}`); + }); + }, + pluginChanged() { + this.manga = undefined; + this.chapters = undefined; + this.mid = undefined; + this.loadPlugin(this.pid); + localStorage.setItem('plugin', this.pid); + }, + get chapterKeys() { + if (this.allChapters.length < 1) return []; + return Object.keys(this.allChapters[0]).filter( + (k) => !['manga_title'].includes(k), + ); + }, + searchChapters(query) { + this.searching = true; + this.allChapters = []; + this.sortOptions = []; + this.chapters = undefined; + this.listManga = false; + fetch( + `${base_url}api/admin/plugin/list?${new URLSearchParams({ + plugin: this.pid, + query, + })}`, + ) + .then((res) => res.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + try { + this.mangaTitle = data.chapters[0].manga_title; + if (!this.mangaTitle) throw new Error(); + } catch (e) { + this.mangaTitle = data.title; + } - this.allChapters = data.chapters; - this.chapters = data.chapters; - }) - .catch((e) => { - alert("danger", `Failed to list chapters. Error: ${e}`); - }) - .finally(() => { - this.searching = false; - }); - }, - searchManga(query) { - this.searching = true; - this.allChapters = []; - this.chapters = undefined; - this.manga = undefined; - fetch( - `${base_url}api/admin/plugin/search?${new URLSearchParams({ - plugin: this.pid, - query: query, - })}` - ) - .then((res) => res.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - this.manga = data.manga; - this.listManga = true; - }) - .catch((e) => { - alert("danger", `Search failed. Error: ${e}`); - }) - .finally(() => { - this.searching = false; - }); - }, - search() { - const query = this.query.trim(); - if (!query) return; + this.allChapters = data.chapters; + this.chapters = data.chapters; + }) + .catch((e) => { + alert('danger', `Failed to list chapters. Error: ${e}`); + }) + .finally(() => { + this.searching = false; + }); + }, + searchManga(query) { + this.searching = true; + this.allChapters = []; + this.chapters = undefined; + this.manga = undefined; + fetch( + `${base_url}api/admin/plugin/search?${new URLSearchParams({ + plugin: this.pid, + query, + })}`, + ) + .then((res) => res.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + this.manga = data.manga; + this.listManga = true; + }) + .catch((e) => { + alert('danger', `Search failed. Error: ${e}`); + }) + .finally(() => { + this.searching = false; + }); + }, + search() { + const query = this.query.trim(); + if (!query) return; - this.manga = undefined; - this.mid = undefined; - if (this.info.version === 1) { - this.searchChapters(query); - } else { - this.searchManga(query); - } - }, - selectAll() { - $("tbody > tr").each((i, e) => { - $(e).addClass("ui-selected"); - }); - }, - clearSelection() { - $("tbody > tr").each((i, e) => { - $(e).removeClass("ui-selected"); - }); - }, - download() { - const selected = $("tbody > tr.ui-selected").get(); - if (selected.length === 0) return; + this.manga = undefined; + this.mid = undefined; + if (this.info.version === 1) { + this.searchChapters(query); + } else { + this.searchManga(query); + } + }, + selectAll() { + $('tbody > tr').each((i, e) => { + $(e).addClass('ui-selected'); + }); + }, + clearSelection() { + $('tbody > tr').each((i, e) => { + $(e).removeClass('ui-selected'); + }); + }, + download() { + const selected = $('tbody > tr.ui-selected').get(); + if (selected.length === 0) return; - UIkit.modal - .confirm(`Download ${selected.length} selected chapters?`) - .then(() => { - const ids = selected.map((e) => e.id); - const chapters = this.chapters.filter((c) => - ids.includes(c.id) - ); - console.log(chapters); - this.adding = true; - fetch(`${base_url}api/admin/plugin/download`, { - method: "POST", - body: JSON.stringify({ - chapters, - plugin: this.pid, - title: this.mangaTitle, - }), - headers: { - "Content-Type": "application/json", - }, - }) - .then((res) => res.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - const successCount = parseInt(data.success); - const failCount = parseInt(data.fail); - alert( - "success", - `${successCount} of ${ - successCount + failCount - } chapters added to the download queue. You can view and manage your download queue on the download manager page.` - ); - }) - .catch((e) => { - alert( - "danger", - `Failed to add chapters to the download queue. Error: ${e}` - ); - }) - .finally(() => { - this.adding = false; - }); - }); - }, - thClicked(event) { - const idx = parseInt(event.currentTarget.id.split("-")[1]); - if (idx === undefined || isNaN(idx)) return; - const curOption = this.sortOptions[idx]; - let option; - this.sortOptions = []; - switch (curOption) { - case 1: - option = -1; - break; - case -1: - option = 0; - break; - default: - option = 1; - } - this.sortOptions[idx] = option; - this.sort(this.chapterKeys[idx], option); - }, - // Returns an array of filtered but unsorted chapters. Useful when - // reseting the sort options. - get filteredChapters() { - let ary = this.allChapters.slice(); + UIkit.modal + .confirm(`Download ${selected.length} selected chapters?`) + .then(() => { + const ids = selected.map((e) => e.id); + const chapters = this.chapters.filter((c) => ids.includes(c.id)); + console.log(chapters); + this.adding = true; + fetch(`${base_url}api/admin/plugin/download`, { + method: 'POST', + body: JSON.stringify({ + chapters, + plugin: this.pid, + title: this.mangaTitle, + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + const successCount = parseInt(data.success); + const failCount = parseInt(data.fail); + alert( + 'success', + `${successCount} of ${ + successCount + failCount + } chapters added to the download queue. You can view and manage your download queue on the download manager page.`, + ); + }) + .catch((e) => { + alert( + 'danger', + `Failed to add chapters to the download queue. Error: ${e}`, + ); + }) + .finally(() => { + this.adding = false; + }); + }); + }, + thClicked(event) { + const idx = parseInt(event.currentTarget.id.split('-')[1]); + if (idx === undefined || isNaN(idx)) return; + const curOption = this.sortOptions[idx]; + let option; + this.sortOptions = []; + switch (curOption) { + case 1: + option = -1; + break; + case -1: + option = 0; + break; + default: + option = 1; + } + this.sortOptions[idx] = option; + this.sort(this.chapterKeys[idx], option); + }, + // Returns an array of filtered but unsorted chapters. Useful when + // reseting the sort options. + get filteredChapters() { + let ary = this.allChapters.slice(); - console.log("initial size:", ary.length); - for (let filter of this.appliedFilters) { - if (!filter.value) continue; - if (filter.type === "array" && filter.value === "all") continue; - if (filter.type.startsWith("number") && isNaN(filter.value)) - continue; + console.log('initial size:', ary.length); + for (let filter of this.appliedFilters) { + if (!filter.value) continue; + if (filter.type === 'array' && filter.value === 'all') continue; + if (filter.type.startsWith('number') && isNaN(filter.value)) continue; - if (filter.type === "string") { - ary = ary.filter((ch) => - ch[filter.key] - .toLowerCase() - .includes(filter.value.toLowerCase()) - ); - } - if (filter.type === "number-min") { - ary = ary.filter( - (ch) => Number(ch[filter.key]) >= Number(filter.value) - ); - } - if (filter.type === "number-max") { - ary = ary.filter( - (ch) => Number(ch[filter.key]) <= Number(filter.value) - ); - } - if (filter.type === "date-min") { - ary = ary.filter( - (ch) => Number(ch[filter.key]) >= Number(filter.value) - ); - } - if (filter.type === "date-max") { - ary = ary.filter( - (ch) => Number(ch[filter.key]) <= Number(filter.value) - ); - } - if (filter.type === "array") { - ary = ary.filter((ch) => - ch[filter.key] - .map((s) => - typeof s === "string" ? s.toLowerCase() : s - ) - .includes(filter.value.toLowerCase()) - ); - } + if (filter.type === 'string') { + ary = ary.filter((ch) => + ch[filter.key].toLowerCase().includes(filter.value.toLowerCase()), + ); + } + if (filter.type === 'number-min') { + ary = ary.filter( + (ch) => Number(ch[filter.key]) >= Number(filter.value), + ); + } + if (filter.type === 'number-max') { + ary = ary.filter( + (ch) => Number(ch[filter.key]) <= Number(filter.value), + ); + } + if (filter.type === 'date-min') { + ary = ary.filter( + (ch) => Number(ch[filter.key]) >= Number(filter.value), + ); + } + if (filter.type === 'date-max') { + ary = ary.filter( + (ch) => Number(ch[filter.key]) <= Number(filter.value), + ); + } + if (filter.type === 'array') { + ary = ary.filter((ch) => + ch[filter.key] + .map((s) => (typeof s === 'string' ? s.toLowerCase() : s)) + .includes(filter.value.toLowerCase()), + ); + } - console.log("filtered size:", ary.length); - } + console.log('filtered size:', ary.length); + } - return ary; - }, - // option: - // - 1: asending - // - -1: desending - // - 0: unsorted - sort(key, option) { - if (option === 0) { - this.chapters = this.filteredChapters; - return; - } + return ary; + }, + // option: + // - 1: asending + // - -1: desending + // - 0: unsorted + sort(key, option) { + if (option === 0) { + this.chapters = this.filteredChapters; + return; + } - this.chapters = this.filteredChapters.sort((a, b) => { - const comp = this.compare(a[key], b[key]); - return option < 0 ? comp * -1 : comp; - }); - }, - compare(a, b) { - if (a === b) return 0; + this.chapters = this.filteredChapters.sort((a, b) => { + const comp = this.compare(a[key], b[key]); + return option < 0 ? comp * -1 : comp; + }); + }, + compare(a, b) { + if (a === b) return 0; - // try numbers (also covers dates) - if (!isNaN(a) && !isNaN(b)) return Number(a) - Number(b); + // try numbers (also covers dates) + if (!isNaN(a) && !isNaN(b)) return Number(a) - Number(b); - const preprocessString = (val) => { - if (typeof val !== "string") return val; - return val.toLowerCase().replace(/\s\s/g, " ").trim(); - }; + const preprocessString = (val) => { + if (typeof val !== 'string') return val; + return val.toLowerCase().replace(/\s\s/g, ' ').trim(); + }; - return preprocessString(a) > preprocessString(b) ? 1 : -1; - }, - fieldType(values) { - if (values.every((v) => this.numIsDate(v))) return "date"; - if (values.every((v) => !isNaN(v))) return "number"; - if (values.every((v) => Array.isArray(v))) return "array"; - return "string"; - }, - get filters() { - if (this.allChapters.length < 1) return []; - const keys = Object.keys(this.allChapters[0]).filter( - (k) => !["manga_title", "id"].includes(k) - ); - return keys.map((k) => { - let values = this.allChapters.map((c) => c[k]); - const type = this.fieldType(values); + return preprocessString(a) > preprocessString(b) ? 1 : -1; + }, + fieldType(values) { + if (values.every((v) => this.numIsDate(v))) return 'date'; + if (values.every((v) => !isNaN(v))) return 'number'; + if (values.every((v) => Array.isArray(v))) return 'array'; + return 'string'; + }, + get filters() { + if (this.allChapters.length < 1) return []; + const keys = Object.keys(this.allChapters[0]).filter( + (k) => !['manga_title', 'id'].includes(k), + ); + return keys.map((k) => { + let values = this.allChapters.map((c) => c[k]); + const type = this.fieldType(values); - if (type === "array") { - // if the type is an array, return the list of available elements - // example: an array of groups or authors - values = Array.from( - new Set( - values.flat().map((v) => { - if (typeof v === "string") - return v.toLowerCase(); - }) - ) - ); - } + if (type === 'array') { + // if the type is an array, return the list of available elements + // example: an array of groups or authors + values = Array.from( + new Set( + values.flat().map((v) => { + if (typeof v === 'string') return v.toLowerCase(); + }), + ), + ); + } - return { - key: k, - type: type, - values: values, - }; - }); - }, - get filterSettings() { - return $("#filter-form input:visible, #filter-form select:visible") - .get() - .map((i) => { - const type = i.getAttribute("data-filter-type"); - let value = i.value.trim(); - if (type.startsWith("date")) - value = value ? Date.parse(value).toString() : ""; - return { - key: i.getAttribute("data-filter-key"), - value: value, - type: type, - }; - }); - }, - applyFilters() { - this.appliedFilters = this.filterSettings; - this.chapters = this.filteredChapters; - this.sortOptions = []; - }, - clearFilters() { - $("#filter-form input") - .get() - .forEach((i) => (i.value = "")); - $("#filter-form select").val("all"); - this.appliedFilters = []; - this.chapters = this.filteredChapters; - this.sortOptions = []; - }, - mangaSelected(event) { - const mid = event.currentTarget.getAttribute("data-id"); - this.mid = mid; - this.searchChapters(mid); - }, - subscribe(modal) { - this.subscribing = true; - fetch(`${base_url}api/admin/plugin/subscriptions`, { - method: "POST", - body: JSON.stringify({ - filters: this.filterSettings, - plugin: this.pid, - name: this.subscriptionName.trim(), - manga: this.mangaTitle, - manga_id: this.mid, - }), - headers: { - "Content-Type": "application/json", - }, - }) - .then((res) => res.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - alert("success", "Subscription created"); - }) - .catch((e) => { - alert("danger", `Failed to subscribe. Error: ${e}`); - }) - .finally(() => { - this.subscribing = false; - UIkit.modal(modal).hide(); - }); - }, - numIsDate(num) { - return !isNaN(num) && Number(num) > 328896000000; // 328896000000 => 1 Jan, 1980 - }, - renderCell(value) { - if (this.numIsDate(value)) - return `${moment(Number(value)).format( - "MMM D, YYYY" - )}`; - const maxLength = 40; - if (value && value.length > maxLength) - return `${value.substr( - 0, - maxLength - )}...
${value}
`; - return `${value}`; - }, - renderFilterRow(ft) { - const key = ft.key; - let type = ft.type; - switch (type) { - case "number-min": - type = "number (minimum value)"; - break; - case "number-max": - type = "number (maximum value)"; - break; - case "date-min": - type = "minimum date"; - break; - case "date-max": - type = "maximum date"; - break; - } - let value = ft.value; + return { + key: k, + type, + values, + }; + }); + }, + get filterSettings() { + return $('#filter-form input:visible, #filter-form select:visible') + .get() + .map((i) => { + const type = i.getAttribute('data-filter-type'); + let value = i.value.trim(); + if (type.startsWith('date')) + value = value ? Date.parse(value).toString() : ''; + return { + key: i.getAttribute('data-filter-key'), + value, + type, + }; + }); + }, + applyFilters() { + this.appliedFilters = this.filterSettings; + this.chapters = this.filteredChapters; + this.sortOptions = []; + }, + clearFilters() { + $('#filter-form input') + .get() + .forEach((i) => (i.value = '')); + $('#filter-form select').val('all'); + this.appliedFilters = []; + this.chapters = this.filteredChapters; + this.sortOptions = []; + }, + mangaSelected(event) { + const mid = event.currentTarget.getAttribute('data-id'); + this.mid = mid; + this.searchChapters(mid); + }, + subscribe(modal) { + this.subscribing = true; + fetch(`${base_url}api/admin/plugin/subscriptions`, { + method: 'POST', + body: JSON.stringify({ + filters: this.filterSettings, + plugin: this.pid, + name: this.subscriptionName.trim(), + manga: this.mangaTitle, + manga_id: this.mid, + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + alert('success', 'Subscription created'); + }) + .catch((e) => { + alert('danger', `Failed to subscribe. Error: ${e}`); + }) + .finally(() => { + this.subscribing = false; + UIkit.modal(modal).hide(); + }); + }, + numIsDate(num) { + return !isNaN(num) && Number(num) > 328896000000; // 328896000000 => 1 Jan, 1980 + }, + renderCell(value) { + if (this.numIsDate(value)) + return `${moment(Number(value)).format('MMM D, YYYY')}`; + const maxLength = 40; + if (value && value.length > maxLength) + return `${value.substr( + 0, + maxLength, + )}...
${value}
`; + return `${value}`; + }, + renderFilterRow(ft) { + const key = ft.key; + let type = ft.type; + switch (type) { + case 'number-min': + type = 'number (minimum value)'; + break; + case 'number-max': + type = 'number (maximum value)'; + break; + case 'date-min': + type = 'minimum date'; + break; + case 'date-max': + type = 'maximum date'; + break; + } + let value = ft.value; - if (ft.type.startsWith("number") && isNaN(value)) value = ""; - else if (ft.type.startsWith("date") && value) - value = moment(Number(value)).format("MMM D, YYYY"); + if (ft.type.startsWith('number') && isNaN(value)) value = ''; + else if (ft.type.startsWith('date') && value) + value = moment(Number(value)).format('MMM D, YYYY'); - return `${key}${type}${value}`; - }, - }; + return `${key}${type}${value}`; + }, + }; }; diff --git a/public/js/reader.js b/public/js/reader.js index 63cef7f..2360a4c 100644 --- a/public/js/reader.js +++ b/public/js/reader.js @@ -1,361 +1,370 @@ const readerComponent = () => { - return { - loading: true, - mode: 'continuous', // Can be 'continuous', 'height' or 'width' - msg: 'Loading the web reader. Please wait...', - alertClass: 'uk-alert-primary', - items: [], - curItem: {}, - enableFlipAnimation: true, - flipAnimation: null, - longPages: false, - lastSavedPage: page, - selectedIndex: 0, // 0: not selected; 1: the first page - margin: 30, - preloadLookahead: 3, - enableRightToLeft: false, - fitType: 'vert', + return { + loading: true, + mode: 'continuous', // Can be 'continuous', 'height' or 'width' + msg: 'Loading the web reader. Please wait...', + alertClass: 'uk-alert-primary', + items: [], + curItem: {}, + enableFlipAnimation: true, + flipAnimation: null, + longPages: false, + lastSavedPage: page, + selectedIndex: 0, // 0: not selected; 1: the first page + margin: 30, + preloadLookahead: 3, + enableRightToLeft: false, + fitType: 'vert', - /** - * Initialize the component by fetching the page dimensions - */ - init(nextTick) { - $.get(`${base_url}api/dimensions/${tid}/${eid}`) - .then(data => { - if (!data.success && data.error) - throw new Error(resp.error); - const dimensions = data.dimensions; + /** + * Initialize the component by fetching the page dimensions + */ + init(nextTick) { + $.get(`${base_url}api/dimensions/${tid}/${eid}`) + .then((data) => { + if (!data.success && data.error) throw new Error(resp.error); + const dimensions = data.dimensions; - this.items = dimensions.map((d, i) => { - return { - id: i + 1, - url: `${base_url}api/page/${tid}/${eid}/${i+1}`, - width: d.width == 0 ? "100%" : d.width, - height: d.height == 0 ? "100%" : d.height, - }; - }); + this.items = dimensions.map((d, i) => { + return { + id: i + 1, + url: `${base_url}api/page/${tid}/${eid}/${i + 1}`, + width: d.width === 0 ? '100%' : d.width, + height: d.height === 0 ? '100%' : d.height, + }; + }); - // Note: for image types not supported by image_size.cr, the width and height will be 0, and so `avgRatio` will be `Infinity`. - // TODO: support more image types in image_size.cr - const avgRatio = dimensions.reduce((acc, cur) => { - return acc + cur.height / cur.width - }, 0) / dimensions.length; + // Note: for image types not supported by image_size.cr, the width and height will be 0, and so `avgRatio` will be `Infinity`. + // TODO: support more image types in image_size.cr + const avgRatio = + dimensions.reduce((acc, cur) => { + return acc + cur.height / cur.width; + }, 0) / dimensions.length; - console.log(avgRatio); - this.longPages = avgRatio > 2; - this.loading = false; - this.mode = localStorage.getItem('mode') || 'continuous'; + console.log(avgRatio); + this.longPages = avgRatio > 2; + this.loading = false; + this.mode = localStorage.getItem('mode') || 'continuous'; - // Here we save a copy of this.mode, and use the copy as - // the model-select value. This is because `updateMode` - // might change this.mode and make it `height` or `width`, - // which are not available in mode-select - const mode = this.mode; - this.updateMode(this.mode, page, nextTick); - $('#mode-select').val(mode); + // Here we save a copy of this.mode, and use the copy as + // the model-select value. This is because `updateMode` + // might change this.mode and make it `height` or `width`, + // which are not available in mode-select + const mode = this.mode; + this.updateMode(this.mode, page, nextTick); + $('#mode-select').val(mode); - const savedMargin = localStorage.getItem('margin'); - if (savedMargin) { - this.margin = savedMargin; - } + const savedMargin = localStorage.getItem('margin'); + if (savedMargin) { + this.margin = savedMargin; + } - // Preload Images - this.preloadLookahead = +(localStorage.getItem('preloadLookahead') ?? 3); - const limit = Math.min(page + this.preloadLookahead, this.items.length); - for (let idx = page + 1; idx <= limit; idx++) { - this.preloadImage(this.items[idx - 1].url); - } + // Preload Images + this.preloadLookahead = +( + localStorage.getItem('preloadLookahead') ?? 3 + ); + const limit = Math.min( + page + this.preloadLookahead, + this.items.length, + ); + for (let idx = page + 1; idx <= limit; idx++) { + this.preloadImage(this.items[idx - 1].url); + } - const savedFitType = localStorage.getItem('fitType'); - if (savedFitType) { - this.fitType = savedFitType; - $('#fit-select').val(savedFitType); - } - const savedFlipAnimation = localStorage.getItem('enableFlipAnimation'); - this.enableFlipAnimation = savedFlipAnimation === null || savedFlipAnimation === 'true'; + const savedFitType = localStorage.getItem('fitType'); + if (savedFitType) { + this.fitType = savedFitType; + $('#fit-select').val(savedFitType); + } + const savedFlipAnimation = localStorage.getItem( + 'enableFlipAnimation', + ); + this.enableFlipAnimation = + savedFlipAnimation === null || savedFlipAnimation === 'true'; - const savedRightToLeft = localStorage.getItem('enableRightToLeft'); - if (savedRightToLeft === null) { - this.enableRightToLeft = false; - } else { - this.enableRightToLeft = (savedRightToLeft === 'true'); - } - }) - .catch(e => { - const errMsg = `Failed to get the page dimensions. ${e}`; - console.error(e); - this.alertClass = 'uk-alert-danger'; - this.msg = errMsg; - }) - }, - /** - * Preload an image, which is expected to be cached - */ - preloadImage(url) { - (new Image()).src = url; - }, - /** - * Handles the `change` event for the page selector - */ - pageChanged() { - const p = parseInt($('#page-select').val()); - this.toPage(p); - }, - /** - * Handles the `change` event for the mode selector - * - * @param {function} nextTick - Alpine $nextTick magic property - */ - modeChanged(nextTick) { - const mode = $('#mode-select').val(); - const curIdx = parseInt($('#page-select').val()); + const savedRightToLeft = localStorage.getItem('enableRightToLeft'); + if (savedRightToLeft === null) { + this.enableRightToLeft = false; + } else { + this.enableRightToLeft = savedRightToLeft === 'true'; + } + }) + .catch((e) => { + const errMsg = `Failed to get the page dimensions. ${e}`; + console.error(e); + this.alertClass = 'uk-alert-danger'; + this.msg = errMsg; + }); + }, + /** + * Preload an image, which is expected to be cached + */ + preloadImage(url) { + new Image().src = url; + }, + /** + * Handles the `change` event for the page selector + */ + pageChanged() { + const p = parseInt($('#page-select').val()); + this.toPage(p); + }, + /** + * Handles the `change` event for the mode selector + * + * @param {function} nextTick - Alpine $nextTick magic property + */ + modeChanged(nextTick) { + const mode = $('#mode-select').val(); + const curIdx = parseInt($('#page-select').val()); - this.updateMode(mode, curIdx, nextTick); - }, - /** - * Handles the window `resize` event - */ - resized() { - if (this.mode === 'continuous') return; + this.updateMode(mode, curIdx, nextTick); + }, + /** + * Handles the window `resize` event + */ + resized() { + if (this.mode === 'continuous') return; - const wideScreen = $(window).width() > $(window).height(); - this.mode = wideScreen ? 'height' : 'width'; - }, - /** - * Handles the window `keydown` event - * - * @param {Event} event - The triggering event - */ - keyHandler(event) { - if (this.mode === 'continuous') return; + const wideScreen = $(window).width() > $(window).height(); + this.mode = wideScreen ? 'height' : 'width'; + }, + /** + * Handles the window `keydown` event + * + * @param {Event} event - The triggering event + */ + keyHandler(event) { + if (this.mode === 'continuous') return; - if (event.key === 'ArrowLeft' || event.key === 'k') - this.flipPage(false ^ this.enableRightToLeft); - if (event.key === 'ArrowRight' || event.key === 'j') - this.flipPage(true ^ this.enableRightToLeft); - }, - /** - * Flips to the next or the previous page - * - * @param {bool} isNext - Whether we are going to the next page - */ - flipPage(isNext) { - const idx = parseInt(this.curItem.id); - const newIdx = idx + (isNext ? 1 : -1); + if (event.key === 'ArrowLeft' || event.key === 'k') + this.flipPage(false ^ this.enableRightToLeft); + if (event.key === 'ArrowRight' || event.key === 'j') + this.flipPage(true ^ this.enableRightToLeft); + }, + /** + * Flips to the next or the previous page + * + * @param {bool} isNext - Whether we are going to the next page + */ + flipPage(isNext) { + const idx = parseInt(this.curItem.id); + const newIdx = idx + (isNext ? 1 : -1); - if (newIdx <= 0) return; - if (newIdx > this.items.length) { - this.showControl(idx); - return; - } + if (newIdx <= 0) return; + if (newIdx > this.items.length) { + this.showControl(idx); + return; + } - if (newIdx + this.preloadLookahead < this.items.length + 1) { - this.preloadImage(this.items[newIdx + this.preloadLookahead - 1].url); - } + if (newIdx + this.preloadLookahead < this.items.length + 1) { + this.preloadImage(this.items[newIdx + this.preloadLookahead - 1].url); + } - this.toPage(newIdx); + this.toPage(newIdx); - if (this.enableFlipAnimation) { - if (isNext ^ this.enableRightToLeft) - this.flipAnimation = 'right'; - else - this.flipAnimation = 'left'; - } + if (this.enableFlipAnimation) { + if (isNext ^ this.enableRightToLeft) this.flipAnimation = 'right'; + else this.flipAnimation = 'left'; + } - setTimeout(() => { - this.flipAnimation = null; - }, 500); + setTimeout(() => { + this.flipAnimation = null; + }, 500); - this.replaceHistory(newIdx); - }, - /** - * Jumps to a specific page - * - * @param {number} idx - One-based index of the page - */ - toPage(idx) { - if (this.mode === 'continuous') { - $(`#${idx}`).get(0).scrollIntoView(true); - } else { - if (idx >= 1 && idx <= this.items.length) { - this.curItem = this.items[idx - 1]; - } - } - this.replaceHistory(idx); - UIkit.modal($('#modal-sections')).hide(); - }, - /** - * Replace the address bar history and save the reading progress if necessary - * - * @param {number} idx - One-based index of the page - */ - replaceHistory(idx) { - const ary = window.location.pathname.split('/'); - ary[ary.length - 1] = idx; - ary.shift(); // remove leading `/` - ary.unshift(window.location.origin); - const url = ary.join('/'); - this.saveProgress(idx); - history.replaceState(null, "", url); - }, - /** - * Updates the backend reading progress if: - * 1) the current page is more than five pages away from the last - * saved page, or - * 2) the average height/width ratio of the pages is over 2, or - * 3) the current page is the first page, or - * 4) the current page is the last page - * - * @param {number} idx - One-based index of the page - * @param {function} cb - Callback - */ - saveProgress(idx, cb) { - idx = parseInt(idx); - if (Math.abs(idx - this.lastSavedPage) >= 5 || - this.longPages || - idx === 1 || idx === this.items.length - ) { - this.lastSavedPage = idx; - console.log('saving progress', idx); + this.replaceHistory(newIdx); + }, + /** + * Jumps to a specific page + * + * @param {number} idx - One-based index of the page + */ + toPage(idx) { + if (this.mode === 'continuous') { + $(`#${idx}`).get(0).scrollIntoView(true); + } else { + if (idx >= 1 && idx <= this.items.length) { + this.curItem = this.items[idx - 1]; + } + } + this.replaceHistory(idx); + UIkit.modal($('#modal-sections')).hide(); + }, + /** + * Replace the address bar history and save the reading progress if necessary + * + * @param {number} idx - One-based index of the page + */ + replaceHistory(idx) { + const ary = window.location.pathname.split('/'); + ary[ary.length - 1] = idx; + ary.shift(); // remove leading `/` + ary.unshift(window.location.origin); + const url = ary.join('/'); + this.saveProgress(idx); + history.replaceState(null, '', url); + }, + /** + * Updates the backend reading progress if: + * 1) the current page is more than five pages away from the last + * saved page, or + * 2) the average height/width ratio of the pages is over 2, or + * 3) the current page is the first page, or + * 4) the current page is the last page + * + * @param {number} idx - One-based index of the page + * @param {function} cb - Callback + */ + saveProgress(idx, cb) { + idx = parseInt(idx); + if ( + Math.abs(idx - this.lastSavedPage) >= 5 || + this.longPages || + idx === 1 || + idx === this.items.length + ) { + this.lastSavedPage = idx; + console.log('saving progress', idx); - const url = `${base_url}api/progress/${tid}/${idx}?${$.param({eid: eid})}`; - $.ajax({ - method: 'PUT', - url: url, - dataType: 'json' - }) - .done(data => { - if (data.error) - alert('danger', data.error); - if (cb) cb(); - }) - .fail((jqXHR, status) => { - alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); - } - }, - /** - * Updates the reader mode - * - * @param {string} mode - Either `continuous` or `paged` - * @param {number} targetPage - The one-based index of the target page - * @param {function} nextTick - Alpine $nextTick magic property - */ - updateMode(mode, targetPage, nextTick) { - localStorage.setItem('mode', mode); + const url = `${base_url}api/progress/${tid}/${idx}?${$.param({ + eid, + })}`; + $.ajax({ + method: 'PUT', + url, + dataType: 'json', + }) + .done((data) => { + if (data.error) alert('danger', data.error); + if (cb) cb(); + }) + .fail((jqXHR, status) => { + alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }); + } + }, + /** + * Updates the reader mode + * + * @param {string} mode - Either `continuous` or `paged` + * @param {number} targetPage - The one-based index of the target page + * @param {function} nextTick - Alpine $nextTick magic property + */ + updateMode(mode, targetPage, nextTick) { + localStorage.setItem('mode', mode); - // The mode to be put into the `mode` prop. It can't be `screen` - let propMode = mode; + // The mode to be put into the `mode` prop. It can't be `screen` + let propMode = mode; - if (mode === 'paged') { - const wideScreen = $(window).width() > $(window).height(); - propMode = wideScreen ? 'height' : 'width'; - } + if (mode === 'paged') { + const wideScreen = $(window).width() > $(window).height(); + propMode = wideScreen ? 'height' : 'width'; + } - this.mode = propMode; + this.mode = propMode; - if (mode === 'continuous') { - nextTick(() => { - this.setupScroller(); - }); - } + if (mode === 'continuous') { + nextTick(() => { + this.setupScroller(); + }); + } - nextTick(() => { - this.toPage(targetPage); - }); - }, - /** - * Handles clicked image - * - * @param {Event} event - The triggering event - */ - clickImage(event) { - const idx = event.currentTarget.id; - this.showControl(idx); - }, - /** - * Shows the control modal - * - * @param {number} idx - selected page index - */ - showControl(idx) { - this.selectedIndex = idx; - UIkit.modal($('#modal-sections')).show(); - }, - /** - * Redirects to a URL - * - * @param {string} url - The target URL - */ - redirect(url) { - window.location.replace(url); - }, - /** - * Set up the scroll handler that calls `replaceHistory` when an image - * enters the view port - */ - setupScroller() { - if (this.mode !== 'continuous') return; - $('img').each((idx, el) => { - $(el).on('inview', (event, inView) => { - if (inView) { - const current = $(event.currentTarget).attr('id'); + nextTick(() => { + this.toPage(targetPage); + }); + }, + /** + * Handles clicked image + * + * @param {Event} event - The triggering event + */ + clickImage(event) { + const idx = event.currentTarget.id; + this.showControl(idx); + }, + /** + * Shows the control modal + * + * @param {number} idx - selected page index + */ + showControl(idx) { + this.selectedIndex = idx; + UIkit.modal($('#modal-sections')).show(); + }, + /** + * Redirects to a URL + * + * @param {string} url - The target URL + */ + redirect(url) { + window.location.replace(url); + }, + /** + * Set up the scroll handler that calls `replaceHistory` when an image + * enters the view port + */ + setupScroller() { + if (this.mode !== 'continuous') return; + $('img').each((idx, el) => { + $(el).on('inview', (event, inView) => { + if (inView) { + const current = $(event.currentTarget).attr('id'); - this.curItem = this.items[current - 1]; - this.replaceHistory(current); - } - }); - }); - }, - /** - * Marks progress as 100% and jumps to the next entry - * - * @param {string} nextUrl - URL of the next entry - */ - nextEntry(nextUrl) { - this.saveProgress(this.items.length, () => { - this.redirect(nextUrl); - }); - }, - /** - * Exits the reader, and sets the reading progress tp 100% - * - * @param {string} exitUrl - The Exit URL - */ - exitReader(exitUrl) { - this.saveProgress(this.items.length, () => { - this.redirect(exitUrl); - }); - }, + this.curItem = this.items[current - 1]; + this.replaceHistory(current); + } + }); + }); + }, + /** + * Marks progress as 100% and jumps to the next entry + * + * @param {string} nextUrl - URL of the next entry + */ + nextEntry(nextUrl) { + this.saveProgress(this.items.length, () => { + this.redirect(nextUrl); + }); + }, + /** + * Exits the reader, and sets the reading progress tp 100% + * + * @param {string} exitUrl - The Exit URL + */ + exitReader(exitUrl) { + this.saveProgress(this.items.length, () => { + this.redirect(exitUrl); + }); + }, - /** - * Handles the `change` event for the entry selector - */ - entryChanged() { - const id = $('#entry-select').val(); - this.redirect(`${base_url}reader/${tid}/${id}`); - }, + /** + * Handles the `change` event for the entry selector + */ + entryChanged() { + const id = $('#entry-select').val(); + this.redirect(`${base_url}reader/${tid}/${id}`); + }, - marginChanged() { - localStorage.setItem('margin', this.margin); - this.toPage(this.selectedIndex); - }, + marginChanged() { + localStorage.setItem('margin', this.margin); + this.toPage(this.selectedIndex); + }, - fitChanged(){ - this.fitType = $('#fit-select').val(); - localStorage.setItem('fitType', this.fitType); - }, + fitChanged() { + this.fitType = $('#fit-select').val(); + localStorage.setItem('fitType', this.fitType); + }, - preloadLookaheadChanged() { - localStorage.setItem('preloadLookahead', this.preloadLookahead); - }, + preloadLookaheadChanged() { + localStorage.setItem('preloadLookahead', this.preloadLookahead); + }, - enableFlipAnimationChanged() { - localStorage.setItem('enableFlipAnimation', this.enableFlipAnimation); - }, + enableFlipAnimationChanged() { + localStorage.setItem('enableFlipAnimation', this.enableFlipAnimation); + }, - enableRightToLeftChanged() { - localStorage.setItem('enableRightToLeft', this.enableRightToLeft); - }, - }; -} + enableRightToLeftChanged() { + localStorage.setItem('enableRightToLeft', this.enableRightToLeft); + }, + }; +}; diff --git a/public/js/search.js b/public/js/search.js index d840353..a738320 100644 --- a/public/js/search.js +++ b/public/js/search.js @@ -1,30 +1,28 @@ -$(function(){ - var filter = []; - var result = []; - $('.uk-card-title').each(function(){ - filter.push($(this).text()); - }); - $('.uk-search-input').keyup(function(){ - var input = $('.uk-search-input').val(); - var regex = new RegExp(input, 'i'); +$(function () { + let filter = []; + let result = []; + $('.uk-card-title').each(function () { + filter.push($(this).text()); + }); + $('.uk-search-input').keyup(function () { + let input = $('.uk-search-input').val(); + let regex = new RegExp(input, 'i'); - if (input === '') { - $('.item').each(function(){ - $(this).removeAttr('hidden'); - }); - } - else { - filter.forEach(function(text, i){ - result[i] = text.match(regex); - }); - $('.item').each(function(i){ - if (result[i]) { - $(this).removeAttr('hidden'); - } - else { - $(this).attr('hidden', ''); - } - }); - } - }); + if (input === '') { + $('.item').each(function () { + $(this).removeAttr('hidden'); + }); + } else { + filter.forEach(function (text, i) { + result[i] = text.match(regex); + }); + $('.item').each(function (i) { + if (result[i]) { + $(this).removeAttr('hidden'); + } else { + $(this).attr('hidden', ''); + } + }); + } + }); }); diff --git a/public/js/sort-items.js b/public/js/sort-items.js index c1eb365..6d193c8 100644 --- a/public/js/sort-items.js +++ b/public/js/sort-items.js @@ -1,15 +1,15 @@ $(() => { - $('#sort-select').change(() => { - const sort = $('#sort-select').find(':selected').attr('id'); - const ary = sort.split('-'); - const by = ary[0]; - const dir = ary[1]; + $('#sort-select').change(() => { + const sort = $('#sort-select').find(':selected').attr('id'); + const ary = sort.split('-'); + const by = ary[0]; + const dir = ary[1]; - const url = `${location.protocol}//${location.host}${location.pathname}`; - const newURL = `${url}?${$.param({ - sort: by, - ascend: dir === 'up' ? 1 : 0 - })}`; - window.location.href = newURL; - }); + const url = `${location.protocol}//${location.host}${location.pathname}`; + const newURL = `${url}?${$.param({ + sort: by, + ascend: dir === 'up' ? 1 : 0, + })}`; + window.location.href = newURL; + }); }); diff --git a/public/js/subscription-manager.js b/public/js/subscription-manager.js index fad4e56..5dcd775 100644 --- a/public/js/subscription-manager.js +++ b/public/js/subscription-manager.js @@ -1,147 +1,143 @@ const component = () => { - return { - subscriptions: [], - plugins: [], - pid: undefined, - subscription: undefined, // selected subscription - loading: false, + return { + subscriptions: [], + plugins: [], + pid: undefined, + subscription: undefined, // selected subscription + loading: false, - init() { - fetch(`${base_url}api/admin/plugin`) - .then((res) => res.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - this.plugins = data.plugins; + init() { + fetch(`${base_url}api/admin/plugin`) + .then((res) => res.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + this.plugins = data.plugins; - const pid = localStorage.getItem("plugin"); - if (pid && this.plugins.map((p) => p.id).includes(pid)) - this.pid = pid; - else if (this.plugins.length > 0) - this.pid = this.plugins[0].id; + const pid = localStorage.getItem('plugin'); + if (pid && this.plugins.map((p) => p.id).includes(pid)) + this.pid = pid; + else if (this.plugins.length > 0) this.pid = this.plugins[0].id; - this.list(pid); - }) - .catch((e) => { - alert( - "danger", - `Failed to list the available plugins. Error: ${e}` - ); - }); - }, - pluginChanged() { - localStorage.setItem("plugin", this.pid); - this.list(this.pid); - }, - list(pid) { - if (!pid) return; - fetch( - `${base_url}api/admin/plugin/subscriptions?${new URLSearchParams( - { - plugin: pid, - } - )}`, - { - method: "GET", - } - ) - .then((response) => response.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - this.subscriptions = data.subscriptions; - }) - .catch((e) => { - alert( - "danger", - `Failed to list subscriptions. Error: ${e}` - ); - }); - }, - renderStrCell(str) { - const maxLength = 40; - if (str.length > maxLength) - return `${str.substring( - 0, - maxLength - )}...
${str}
`; - return `${str}`; - }, - renderDateCell(timestamp) { - return `${moment - .duration(moment.unix(timestamp).diff(moment())) - .humanize(true)}`; - }, - selected(event, modal) { - const id = event.currentTarget.getAttribute("sid"); - this.subscription = this.subscriptions.find((s) => s.id === id); - UIkit.modal(modal).show(); - }, - renderFilterRow(ft) { - const key = ft.key; - let type = ft.type; - switch (type) { - case "number-min": - type = "number (minimum value)"; - break; - case "number-max": - type = "number (maximum value)"; - break; - case "date-min": - type = "minimum date"; - break; - case "date-max": - type = "maximum date"; - break; - } - let value = ft.value; + this.list(pid); + }) + .catch((e) => { + alert('danger', `Failed to list the available plugins. Error: ${e}`); + }); + }, + pluginChanged() { + localStorage.setItem('plugin', this.pid); + this.list(this.pid); + }, + list(pid) { + if (!pid) return; + fetch( + `${base_url}api/admin/plugin/subscriptions?${new URLSearchParams({ + plugin: pid, + })}`, + { + method: 'GET', + }, + ) + .then((response) => response.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + this.subscriptions = data.subscriptions; + }) + .catch((e) => { + alert('danger', `Failed to list subscriptions. Error: ${e}`); + }); + }, + renderStrCell(str) { + const maxLength = 40; + if (str.length > maxLength) + return `${str.substring( + 0, + maxLength, + )}...
${str}
`; + return `${str}`; + }, + renderDateCell(timestamp) { + return `${moment + .duration(moment.unix(timestamp).diff(moment())) + .humanize(true)}`; + }, + selected(event, modal) { + const id = event.currentTarget.getAttribute('sid'); + this.subscription = this.subscriptions.find((s) => s.id === id); + UIkit.modal(modal).show(); + }, + renderFilterRow(ft) { + const key = ft.key; + let type = ft.type; + switch (type) { + case 'number-min': + type = 'number (minimum value)'; + break; + case 'number-max': + type = 'number (maximum value)'; + break; + case 'date-min': + type = 'minimum date'; + break; + case 'date-max': + type = 'maximum date'; + break; + } + let value = ft.value; - if (ft.type.startsWith("number") && isNaN(value)) value = ""; - else if (ft.type.startsWith("date") && value) - value = moment(Number(value)).format("MMM D, YYYY"); + if (ft.type.startsWith('number') && isNaN(value)) value = ''; + else if (ft.type.startsWith('date') && value) + value = moment(Number(value)).format('MMM D, YYYY'); - return `${key}${type}${value}`; - }, - actionHandler(event, type) { - const id = $(event.currentTarget).closest("tr").attr("sid"); - if (type !== 'delete') return this.action(id, type); - UIkit.modal.confirm('Are you sure you want to delete the subscription? This cannot be undone.', { - labels: { - ok: 'Yes, delete it', - cancel: 'Cancel' - } - }).then(() => { - this.action(id, type); - }); - }, - action(id, type) { - if (this.loading) return; - this.loading = true; - fetch( - `${base_url}api/admin/plugin/subscriptions${type === 'update' ? '/update' : ''}?${new URLSearchParams( - { - plugin: this.pid, - subscription: id, - } - )}`, - { - method: type === 'delete' ? "DELETE" : 'POST' - } - ) - .then((response) => response.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - if (type === 'update') - alert("success", `Checking updates for subscription ${id}. Check the log for the progress or come back to this page later.`); - }) - .catch((e) => { - alert( - "danger", - `Failed to ${type} subscription. Error: ${e}` - ); - }) - .finally(() => { - this.loading = false; - this.list(this.pid); - }); - }, - }; + return `${key}${type}${value}`; + }, + actionHandler(event, type) { + const id = $(event.currentTarget).closest('tr').attr('sid'); + if (type !== 'delete') return this.action(id, type); + UIkit.modal + .confirm( + 'Are you sure you want to delete the subscription? This cannot be undone.', + { + labels: { + ok: 'Yes, delete it', + cancel: 'Cancel', + }, + }, + ) + .then(() => { + this.action(id, type); + }); + }, + action(id, type) { + if (this.loading) return; + this.loading = true; + fetch( + `${base_url}api/admin/plugin/subscriptions${ + type === 'update' ? '/update' : '' + }?${new URLSearchParams({ + plugin: this.pid, + subscription: id, + })}`, + { + method: type === 'delete' ? 'DELETE' : 'POST', + }, + ) + .then((response) => response.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + if (type === 'update') + alert( + 'success', + `Checking updates for subscription ${id}. Check the log for the progress or come back to this page later.`, + ); + }) + .catch((e) => { + alert('danger', `Failed to ${type} subscription. Error: ${e}`); + }) + .finally(() => { + this.loading = false; + this.list(this.pid); + }); + }, + }; }; diff --git a/public/js/subscription.js b/public/js/subscription.js index ed2cb17..967d231 100644 --- a/public/js/subscription.js +++ b/public/js/subscription.js @@ -1,82 +1,112 @@ const component = () => { - return { - available: undefined, - subscriptions: [], + return { + available: undefined, + subscriptions: [], - init() { - $.getJSON(`${base_url}api/admin/mangadex/expires`) - .done((data) => { - if (data.error) { - alert('danger', 'Failed to check MangaDex integration status. Error: ' + data.error); - return; - } - this.available = Boolean(data.expires && data.expires > Math.floor(Date.now() / 1000)); + init() { + $.getJSON(`${base_url}api/admin/mangadex/expires`) + .done((data) => { + if (data.error) { + alert( + 'danger', + 'Failed to check MangaDex integration status. Error: ' + + data.error, + ); + return; + } + this.available = Boolean( + data.expires && data.expires > Math.floor(Date.now() / 1000), + ); - if (this.available) this.getSubscriptions(); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }) - }, + if (this.available) this.getSubscriptions(); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); + }, - getSubscriptions() { - $.getJSON(`${base_url}api/admin/mangadex/subscriptions`) - .done(data => { - if (data.error) { - alert('danger', 'Failed to get subscriptions. Error: ' + data.error); - return; - } - this.subscriptions = data.subscriptions; - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to get subscriptions. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }) - }, + getSubscriptions() { + $.getJSON(`${base_url}api/admin/mangadex/subscriptions`) + .done((data) => { + if (data.error) { + alert( + 'danger', + 'Failed to get subscriptions. Error: ' + data.error, + ); + return; + } + this.subscriptions = data.subscriptions; + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to get subscriptions. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); + }, - rm(event) { - const id = event.currentTarget.parentNode.getAttribute('data-id'); - $.ajax({ - type: 'DELETE', - url: `${base_url}api/admin/mangadex/subscriptions/${id}`, - contentType: 'application/json' - }) - .done(data => { - if (data.error) { - alert('danger', `Failed to delete subscription. Error: ${data.error}`); - } - this.getSubscriptions(); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to delete subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); - }, + rm(event) { + const id = event.currentTarget.parentNode.getAttribute('data-id'); + $.ajax({ + type: 'DELETE', + url: `${base_url}api/admin/mangadex/subscriptions/${id}`, + contentType: 'application/json', + }) + .done((data) => { + if (data.error) { + alert( + 'danger', + `Failed to delete subscription. Error: ${data.error}`, + ); + } + this.getSubscriptions(); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to delete subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); + }, - check(event) { - const id = event.currentTarget.parentNode.getAttribute('data-id'); - $.ajax({ - type: 'POST', - url: `${base_url}api/admin/mangadex/subscriptions/check/${id}`, - contentType: 'application/json' - }) - .done(data => { - if (data.error) { - alert('danger', `Failed to check subscription. Error: ${data.error}`); - return; - } - alert('success', 'Mango is now checking the subscription for updates. This might take a while, but you can safely leave the page.'); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to check subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); - }, + check(event) { + const id = event.currentTarget.parentNode.getAttribute('data-id'); + $.ajax({ + type: 'POST', + url: `${base_url}api/admin/mangadex/subscriptions/check/${id}`, + contentType: 'application/json', + }) + .done((data) => { + if (data.error) { + alert( + 'danger', + `Failed to check subscription. Error: ${data.error}`, + ); + return; + } + alert( + 'success', + 'Mango is now checking the subscription for updates. This might take a while, but you can safely leave the page.', + ); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to check subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); + }, - formatRange(min, max) { - if (!isNaN(min) && isNaN(max)) return `≥ ${min}`; - if (isNaN(min) && !isNaN(max)) return `≤ ${max}`; - if (isNaN(min) && isNaN(max)) return 'All'; + formatRange(min, max) { + if (!isNaN(min) && isNaN(max)) return `≥ ${min}`; + if (isNaN(min) && !isNaN(max)) return `≤ ${max}`; + if (isNaN(min) && isNaN(max)) return 'All'; - if (min === max) return `= ${min}`; - return `${min} - ${max}`; - } - }; + if (min === max) return `= ${min}`; + return `${min} - ${max}`; + }, + }; }; diff --git a/public/js/title.js b/public/js/title.js index 5d0e49f..15c4706 100644 --- a/public/js/title.js +++ b/public/js/title.js @@ -1,392 +1,421 @@ $(() => { - setupAcard(); + setupAcard(); }); const setupAcard = () => { - $('.acard.is_entry').click((e) => { - if ($(e.target).hasClass('no-modal')) return; - const card = $(e.target).closest('.acard'); - - showModal( - $(card).attr('data-encoded-path'), - parseInt($(card).attr('data-pages')), - parseFloat($(card).attr('data-progress')), - $(card).attr('data-encoded-book-title'), - $(card).attr('data-encoded-title'), - $(card).attr('data-book-id'), - $(card).attr('data-id') - ); - }); + $('.acard.is_entry').click((e) => { + if ($(e.target).hasClass('no-modal')) return; + const card = $(e.target).closest('.acard'); + + showModal( + $(card).attr('data-encoded-path'), + parseInt($(card).attr('data-pages')), + parseFloat($(card).attr('data-progress')), + $(card).attr('data-encoded-book-title'), + $(card).attr('data-encoded-title'), + $(card).attr('data-book-id'), + $(card).attr('data-id'), + ); + }); }; -function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) { - const zipPath = decodeURIComponent(encodedPath); - const title = decodeURIComponent(encodedeTitle); - const entry = decodeURIComponent(encodedEntryTitle); - $('#modal button, #modal a').each(function() { - $(this).removeAttr('hidden'); - }); - if (percentage === 0) { - $('#continue-btn').attr('hidden', ''); - $('#unread-btn').attr('hidden', ''); - } else if (percentage === 100) { - $('#read-btn').attr('hidden', ''); - $('#continue-btn').attr('hidden', ''); - } else { - $('#continue-btn').text('Continue from ' + percentage + '%'); - } - - $('#modal-entry-title').find('span').text(entry); - $('#modal-entry-title').next().attr('data-id', titleID); - $('#modal-entry-title').next().attr('data-entry-id', entryID); - $('#modal-entry-title').next().find('.title-rename-field').val(entry); - $('#path-text').text(zipPath); - $('#pages-text').text(pages + ' pages'); - - $('#beginning-btn').attr('href', `${base_url}reader/${titleID}/${entryID}/1`); - $('#continue-btn').attr('href', `${base_url}reader/${titleID}/${entryID}`); - - $('#read-btn').click(function() { - updateProgress(titleID, entryID, pages); - }); - $('#unread-btn').click(function() { - updateProgress(titleID, entryID, 0); - }); - - $('#modal-edit-btn').attr('onclick', `edit("${entryID}")`); - - $('#modal-download-btn').attr('href', `${base_url}api/download/${titleID}/${entryID}`); - - UIkit.modal($('#modal')).show(); +function showModal( + encodedPath, + pages, + percentage, + encodedeTitle, + encodedEntryTitle, + titleID, + entryID, +) { + const zipPath = decodeURIComponent(encodedPath); + const title = decodeURIComponent(encodedeTitle); + const entry = decodeURIComponent(encodedEntryTitle); + $('#modal button, #modal a').each(function () { + $(this).removeAttr('hidden'); + }); + if (percentage === 0) { + $('#continue-btn').attr('hidden', ''); + $('#unread-btn').attr('hidden', ''); + } else if (percentage === 100) { + $('#read-btn').attr('hidden', ''); + $('#continue-btn').attr('hidden', ''); + } else { + $('#continue-btn').text('Continue from ' + percentage + '%'); + } + + $('#modal-entry-title').find('span').text(entry); + $('#modal-entry-title').next().attr('data-id', titleID); + $('#modal-entry-title').next().attr('data-entry-id', entryID); + $('#modal-entry-title').next().find('.title-rename-field').val(entry); + $('#path-text').text(zipPath); + $('#pages-text').text(pages + ' pages'); + + $('#beginning-btn').attr('href', `${base_url}reader/${titleID}/${entryID}/1`); + $('#continue-btn').attr('href', `${base_url}reader/${titleID}/${entryID}`); + + $('#read-btn').click(function () { + updateProgress(titleID, entryID, pages); + }); + $('#unread-btn').click(function () { + updateProgress(titleID, entryID, 0); + }); + + $('#modal-edit-btn').attr('onclick', `edit("${entryID}")`); + + $('#modal-download-btn').attr( + 'href', + `${base_url}api/download/${titleID}/${entryID}`, + ); + + UIkit.modal($('#modal')).show(); } UIkit.util.on(document, 'hidden', '#modal', () => { - $('#read-btn').off('click'); - $('#unread-btn').off('click'); + $('#read-btn').off('click'); + $('#unread-btn').off('click'); }); const updateProgress = (tid, eid, page) => { - let url = `${base_url}api/progress/${tid}/${page}` - const query = $.param({ - eid: eid - }); - if (eid) - url += `?${query}`; - - $.ajax({ - method: 'PUT', - url: url, - dataType: 'json' - }) - .done(data => { - if (data.success) { - location.reload(); - } else { - error = data.error; - alert('danger', error); - } - }) - .fail((jqXHR, status) => { - alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); + let url = `${base_url}api/progress/${tid}/${page}`; + const query = $.param({ + eid, + }); + if (eid) url += `?${query}`; + + $.ajax({ + method: 'PUT', + url, + dataType: 'json', + }) + .done((data) => { + if (data.success) { + location.reload(); + } else { + error = data.error; + alert('danger', error); + } + }) + .fail((jqXHR, status) => { + alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }); }; const renameSubmit = (name, eid) => { - const upload = $('.upload-field'); - const titleId = upload.attr('data-title-id'); - - if (name.length === 0) { - alert('danger', 'The display name should not be empty'); - return; - } - - const query = $.param({ - eid: eid - }); - let url = `${base_url}api/admin/display_name/${titleId}/${name}`; - if (eid) - url += `?${query}`; - - $.ajax({ - type: 'PUT', - url: url, - contentType: "application/json", - dataType: 'json' - }) - .done(data => { - if (data.error) { - alert('danger', `Failed to update display name. Error: ${data.error}`); - return; - } - location.reload(); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to update display name. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); + const upload = $('.upload-field'); + const titleId = upload.attr('data-title-id'); + + if (name.length === 0) { + alert('danger', 'The display name should not be empty'); + return; + } + + const query = $.param({ + eid, + }); + let url = `${base_url}api/admin/display_name/${titleId}/${name}`; + if (eid) url += `?${query}`; + + $.ajax({ + type: 'PUT', + url, + contentType: 'application/json', + dataType: 'json', + }) + .done((data) => { + if (data.error) { + alert('danger', `Failed to update display name. Error: ${data.error}`); + return; + } + location.reload(); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to update display name. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); }; const renameSortNameSubmit = (name, eid) => { - const upload = $('.upload-field'); - const titleId = upload.attr('data-title-id'); - - const params = {}; - if (eid) params.eid = eid; - if (name) params.name = name; - const query = $.param(params); - let url = `${base_url}api/admin/sort_title/${titleId}?${query}`; - - $.ajax({ - type: 'PUT', - url, - contentType: 'application/json', - dataType: 'json' - }) - .done(data => { - if (data.error) { - alert('danger', `Failed to update sort title. Error: ${data.error}`); - return; - } - location.reload(); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to update sort title. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); + const upload = $('.upload-field'); + const titleId = upload.attr('data-title-id'); + + const params = {}; + if (eid) params.eid = eid; + if (name) params.name = name; + const query = $.param(params); + let url = `${base_url}api/admin/sort_title/${titleId}?${query}`; + + $.ajax({ + type: 'PUT', + url, + contentType: 'application/json', + dataType: 'json', + }) + .done((data) => { + if (data.error) { + alert('danger', `Failed to update sort title. Error: ${data.error}`); + return; + } + location.reload(); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to update sort title. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); }; const edit = (eid) => { - const cover = $('#edit-modal #cover'); - let url = cover.attr('data-title-cover'); - let displayName = $('h2.uk-title > span').text(); - let fileTitle = $('h2.uk-title').attr('data-file-title'); - let sortTitle = $('h2.uk-title').attr('data-sort-title'); - - if (eid) { - const item = $(`#${eid}`); - url = item.find('img').attr('data-src'); - displayName = item.find('.uk-card-title').attr('data-title'); - fileTitle = item.find('.uk-card-title').attr('data-file-title'); - sortTitle = item.find('.uk-card-title').attr('data-sort-title'); - $('#title-progress-control').attr('hidden', ''); - } else { - $('#title-progress-control').removeAttr('hidden'); - } - - cover.attr('data-src', url); - - const displayNameField = $('#display-name-field'); - displayNameField.attr('value', displayName); - displayNameField.attr('placeholder', fileTitle); - displayNameField.keyup(event => { - if (event.keyCode === 13) { - renameSubmit(displayNameField.val() || fileTitle, eid); - } - }); - displayNameField.siblings('a.uk-form-icon').click(() => { - renameSubmit(displayNameField.val() || fileTitle, eid); - }); - - const sortTitleField = $('#sort-title-field'); - sortTitleField.val(sortTitle); - sortTitleField.attr('placeholder', fileTitle); - sortTitleField.keyup(event => { - if (event.keyCode === 13) { - renameSortNameSubmit(sortTitleField.val(), eid); - } - }); - sortTitleField.siblings('a.uk-form-icon').click(() => { - renameSortNameSubmit(sortTitleField.val(), eid); - }); - - setupUpload(eid); - - UIkit.modal($('#edit-modal')).show(); + const cover = $('#edit-modal #cover'); + let url = cover.attr('data-title-cover'); + let displayName = $('h2.uk-title > span').text(); + let fileTitle = $('h2.uk-title').attr('data-file-title'); + let sortTitle = $('h2.uk-title').attr('data-sort-title'); + + if (eid) { + const item = $(`#${eid}`); + url = item.find('img').attr('data-src'); + displayName = item.find('.uk-card-title').attr('data-title'); + fileTitle = item.find('.uk-card-title').attr('data-file-title'); + sortTitle = item.find('.uk-card-title').attr('data-sort-title'); + $('#title-progress-control').attr('hidden', ''); + } else { + $('#title-progress-control').removeAttr('hidden'); + } + + cover.attr('data-src', url); + + const displayNameField = $('#display-name-field'); + displayNameField.attr('value', displayName); + displayNameField.attr('placeholder', fileTitle); + displayNameField.keyup((event) => { + if (event.keyCode === 13) { + renameSubmit(displayNameField.val() || fileTitle, eid); + } + }); + displayNameField.siblings('a.uk-form-icon').click(() => { + renameSubmit(displayNameField.val() || fileTitle, eid); + }); + + const sortTitleField = $('#sort-title-field'); + sortTitleField.val(sortTitle); + sortTitleField.attr('placeholder', fileTitle); + sortTitleField.keyup((event) => { + if (event.keyCode === 13) { + renameSortNameSubmit(sortTitleField.val(), eid); + } + }); + sortTitleField.siblings('a.uk-form-icon').click(() => { + renameSortNameSubmit(sortTitleField.val(), eid); + }); + + setupUpload(eid); + + UIkit.modal($('#edit-modal')).show(); }; UIkit.util.on(document, 'hidden', '#edit-modal', () => { - const displayNameField = $('#display-name-field'); - displayNameField.off('keyup'); - displayNameField.off('click'); + const displayNameField = $('#display-name-field'); + displayNameField.off('keyup'); + displayNameField.off('click'); - const sortTitleField = $('#sort-title-field'); - sortTitleField.off('keyup'); - sortTitleField.off('click'); + const sortTitleField = $('#sort-title-field'); + sortTitleField.off('keyup'); + sortTitleField.off('click'); }); const setupUpload = (eid) => { - const upload = $('.upload-field'); - const bar = $('#upload-progress').get(0); - const titleId = upload.attr('data-title-id'); - const queryObj = { - tid: titleId - }; - if (eid) - queryObj['eid'] = eid; - const query = $.param(queryObj); - const url = `${base_url}api/admin/upload/cover?${query}`; - UIkit.upload('.upload-field', { - url: url, - name: 'file', - error: (e) => { - alert('danger', `Failed to upload cover image: ${e.toString()}`); - }, - loadStart: (e) => { - $(bar).removeAttr('hidden'); - bar.max = e.total; - bar.value = e.loaded; - }, - progress: (e) => { - bar.max = e.total; - bar.value = e.loaded; - }, - loadEnd: (e) => { - bar.max = e.total; - bar.value = e.loaded; - }, - completeAll: () => { - $(bar).attr('hidden', ''); - location.reload(); - } - }); + const upload = $('.upload-field'); + const bar = $('#upload-progress').get(0); + const titleId = upload.attr('data-title-id'); + const queryObj = { + tid: titleId, + }; + if (eid) queryObj['eid'] = eid; + const query = $.param(queryObj); + const url = `${base_url}api/admin/upload/cover?${query}`; + UIkit.upload('.upload-field', { + url, + name: 'file', + error: (e) => { + alert('danger', `Failed to upload cover image: ${e.toString()}`); + }, + loadStart: (e) => { + $(bar).removeAttr('hidden'); + bar.max = e.total; + bar.value = e.loaded; + }, + progress: (e) => { + bar.max = e.total; + bar.value = e.loaded; + }, + loadEnd: (e) => { + bar.max = e.total; + bar.value = e.loaded; + }, + completeAll: () => { + $(bar).attr('hidden', ''); + location.reload(); + }, + }); }; const deselectAll = () => { - $('.item .uk-card').each((i, e) => { - const data = e.__x.$data; - data['selected'] = false; - }); - $('#select-bar')[0].__x.$data['count'] = 0; + $('.item .uk-card').each((i, e) => { + const data = e.__x.$data; + data['selected'] = false; + }); + $('#select-bar')[0].__x.$data['count'] = 0; }; const selectAll = () => { - let count = 0; - $('.item .uk-card').each((i, e) => { - const data = e.__x.$data; - if (!data['disabled']) { - data['selected'] = true; - count++; - } - }); - $('#select-bar')[0].__x.$data['count'] = count; + let count = 0; + $('.item .uk-card').each((i, e) => { + const data = e.__x.$data; + if (!data['disabled']) { + data['selected'] = true; + count++; + } + }); + $('#select-bar')[0].__x.$data['count'] = count; }; const selectedIDs = () => { - const ary = []; - $('.item .uk-card').each((i, e) => { - const data = e.__x.$data; - if (!data['disabled'] && data['selected']) { - const item = $(e).closest('.item'); - ary.push($(item).attr('id')); - } - }); - return ary; + const ary = []; + $('.item .uk-card').each((i, e) => { + const data = e.__x.$data; + if (!data['disabled'] && data['selected']) { + const item = $(e).closest('.item'); + ary.push($(item).attr('id')); + } + }); + return ary; }; const bulkProgress = (action, el) => { - const tid = $(el).attr('data-id'); - const ids = selectedIDs(); - const url = `${base_url}api/bulk_progress/${action}/${tid}`; - $.ajax({ - type: 'PUT', - url: url, - contentType: "application/json", - dataType: 'json', - data: JSON.stringify({ - ids: ids - }) - }) - .done(data => { - if (data.error) { - alert('danger', `Failed to mark entries as ${action}. Error: ${data.error}`); - return; - } - location.reload(); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }) - .always(() => { - deselectAll(); - }); + const tid = $(el).attr('data-id'); + const ids = selectedIDs(); + const url = `${base_url}api/bulk_progress/${action}/${tid}`; + $.ajax({ + type: 'PUT', + url, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify({ + ids, + }), + }) + .done((data) => { + if (data.error) { + alert( + 'danger', + `Failed to mark entries as ${action}. Error: ${data.error}`, + ); + return; + } + location.reload(); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }) + .always(() => { + deselectAll(); + }); }; const tagsComponent = () => { - return { - isAdmin: false, - tags: [], - tid: $('.upload-field').attr('data-title-id'), - loading: true, - - load(admin) { - this.isAdmin = admin; - - $('.tag-select').select2({ - tags: true, - placeholder: this.isAdmin ? 'Tag the title' : 'No tags found', - disabled: !this.isAdmin, - templateSelection(state) { - const a = document.createElement('a'); - a.setAttribute('href', `${base_url}tags/${encodeURIComponent(state.text)}`); - a.setAttribute('class', 'uk-link-reset'); - a.onclick = event => { - event.stopPropagation(); - }; - a.innerText = state.text; - return a; - } - }); - - this.request(`${base_url}api/tags`, 'GET', (data) => { - const allTags = data.tags; - const url = `${base_url}api/tags/${this.tid}`; - this.request(url, 'GET', data => { - this.tags = data.tags; - allTags.forEach(t => { - const op = new Option(t, t, false, this.tags.indexOf(t) >= 0); - $('.tag-select').append(op); - }); - $('.tag-select').on('select2:select', e => { - this.onAdd(e); - }); - $('.tag-select').on('select2:unselect', e => { - this.onDelete(e); - }); - $('.tag-select').on('change', () => { - this.onChange(); - }); - $('.tag-select').trigger('change'); - this.loading = false; - }); - }); - }, - onChange() { - this.tags = $('.tag-select').select2('data').map(o => o.text); - }, - onAdd(event) { - const tag = event.params.data.text; - const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; - this.request(url, 'PUT'); - }, - onDelete(event) { - const tag = event.params.data.text; - const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; - this.request(url, 'DELETE'); - }, - request(url, method, cb) { - $.ajax({ - url: url, - method: method, - dataType: 'json' - }) - .done(data => { - if (data.success) { - if (cb) cb(data); - } else { - alert('danger', data.error); - } - }) - .fail((jqXHR, status) => { - alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); - } - }; + return { + isAdmin: false, + tags: [], + tid: $('.upload-field').attr('data-title-id'), + loading: true, + + load(admin) { + this.isAdmin = admin; + + $('.tag-select').select2({ + tags: true, + placeholder: this.isAdmin ? 'Tag the title' : 'No tags found', + disabled: !this.isAdmin, + templateSelection(state) { + const a = document.createElement('a'); + a.setAttribute( + 'href', + `${base_url}tags/${encodeURIComponent(state.text)}`, + ); + a.setAttribute('class', 'uk-link-reset'); + a.onclick = (event) => { + event.stopPropagation(); + }; + a.innerText = state.text; + return a; + }, + }); + + this.request(`${base_url}api/tags`, 'GET', (data) => { + const allTags = data.tags; + const url = `${base_url}api/tags/${this.tid}`; + this.request(url, 'GET', (data) => { + this.tags = data.tags; + allTags.forEach((t) => { + const op = new Option(t, t, false, this.tags.indexOf(t) >= 0); + $('.tag-select').append(op); + }); + $('.tag-select').on('select2:select', (e) => { + this.onAdd(e); + }); + $('.tag-select').on('select2:unselect', (e) => { + this.onDelete(e); + }); + $('.tag-select').on('change', () => { + this.onChange(); + }); + $('.tag-select').trigger('change'); + this.loading = false; + }); + }); + }, + onChange() { + this.tags = $('.tag-select') + .select2('data') + .map((o) => o.text); + }, + onAdd(event) { + const tag = event.params.data.text; + const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent( + tag, + )}`; + this.request(url, 'PUT'); + }, + onDelete(event) { + const tag = event.params.data.text; + const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent( + tag, + )}`; + this.request(url, 'DELETE'); + }, + request(url, method, cb) { + $.ajax({ + url, + method, + dataType: 'json', + }) + .done((data) => { + if (data.success) { + if (cb) cb(data); + } else { + alert('danger', data.error); + } + }) + .fail((jqXHR, status) => { + alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }); + }, + }; }; diff --git a/public/js/user-edit.js b/public/js/user-edit.js index 0af46b9..1080fd7 100644 --- a/public/js/user-edit.js +++ b/public/js/user-edit.js @@ -1,6 +1,6 @@ $(() => { - var target = base_url + 'admin/user/edit'; - if (username) target += username; - $('form').attr('action', target); - if (error) alert('danger', error); + let target = base_url + 'admin/user/edit'; + if (username) target += username; + $('form').attr('action', target); + if (error) alert('danger', error); }); diff --git a/public/js/user.js b/public/js/user.js index b1b910f..2ee4c16 100644 --- a/public/js/user.js +++ b/public/js/user.js @@ -1,16 +1,17 @@ const remove = (username) => { - $.ajax({ - url: `${base_url}api/admin/user/delete/${username}`, - type: 'DELETE', - dataType: 'json' - }) - .done(data => { - if (data.success) - location.reload(); - else - alert('danger', data.error); - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to delete the user. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }); + $.ajax({ + url: `${base_url}api/admin/user/delete/${username}`, + type: 'DELETE', + dataType: 'json', + }) + .done((data) => { + if (data.success) location.reload(); + else alert('danger', data.error); + }) + .fail((jqXHR, status) => { + alert( + 'danger', + `Failed to delete the user. Error: [${jqXHR.status}] ${jqXHR.statusText}`, + ); + }); }; From 7ef2e4d162a95cedc69564360c088d7a37cf8ab3 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 19 Aug 2022 12:06:40 +0000 Subject: [PATCH 4/9] Add eslint to make check --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 6c5eebf..4caa105 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ test: check: crystal tool format --check ./bin/ameba + yarn lint arm32v7: crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='arm-linux-gnueabihf' -o mango-arm32v7 From 5bdeca94fefdc6569d9aeeac4af1980f92a99b9a Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 19 Aug 2022 12:12:45 +0000 Subject: [PATCH 5/9] Merge dev --- public/js/subscription-manager.js | 150 ++++++++++++++---------------- 1 file changed, 71 insertions(+), 79 deletions(-) diff --git a/public/js/subscription-manager.js b/public/js/subscription-manager.js index 089d68c..e54869d 100644 --- a/public/js/subscription-manager.js +++ b/public/js/subscription-manager.js @@ -13,86 +13,78 @@ const component = () => { if (!data.success) throw new Error(data.error); this.plugins = data.plugins; - let pid = localStorage.getItem("plugin"); - if (!pid || !this.plugins.find((p) => p.id === pid)) { - pid = this.plugins[0].id; - } + let pid = localStorage.getItem('plugin'); + if (!pid || !this.plugins.find((p) => p.id === pid)) { + pid = this.plugins[0].id; + } - this.pid = pid; - this.list(pid); - }) - .catch((e) => { - alert( - "danger", - `Failed to list the available plugins. Error: ${e}` - ); - }); - }, - pluginChanged() { - localStorage.setItem("plugin", this.pid); - this.list(this.pid); - }, - list(pid) { - if (!pid) return; - fetch( - `${base_url}api/admin/plugin/subscriptions?${new URLSearchParams( - { - plugin: pid, - } - )}`, - { - method: "GET", - } - ) - .then((response) => response.json()) - .then((data) => { - if (!data.success) throw new Error(data.error); - this.subscriptions = data.subscriptions; - }) - .catch((e) => { - alert( - "danger", - `Failed to list subscriptions. Error: ${e}` - ); - }); - }, - renderStrCell(str) { - const maxLength = 40; - if (str.length > maxLength) - return `${str.substring( - 0, - maxLength - )}...
${str}
`; - return `${str}`; - }, - renderDateCell(timestamp) { - return `${moment - .duration(moment.unix(timestamp).diff(moment())) - .humanize(true)}`; - }, - selected(event, modal) { - const id = event.currentTarget.getAttribute("sid"); - this.subscription = this.subscriptions.find((s) => s.id === id); - UIkit.modal(modal).show(); - }, - renderFilterRow(ft) { - const key = ft.key; - let type = ft.type; - switch (type) { - case "number-min": - type = "number (minimum value)"; - break; - case "number-max": - type = "number (maximum value)"; - break; - case "date-min": - type = "minimum date"; - break; - case "date-max": - type = "maximum date"; - break; - } - let value = ft.value; + this.pid = pid; + this.list(pid); + }) + .catch((e) => { + alert('danger', `Failed to list the available plugins. Error: ${e}`); + }); + }, + pluginChanged() { + localStorage.setItem('plugin', this.pid); + this.list(this.pid); + }, + list(pid) { + if (!pid) return; + fetch( + `${base_url}api/admin/plugin/subscriptions?${new URLSearchParams({ + plugin: pid, + })}`, + { + method: 'GET', + }, + ) + .then((response) => response.json()) + .then((data) => { + if (!data.success) throw new Error(data.error); + this.subscriptions = data.subscriptions; + }) + .catch((e) => { + alert('danger', `Failed to list subscriptions. Error: ${e}`); + }); + }, + renderStrCell(str) { + const maxLength = 40; + if (str.length > maxLength) + return `${str.substring( + 0, + maxLength, + )}...
${str}
`; + return `${str}`; + }, + renderDateCell(timestamp) { + return `${moment + .duration(moment.unix(timestamp).diff(moment())) + .humanize(true)}`; + }, + selected(event, modal) { + const id = event.currentTarget.getAttribute('sid'); + this.subscription = this.subscriptions.find((s) => s.id === id); + UIkit.modal(modal).show(); + }, + renderFilterRow(ft) { + const key = ft.key; + let type = ft.type; + switch (type) { + case 'number-min': + type = 'number (minimum value)'; + break; + case 'number-max': + type = 'number (maximum value)'; + break; + case 'date-min': + type = 'minimum date'; + break; + case 'date-max': + type = 'maximum date'; + break; + } + let value = ft.value; if (ft.type.startsWith('number') && isNaN(value)) value = ''; else if (ft.type.startsWith('date') && value) From ce559984e6b6f37c3720b8af9d760dd28cb59c09 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 5 Sep 2022 03:17:36 +0000 Subject: [PATCH 6/9] Fix uikit version to ~3.14.0 (fixes #334) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63caa83..5fcc154 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,6 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^5.14.0", - "uikit": "^3.5.4" + "uikit": "~3.14.0" } } From 61d6c2e1d9836c97ac13240bcd1c52b7d093fbbc Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 5 Sep 2022 03:18:44 +0000 Subject: [PATCH 7/9] Fix incorrect default DB path --- src/config.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.cr b/src/config.cr index 2384431..aefea40 100644 --- a/src/config.cr +++ b/src/config.cr @@ -8,7 +8,7 @@ class Config "session_secret" => "mango-session-secret", "library_path" => "~/mango/library", "library_cache_path" => "~/mango/library.yml.gz", - "db_path" => "~/mango.db", + "db_path" => "~/mango/mango.db", "queue_db_path" => "~/mango/queue.db", "scan_interval_minutes" => 5, "thumbnail_generation_interval_hours" => 24, From 6f39c2a74ccf72b9439d44e0a2d1478ec948e7c6 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 9 Sep 2022 13:47:30 +0000 Subject: [PATCH 8/9] Restrict table selection to id seletable fixes #335 --- public/js/plugin-download.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/js/plugin-download.js b/public/js/plugin-download.js index 18a5be1..ebdb680 100644 --- a/public/js/plugin-download.js +++ b/public/js/plugin-download.js @@ -146,17 +146,17 @@ const component = () => { } }, selectAll() { - $('tbody > tr').each((i, e) => { + $('tbody#selectable > tr').each((i, e) => { $(e).addClass('ui-selected'); }); }, clearSelection() { - $('tbody > tr').each((i, e) => { + $('tbody#selectable > tr').each((i, e) => { $(e).removeClass('ui-selected'); }); }, download() { - const selected = $('tbody > tr.ui-selected').get(); + const selected = $('tbody#selectable > tr.ui-selected').get(); if (selected.length === 0) return; UIkit.modal From 098d597fadf718c77a220710eb0f6a4f5a3a6ca2 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 9 Sep 2022 13:56:15 +0000 Subject: [PATCH 9/9] Bump version to 0.27.1 --- README.md | 2 +- shard.yml | 2 +- src/mango.cr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b8f7e41..e93b819 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r ### CLI ``` - Mango - Manga Server and Web Reader. Version 0.27.0 + Mango - Manga Server and Web Reader. Version 0.27.1 Usage: diff --git a/shard.yml b/shard.yml index 9eb4d01..fa17144 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: mango -version: 0.27.0 +version: 0.27.1 authors: - Alex Ling diff --git a/src/mango.cr b/src/mango.cr index ed0af49..4aeb179 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -7,7 +7,7 @@ require "option_parser" require "clim" require "tallboy" -MANGO_VERSION = "0.27.0" +MANGO_VERSION = "0.27.1" # From http://www.network-science.de/ascii/ BANNER = %{