Compare commits

..

13 Commits

Author SHA1 Message Date
Alex Ling 098d597fad Bump version to 0.27.1 2022-09-09 13:56:15 +00:00
Alex Ling 6f39c2a74c Restrict table selection to id seletable
fixes #335
2022-09-09 13:47:30 +00:00
Alex Ling 61d6c2e1d9 Fix incorrect default DB path 2022-09-05 03:18:44 +00:00
Alex Ling ce559984e6 Fix uikit version to ~3.14.0 (fixes #334) 2022-09-05 03:17:36 +00:00
Alex Ling 76b4666708 Merge pull request #331 from getmango/feature/eslint
Add ESLint
2022-08-19 20:31:58 +08:00
Alex Ling 5bdeca94fe Merge dev 2022-08-19 12:12:45 +00:00
Alex Ling f8c569f204 Merge branch 'dev' into feature/eslint 2022-08-19 12:11:00 +00:00
Alex Ling 7ef2e4d162 Add eslint to make check 2022-08-19 12:06:40 +00:00
Alex Ling 28c098a56e Add eslint and style fix 2022-08-19 12:05:38 +00:00
Alex Ling 2597b4ce60 Merge pull request #330 from getmango/fix/subscription-manager-single-plugin
Correctly load subscriptions when there's only one plugin
2022-08-19 19:32:49 +08:00
Alex Ling cd3ee0728c Handle the case where only one plugin is installed
fixes #329
2022-08-19 11:07:45 +00:00
Alex Ling e4af194d0c Merge pull request #328 from dudeitssm/patch
Fix typo: "Pleae" should be "Please"
2022-08-10 20:54:11 +08:00
dudeitssm 586ebf8dc8 Fix typo: "Pleae" should be "Please" 2022-08-01 00:06:31 +00:00
25 changed files with 1803 additions and 1686 deletions
+11
View File
@@ -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',
},
};
+6
View File
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}
+1
View File
@@ -29,6 +29,7 @@ test:
check: check:
crystal tool format --check crystal tool format --check
./bin/ameba ./bin/ameba
yarn lint
arm32v7: arm32v7:
crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='arm-linux-gnueabihf' -o mango-arm32v7 crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='arm-linux-gnueabihf' -o mango-arm32v7
+1 -4
View File
@@ -4,9 +4,6 @@
[![Patreon](https://img.shields.io/badge/support-patreon-brightgreen?link=https://www.patreon.com/hkalexling)](https://www.patreon.com/hkalexling) ![Build](https://github.com/hkalexling/Mango/workflows/Build/badge.svg) [![Gitter](https://badges.gitter.im/mango-cr/mango.svg)](https://gitter.im/mango-cr/mango?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Discord](https://img.shields.io/discord/855633663425118228?label=discord)](http://discord.com/invite/ezKtacCp9Q) [![Patreon](https://img.shields.io/badge/support-patreon-brightgreen?link=https://www.patreon.com/hkalexling)](https://www.patreon.com/hkalexling) ![Build](https://github.com/hkalexling/Mango/workflows/Build/badge.svg) [![Gitter](https://badges.gitter.im/mango-cr/mango.svg)](https://gitter.im/mango-cr/mango?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Discord](https://img.shields.io/discord/855633663425118228?label=discord)](http://discord.com/invite/ezKtacCp9Q)
> [!CAUTION]
> As of March 2025, Mango is no longer maintained. We are incredibly grateful to everyone who used it, contributed, or gave feedback along the way - thank you! Unfortunately, we just don't have the time to keep it going right now. That said, it's open source, so you're more than welcome to fork it, build on it, or maintain your own version. If you're looking for alternatives, check out the wiki for similar projects. We might return to it someday, but for now, we don't recommend using it as-is - running unmaintained software can introduce security risks.
Mango is a self-hosted manga server and reader. Its features include Mango is a self-hosted manga server and reader. Its features include
- Multi-user support - Multi-user support
@@ -54,7 +51,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
### CLI ### CLI
``` ```
Mango - Manga Server and Web Reader. Version 0.27.0 Mango - Manga Server and Web Reader. Version 0.27.1
Usage: Usage:
+36 -21
View File
@@ -5,12 +5,16 @@ const minifyCss = require('gulp-minify-css');
const less = require('gulp-less'); const less = require('gulp-less');
gulp.task('copy-img', () => { gulp.task('copy-img', () => {
return gulp.src('node_modules/uikit/src/images/backgrounds/*.svg') return gulp
.src('node_modules/uikit/src/images/backgrounds/*.svg')
.pipe(gulp.dest('public/img')); .pipe(gulp.dest('public/img'));
}); });
gulp.task('copy-font', () => { gulp.task('copy-font', () => {
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff**') return gulp
.src(
'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff**',
)
.pipe(gulp.dest('public/webfonts')); .pipe(gulp.dest('public/webfonts'));
}); });
@@ -19,48 +23,59 @@ gulp.task('node-modules-copy', gulp.parallel('copy-img', 'copy-font'));
// Compile less // Compile less
gulp.task('less', () => { gulp.task('less', () => {
return gulp.src([ return gulp
'public/css/mango.less', .src(['public/css/mango.less', 'public/css/tags.less'])
'public/css/tags.less'
])
.pipe(less()) .pipe(less())
.pipe(gulp.dest('public/css')); .pipe(gulp.dest('public/css'));
}); });
// Transpile and minify JS files and output to dist // Transpile and minify JS files and output to dist
gulp.task('babel', () => { gulp.task('babel', () => {
return gulp.src(['public/js/*.js', '!public/js/*.min.js']) return gulp
.pipe(babel({ .src(['public/js/*.js', '!public/js/*.min.js'])
.pipe(
babel({
presets: [ presets: [
['@babel/preset-env', { [
targets: '>0.25%, not dead, ios>=9' '@babel/preset-env',
}] {
targets: '>0.25%, not dead, ios>=9',
},
], ],
})) ],
.pipe(minify({ }),
)
.pipe(
minify({
removeConsole: true, removeConsole: true,
builtIns: false builtIns: false,
})) }),
)
.pipe(gulp.dest('dist/js')); .pipe(gulp.dest('dist/js'));
}); });
// Minify CSS and output to dist // Minify CSS and output to dist
gulp.task('minify-css', () => { gulp.task('minify-css', () => {
return gulp.src('public/css/*.css') return gulp
.src('public/css/*.css')
.pipe(minifyCss()) .pipe(minifyCss())
.pipe(gulp.dest('dist/css')); .pipe(gulp.dest('dist/css'));
}); });
// Copy static files (includeing images) to dist // Copy static files (includeing images) to dist
gulp.task('copy-files', () => { gulp.task('copy-files', () => {
return gulp.src([ return gulp
.src(
[
'public/*.*', 'public/*.*',
'public/img/**', 'public/img/**',
'public/webfonts/*', 'public/webfonts/*',
'public/js/*.min.js' 'public/js/*.min.js',
], { ],
base: 'public' {
}) base: 'public',
},
)
.pipe(gulp.dest('dist')); .pipe(gulp.dest('dist'));
}); });
+8 -3
View File
@@ -6,20 +6,25 @@
"author": "Alex Ling <hkalexling@gmail.com>", "author": "Alex Ling <hkalexling@gmail.com>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.18.9",
"@babel/preset-env": "^7.11.5", "@babel/preset-env": "^7.11.5",
"all-contributors-cli": "^6.19.0", "all-contributors-cli": "^6.19.0",
"eslint": "^8.22.0",
"eslint-plugin-prettier": "^4.2.1",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-babel": "^8.0.0", "gulp-babel": "^8.0.0",
"gulp-babel-minify": "^0.5.1", "gulp-babel-minify": "^0.5.1",
"gulp-less": "^4.0.1", "gulp-less": "^4.0.1",
"gulp-minify-css": "^1.2.4", "gulp-minify-css": "^1.2.4",
"less": "^3.11.3" "less": "^3.11.3",
"prettier": "^2.7.1"
}, },
"scripts": { "scripts": {
"uglify": "gulp" "uglify": "gulp",
"lint": "eslint public/js *.js --ext .js"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",
"uikit": "^3.5.4" "uikit": "~3.14.0"
} }
} }
+5 -7
View File
@@ -27,11 +27,11 @@ const component = () => {
this.scanMs = -1; this.scanMs = -1;
this.scanTitles = 0; this.scanTitles = 0;
$.post(`${base_url}api/admin/scan`) $.post(`${base_url}api/admin/scan`)
.then(data => { .then((data) => {
this.scanMs = data.milliseconds; this.scanMs = data.milliseconds;
this.scanTitles = data.titles; this.scanTitles = data.titles;
}) })
.catch(e => { .catch((e) => {
alert('danger', `Failed to trigger a scan. Error: ${e}`); alert('danger', `Failed to trigger a scan. Error: ${e}`);
}) })
.always(() => { .always(() => {
@@ -42,14 +42,12 @@ const component = () => {
if (this.generating) return; if (this.generating) return;
this.generating = true; this.generating = true;
this.progress = 0.0; this.progress = 0.0;
$.post(`${base_url}api/admin/generate_thumbnails`) $.post(`${base_url}api/admin/generate_thumbnails`).then(() => {
.then(() => { this.getProgress();
this.getProgress()
}); });
}, },
getProgress() { getProgress() {
$.get(`${base_url}api/admin/thumbnail_progress`) $.get(`${base_url}api/admin/thumbnail_progress`).then((data) => {
.then(data => {
this.progress = data.progress; this.progress = data.progress;
this.generating = data.progress > 0; this.generating = data.progress > 0;
}); });
+1 -1
View File
@@ -2,5 +2,5 @@ const alert = (level, text) => {
$('#alert').empty(); $('#alert').empty();
const html = `<div class="uk-alert-${level}" uk-alert><a class="uk-alert-close" uk-close></a><p>${text}</p></div>`; const html = `<div class="uk-alert-${level}" uk-alert><a class="uk-alert-close" uk-close></a><p>${text}</p></div>`;
$('#alert').append(html); $('#alert').append(html);
$("html, body").animate({ scrollTop: 0 }); $('html, body').animate({ scrollTop: 0 });
}; };
+8 -4
View File
@@ -41,7 +41,10 @@ const getProp = (key, selector = '#root') => {
* @return {bool} * @return {bool}
*/ */
const preferDarkMode = () => { const preferDarkMode = () => {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; return (
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
);
}; };
/** /**
@@ -87,7 +90,7 @@ const loadTheme = () => {
* @function saveThemeSetting * @function saveThemeSetting
* @param {string} setting - A theme setting * @param {string} setting - A theme setting
*/ */
const saveThemeSetting = setting => { const saveThemeSetting = (setting) => {
if (!validThemeSetting(setting)) setting = 'system'; if (!validThemeSetting(setting)) setting = 'system';
localStorage.setItem('theme', setting); localStorage.setItem('theme', setting);
}; };
@@ -134,8 +137,9 @@ $(() => {
// on system dark mode setting change // on system dark mode setting change
if (window.matchMedia) { if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)') window
.addEventListener('change', event => { .matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (event) => {
if (loadThemeSetting() === 'system') if (loadThemeSetting() === 'system')
setTheme(event.matches ? 'dark' : 'light'); setTheme(event.matches ? 'dark' : 'light');
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ const truncate = (e) => {
} else { } else {
$(e).removeAttr('uk-tooltip'); $(e).removeAttr('uk-tooltip');
} }
} },
}); });
}; };
+40 -21
View File
@@ -7,22 +7,22 @@ const component = () => {
ws: undefined, ws: undefined,
wsConnect(secure = true) { wsConnect(secure = true) {
const url = `${secure ? 'wss' : 'ws'}://${location.host}${base_url}api/admin/mangadex/queue`; const url = `${secure ? 'wss' : 'ws'}://${
location.host
}${base_url}api/admin/mangadex/queue`;
console.log(`Connecting to ${url}`); console.log(`Connecting to ${url}`);
this.ws = new WebSocket(url); this.ws = new WebSocket(url);
this.ws.onmessage = event => { this.ws.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
this.jobs = data.jobs; this.jobs = data.jobs;
this.paused = data.paused; this.paused = data.paused;
}; };
this.ws.onclose = () => { this.ws.onclose = () => {
if (this.ws.failed) if (this.ws.failed) return this.wsConnect(false);
return this.wsConnect(false);
alert('danger', 'Socket connection closed'); alert('danger', 'Socket connection closed');
}; };
this.ws.onerror = () => { this.ws.onerror = () => {
if (secure) if (secure) return (this.ws.failed = true);
return this.ws.failed = true;
alert('danger', 'Socket connection failed'); alert('danger', 'Socket connection failed');
}; };
}, },
@@ -35,18 +35,24 @@ const component = () => {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: base_url + 'api/admin/mangadex/queue', url: base_url + 'api/admin/mangadex/queue',
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (!data.success && data.error) { if (!data.success && data.error) {
alert('danger', `Failed to fetch download queue. Error: ${data.error}`); alert(
'danger',
`Failed to fetch download queue. Error: ${data.error}`,
);
return; return;
} }
this.jobs = data.jobs; this.jobs = data.jobs;
this.paused = data.paused; this.paused = data.paused;
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}) })
.always(() => { .always(() => {
this.loading = false; this.loading = false;
@@ -55,26 +61,36 @@ const component = () => {
jobAction(action, event) { jobAction(action, event) {
let url = `${base_url}api/admin/mangadex/queue/${action}`; let url = `${base_url}api/admin/mangadex/queue/${action}`;
if (event) { if (event) {
const id = event.currentTarget.closest('tr').id.split('-').slice(1).join('-'); const id = event.currentTarget
.closest('tr')
.id.split('-')
.slice(1)
.join('-');
url = `${url}?${$.param({ url = `${url}?${$.param({
id: id id,
})}`; })}`;
} }
console.log(url); console.log(url);
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: url, url,
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (!data.success && data.error) { if (!data.success && data.error) {
alert('danger', `Failed to ${action} job from download queue. Error: ${data.error}`); alert(
'danger',
`Failed to ${action} job from download queue. Error: ${data.error}`,
);
return; return;
} }
this.load(); this.load();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}); });
}, },
toggle() { toggle() {
@@ -83,11 +99,14 @@ const component = () => {
const url = `${base_url}api/admin/mangadex/queue/${action}`; const url = `${base_url}api/admin/mangadex/queue/${action}`;
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: url, url,
dataType: 'json' dataType: 'json',
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}) })
.always(() => { .always(() => {
this.load(); this.load();
@@ -111,6 +130,6 @@ const component = () => {
break; break;
} }
return cls; return cls;
} },
}; };
}; };
+28 -14
View File
@@ -7,9 +7,9 @@ const component = () => {
load() { load() {
this.loading = true; this.loading = true;
this.request('GET', `${base_url}api/admin/titles/missing`, data => { this.request('GET', `${base_url}api/admin/titles/missing`, (data) => {
this.titles = data.titles; this.titles = data.titles;
this.request('GET', `${base_url}api/admin/entries/missing`, data => { this.request('GET', `${base_url}api/admin/entries/missing`, (data) => {
this.entries = data.entries; this.entries = data.entries;
this.loading = false; this.loading = false;
this.empty = this.entries.length === 0 && this.titles.length === 0; this.empty = this.entries.length === 0 && this.titles.length === 0;
@@ -19,22 +19,33 @@ const component = () => {
rm(event) { rm(event) {
const rawID = event.currentTarget.closest('tr').id; const rawID = event.currentTarget.closest('tr').id;
const [type, id] = rawID.split('-'); const [type, id] = rawID.split('-');
const url = `${base_url}api/admin/${type === 'title' ? 'titles' : 'entries'}/missing/${id}`; const url = `${base_url}api/admin/${
type === 'title' ? 'titles' : 'entries'
}/missing/${id}`;
this.request('DELETE', url, () => { this.request('DELETE', url, () => {
this.load(); this.load();
}); });
}, },
rmAll() { rmAll() {
UIkit.modal.confirm('Are you sure? All metadata associated with these items, including their tags and thumbnails, will be deleted from the database.', { UIkit.modal
.confirm(
'Are you sure? All metadata associated with these items, including their tags and thumbnails, will be deleted from the database.',
{
labels: { labels: {
ok: 'Yes, delete them', ok: 'Yes, delete them',
cancel: 'Cancel' cancel: 'Cancel',
} },
}).then(() => { },
)
.then(() => {
this.request('DELETE', `${base_url}api/admin/titles/missing`, () => { this.request('DELETE', `${base_url}api/admin/titles/missing`, () => {
this.request('DELETE', `${base_url}api/admin/entries/missing`, () => { this.request(
'DELETE',
`${base_url}api/admin/entries/missing`,
() => {
this.load(); this.load();
}); },
);
}); });
}); });
}, },
@@ -42,10 +53,10 @@ const component = () => {
console.log(url); console.log(url);
$.ajax({ $.ajax({
type: method, type: method,
url: url, url,
contentType: 'application/json' contentType: 'application/json',
}) })
.done(data => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', `Failed to ${method} ${url}. Error: ${data.error}`); alert('danger', `Failed to ${method} ${url}. Error: ${data.error}`);
return; return;
@@ -53,8 +64,11 @@ const component = () => {
if (cb) cb(data); if (cb) cb(data);
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to ${method} ${url}. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to ${method} ${url}. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}); });
} },
}; };
}; };
+106 -123
View File
@@ -8,8 +8,8 @@ const component = () => {
manga: undefined, // undefined: not searched yet, []: empty manga: undefined, // undefined: not searched yet, []: empty
mid: undefined, // id of the selected manga mid: undefined, // id of the selected manga
allChapters: [], allChapters: [],
query: "", query: '',
mangaTitle: "", mangaTitle: '',
searching: false, searching: false,
adding: false, adding: false,
sortOptions: [], sortOptions: [],
@@ -18,16 +18,16 @@ const component = () => {
chaptersLimit: 500, chaptersLimit: 500,
listManga: false, listManga: false,
subscribing: false, subscribing: false,
subscriptionName: "", subscriptionName: '',
init() { init() {
const tableObserver = new MutationObserver(() => { const tableObserver = new MutationObserver(() => {
console.log("table mutated"); console.log('table mutated');
$("#selectable").selectable({ $('#selectable').selectable({
filter: "tr", filter: 'tr',
}); });
}); });
tableObserver.observe($("table").get(0), { tableObserver.observe($('table').get(0), {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
@@ -37,25 +37,21 @@ const component = () => {
if (!data.success) throw new Error(data.error); if (!data.success) throw new Error(data.error);
this.plugins = data.plugins; this.plugins = data.plugins;
const pid = localStorage.getItem("plugin"); const pid = localStorage.getItem('plugin');
if (pid && this.plugins.map((p) => p.id).includes(pid)) if (pid && this.plugins.map((p) => p.id).includes(pid))
return this.loadPlugin(pid); return this.loadPlugin(pid);
if (this.plugins.length > 0) if (this.plugins.length > 0) this.loadPlugin(this.plugins[0].id);
this.loadPlugin(this.plugins[0].id);
}) })
.catch((e) => { .catch((e) => {
alert( alert('danger', `Failed to list the available plugins. Error: ${e}`);
"danger",
`Failed to list the available plugins. Error: ${e}`
);
}); });
}, },
loadPlugin(pid) { loadPlugin(pid) {
fetch( fetch(
`${base_url}api/admin/plugin/info?${new URLSearchParams({ `${base_url}api/admin/plugin/info?${new URLSearchParams({
plugin: pid, plugin: pid,
})}` })}`,
) )
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
@@ -65,10 +61,7 @@ const component = () => {
this.pid = pid; this.pid = pid;
}) })
.catch((e) => { .catch((e) => {
alert( alert('danger', `Failed to get plugin metadata. Error: ${e}`);
"danger",
`Failed to get plugin metadata. Error: ${e}`
);
}); });
}, },
pluginChanged() { pluginChanged() {
@@ -76,12 +69,12 @@ const component = () => {
this.chapters = undefined; this.chapters = undefined;
this.mid = undefined; this.mid = undefined;
this.loadPlugin(this.pid); this.loadPlugin(this.pid);
localStorage.setItem("plugin", this.pid); localStorage.setItem('plugin', this.pid);
}, },
get chapterKeys() { get chapterKeys() {
if (this.allChapters.length < 1) return []; if (this.allChapters.length < 1) return [];
return Object.keys(this.allChapters[0]).filter( return Object.keys(this.allChapters[0]).filter(
(k) => !["manga_title"].includes(k) (k) => !['manga_title'].includes(k),
); );
}, },
searchChapters(query) { searchChapters(query) {
@@ -93,8 +86,8 @@ const component = () => {
fetch( fetch(
`${base_url}api/admin/plugin/list?${new URLSearchParams({ `${base_url}api/admin/plugin/list?${new URLSearchParams({
plugin: this.pid, plugin: this.pid,
query: query, query,
})}` })}`,
) )
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
@@ -110,7 +103,7 @@ const component = () => {
this.chapters = data.chapters; this.chapters = data.chapters;
}) })
.catch((e) => { .catch((e) => {
alert("danger", `Failed to list chapters. Error: ${e}`); alert('danger', `Failed to list chapters. Error: ${e}`);
}) })
.finally(() => { .finally(() => {
this.searching = false; this.searching = false;
@@ -124,8 +117,8 @@ const component = () => {
fetch( fetch(
`${base_url}api/admin/plugin/search?${new URLSearchParams({ `${base_url}api/admin/plugin/search?${new URLSearchParams({
plugin: this.pid, plugin: this.pid,
query: query, query,
})}` })}`,
) )
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
@@ -134,7 +127,7 @@ const component = () => {
this.listManga = true; this.listManga = true;
}) })
.catch((e) => { .catch((e) => {
alert("danger", `Search failed. Error: ${e}`); alert('danger', `Search failed. Error: ${e}`);
}) })
.finally(() => { .finally(() => {
this.searching = false; this.searching = false;
@@ -153,37 +146,35 @@ const component = () => {
} }
}, },
selectAll() { selectAll() {
$("tbody > tr").each((i, e) => { $('tbody#selectable > tr').each((i, e) => {
$(e).addClass("ui-selected"); $(e).addClass('ui-selected');
}); });
}, },
clearSelection() { clearSelection() {
$("tbody > tr").each((i, e) => { $('tbody#selectable > tr').each((i, e) => {
$(e).removeClass("ui-selected"); $(e).removeClass('ui-selected');
}); });
}, },
download() { download() {
const selected = $("tbody > tr.ui-selected").get(); const selected = $('tbody#selectable > tr.ui-selected').get();
if (selected.length === 0) return; if (selected.length === 0) return;
UIkit.modal UIkit.modal
.confirm(`Download ${selected.length} selected chapters?`) .confirm(`Download ${selected.length} selected chapters?`)
.then(() => { .then(() => {
const ids = selected.map((e) => e.id); const ids = selected.map((e) => e.id);
const chapters = this.chapters.filter((c) => const chapters = this.chapters.filter((c) => ids.includes(c.id));
ids.includes(c.id)
);
console.log(chapters); console.log(chapters);
this.adding = true; this.adding = true;
fetch(`${base_url}api/admin/plugin/download`, { fetch(`${base_url}api/admin/plugin/download`, {
method: "POST", method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
chapters, chapters,
plugin: this.pid, plugin: this.pid,
title: this.mangaTitle, title: this.mangaTitle,
}), }),
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}) })
.then((res) => res.json()) .then((res) => res.json())
@@ -192,16 +183,16 @@ const component = () => {
const successCount = parseInt(data.success); const successCount = parseInt(data.success);
const failCount = parseInt(data.fail); const failCount = parseInt(data.fail);
alert( alert(
"success", 'success',
`${successCount} of ${ `${successCount} of ${
successCount + failCount successCount + failCount
} chapters added to the download queue. You can view and manage your download queue on the <a href="${base_url}admin/downloads">download manager page</a>.` } chapters added to the download queue. You can view and manage your download queue on the <a href="${base_url}admin/downloads">download manager page</a>.`,
); );
}) })
.catch((e) => { .catch((e) => {
alert( alert(
"danger", 'danger',
`Failed to add chapters to the download queue. Error: ${e}` `Failed to add chapters to the download queue. Error: ${e}`,
); );
}) })
.finally(() => { .finally(() => {
@@ -210,7 +201,7 @@ const component = () => {
}); });
}, },
thClicked(event) { thClicked(event) {
const idx = parseInt(event.currentTarget.id.split("-")[1]); const idx = parseInt(event.currentTarget.id.split('-')[1]);
if (idx === undefined || isNaN(idx)) return; if (idx === undefined || isNaN(idx)) return;
const curOption = this.sortOptions[idx]; const curOption = this.sortOptions[idx];
let option; let option;
@@ -233,51 +224,46 @@ const component = () => {
get filteredChapters() { get filteredChapters() {
let ary = this.allChapters.slice(); let ary = this.allChapters.slice();
console.log("initial size:", ary.length); console.log('initial size:', ary.length);
for (let filter of this.appliedFilters) { for (let filter of this.appliedFilters) {
if (!filter.value) continue; if (!filter.value) continue;
if (filter.type === "array" && filter.value === "all") continue; if (filter.type === 'array' && filter.value === 'all') continue;
if (filter.type.startsWith("number") && isNaN(filter.value)) if (filter.type.startsWith('number') && isNaN(filter.value)) continue;
continue;
if (filter.type === "string") { 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) => ary = ary.filter((ch) =>
ch[filter.key] ch[filter.key]
.toLowerCase() .map((s) => (typeof s === 'string' ? s.toLowerCase() : s))
.includes(filter.value.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; return ary;
@@ -304,59 +290,58 @@ const component = () => {
if (!isNaN(a) && !isNaN(b)) return Number(a) - Number(b); if (!isNaN(a) && !isNaN(b)) return Number(a) - Number(b);
const preprocessString = (val) => { const preprocessString = (val) => {
if (typeof val !== "string") return val; if (typeof val !== 'string') return val;
return val.toLowerCase().replace(/\s\s/g, " ").trim(); return val.toLowerCase().replace(/\s\s/g, ' ').trim();
}; };
return preprocessString(a) > preprocessString(b) ? 1 : -1; return preprocessString(a) > preprocessString(b) ? 1 : -1;
}, },
fieldType(values) { fieldType(values) {
if (values.every((v) => this.numIsDate(v))) return "date"; if (values.every((v) => this.numIsDate(v))) return 'date';
if (values.every((v) => !isNaN(v))) return "number"; if (values.every((v) => !isNaN(v))) return 'number';
if (values.every((v) => Array.isArray(v))) return "array"; if (values.every((v) => Array.isArray(v))) return 'array';
return "string"; return 'string';
}, },
get filters() { get filters() {
if (this.allChapters.length < 1) return []; if (this.allChapters.length < 1) return [];
const keys = Object.keys(this.allChapters[0]).filter( const keys = Object.keys(this.allChapters[0]).filter(
(k) => !["manga_title", "id"].includes(k) (k) => !['manga_title', 'id'].includes(k),
); );
return keys.map((k) => { return keys.map((k) => {
let values = this.allChapters.map((c) => c[k]); let values = this.allChapters.map((c) => c[k]);
const type = this.fieldType(values); const type = this.fieldType(values);
if (type === "array") { if (type === 'array') {
// if the type is an array, return the list of available elements // if the type is an array, return the list of available elements
// example: an array of groups or authors // example: an array of groups or authors
values = Array.from( values = Array.from(
new Set( new Set(
values.flat().map((v) => { values.flat().map((v) => {
if (typeof v === "string") if (typeof v === 'string') return v.toLowerCase();
return v.toLowerCase(); }),
}) ),
)
); );
} }
return { return {
key: k, key: k,
type: type, type,
values: values, values,
}; };
}); });
}, },
get filterSettings() { get filterSettings() {
return $("#filter-form input:visible, #filter-form select:visible") return $('#filter-form input:visible, #filter-form select:visible')
.get() .get()
.map((i) => { .map((i) => {
const type = i.getAttribute("data-filter-type"); const type = i.getAttribute('data-filter-type');
let value = i.value.trim(); let value = i.value.trim();
if (type.startsWith("date")) if (type.startsWith('date'))
value = value ? Date.parse(value).toString() : ""; value = value ? Date.parse(value).toString() : '';
return { return {
key: i.getAttribute("data-filter-key"), key: i.getAttribute('data-filter-key'),
value: value, value,
type: type, type,
}; };
}); });
}, },
@@ -366,23 +351,23 @@ const component = () => {
this.sortOptions = []; this.sortOptions = [];
}, },
clearFilters() { clearFilters() {
$("#filter-form input") $('#filter-form input')
.get() .get()
.forEach((i) => (i.value = "")); .forEach((i) => (i.value = ''));
$("#filter-form select").val("all"); $('#filter-form select').val('all');
this.appliedFilters = []; this.appliedFilters = [];
this.chapters = this.filteredChapters; this.chapters = this.filteredChapters;
this.sortOptions = []; this.sortOptions = [];
}, },
mangaSelected(event) { mangaSelected(event) {
const mid = event.currentTarget.getAttribute("data-id"); const mid = event.currentTarget.getAttribute('data-id');
this.mid = mid; this.mid = mid;
this.searchChapters(mid); this.searchChapters(mid);
}, },
subscribe(modal) { subscribe(modal) {
this.subscribing = true; this.subscribing = true;
fetch(`${base_url}api/admin/plugin/subscriptions`, { fetch(`${base_url}api/admin/plugin/subscriptions`, {
method: "POST", method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
filters: this.filterSettings, filters: this.filterSettings,
plugin: this.pid, plugin: this.pid,
@@ -391,16 +376,16 @@ const component = () => {
manga_id: this.mid, manga_id: this.mid,
}), }),
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
if (!data.success) throw new Error(data.error); if (!data.success) throw new Error(data.error);
alert("success", "Subscription created"); alert('success', 'Subscription created');
}) })
.catch((e) => { .catch((e) => {
alert("danger", `Failed to subscribe. Error: ${e}`); alert('danger', `Failed to subscribe. Error: ${e}`);
}) })
.finally(() => { .finally(() => {
this.subscribing = false; this.subscribing = false;
@@ -412,14 +397,12 @@ const component = () => {
}, },
renderCell(value) { renderCell(value) {
if (this.numIsDate(value)) if (this.numIsDate(value))
return `<span>${moment(Number(value)).format( return `<span>${moment(Number(value)).format('MMM D, YYYY')}</span>`;
"MMM D, YYYY"
)}</span>`;
const maxLength = 40; const maxLength = 40;
if (value && value.length > maxLength) if (value && value.length > maxLength)
return `<span>${value.substr( return `<span>${value.substr(
0, 0,
maxLength maxLength,
)}...</span><div uk-dropdown>${value}</div>`; )}...</span><div uk-dropdown>${value}</div>`;
return `<span>${value}</span>`; return `<span>${value}</span>`;
}, },
@@ -427,24 +410,24 @@ const component = () => {
const key = ft.key; const key = ft.key;
let type = ft.type; let type = ft.type;
switch (type) { switch (type) {
case "number-min": case 'number-min':
type = "number (minimum value)"; type = 'number (minimum value)';
break; break;
case "number-max": case 'number-max':
type = "number (maximum value)"; type = 'number (maximum value)';
break; break;
case "date-min": case 'date-min':
type = "minimum date"; type = 'minimum date';
break; break;
case "date-max": case 'date-max':
type = "maximum date"; type = 'maximum date';
break; break;
} }
let value = ft.value; let value = ft.value;
if (ft.type.startsWith("number") && isNaN(value)) value = ""; if (ft.type.startsWith('number') && isNaN(value)) value = '';
else if (ft.type.startsWith("date") && value) else if (ft.type.startsWith('date') && value)
value = moment(Number(value)).format("MMM D, YYYY"); value = moment(Number(value)).format('MMM D, YYYY');
return `<td>${key}</td><td>${type}</td><td>${value}</td>`; return `<td>${key}</td><td>${type}</td><td>${value}</td>`;
}, },
+40 -31
View File
@@ -21,24 +21,24 @@ const readerComponent = () => {
*/ */
init(nextTick) { init(nextTick) {
$.get(`${base_url}api/dimensions/${tid}/${eid}`) $.get(`${base_url}api/dimensions/${tid}/${eid}`)
.then(data => { .then((data) => {
if (!data.success && data.error) if (!data.success && data.error) throw new Error(resp.error);
throw new Error(resp.error);
const dimensions = data.dimensions; const dimensions = data.dimensions;
this.items = dimensions.map((d, i) => { this.items = dimensions.map((d, i) => {
return { return {
id: i + 1, id: i + 1,
url: `${base_url}api/page/${tid}/${eid}/${i+1}`, url: `${base_url}api/page/${tid}/${eid}/${i + 1}`,
width: d.width == 0 ? "100%" : d.width, width: d.width === 0 ? '100%' : d.width,
height: d.height == 0 ? "100%" : d.height, 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`. // 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 // TODO: support more image types in image_size.cr
const avgRatio = dimensions.reduce((acc, cur) => { const avgRatio =
return acc + cur.height / cur.width dimensions.reduce((acc, cur) => {
return acc + cur.height / cur.width;
}, 0) / dimensions.length; }, 0) / dimensions.length;
console.log(avgRatio); console.log(avgRatio);
@@ -60,8 +60,13 @@ const readerComponent = () => {
} }
// Preload Images // Preload Images
this.preloadLookahead = +(localStorage.getItem('preloadLookahead') ?? 3); this.preloadLookahead = +(
const limit = Math.min(page + this.preloadLookahead, this.items.length); localStorage.getItem('preloadLookahead') ?? 3
);
const limit = Math.min(
page + this.preloadLookahead,
this.items.length,
);
for (let idx = page + 1; idx <= limit; idx++) { for (let idx = page + 1; idx <= limit; idx++) {
this.preloadImage(this.items[idx - 1].url); this.preloadImage(this.items[idx - 1].url);
} }
@@ -71,28 +76,31 @@ const readerComponent = () => {
this.fitType = savedFitType; this.fitType = savedFitType;
$('#fit-select').val(savedFitType); $('#fit-select').val(savedFitType);
} }
const savedFlipAnimation = localStorage.getItem('enableFlipAnimation'); const savedFlipAnimation = localStorage.getItem(
this.enableFlipAnimation = savedFlipAnimation === null || savedFlipAnimation === 'true'; 'enableFlipAnimation',
);
this.enableFlipAnimation =
savedFlipAnimation === null || savedFlipAnimation === 'true';
const savedRightToLeft = localStorage.getItem('enableRightToLeft'); const savedRightToLeft = localStorage.getItem('enableRightToLeft');
if (savedRightToLeft === null) { if (savedRightToLeft === null) {
this.enableRightToLeft = false; this.enableRightToLeft = false;
} else { } else {
this.enableRightToLeft = (savedRightToLeft === 'true'); this.enableRightToLeft = savedRightToLeft === 'true';
} }
}) })
.catch(e => { .catch((e) => {
const errMsg = `Failed to get the page dimensions. ${e}`; const errMsg = `Failed to get the page dimensions. ${e}`;
console.error(e); console.error(e);
this.alertClass = 'uk-alert-danger'; this.alertClass = 'uk-alert-danger';
this.msg = errMsg; this.msg = errMsg;
}) });
}, },
/** /**
* Preload an image, which is expected to be cached * Preload an image, which is expected to be cached
*/ */
preloadImage(url) { preloadImage(url) {
(new Image()).src = url; new Image().src = url;
}, },
/** /**
* Handles the `change` event for the page selector * Handles the `change` event for the page selector
@@ -156,10 +164,8 @@ const readerComponent = () => {
this.toPage(newIdx); this.toPage(newIdx);
if (this.enableFlipAnimation) { if (this.enableFlipAnimation) {
if (isNext ^ this.enableRightToLeft) if (isNext ^ this.enableRightToLeft) this.flipAnimation = 'right';
this.flipAnimation = 'right'; else this.flipAnimation = 'left';
else
this.flipAnimation = 'left';
} }
setTimeout(() => { setTimeout(() => {
@@ -196,7 +202,7 @@ const readerComponent = () => {
ary.unshift(window.location.origin); ary.unshift(window.location.origin);
const url = ary.join('/'); const url = ary.join('/');
this.saveProgress(idx); this.saveProgress(idx);
history.replaceState(null, "", url); history.replaceState(null, '', url);
}, },
/** /**
* Updates the backend reading progress if: * Updates the backend reading progress if:
@@ -211,22 +217,25 @@ const readerComponent = () => {
*/ */
saveProgress(idx, cb) { saveProgress(idx, cb) {
idx = parseInt(idx); idx = parseInt(idx);
if (Math.abs(idx - this.lastSavedPage) >= 5 || if (
Math.abs(idx - this.lastSavedPage) >= 5 ||
this.longPages || this.longPages ||
idx === 1 || idx === this.items.length idx === 1 ||
idx === this.items.length
) { ) {
this.lastSavedPage = idx; this.lastSavedPage = idx;
console.log('saving progress', idx); console.log('saving progress', idx);
const url = `${base_url}api/progress/${tid}/${idx}?${$.param({eid: eid})}`; const url = `${base_url}api/progress/${tid}/${idx}?${$.param({
eid,
})}`;
$.ajax({ $.ajax({
method: 'PUT', method: 'PUT',
url: url, url,
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (data.error) if (data.error) alert('danger', data.error);
alert('danger', data.error);
if (cb) cb(); if (cb) cb();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
@@ -341,7 +350,7 @@ const readerComponent = () => {
this.toPage(this.selectedIndex); this.toPage(this.selectedIndex);
}, },
fitChanged(){ fitChanged() {
this.fitType = $('#fit-select').val(); this.fitType = $('#fit-select').val();
localStorage.setItem('fitType', this.fitType); localStorage.setItem('fitType', this.fitType);
}, },
@@ -358,4 +367,4 @@ const readerComponent = () => {
localStorage.setItem('enableRightToLeft', this.enableRightToLeft); localStorage.setItem('enableRightToLeft', this.enableRightToLeft);
}, },
}; };
} };
+12 -14
View File
@@ -1,27 +1,25 @@
$(function(){ $(function () {
var filter = []; let filter = [];
var result = []; let result = [];
$('.uk-card-title').each(function(){ $('.uk-card-title').each(function () {
filter.push($(this).text()); filter.push($(this).text());
}); });
$('.uk-search-input').keyup(function(){ $('.uk-search-input').keyup(function () {
var input = $('.uk-search-input').val(); let input = $('.uk-search-input').val();
var regex = new RegExp(input, 'i'); let regex = new RegExp(input, 'i');
if (input === '') { if (input === '') {
$('.item').each(function(){ $('.item').each(function () {
$(this).removeAttr('hidden'); $(this).removeAttr('hidden');
}); });
} } else {
else { filter.forEach(function (text, i) {
filter.forEach(function(text, i){
result[i] = text.match(regex); result[i] = text.match(regex);
}); });
$('.item').each(function(i){ $('.item').each(function (i) {
if (result[i]) { if (result[i]) {
$(this).removeAttr('hidden'); $(this).removeAttr('hidden');
} } else {
else {
$(this).attr('hidden', ''); $(this).attr('hidden', '');
} }
}); });
+1 -1
View File
@@ -8,7 +8,7 @@ $(() => {
const url = `${location.protocol}//${location.host}${location.pathname}`; const url = `${location.protocol}//${location.host}${location.pathname}`;
const newURL = `${url}?${$.param({ const newURL = `${url}?${$.param({
sort: by, sort: by,
ascend: dir === 'up' ? 1 : 0 ascend: dir === 'up' ? 1 : 0,
})}`; })}`;
window.location.href = newURL; window.location.href = newURL;
}); });
+46 -49
View File
@@ -13,36 +13,31 @@ const component = () => {
if (!data.success) throw new Error(data.error); if (!data.success) throw new Error(data.error);
this.plugins = data.plugins; this.plugins = data.plugins;
const pid = localStorage.getItem("plugin"); let pid = localStorage.getItem('plugin');
if (pid && this.plugins.map((p) => p.id).includes(pid)) if (!pid || !this.plugins.find((p) => p.id === pid)) {
this.pid = pid; pid = this.plugins[0].id;
else if (this.plugins.length > 0) }
this.pid = this.plugins[0].id;
this.pid = pid;
this.list(pid); this.list(pid);
}) })
.catch((e) => { .catch((e) => {
alert( alert('danger', `Failed to list the available plugins. Error: ${e}`);
"danger",
`Failed to list the available plugins. Error: ${e}`
);
}); });
}, },
pluginChanged() { pluginChanged() {
localStorage.setItem("plugin", this.pid); localStorage.setItem('plugin', this.pid);
this.list(this.pid); this.list(this.pid);
}, },
list(pid) { list(pid) {
if (!pid) return; if (!pid) return;
fetch( fetch(
`${base_url}api/admin/plugin/subscriptions?${new URLSearchParams( `${base_url}api/admin/plugin/subscriptions?${new URLSearchParams({
{
plugin: pid, plugin: pid,
} })}`,
)}`,
{ {
method: "GET", method: 'GET',
} },
) )
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
@@ -50,10 +45,7 @@ const component = () => {
this.subscriptions = data.subscriptions; this.subscriptions = data.subscriptions;
}) })
.catch((e) => { .catch((e) => {
alert( alert('danger', `Failed to list subscriptions. Error: ${e}`);
"danger",
`Failed to list subscriptions. Error: ${e}`
);
}); });
}, },
renderStrCell(str) { renderStrCell(str) {
@@ -61,7 +53,7 @@ const component = () => {
if (str.length > maxLength) if (str.length > maxLength)
return `<td><span>${str.substring( return `<td><span>${str.substring(
0, 0,
maxLength maxLength,
)}...</span><div uk-dropdown>${str}</div></td>`; )}...</span><div uk-dropdown>${str}</div></td>`;
return `<td>${str}</td>`; return `<td>${str}</td>`;
}, },
@@ -71,7 +63,7 @@ const component = () => {
.humanize(true)}</td>`; .humanize(true)}</td>`;
}, },
selected(event, modal) { selected(event, modal) {
const id = event.currentTarget.getAttribute("sid"); const id = event.currentTarget.getAttribute('sid');
this.subscription = this.subscriptions.find((s) => s.id === id); this.subscription = this.subscriptions.find((s) => s.id === id);
UIkit.modal(modal).show(); UIkit.modal(modal).show();
}, },
@@ -79,36 +71,41 @@ const component = () => {
const key = ft.key; const key = ft.key;
let type = ft.type; let type = ft.type;
switch (type) { switch (type) {
case "number-min": case 'number-min':
type = "number (minimum value)"; type = 'number (minimum value)';
break; break;
case "number-max": case 'number-max':
type = "number (maximum value)"; type = 'number (maximum value)';
break; break;
case "date-min": case 'date-min':
type = "minimum date"; type = 'minimum date';
break; break;
case "date-max": case 'date-max':
type = "maximum date"; type = 'maximum date';
break; break;
} }
let value = ft.value; let value = ft.value;
if (ft.type.startsWith("number") && isNaN(value)) value = ""; if (ft.type.startsWith('number') && isNaN(value)) value = '';
else if (ft.type.startsWith("date") && value) else if (ft.type.startsWith('date') && value)
value = moment(Number(value)).format("MMM D, YYYY"); value = moment(Number(value)).format('MMM D, YYYY');
return `<td>${key}</td><td>${type}</td><td>${value}</td>`; return `<td>${key}</td><td>${type}</td><td>${value}</td>`;
}, },
actionHandler(event, type) { actionHandler(event, type) {
const id = $(event.currentTarget).closest("tr").attr("sid"); const id = $(event.currentTarget).closest('tr').attr('sid');
if (type !== 'delete') return this.action(id, type); if (type !== 'delete') return this.action(id, type);
UIkit.modal.confirm('Are you sure you want to delete the subscription? This cannot be undone.', { UIkit.modal
.confirm(
'Are you sure you want to delete the subscription? This cannot be undone.',
{
labels: { labels: {
ok: 'Yes, delete it', ok: 'Yes, delete it',
cancel: 'Cancel' cancel: 'Cancel',
} },
}).then(() => { },
)
.then(() => {
this.action(id, type); this.action(id, type);
}); });
}, },
@@ -116,27 +113,27 @@ const component = () => {
if (this.loading) return; if (this.loading) return;
this.loading = true; this.loading = true;
fetch( fetch(
`${base_url}api/admin/plugin/subscriptions${type === 'update' ? '/update' : ''}?${new URLSearchParams( `${base_url}api/admin/plugin/subscriptions${
{ type === 'update' ? '/update' : ''
}?${new URLSearchParams({
plugin: this.pid, plugin: this.pid,
subscription: id, subscription: id,
} })}`,
)}`,
{ {
method: type === 'delete' ? "DELETE" : 'POST' method: type === 'delete' ? 'DELETE' : 'POST',
} },
) )
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (!data.success) throw new Error(data.error); if (!data.success) throw new Error(data.error);
if (type === 'update') if (type === 'update')
alert("success", `Checking updates for subscription ${id}. Check the log for the progress or come back to this page later.`); alert(
'success',
`Checking updates for subscription ${id}. Check the log for the progress or come back to this page later.`,
);
}) })
.catch((e) => { .catch((e) => {
alert( alert('danger', `Failed to ${type} subscription. Error: ${e}`);
"danger",
`Failed to ${type} subscription. Error: ${e}`
);
}) })
.finally(() => { .finally(() => {
this.loading = false; this.loading = false;
+48 -18
View File
@@ -7,30 +7,45 @@ const component = () => {
$.getJSON(`${base_url}api/admin/mangadex/expires`) $.getJSON(`${base_url}api/admin/mangadex/expires`)
.done((data) => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', 'Failed to check MangaDex integration status. Error: ' + data.error); alert(
'danger',
'Failed to check MangaDex integration status. Error: ' +
data.error,
);
return; return;
} }
this.available = Boolean(data.expires && data.expires > Math.floor(Date.now() / 1000)); this.available = Boolean(
data.expires && data.expires > Math.floor(Date.now() / 1000),
);
if (this.available) this.getSubscriptions(); if (this.available) this.getSubscriptions();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
}) 'danger',
`Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
});
}, },
getSubscriptions() { getSubscriptions() {
$.getJSON(`${base_url}api/admin/mangadex/subscriptions`) $.getJSON(`${base_url}api/admin/mangadex/subscriptions`)
.done(data => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', 'Failed to get subscriptions. Error: ' + data.error); alert(
'danger',
'Failed to get subscriptions. Error: ' + data.error,
);
return; return;
} }
this.subscriptions = data.subscriptions; this.subscriptions = data.subscriptions;
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to get subscriptions. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
}) 'danger',
`Failed to get subscriptions. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
});
}, },
rm(event) { rm(event) {
@@ -38,16 +53,22 @@ const component = () => {
$.ajax({ $.ajax({
type: 'DELETE', type: 'DELETE',
url: `${base_url}api/admin/mangadex/subscriptions/${id}`, url: `${base_url}api/admin/mangadex/subscriptions/${id}`,
contentType: 'application/json' contentType: 'application/json',
}) })
.done(data => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', `Failed to delete subscription. Error: ${data.error}`); alert(
'danger',
`Failed to delete subscription. Error: ${data.error}`,
);
} }
this.getSubscriptions(); this.getSubscriptions();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to delete subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to delete subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}); });
}, },
@@ -56,17 +77,26 @@ const component = () => {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: `${base_url}api/admin/mangadex/subscriptions/check/${id}`, url: `${base_url}api/admin/mangadex/subscriptions/check/${id}`,
contentType: 'application/json' contentType: 'application/json',
}) })
.done(data => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', `Failed to check subscription. Error: ${data.error}`); alert(
'danger',
`Failed to check subscription. Error: ${data.error}`,
);
return; return;
} }
alert('success', 'Mango is now checking the subscription for updates. This might take a while, but you can safely leave the page.'); 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) => { .fail((jqXHR, status) => {
alert('danger', `Failed to check subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to check subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}); });
}, },
@@ -77,6 +107,6 @@ const component = () => {
if (min === max) return `= ${min}`; if (min === max) return `= ${min}`;
return `${min} - ${max}`; return `${min} - ${max}`;
} },
}; };
}; };
+82 -53
View File
@@ -14,16 +14,24 @@ const setupAcard = () => {
$(card).attr('data-encoded-book-title'), $(card).attr('data-encoded-book-title'),
$(card).attr('data-encoded-title'), $(card).attr('data-encoded-title'),
$(card).attr('data-book-id'), $(card).attr('data-book-id'),
$(card).attr('data-id') $(card).attr('data-id'),
); );
}); });
}; };
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) { function showModal(
encodedPath,
pages,
percentage,
encodedeTitle,
encodedEntryTitle,
titleID,
entryID,
) {
const zipPath = decodeURIComponent(encodedPath); const zipPath = decodeURIComponent(encodedPath);
const title = decodeURIComponent(encodedeTitle); const title = decodeURIComponent(encodedeTitle);
const entry = decodeURIComponent(encodedEntryTitle); const entry = decodeURIComponent(encodedEntryTitle);
$('#modal button, #modal a').each(function() { $('#modal button, #modal a').each(function () {
$(this).removeAttr('hidden'); $(this).removeAttr('hidden');
}); });
if (percentage === 0) { if (percentage === 0) {
@@ -46,16 +54,19 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
$('#beginning-btn').attr('href', `${base_url}reader/${titleID}/${entryID}/1`); $('#beginning-btn').attr('href', `${base_url}reader/${titleID}/${entryID}/1`);
$('#continue-btn').attr('href', `${base_url}reader/${titleID}/${entryID}`); $('#continue-btn').attr('href', `${base_url}reader/${titleID}/${entryID}`);
$('#read-btn').click(function() { $('#read-btn').click(function () {
updateProgress(titleID, entryID, pages); updateProgress(titleID, entryID, pages);
}); });
$('#unread-btn').click(function() { $('#unread-btn').click(function () {
updateProgress(titleID, entryID, 0); updateProgress(titleID, entryID, 0);
}); });
$('#modal-edit-btn').attr('onclick', `edit("${entryID}")`); $('#modal-edit-btn').attr('onclick', `edit("${entryID}")`);
$('#modal-download-btn').attr('href', `${base_url}api/download/${titleID}/${entryID}`); $('#modal-download-btn').attr(
'href',
`${base_url}api/download/${titleID}/${entryID}`,
);
UIkit.modal($('#modal')).show(); UIkit.modal($('#modal')).show();
} }
@@ -66,19 +77,18 @@ UIkit.util.on(document, 'hidden', '#modal', () => {
}); });
const updateProgress = (tid, eid, page) => { const updateProgress = (tid, eid, page) => {
let url = `${base_url}api/progress/${tid}/${page}` let url = `${base_url}api/progress/${tid}/${page}`;
const query = $.param({ const query = $.param({
eid: eid eid,
}); });
if (eid) if (eid) url += `?${query}`;
url += `?${query}`;
$.ajax({ $.ajax({
method: 'PUT', method: 'PUT',
url: url, url,
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (data.success) { if (data.success) {
location.reload(); location.reload();
} else { } else {
@@ -101,19 +111,18 @@ const renameSubmit = (name, eid) => {
} }
const query = $.param({ const query = $.param({
eid: eid eid,
}); });
let url = `${base_url}api/admin/display_name/${titleId}/${name}`; let url = `${base_url}api/admin/display_name/${titleId}/${name}`;
if (eid) if (eid) url += `?${query}`;
url += `?${query}`;
$.ajax({ $.ajax({
type: 'PUT', type: 'PUT',
url: url, url,
contentType: "application/json", contentType: 'application/json',
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', `Failed to update display name. Error: ${data.error}`); alert('danger', `Failed to update display name. Error: ${data.error}`);
return; return;
@@ -121,7 +130,10 @@ const renameSubmit = (name, eid) => {
location.reload(); location.reload();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to update display name. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to update display name. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}); });
}; };
@@ -139,9 +151,9 @@ const renameSortNameSubmit = (name, eid) => {
type: 'PUT', type: 'PUT',
url, url,
contentType: 'application/json', contentType: 'application/json',
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', `Failed to update sort title. Error: ${data.error}`); alert('danger', `Failed to update sort title. Error: ${data.error}`);
return; return;
@@ -149,7 +161,10 @@ const renameSortNameSubmit = (name, eid) => {
location.reload(); location.reload();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to update sort title. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to update sort title. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}); });
}; };
@@ -176,7 +191,7 @@ const edit = (eid) => {
const displayNameField = $('#display-name-field'); const displayNameField = $('#display-name-field');
displayNameField.attr('value', displayName); displayNameField.attr('value', displayName);
displayNameField.attr('placeholder', fileTitle); displayNameField.attr('placeholder', fileTitle);
displayNameField.keyup(event => { displayNameField.keyup((event) => {
if (event.keyCode === 13) { if (event.keyCode === 13) {
renameSubmit(displayNameField.val() || fileTitle, eid); renameSubmit(displayNameField.val() || fileTitle, eid);
} }
@@ -188,7 +203,7 @@ const edit = (eid) => {
const sortTitleField = $('#sort-title-field'); const sortTitleField = $('#sort-title-field');
sortTitleField.val(sortTitle); sortTitleField.val(sortTitle);
sortTitleField.attr('placeholder', fileTitle); sortTitleField.attr('placeholder', fileTitle);
sortTitleField.keyup(event => { sortTitleField.keyup((event) => {
if (event.keyCode === 13) { if (event.keyCode === 13) {
renameSortNameSubmit(sortTitleField.val(), eid); renameSortNameSubmit(sortTitleField.val(), eid);
} }
@@ -217,14 +232,13 @@ const setupUpload = (eid) => {
const bar = $('#upload-progress').get(0); const bar = $('#upload-progress').get(0);
const titleId = upload.attr('data-title-id'); const titleId = upload.attr('data-title-id');
const queryObj = { const queryObj = {
tid: titleId tid: titleId,
}; };
if (eid) if (eid) queryObj['eid'] = eid;
queryObj['eid'] = eid;
const query = $.param(queryObj); const query = $.param(queryObj);
const url = `${base_url}api/admin/upload/cover?${query}`; const url = `${base_url}api/admin/upload/cover?${query}`;
UIkit.upload('.upload-field', { UIkit.upload('.upload-field', {
url: url, url,
name: 'file', name: 'file',
error: (e) => { error: (e) => {
alert('danger', `Failed to upload cover image: ${e.toString()}`); alert('danger', `Failed to upload cover image: ${e.toString()}`);
@@ -245,7 +259,7 @@ const setupUpload = (eid) => {
completeAll: () => { completeAll: () => {
$(bar).attr('hidden', ''); $(bar).attr('hidden', '');
location.reload(); location.reload();
} },
}); });
}; };
@@ -287,22 +301,28 @@ const bulkProgress = (action, el) => {
const url = `${base_url}api/bulk_progress/${action}/${tid}`; const url = `${base_url}api/bulk_progress/${action}/${tid}`;
$.ajax({ $.ajax({
type: 'PUT', type: 'PUT',
url: url, url,
contentType: "application/json", contentType: 'application/json',
dataType: 'json', dataType: 'json',
data: JSON.stringify({ data: JSON.stringify({
ids: ids ids,
}),
}) })
}) .done((data) => {
.done(data => {
if (data.error) { if (data.error) {
alert('danger', `Failed to mark entries as ${action}. Error: ${data.error}`); alert(
'danger',
`Failed to mark entries as ${action}. Error: ${data.error}`,
);
return; return;
} }
location.reload(); location.reload();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}) })
.always(() => { .always(() => {
deselectAll(); deselectAll();
@@ -325,29 +345,32 @@ const tagsComponent = () => {
disabled: !this.isAdmin, disabled: !this.isAdmin,
templateSelection(state) { templateSelection(state) {
const a = document.createElement('a'); const a = document.createElement('a');
a.setAttribute('href', `${base_url}tags/${encodeURIComponent(state.text)}`); a.setAttribute(
'href',
`${base_url}tags/${encodeURIComponent(state.text)}`,
);
a.setAttribute('class', 'uk-link-reset'); a.setAttribute('class', 'uk-link-reset');
a.onclick = event => { a.onclick = (event) => {
event.stopPropagation(); event.stopPropagation();
}; };
a.innerText = state.text; a.innerText = state.text;
return a; return a;
} },
}); });
this.request(`${base_url}api/tags`, 'GET', (data) => { this.request(`${base_url}api/tags`, 'GET', (data) => {
const allTags = data.tags; const allTags = data.tags;
const url = `${base_url}api/tags/${this.tid}`; const url = `${base_url}api/tags/${this.tid}`;
this.request(url, 'GET', data => { this.request(url, 'GET', (data) => {
this.tags = data.tags; this.tags = data.tags;
allTags.forEach(t => { allTags.forEach((t) => {
const op = new Option(t, t, false, this.tags.indexOf(t) >= 0); const op = new Option(t, t, false, this.tags.indexOf(t) >= 0);
$('.tag-select').append(op); $('.tag-select').append(op);
}); });
$('.tag-select').on('select2:select', e => { $('.tag-select').on('select2:select', (e) => {
this.onAdd(e); this.onAdd(e);
}); });
$('.tag-select').on('select2:unselect', e => { $('.tag-select').on('select2:unselect', (e) => {
this.onDelete(e); this.onDelete(e);
}); });
$('.tag-select').on('change', () => { $('.tag-select').on('change', () => {
@@ -359,25 +382,31 @@ const tagsComponent = () => {
}); });
}, },
onChange() { onChange() {
this.tags = $('.tag-select').select2('data').map(o => o.text); this.tags = $('.tag-select')
.select2('data')
.map((o) => o.text);
}, },
onAdd(event) { onAdd(event) {
const tag = event.params.data.text; const tag = event.params.data.text;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(
tag,
)}`;
this.request(url, 'PUT'); this.request(url, 'PUT');
}, },
onDelete(event) { onDelete(event) {
const tag = event.params.data.text; const tag = event.params.data.text;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(
tag,
)}`;
this.request(url, 'DELETE'); this.request(url, 'DELETE');
}, },
request(url, method, cb) { request(url, method, cb) {
$.ajax({ $.ajax({
url: url, url,
method: method, method,
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (data.success) { if (data.success) {
if (cb) cb(data); if (cb) cb(data);
} else { } else {
@@ -387,6 +416,6 @@ const tagsComponent = () => {
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
}); });
} },
}; };
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
$(() => { $(() => {
var target = base_url + 'admin/user/edit'; let target = base_url + 'admin/user/edit';
if (username) target += username; if (username) target += username;
$('form').attr('action', target); $('form').attr('action', target);
if (error) alert('danger', error); if (error) alert('danger', error);
+8 -7
View File
@@ -2,15 +2,16 @@ const remove = (username) => {
$.ajax({ $.ajax({
url: `${base_url}api/admin/user/delete/${username}`, url: `${base_url}api/admin/user/delete/${username}`,
type: 'DELETE', type: 'DELETE',
dataType: 'json' dataType: 'json',
}) })
.done(data => { .done((data) => {
if (data.success) if (data.success) location.reload();
location.reload(); else alert('danger', data.error);
else
alert('danger', data.error);
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to delete the user. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
'danger',
`Failed to delete the user. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
}); });
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
name: mango name: mango
version: 0.27.0 version: 0.27.1
authors: authors:
- Alex Ling <hkalexling@gmail.com> - Alex Ling <hkalexling@gmail.com>
+1 -1
View File
@@ -8,7 +8,7 @@ class Config
"session_secret" => "mango-session-secret", "session_secret" => "mango-session-secret",
"library_path" => "~/mango/library", "library_path" => "~/mango/library",
"library_cache_path" => "~/mango/library.yml.gz", "library_cache_path" => "~/mango/library.yml.gz",
"db_path" => "~/mango.db", "db_path" => "~/mango/mango.db",
"queue_db_path" => "~/mango/queue.db", "queue_db_path" => "~/mango/queue.db",
"scan_interval_minutes" => 5, "scan_interval_minutes" => 5,
"thumbnail_generation_interval_hours" => 24, "thumbnail_generation_interval_hours" => 24,
+1 -1
View File
@@ -7,7 +7,7 @@ require "option_parser"
require "clim" require "clim"
require "tallboy" require "tallboy"
MANGO_VERSION = "0.27.0" MANGO_VERSION = "0.27.1"
# From http://www.network-science.de/ascii/ # From http://www.network-science.de/ascii/
BANNER = %{ BANNER = %{
+1 -1
View File
@@ -184,7 +184,7 @@ def delete_cache_and_exit(path : String)
File.delete path File.delete path
Logger.fatal "Invalid library cache deleted. Mango needs to " \ Logger.fatal "Invalid library cache deleted. Mango needs to " \
"perform a full reset to recover from this. " \ "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" Logger.fatal "Exiting"
exit 1 exit 1
end end