Merge pull request #331 from getmango/feature/eslint

Add ESLint
This commit is contained in:
Alex Ling 2022-08-19 20:31:58 +08:00 committed by GitHub
commit 76b4666708
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1797 additions and 1677 deletions

11
.eslintrc.js Normal file
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
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}

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

View File

@ -5,13 +5,17 @@ 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
.pipe(gulp.dest('public/img')); .src('node_modules/uikit/src/images/backgrounds/*.svg')
.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
.pipe(gulp.dest('public/webfonts')); .src(
'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff**',
)
.pipe(gulp.dest('public/webfonts'));
}); });
// Copy files from node_modules // Copy files from node_modules
@ -19,49 +23,60 @@ 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(gulp.dest('public/css'));
.pipe(less())
.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'])
presets: [ .pipe(
['@babel/preset-env', { babel({
targets: '>0.25%, not dead, ios>=9' presets: [
}] [
], '@babel/preset-env',
})) {
.pipe(minify({ targets: '>0.25%, not dead, ios>=9',
removeConsole: true, },
builtIns: false ],
})) ],
.pipe(gulp.dest('dist/js')); }),
)
.pipe(
minify({
removeConsole: true,
builtIns: false,
}),
)
.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
.pipe(minifyCss()) .src('public/css/*.css')
.pipe(gulp.dest('dist/css')); .pipe(minifyCss())
.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
'public/*.*', .src(
'public/img/**', [
'public/webfonts/*', 'public/*.*',
'public/js/*.min.js' 'public/img/**',
], { 'public/webfonts/*',
base: 'public' 'public/js/*.min.js',
}) ],
.pipe(gulp.dest('dist')); {
base: 'public',
},
)
.pipe(gulp.dest('dist'));
}); });
// Set up the public folder for development // Set up the public folder for development

View File

@ -6,17 +6,22 @@
"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",

View File

@ -1,58 +1,56 @@
const component = () => { const component = () => {
return { return {
progress: 1.0, progress: 1.0,
generating: false, generating: false,
scanning: false, scanning: false,
scanTitles: 0, scanTitles: 0,
scanMs: -1, scanMs: -1,
themeSetting: '', themeSetting: '',
init() { init() {
this.getProgress(); this.getProgress();
setInterval(() => { setInterval(() => {
this.getProgress(); this.getProgress();
}, 5000); }, 5000);
const setting = loadThemeSetting(); const setting = loadThemeSetting();
this.themeSetting = setting.charAt(0).toUpperCase() + setting.slice(1); this.themeSetting = setting.charAt(0).toUpperCase() + setting.slice(1);
}, },
themeChanged(event) { themeChanged(event) {
const newSetting = $(event.currentTarget).val().toLowerCase(); const newSetting = $(event.currentTarget).val().toLowerCase();
saveThemeSetting(newSetting); saveThemeSetting(newSetting);
setTheme(); setTheme();
}, },
scan() { scan() {
if (this.scanning) return; if (this.scanning) return;
this.scanning = true; this.scanning = true;
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(() => {
this.scanning = false; this.scanning = false;
}); });
}, },
generateThumbnails() { generateThumbnails() {
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`).then((data) => {
$.get(`${base_url}api/admin/thumbnail_progress`) this.progress = data.progress;
.then(data => { this.generating = data.progress > 0;
this.progress = data.progress; });
this.generating = data.progress > 0; },
}); };
},
};
}; };

View File

@ -1,6 +1,6 @@
const alert = (level, text) => { 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 });
}; };

View File

@ -11,7 +11,7 @@
* @param {string} selector - The jQuery selector to the root element * @param {string} selector - The jQuery selector to the root element
*/ */
const setProp = (key, prop, selector = '#root') => { 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 * @return {*} The data property
*/ */
const getProp = (key, selector = '#root') => { 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} * @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
);
}; };
/** /**
@ -52,7 +55,7 @@ const preferDarkMode = () => {
* @return {bool} * @return {bool}
*/ */
const validThemeSetting = (theme) => { 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') * @return {string} A theme setting ('dark', 'light', or 'system')
*/ */
const loadThemeSetting = () => { const loadThemeSetting = () => {
let str = localStorage.getItem('theme'); let str = localStorage.getItem('theme');
if (!str || !validThemeSetting(str)) str = 'system'; if (!str || !validThemeSetting(str)) str = 'system';
return str; return str;
}; };
/** /**
@ -74,11 +77,11 @@ const loadThemeSetting = () => {
* @return {string} The current theme to use ('dark' or 'light') * @return {string} The current theme to use ('dark' or 'light')
*/ */
const loadTheme = () => { const loadTheme = () => {
let setting = loadThemeSetting(); let setting = loadThemeSetting();
if (setting === 'system') { if (setting === 'system') {
setting = preferDarkMode() ? 'dark' : 'light'; setting = preferDarkMode() ? 'dark' : 'light';
} }
return setting; return setting;
}; };
/** /**
@ -87,9 +90,9 @@ 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);
}; };
/** /**
@ -99,10 +102,10 @@ const saveThemeSetting = setting => {
* @function toggleTheme * @function toggleTheme
*/ */
const toggleTheme = () => { const toggleTheme = () => {
const theme = loadTheme(); const theme = loadTheme();
const newTheme = theme === 'dark' ? 'light' : 'dark'; const newTheme = theme === 'dark' ? 'light' : 'dark';
saveThemeSetting(newTheme); saveThemeSetting(newTheme);
setTheme(newTheme); setTheme(newTheme);
}; };
/** /**
@ -113,31 +116,32 @@ const toggleTheme = () => {
* `loadTheme` to get a theme and apply it. * `loadTheme` to get a theme and apply it.
*/ */
const setTheme = (theme) => { const setTheme = (theme) => {
if (!theme) theme = loadTheme(); if (!theme) theme = loadTheme();
if (theme === 'dark') { if (theme === 'dark') {
$('html').css('background', 'rgb(20, 20, 20)'); $('html').css('background', 'rgb(20, 20, 20)');
$('body').addClass('uk-light'); $('body').addClass('uk-light');
$('.ui-widget-content').addClass('dark'); $('.ui-widget-content').addClass('dark');
} else { } else {
$('html').css('background', ''); $('html').css('background', '');
$('body').removeClass('uk-light'); $('body').removeClass('uk-light');
$('.ui-widget-content').removeClass('dark'); $('.ui-widget-content').removeClass('dark');
} }
}; };
// do it before document is ready to prevent the initial flash of white on // do it before document is ready to prevent the initial flash of white on
// most pages // most pages
setTheme(); setTheme();
$(() => { $(() => {
// hack for the reader page // hack for the reader page
setTheme(); setTheme();
// 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)')
if (loadThemeSetting() === 'system') .addEventListener('change', (event) => {
setTheme(event.matches ? 'dark' : 'light'); if (loadThemeSetting() === 'system')
}); setTheme(event.matches ? 'dark' : 'light');
} });
}
}); });

View File

@ -5,22 +5,22 @@
* @param {object} e - The title element to truncate * @param {object} e - The title element to truncate
*/ */
const truncate = (e) => { const truncate = (e) => {
$(e).dotdotdot({ $(e).dotdotdot({
truncate: 'letter', truncate: 'letter',
watch: true, watch: true,
callback: (truncated) => { callback: (truncated) => {
if (truncated) { if (truncated) {
$(e).attr('uk-tooltip', $(e).attr('data-title')); $(e).attr('uk-tooltip', $(e).attr('data-title'));
} else { } else {
$(e).removeAttr('uk-tooltip'); $(e).removeAttr('uk-tooltip');
} }
} },
}); });
}; };
$('.uk-card-title').each((i, e) => { $('.uk-card-title').each((i, e) => {
// Truncate the title when it first enters the view // Truncate the title when it first enters the view
$(e).one('inview', () => { $(e).one('inview', () => {
truncate(e); truncate(e);
}); });
}); });

View File

@ -1,116 +1,135 @@
const component = () => { const component = () => {
return { return {
jobs: [], jobs: [],
paused: undefined, paused: undefined,
loading: false, loading: false,
toggling: false, toggling: false,
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'}://${
console.log(`Connecting to ${url}`); location.host
this.ws = new WebSocket(url); }${base_url}api/admin/mangadex/queue`;
this.ws.onmessage = event => { console.log(`Connecting to ${url}`);
const data = JSON.parse(event.data); this.ws = new WebSocket(url);
this.jobs = data.jobs; this.ws.onmessage = (event) => {
this.paused = data.paused; const data = JSON.parse(event.data);
}; this.jobs = data.jobs;
this.ws.onclose = () => { this.paused = data.paused;
if (this.ws.failed) };
return this.wsConnect(false); this.ws.onclose = () => {
alert('danger', 'Socket connection closed'); if (this.ws.failed) return this.wsConnect(false);
}; alert('danger', 'Socket connection closed');
this.ws.onerror = () => { };
if (secure) this.ws.onerror = () => {
return this.ws.failed = true; if (secure) return (this.ws.failed = true);
alert('danger', 'Socket connection failed'); alert('danger', 'Socket connection failed');
}; };
}, },
init() { init() {
this.wsConnect(); this.wsConnect();
this.load(); this.load();
}, },
load() { load() {
this.loading = true; this.loading = true;
$.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(
return; 'danger',
} `Failed to fetch download queue. Error: ${data.error}`,
this.jobs = data.jobs; );
this.paused = data.paused; return;
}) }
.fail((jqXHR, status) => { this.jobs = data.jobs;
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); this.paused = data.paused;
}) })
.always(() => { .fail((jqXHR, status) => {
this.loading = false; alert(
}); 'danger',
}, `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
jobAction(action, event) { );
let url = `${base_url}api/admin/mangadex/queue/${action}`; })
if (event) { .always(() => {
const id = event.currentTarget.closest('tr').id.split('-').slice(1).join('-'); this.loading = false;
url = `${url}?${$.param({ });
id: id },
})}`; jobAction(action, event) {
} let url = `${base_url}api/admin/mangadex/queue/${action}`;
console.log(url); if (event) {
$.ajax({ const id = event.currentTarget
type: 'POST', .closest('tr')
url: url, .id.split('-')
dataType: 'json' .slice(1)
}) .join('-');
.done(data => { url = `${url}?${$.param({
if (!data.success && data.error) { id,
alert('danger', `Failed to ${action} job from download queue. Error: ${data.error}`); })}`;
return; }
} console.log(url);
this.load(); $.ajax({
}) type: 'POST',
.fail((jqXHR, status) => { url,
alert('danger', `Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); dataType: 'json',
}); })
}, .done((data) => {
toggle() { if (!data.success && data.error) {
this.toggling = true; alert(
const action = this.paused ? 'resume' : 'pause'; 'danger',
const url = `${base_url}api/admin/mangadex/queue/${action}`; `Failed to ${action} job from download queue. Error: ${data.error}`,
$.ajax({ );
type: 'POST', return;
url: url, }
dataType: 'json' this.load();
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert(
}) 'danger',
.always(() => { `Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
this.load(); );
this.toggling = false; });
}); },
}, toggle() {
statusClass(status) { this.toggling = true;
let cls = 'label '; const action = this.paused ? 'resume' : 'pause';
switch (status) { const url = `${base_url}api/admin/mangadex/queue/${action}`;
case 'Pending': $.ajax({
cls += 'label-pending'; type: 'POST',
break; url,
case 'Completed': dataType: 'json',
cls += 'label-success'; })
break; .fail((jqXHR, status) => {
case 'Error': alert(
cls += 'label-danger'; 'danger',
break; `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
case 'MissingPages': );
cls += 'label-warning'; })
break; .always(() => {
} this.load();
return cls; 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;
},
};
}; };

View File

@ -1,60 +1,74 @@
const component = () => { const component = () => {
return { return {
empty: true, empty: true,
titles: [], titles: [],
entries: [], entries: [],
loading: true, loading: true,
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;
}); });
}); });
}, },
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/${
this.request('DELETE', url, () => { type === 'title' ? 'titles' : 'entries'
this.load(); }/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: { rmAll() {
ok: 'Yes, delete them', UIkit.modal
cancel: 'Cancel' .confirm(
} 'Are you sure? All metadata associated with these items, including their tags and thumbnails, will be deleted from the database.',
}).then(() => { {
this.request('DELETE', `${base_url}api/admin/titles/missing`, () => { labels: {
this.request('DELETE', `${base_url}api/admin/entries/missing`, () => { ok: 'Yes, delete them',
this.load(); cancel: 'Cancel',
}); },
}); },
}); )
}, .then(() => {
request(method, url, cb) { this.request('DELETE', `${base_url}api/admin/titles/missing`, () => {
console.log(url); this.request(
$.ajax({ 'DELETE',
type: method, `${base_url}api/admin/entries/missing`,
url: url, () => {
contentType: 'application/json' this.load();
}) },
.done(data => { );
if (data.error) { });
alert('danger', `Failed to ${method} ${url}. Error: ${data.error}`); });
return; },
} request(method, url, cb) {
if (cb) cb(data); console.log(url);
}) $.ajax({
.fail((jqXHR, status) => { type: method,
alert('danger', `Failed to ${method} ${url}. Error: [${jqXHR.status}] ${jqXHR.statusText}`); 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}`,
);
});
},
};
}; };

View File

@ -1,452 +1,435 @@
const component = () => { const component = () => {
return { return {
plugins: [], plugins: [],
subscribable: false, subscribable: false,
info: undefined, info: undefined,
pid: undefined, pid: undefined,
chapters: undefined, // undefined: not searched yet, []: empty chapters: undefined, // undefined: not searched yet, []: empty
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: [],
showFilters: false, showFilters: false,
appliedFilters: [], appliedFilters: [],
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,
}); });
fetch(`${base_url}api/admin/plugin`) fetch(`${base_url}api/admin/plugin`)
.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);
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('danger', `Failed to list the available plugins. Error: ${e}`);
alert( });
"danger", },
`Failed to list the available plugins. Error: ${e}` loadPlugin(pid) {
); fetch(
}); `${base_url}api/admin/plugin/info?${new URLSearchParams({
}, plugin: pid,
loadPlugin(pid) { })}`,
fetch( )
`${base_url}api/admin/plugin/info?${new URLSearchParams({ .then((res) => res.json())
plugin: pid, .then((data) => {
})}` if (!data.success) throw new Error(data.error);
) this.info = data.info;
.then((res) => res.json()) this.subscribable = data.subscribable;
.then((data) => { this.pid = pid;
if (!data.success) throw new Error(data.error); })
this.info = data.info; .catch((e) => {
this.subscribable = data.subscribable; alert('danger', `Failed to get plugin metadata. Error: ${e}`);
this.pid = pid; });
}) },
.catch((e) => { pluginChanged() {
alert( this.manga = undefined;
"danger", this.chapters = undefined;
`Failed to get plugin metadata. Error: ${e}` this.mid = undefined;
); this.loadPlugin(this.pid);
}); localStorage.setItem('plugin', this.pid);
}, },
pluginChanged() { get chapterKeys() {
this.manga = undefined; if (this.allChapters.length < 1) return [];
this.chapters = undefined; return Object.keys(this.allChapters[0]).filter(
this.mid = undefined; (k) => !['manga_title'].includes(k),
this.loadPlugin(this.pid); );
localStorage.setItem("plugin", this.pid); },
}, searchChapters(query) {
get chapterKeys() { this.searching = true;
if (this.allChapters.length < 1) return []; this.allChapters = [];
return Object.keys(this.allChapters[0]).filter( this.sortOptions = [];
(k) => !["manga_title"].includes(k) this.chapters = undefined;
); this.listManga = false;
}, fetch(
searchChapters(query) { `${base_url}api/admin/plugin/list?${new URLSearchParams({
this.searching = true; plugin: this.pid,
this.allChapters = []; query,
this.sortOptions = []; })}`,
this.chapters = undefined; )
this.listManga = false; .then((res) => res.json())
fetch( .then((data) => {
`${base_url}api/admin/plugin/list?${new URLSearchParams({ if (!data.success) throw new Error(data.error);
plugin: this.pid, try {
query: query, this.mangaTitle = data.chapters[0].manga_title;
})}` if (!this.mangaTitle) throw new Error();
) } catch (e) {
.then((res) => res.json()) this.mangaTitle = data.title;
.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.allChapters = data.chapters;
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;
}); });
}, },
searchManga(query) { searchManga(query) {
this.searching = true; this.searching = true;
this.allChapters = []; this.allChapters = [];
this.chapters = undefined; this.chapters = undefined;
this.manga = undefined; this.manga = undefined;
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) => {
if (!data.success) throw new Error(data.error); if (!data.success) throw new Error(data.error);
this.manga = data.manga; this.manga = data.manga;
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;
}); });
}, },
search() { search() {
const query = this.query.trim(); const query = this.query.trim();
if (!query) return; if (!query) return;
this.manga = undefined; this.manga = undefined;
this.mid = undefined; this.mid = undefined;
if (this.info.version === 1) { if (this.info.version === 1) {
this.searchChapters(query); this.searchChapters(query);
} else { } else {
this.searchManga(query); this.searchManga(query);
} }
}, },
selectAll() { selectAll() {
$("tbody > tr").each((i, e) => { $('tbody > tr').each((i, e) => {
$(e).addClass("ui-selected"); $(e).addClass('ui-selected');
}); });
}, },
clearSelection() { clearSelection() {
$("tbody > tr").each((i, e) => { $('tbody > tr').each((i, e) => {
$(e).removeClass("ui-selected"); $(e).removeClass('ui-selected');
}); });
}, },
download() { download() {
const selected = $("tbody > tr.ui-selected").get(); const selected = $('tbody > 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);
); this.adding = true;
console.log(chapters); fetch(`${base_url}api/admin/plugin/download`, {
this.adding = true; method: 'POST',
fetch(`${base_url}api/admin/plugin/download`, { body: JSON.stringify({
method: "POST", chapters,
body: JSON.stringify({ plugin: this.pid,
chapters, title: this.mangaTitle,
plugin: this.pid, }),
title: this.mangaTitle, headers: {
}), 'Content-Type': 'application/json',
headers: { },
"Content-Type": "application/json", })
}, .then((res) => res.json())
}) .then((data) => {
.then((res) => res.json()) if (!data.success) throw new Error(data.error);
.then((data) => { const successCount = parseInt(data.success);
if (!data.success) throw new Error(data.error); const failCount = parseInt(data.fail);
const successCount = parseInt(data.success); alert(
const failCount = parseInt(data.fail); 'success',
alert( `${successCount} of ${
"success", successCount + failCount
`${successCount} of ${ } 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>.`,
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>.` })
); .catch((e) => {
}) alert(
.catch((e) => { 'danger',
alert( `Failed to add chapters to the download queue. Error: ${e}`,
"danger", );
`Failed to add chapters to the download queue. Error: ${e}` })
); .finally(() => {
}) this.adding = false;
.finally(() => { });
this.adding = false; });
}); },
}); thClicked(event) {
}, const idx = parseInt(event.currentTarget.id.split('-')[1]);
thClicked(event) { if (idx === undefined || isNaN(idx)) return;
const idx = parseInt(event.currentTarget.id.split("-")[1]); const curOption = this.sortOptions[idx];
if (idx === undefined || isNaN(idx)) return; let option;
const curOption = this.sortOptions[idx]; this.sortOptions = [];
let option; switch (curOption) {
this.sortOptions = []; case 1:
switch (curOption) { option = -1;
case 1: break;
option = -1; case -1:
break; option = 0;
case -1: break;
option = 0; default:
break; option = 1;
default: }
option = 1; this.sortOptions[idx] = option;
} this.sort(this.chapterKeys[idx], option);
this.sortOptions[idx] = option; },
this.sort(this.chapterKeys[idx], option); // Returns an array of filtered but unsorted chapters. Useful when
}, // reseting the sort options.
// Returns an array of filtered but unsorted chapters. Useful when get filteredChapters() {
// reseting the sort options. let ary = this.allChapters.slice();
get filteredChapters() {
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) => ary = ary.filter((ch) =>
ch[filter.key] ch[filter.key].toLowerCase().includes(filter.value.toLowerCase()),
.toLowerCase() );
.includes(filter.value.toLowerCase()) }
); if (filter.type === 'number-min') {
} ary = ary.filter(
if (filter.type === "number-min") { (ch) => Number(ch[filter.key]) >= Number(filter.value),
ary = ary.filter( );
(ch) => Number(ch[filter.key]) >= Number(filter.value) }
); if (filter.type === 'number-max') {
} ary = ary.filter(
if (filter.type === "number-max") { (ch) => Number(ch[filter.key]) <= Number(filter.value),
ary = ary.filter( );
(ch) => Number(ch[filter.key]) <= Number(filter.value) }
); if (filter.type === 'date-min') {
} ary = ary.filter(
if (filter.type === "date-min") { (ch) => Number(ch[filter.key]) >= Number(filter.value),
ary = ary.filter( );
(ch) => Number(ch[filter.key]) >= Number(filter.value) }
); if (filter.type === 'date-max') {
} ary = ary.filter(
if (filter.type === "date-max") { (ch) => Number(ch[filter.key]) <= Number(filter.value),
ary = ary.filter( );
(ch) => Number(ch[filter.key]) <= Number(filter.value) }
); if (filter.type === 'array') {
} ary = ary.filter((ch) =>
if (filter.type === "array") { ch[filter.key]
ary = ary.filter((ch) => .map((s) => (typeof s === 'string' ? s.toLowerCase() : s))
ch[filter.key] .includes(filter.value.toLowerCase()),
.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;
}, },
// option: // option:
// - 1: asending // - 1: asending
// - -1: desending // - -1: desending
// - 0: unsorted // - 0: unsorted
sort(key, option) { sort(key, option) {
if (option === 0) { if (option === 0) {
this.chapters = this.filteredChapters; this.chapters = this.filteredChapters;
return; return;
} }
this.chapters = this.filteredChapters.sort((a, b) => { this.chapters = this.filteredChapters.sort((a, b) => {
const comp = this.compare(a[key], b[key]); const comp = this.compare(a[key], b[key]);
return option < 0 ? comp * -1 : comp; return option < 0 ? comp * -1 : comp;
}); });
}, },
compare(a, b) { compare(a, b) {
if (a === b) return 0; if (a === b) return 0;
// try numbers (also covers dates) // try numbers (also covers dates)
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,
}; };
}); });
}, },
applyFilters() { applyFilters() {
this.appliedFilters = this.filterSettings; this.appliedFilters = this.filterSettings;
this.chapters = this.filteredChapters; this.chapters = this.filteredChapters;
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,
name: this.subscriptionName.trim(), name: this.subscriptionName.trim(),
manga: this.mangaTitle, manga: this.mangaTitle,
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;
UIkit.modal(modal).hide(); UIkit.modal(modal).hide();
}); });
}, },
numIsDate(num) { numIsDate(num) {
return !isNaN(num) && Number(num) > 328896000000; // 328896000000 => 1 Jan, 1980 return !isNaN(num) && Number(num) > 328896000000; // 328896000000 => 1 Jan, 1980
}, },
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" const maxLength = 40;
)}</span>`; if (value && value.length > maxLength)
const maxLength = 40; return `<span>${value.substr(
if (value && value.length > maxLength) 0,
return `<span>${value.substr( maxLength,
0, )}...</span><div uk-dropdown>${value}</div>`;
maxLength return `<span>${value}</span>`;
)}...</span><div uk-dropdown>${value}</div>`; },
return `<span>${value}</span>`; renderFilterRow(ft) {
}, const key = ft.key;
renderFilterRow(ft) { let type = ft.type;
const key = ft.key; switch (type) {
let type = ft.type; case 'number-min':
switch (type) { type = 'number (minimum value)';
case "number-min": break;
type = "number (minimum value)"; case 'number-max':
break; type = 'number (maximum value)';
case "number-max": break;
type = "number (maximum value)"; case 'date-min':
break; type = 'minimum date';
case "date-min": break;
type = "minimum date"; case 'date-max':
break; type = 'maximum date';
case "date-max": break;
type = "maximum date"; }
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>`;
}, },
}; };
}; };

View File

@ -1,361 +1,370 @@
const readerComponent = () => { const readerComponent = () => {
return { return {
loading: true, loading: true,
mode: 'continuous', // Can be 'continuous', 'height' or 'width' mode: 'continuous', // Can be 'continuous', 'height' or 'width'
msg: 'Loading the web reader. Please wait...', msg: 'Loading the web reader. Please wait...',
alertClass: 'uk-alert-primary', alertClass: 'uk-alert-primary',
items: [], items: [],
curItem: {}, curItem: {},
enableFlipAnimation: true, enableFlipAnimation: true,
flipAnimation: null, flipAnimation: null,
longPages: false, longPages: false,
lastSavedPage: page, lastSavedPage: page,
selectedIndex: 0, // 0: not selected; 1: the first page selectedIndex: 0, // 0: not selected; 1: the first page
margin: 30, margin: 30,
preloadLookahead: 3, preloadLookahead: 3,
enableRightToLeft: false, enableRightToLeft: false,
fitType: 'vert', fitType: 'vert',
/** /**
* Initialize the component by fetching the page dimensions * Initialize the component by fetching the page dimensions
*/ */
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) => {
}, 0) / dimensions.length; return acc + cur.height / cur.width;
}, 0) / dimensions.length;
console.log(avgRatio); console.log(avgRatio);
this.longPages = avgRatio > 2; this.longPages = avgRatio > 2;
this.loading = false; this.loading = false;
this.mode = localStorage.getItem('mode') || 'continuous'; this.mode = localStorage.getItem('mode') || 'continuous';
// Here we save a copy of this.mode, and use the copy as // Here we save a copy of this.mode, and use the copy as
// the model-select value. This is because `updateMode` // the model-select value. This is because `updateMode`
// might change this.mode and make it `height` or `width`, // might change this.mode and make it `height` or `width`,
// which are not available in mode-select // which are not available in mode-select
const mode = this.mode; const mode = this.mode;
this.updateMode(this.mode, page, nextTick); this.updateMode(this.mode, page, nextTick);
$('#mode-select').val(mode); $('#mode-select').val(mode);
const savedMargin = localStorage.getItem('margin'); const savedMargin = localStorage.getItem('margin');
if (savedMargin) { if (savedMargin) {
this.margin = savedMargin; this.margin = savedMargin;
} }
// 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
for (let idx = page + 1; idx <= limit; idx++) { );
this.preloadImage(this.items[idx - 1].url); 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'); const savedFitType = localStorage.getItem('fitType');
if (savedFitType) { if (savedFitType) {
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
*/ */
pageChanged() { pageChanged() {
const p = parseInt($('#page-select').val()); const p = parseInt($('#page-select').val());
this.toPage(p); this.toPage(p);
}, },
/** /**
* Handles the `change` event for the mode selector * Handles the `change` event for the mode selector
* *
* @param {function} nextTick - Alpine $nextTick magic property * @param {function} nextTick - Alpine $nextTick magic property
*/ */
modeChanged(nextTick) { modeChanged(nextTick) {
const mode = $('#mode-select').val(); const mode = $('#mode-select').val();
const curIdx = parseInt($('#page-select').val()); const curIdx = parseInt($('#page-select').val());
this.updateMode(mode, curIdx, nextTick); this.updateMode(mode, curIdx, nextTick);
}, },
/** /**
* Handles the window `resize` event * Handles the window `resize` event
*/ */
resized() { resized() {
if (this.mode === 'continuous') return; if (this.mode === 'continuous') return;
const wideScreen = $(window).width() > $(window).height(); const wideScreen = $(window).width() > $(window).height();
this.mode = wideScreen ? 'height' : 'width'; this.mode = wideScreen ? 'height' : 'width';
}, },
/** /**
* Handles the window `keydown` event * Handles the window `keydown` event
* *
* @param {Event} event - The triggering event * @param {Event} event - The triggering event
*/ */
keyHandler(event) { keyHandler(event) {
if (this.mode === 'continuous') return; if (this.mode === 'continuous') return;
if (event.key === 'ArrowLeft' || event.key === 'k') if (event.key === 'ArrowLeft' || event.key === 'k')
this.flipPage(false ^ this.enableRightToLeft); this.flipPage(false ^ this.enableRightToLeft);
if (event.key === 'ArrowRight' || event.key === 'j') if (event.key === 'ArrowRight' || event.key === 'j')
this.flipPage(true ^ this.enableRightToLeft); this.flipPage(true ^ this.enableRightToLeft);
}, },
/** /**
* Flips to the next or the previous page * Flips to the next or the previous page
* *
* @param {bool} isNext - Whether we are going to the next page * @param {bool} isNext - Whether we are going to the next page
*/ */
flipPage(isNext) { flipPage(isNext) {
const idx = parseInt(this.curItem.id); const idx = parseInt(this.curItem.id);
const newIdx = idx + (isNext ? 1 : -1); const newIdx = idx + (isNext ? 1 : -1);
if (newIdx <= 0) return; if (newIdx <= 0) return;
if (newIdx > this.items.length) { if (newIdx > this.items.length) {
this.showControl(idx); this.showControl(idx);
return; return;
} }
if (newIdx + this.preloadLookahead < this.items.length + 1) { if (newIdx + this.preloadLookahead < this.items.length + 1) {
this.preloadImage(this.items[newIdx + this.preloadLookahead - 1].url); this.preloadImage(this.items[newIdx + this.preloadLookahead - 1].url);
} }
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(() => {
this.flipAnimation = null; this.flipAnimation = null;
}, 500); }, 500);
this.replaceHistory(newIdx); this.replaceHistory(newIdx);
}, },
/** /**
* Jumps to a specific page * Jumps to a specific page
* *
* @param {number} idx - One-based index of the page * @param {number} idx - One-based index of the page
*/ */
toPage(idx) { toPage(idx) {
if (this.mode === 'continuous') { if (this.mode === 'continuous') {
$(`#${idx}`).get(0).scrollIntoView(true); $(`#${idx}`).get(0).scrollIntoView(true);
} else { } else {
if (idx >= 1 && idx <= this.items.length) { if (idx >= 1 && idx <= this.items.length) {
this.curItem = this.items[idx - 1]; this.curItem = this.items[idx - 1];
} }
} }
this.replaceHistory(idx); this.replaceHistory(idx);
UIkit.modal($('#modal-sections')).hide(); UIkit.modal($('#modal-sections')).hide();
}, },
/** /**
* Replace the address bar history and save the reading progress if necessary * Replace the address bar history and save the reading progress if necessary
* *
* @param {number} idx - One-based index of the page * @param {number} idx - One-based index of the page
*/ */
replaceHistory(idx) { replaceHistory(idx) {
const ary = window.location.pathname.split('/'); const ary = window.location.pathname.split('/');
ary[ary.length - 1] = idx; ary[ary.length - 1] = idx;
ary.shift(); // remove leading `/` ary.shift(); // remove leading `/`
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:
* 1) the current page is more than five pages away from the last * 1) the current page is more than five pages away from the last
* saved page, or * saved page, or
* 2) the average height/width ratio of the pages is over 2, or * 2) the average height/width ratio of the pages is over 2, or
* 3) the current page is the first page, or * 3) the current page is the first page, or
* 4) the current page is the last page * 4) the current page is the last page
* *
* @param {number} idx - One-based index of the page * @param {number} idx - One-based index of the page
* @param {function} cb - Callback * @param {function} cb - Callback
*/ */
saveProgress(idx, cb) { saveProgress(idx, cb) {
idx = parseInt(idx); idx = parseInt(idx);
if (Math.abs(idx - this.lastSavedPage) >= 5 || if (
this.longPages || Math.abs(idx - this.lastSavedPage) >= 5 ||
idx === 1 || idx === this.items.length this.longPages ||
) { idx === 1 ||
this.lastSavedPage = idx; idx === this.items.length
console.log('saving progress', idx); ) {
this.lastSavedPage = 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({
$.ajax({ eid,
method: 'PUT', })}`;
url: url, $.ajax({
dataType: 'json' method: 'PUT',
}) url,
.done(data => { dataType: 'json',
if (data.error) })
alert('danger', data.error); .done((data) => {
if (cb) cb(); if (data.error) alert('danger', data.error);
}) if (cb) cb();
.fail((jqXHR, status) => { })
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); .fail((jqXHR, status) => {
}); alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
} });
}, }
/** },
* Updates the reader mode /**
* * Updates the reader mode
* @param {string} mode - Either `continuous` or `paged` *
* @param {number} targetPage - The one-based index of the target page * @param {string} mode - Either `continuous` or `paged`
* @param {function} nextTick - Alpine $nextTick magic property * @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); updateMode(mode, targetPage, nextTick) {
localStorage.setItem('mode', mode);
// The mode to be put into the `mode` prop. It can't be `screen` // The mode to be put into the `mode` prop. It can't be `screen`
let propMode = mode; let propMode = mode;
if (mode === 'paged') { if (mode === 'paged') {
const wideScreen = $(window).width() > $(window).height(); const wideScreen = $(window).width() > $(window).height();
propMode = wideScreen ? 'height' : 'width'; propMode = wideScreen ? 'height' : 'width';
} }
this.mode = propMode; this.mode = propMode;
if (mode === 'continuous') { if (mode === 'continuous') {
nextTick(() => { nextTick(() => {
this.setupScroller(); this.setupScroller();
}); });
} }
nextTick(() => { nextTick(() => {
this.toPage(targetPage); this.toPage(targetPage);
}); });
}, },
/** /**
* Handles clicked image * Handles clicked image
* *
* @param {Event} event - The triggering event * @param {Event} event - The triggering event
*/ */
clickImage(event) { clickImage(event) {
const idx = event.currentTarget.id; const idx = event.currentTarget.id;
this.showControl(idx); this.showControl(idx);
}, },
/** /**
* Shows the control modal * Shows the control modal
* *
* @param {number} idx - selected page index * @param {number} idx - selected page index
*/ */
showControl(idx) { showControl(idx) {
this.selectedIndex = idx; this.selectedIndex = idx;
UIkit.modal($('#modal-sections')).show(); UIkit.modal($('#modal-sections')).show();
}, },
/** /**
* Redirects to a URL * Redirects to a URL
* *
* @param {string} url - The target URL * @param {string} url - The target URL
*/ */
redirect(url) { redirect(url) {
window.location.replace(url); window.location.replace(url);
}, },
/** /**
* Set up the scroll handler that calls `replaceHistory` when an image * Set up the scroll handler that calls `replaceHistory` when an image
* enters the view port * enters the view port
*/ */
setupScroller() { setupScroller() {
if (this.mode !== 'continuous') return; if (this.mode !== 'continuous') return;
$('img').each((idx, el) => { $('img').each((idx, el) => {
$(el).on('inview', (event, inView) => { $(el).on('inview', (event, inView) => {
if (inView) { if (inView) {
const current = $(event.currentTarget).attr('id'); const current = $(event.currentTarget).attr('id');
this.curItem = this.items[current - 1]; this.curItem = this.items[current - 1];
this.replaceHistory(current); this.replaceHistory(current);
} }
}); });
}); });
}, },
/** /**
* Marks progress as 100% and jumps to the next entry * Marks progress as 100% and jumps to the next entry
* *
* @param {string} nextUrl - URL of the next entry * @param {string} nextUrl - URL of the next entry
*/ */
nextEntry(nextUrl) { nextEntry(nextUrl) {
this.saveProgress(this.items.length, () => { this.saveProgress(this.items.length, () => {
this.redirect(nextUrl); this.redirect(nextUrl);
}); });
}, },
/** /**
* Exits the reader, and sets the reading progress tp 100% * Exits the reader, and sets the reading progress tp 100%
* *
* @param {string} exitUrl - The Exit URL * @param {string} exitUrl - The Exit URL
*/ */
exitReader(exitUrl) { exitReader(exitUrl) {
this.saveProgress(this.items.length, () => { this.saveProgress(this.items.length, () => {
this.redirect(exitUrl); this.redirect(exitUrl);
}); });
}, },
/** /**
* Handles the `change` event for the entry selector * Handles the `change` event for the entry selector
*/ */
entryChanged() { entryChanged() {
const id = $('#entry-select').val(); const id = $('#entry-select').val();
this.redirect(`${base_url}reader/${tid}/${id}`); this.redirect(`${base_url}reader/${tid}/${id}`);
}, },
marginChanged() { marginChanged() {
localStorage.setItem('margin', this.margin); localStorage.setItem('margin', this.margin);
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);
}, },
preloadLookaheadChanged() { preloadLookaheadChanged() {
localStorage.setItem('preloadLookahead', this.preloadLookahead); localStorage.setItem('preloadLookahead', this.preloadLookahead);
}, },
enableFlipAnimationChanged() { enableFlipAnimationChanged() {
localStorage.setItem('enableFlipAnimation', this.enableFlipAnimation); localStorage.setItem('enableFlipAnimation', this.enableFlipAnimation);
}, },
enableRightToLeftChanged() { enableRightToLeftChanged() {
localStorage.setItem('enableRightToLeft', this.enableRightToLeft); localStorage.setItem('enableRightToLeft', this.enableRightToLeft);
}, },
}; };
} };

View File

@ -1,30 +1,28 @@
$(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 {
} $(this).attr('hidden', '');
else { }
$(this).attr('hidden', ''); });
} }
}); });
}
});
}); });

View File

@ -1,15 +1,15 @@
$(() => { $(() => {
$('#sort-select').change(() => { $('#sort-select').change(() => {
const sort = $('#sort-select').find(':selected').attr('id'); const sort = $('#sort-select').find(':selected').attr('id');
const ary = sort.split('-'); const ary = sort.split('-');
const by = ary[0]; const by = ary[0];
const dir = ary[1]; const dir = ary[1];
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;
}); });
}); });

View File

@ -1,147 +1,144 @@
const component = () => { const component = () => {
return { return {
subscriptions: [], subscriptions: [],
plugins: [], plugins: [],
pid: undefined, pid: undefined,
subscription: undefined, // selected subscription subscription: undefined, // selected subscription
loading: false, loading: false,
init() { init() {
fetch(`${base_url}api/admin/plugin`) fetch(`${base_url}api/admin/plugin`)
.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);
this.plugins = data.plugins; this.plugins = data.plugins;
let pid = localStorage.getItem("plugin"); let pid = localStorage.getItem('plugin');
if (!pid || !this.plugins.find((p) => p.id === pid)) { if (!pid || !this.plugins.find((p) => p.id === pid)) {
pid = this.plugins[0].id; pid = this.plugins[0].id;
} }
this.pid = pid; 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() {
}); localStorage.setItem('plugin', this.pid);
}, this.list(this.pid);
pluginChanged() { },
localStorage.setItem("plugin", this.pid); list(pid) {
this.list(this.pid); if (!pid) return;
}, fetch(
list(pid) { `${base_url}api/admin/plugin/subscriptions?${new URLSearchParams({
if (!pid) return; plugin: pid,
fetch( })}`,
`${base_url}api/admin/plugin/subscriptions?${new URLSearchParams( {
{ method: 'GET',
plugin: pid, },
} )
)}`, .then((response) => response.json())
{ .then((data) => {
method: "GET", if (!data.success) throw new Error(data.error);
} this.subscriptions = data.subscriptions;
) })
.then((response) => response.json()) .catch((e) => {
.then((data) => { alert('danger', `Failed to list subscriptions. Error: ${e}`);
if (!data.success) throw new Error(data.error); });
this.subscriptions = data.subscriptions; },
}) renderStrCell(str) {
.catch((e) => { const maxLength = 40;
alert( if (str.length > maxLength)
"danger", return `<td><span>${str.substring(
`Failed to list subscriptions. Error: ${e}` 0,
); maxLength,
}); )}...</span><div uk-dropdown>${str}</div></td>`;
}, return `<td>${str}</td>`;
renderStrCell(str) { },
const maxLength = 40; renderDateCell(timestamp) {
if (str.length > maxLength) return `<td>${moment
return `<td><span>${str.substring( .duration(moment.unix(timestamp).diff(moment()))
0, .humanize(true)}</td>`;
maxLength },
)}...</span><div uk-dropdown>${str}</div></td>`; selected(event, modal) {
return `<td>${str}</td>`; const id = event.currentTarget.getAttribute('sid');
}, this.subscription = this.subscriptions.find((s) => s.id === id);
renderDateCell(timestamp) { UIkit.modal(modal).show();
return `<td>${moment },
.duration(moment.unix(timestamp).diff(moment())) renderFilterRow(ft) {
.humanize(true)}</td>`; const key = ft.key;
}, let type = ft.type;
selected(event, modal) { switch (type) {
const id = event.currentTarget.getAttribute("sid"); case 'number-min':
this.subscription = this.subscriptions.find((s) => s.id === id); type = 'number (minimum value)';
UIkit.modal(modal).show(); break;
}, case 'number-max':
renderFilterRow(ft) { type = 'number (maximum value)';
const key = ft.key; break;
let type = ft.type; case 'date-min':
switch (type) { type = 'minimum date';
case "number-min": break;
type = "number (minimum value)"; case 'date-max':
break; type = 'maximum date';
case "number-max": break;
type = "number (maximum value)"; }
break; let value = ft.value;
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 = ""; 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
labels: { .confirm(
ok: 'Yes, delete it', 'Are you sure you want to delete the subscription? This cannot be undone.',
cancel: 'Cancel' {
} labels: {
}).then(() => { ok: 'Yes, delete it',
this.action(id, type); cancel: 'Cancel',
}); },
}, },
action(id, type) { )
if (this.loading) return; .then(() => {
this.loading = true; this.action(id, type);
fetch( });
`${base_url}api/admin/plugin/subscriptions${type === 'update' ? '/update' : ''}?${new URLSearchParams( },
{ action(id, type) {
plugin: this.pid, if (this.loading) return;
subscription: id, this.loading = true;
} fetch(
)}`, `${base_url}api/admin/plugin/subscriptions${
{ type === 'update' ? '/update' : ''
method: type === 'delete' ? "DELETE" : 'POST' }?${new URLSearchParams({
} plugin: this.pid,
) subscription: id,
.then((response) => response.json()) })}`,
.then((data) => { {
if (!data.success) throw new Error(data.error); method: type === 'delete' ? 'DELETE' : 'POST',
if (type === 'update') },
alert("success", `Checking updates for subscription ${id}. Check the log for the progress or come back to this page later.`); )
}) .then((response) => response.json())
.catch((e) => { .then((data) => {
alert( if (!data.success) throw new Error(data.error);
"danger", if (type === 'update')
`Failed to ${type} subscription. Error: ${e}` alert(
); 'success',
}) `Checking updates for subscription ${id}. Check the log for the progress or come back to this page later.`,
.finally(() => { );
this.loading = false; })
this.list(this.pid); .catch((e) => {
}); alert('danger', `Failed to ${type} subscription. Error: ${e}`);
}, })
}; .finally(() => {
this.loading = false;
this.list(this.pid);
});
},
};
}; };

View File

@ -1,82 +1,112 @@
const component = () => { const component = () => {
return { return {
available: undefined, available: undefined,
subscriptions: [], subscriptions: [],
init() { init() {
$.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(
return; 'danger',
} 'Failed to check MangaDex integration status. Error: ' +
this.available = Boolean(data.expires && data.expires > Math.floor(Date.now() / 1000)); data.error,
);
return;
}
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(
return; 'danger',
} 'Failed to get subscriptions. Error: ' + data.error,
this.subscriptions = data.subscriptions; );
}) return;
.fail((jqXHR, status) => { }
alert('danger', `Failed to get subscriptions. Error: [${jqXHR.status}] ${jqXHR.statusText}`); this.subscriptions = data.subscriptions;
}) })
}, .fail((jqXHR, status) => {
alert(
'danger',
`Failed to get subscriptions. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
});
},
rm(event) { rm(event) {
const id = event.currentTarget.parentNode.getAttribute('data-id'); const id = event.currentTarget.parentNode.getAttribute('data-id');
$.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',
this.getSubscriptions(); `Failed to delete subscription. Error: ${data.error}`,
}) );
.fail((jqXHR, status) => { }
alert('danger', `Failed to delete subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`); this.getSubscriptions();
}); })
}, .fail((jqXHR, status) => {
alert(
'danger',
`Failed to delete subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
});
},
check(event) { check(event) {
const id = event.currentTarget.parentNode.getAttribute('data-id'); const id = event.currentTarget.parentNode.getAttribute('data-id');
$.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(
return; 'danger',
} `Failed to check subscription. Error: ${data.error}`,
alert('success', 'Mango is now checking the subscription for updates. This might take a while, but you can safely leave the page.'); );
}) return;
.fail((jqXHR, status) => { }
alert('danger', `Failed to check subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`); 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) { formatRange(min, max) {
if (!isNaN(min) && isNaN(max)) return `${min}`; if (!isNaN(min) && isNaN(max)) return `${min}`;
if (isNaN(min) && !isNaN(max)) return `${max}`; if (isNaN(min) && !isNaN(max)) return `${max}`;
if (isNaN(min) && isNaN(max)) return 'All'; if (isNaN(min) && isNaN(max)) return 'All';
if (min === max) return `= ${min}`; if (min === max) return `= ${min}`;
return `${min} - ${max}`; return `${min} - ${max}`;
} },
}; };
}; };

View File

@ -1,392 +1,421 @@
$(() => { $(() => {
setupAcard(); setupAcard();
}); });
const setupAcard = () => { const setupAcard = () => {
$('.acard.is_entry').click((e) => { $('.acard.is_entry').click((e) => {
if ($(e.target).hasClass('no-modal')) return; if ($(e.target).hasClass('no-modal')) return;
const card = $(e.target).closest('.acard'); const card = $(e.target).closest('.acard');
showModal( showModal(
$(card).attr('data-encoded-path'), $(card).attr('data-encoded-path'),
parseInt($(card).attr('data-pages')), parseInt($(card).attr('data-pages')),
parseFloat($(card).attr('data-progress')), parseFloat($(card).attr('data-progress')),
$(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(
const zipPath = decodeURIComponent(encodedPath); encodedPath,
const title = decodeURIComponent(encodedeTitle); pages,
const entry = decodeURIComponent(encodedEntryTitle); percentage,
$('#modal button, #modal a').each(function() { encodedeTitle,
$(this).removeAttr('hidden'); encodedEntryTitle,
}); titleID,
if (percentage === 0) { entryID,
$('#continue-btn').attr('hidden', ''); ) {
$('#unread-btn').attr('hidden', ''); const zipPath = decodeURIComponent(encodedPath);
} else if (percentage === 100) { const title = decodeURIComponent(encodedeTitle);
$('#read-btn').attr('hidden', ''); const entry = decodeURIComponent(encodedEntryTitle);
$('#continue-btn').attr('hidden', ''); $('#modal button, #modal a').each(function () {
} else { $(this).removeAttr('hidden');
$('#continue-btn').text('Continue from ' + percentage + '%'); });
} 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').find('span').text(entry);
$('#modal-entry-title').next().attr('data-id', titleID); $('#modal-entry-title').next().attr('data-id', titleID);
$('#modal-entry-title').next().attr('data-entry-id', entryID); $('#modal-entry-title').next().attr('data-entry-id', entryID);
$('#modal-entry-title').next().find('.title-rename-field').val(entry); $('#modal-entry-title').next().find('.title-rename-field').val(entry);
$('#path-text').text(zipPath); $('#path-text').text(zipPath);
$('#pages-text').text(pages + ' pages'); $('#pages-text').text(pages + ' pages');
$('#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();
} }
UIkit.util.on(document, 'hidden', '#modal', () => { UIkit.util.on(document, 'hidden', '#modal', () => {
$('#read-btn').off('click'); $('#read-btn').off('click');
$('#unread-btn').off('click'); $('#unread-btn').off('click');
}); });
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 {
error = data.error; error = data.error;
alert('danger', error); alert('danger', error);
} }
}) })
.fail((jqXHR, status) => { .fail((jqXHR, status) => {
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
}); });
}; };
const renameSubmit = (name, eid) => { const renameSubmit = (name, eid) => {
const upload = $('.upload-field'); const upload = $('.upload-field');
const titleId = upload.attr('data-title-id'); const titleId = upload.attr('data-title-id');
if (name.length === 0) { if (name.length === 0) {
alert('danger', 'The display name should not be empty'); alert('danger', 'The display name should not be empty');
return; return;
} }
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;
} }
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}`,
);
});
}; };
const renameSortNameSubmit = (name, eid) => { const renameSortNameSubmit = (name, eid) => {
const upload = $('.upload-field'); const upload = $('.upload-field');
const titleId = upload.attr('data-title-id'); const titleId = upload.attr('data-title-id');
const params = {}; const params = {};
if (eid) params.eid = eid; if (eid) params.eid = eid;
if (name) params.name = name; if (name) params.name = name;
const query = $.param(params); const query = $.param(params);
let url = `${base_url}api/admin/sort_title/${titleId}?${query}`; let url = `${base_url}api/admin/sort_title/${titleId}?${query}`;
$.ajax({ $.ajax({
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;
} }
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}`,
);
});
}; };
const edit = (eid) => { const edit = (eid) => {
const cover = $('#edit-modal #cover'); const cover = $('#edit-modal #cover');
let url = cover.attr('data-title-cover'); let url = cover.attr('data-title-cover');
let displayName = $('h2.uk-title > span').text(); let displayName = $('h2.uk-title > span').text();
let fileTitle = $('h2.uk-title').attr('data-file-title'); let fileTitle = $('h2.uk-title').attr('data-file-title');
let sortTitle = $('h2.uk-title').attr('data-sort-title'); let sortTitle = $('h2.uk-title').attr('data-sort-title');
if (eid) { if (eid) {
const item = $(`#${eid}`); const item = $(`#${eid}`);
url = item.find('img').attr('data-src'); url = item.find('img').attr('data-src');
displayName = item.find('.uk-card-title').attr('data-title'); displayName = item.find('.uk-card-title').attr('data-title');
fileTitle = item.find('.uk-card-title').attr('data-file-title'); fileTitle = item.find('.uk-card-title').attr('data-file-title');
sortTitle = item.find('.uk-card-title').attr('data-sort-title'); sortTitle = item.find('.uk-card-title').attr('data-sort-title');
$('#title-progress-control').attr('hidden', ''); $('#title-progress-control').attr('hidden', '');
} else { } else {
$('#title-progress-control').removeAttr('hidden'); $('#title-progress-control').removeAttr('hidden');
} }
cover.attr('data-src', url); cover.attr('data-src', url);
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);
} }
}); });
displayNameField.siblings('a.uk-form-icon').click(() => { displayNameField.siblings('a.uk-form-icon').click(() => {
renameSubmit(displayNameField.val() || fileTitle, eid); renameSubmit(displayNameField.val() || fileTitle, 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);
} }
}); });
sortTitleField.siblings('a.uk-form-icon').click(() => { sortTitleField.siblings('a.uk-form-icon').click(() => {
renameSortNameSubmit(sortTitleField.val(), eid); renameSortNameSubmit(sortTitleField.val(), eid);
}); });
setupUpload(eid); setupUpload(eid);
UIkit.modal($('#edit-modal')).show(); UIkit.modal($('#edit-modal')).show();
}; };
UIkit.util.on(document, 'hidden', '#edit-modal', () => { UIkit.util.on(document, 'hidden', '#edit-modal', () => {
const displayNameField = $('#display-name-field'); const displayNameField = $('#display-name-field');
displayNameField.off('keyup'); displayNameField.off('keyup');
displayNameField.off('click'); displayNameField.off('click');
const sortTitleField = $('#sort-title-field'); const sortTitleField = $('#sort-title-field');
sortTitleField.off('keyup'); sortTitleField.off('keyup');
sortTitleField.off('click'); sortTitleField.off('click');
}); });
const setupUpload = (eid) => { const setupUpload = (eid) => {
const upload = $('.upload-field'); const upload = $('.upload-field');
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()}`); },
}, loadStart: (e) => {
loadStart: (e) => { $(bar).removeAttr('hidden');
$(bar).removeAttr('hidden'); bar.max = e.total;
bar.max = e.total; bar.value = e.loaded;
bar.value = e.loaded; },
}, progress: (e) => {
progress: (e) => { bar.max = e.total;
bar.max = e.total; bar.value = e.loaded;
bar.value = e.loaded; },
}, loadEnd: (e) => {
loadEnd: (e) => { bar.max = e.total;
bar.max = e.total; bar.value = e.loaded;
bar.value = e.loaded; },
}, completeAll: () => {
completeAll: () => { $(bar).attr('hidden', '');
$(bar).attr('hidden', ''); location.reload();
location.reload(); },
} });
});
}; };
const deselectAll = () => { const deselectAll = () => {
$('.item .uk-card').each((i, e) => { $('.item .uk-card').each((i, e) => {
const data = e.__x.$data; const data = e.__x.$data;
data['selected'] = false; data['selected'] = false;
}); });
$('#select-bar')[0].__x.$data['count'] = 0; $('#select-bar')[0].__x.$data['count'] = 0;
}; };
const selectAll = () => { const selectAll = () => {
let count = 0; let count = 0;
$('.item .uk-card').each((i, e) => { $('.item .uk-card').each((i, e) => {
const data = e.__x.$data; const data = e.__x.$data;
if (!data['disabled']) { if (!data['disabled']) {
data['selected'] = true; data['selected'] = true;
count++; count++;
} }
}); });
$('#select-bar')[0].__x.$data['count'] = count; $('#select-bar')[0].__x.$data['count'] = count;
}; };
const selectedIDs = () => { const selectedIDs = () => {
const ary = []; const ary = [];
$('.item .uk-card').each((i, e) => { $('.item .uk-card').each((i, e) => {
const data = e.__x.$data; const data = e.__x.$data;
if (!data['disabled'] && data['selected']) { if (!data['disabled'] && data['selected']) {
const item = $(e).closest('.item'); const item = $(e).closest('.item');
ary.push($(item).attr('id')); ary.push($(item).attr('id'));
} }
}); });
return ary; return ary;
}; };
const bulkProgress = (action, el) => { const bulkProgress = (action, el) => {
const tid = $(el).attr('data-id'); const tid = $(el).attr('data-id');
const ids = selectedIDs(); const ids = selectedIDs();
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(
return; 'danger',
} `Failed to mark entries as ${action}. Error: ${data.error}`,
location.reload(); );
}) return;
.fail((jqXHR, status) => { }
alert('danger', `Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`); location.reload();
}) })
.always(() => { .fail((jqXHR, status) => {
deselectAll(); alert(
}); 'danger',
`Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
);
})
.always(() => {
deselectAll();
});
}; };
const tagsComponent = () => { const tagsComponent = () => {
return { return {
isAdmin: false, isAdmin: false,
tags: [], tags: [],
tid: $('.upload-field').attr('data-title-id'), tid: $('.upload-field').attr('data-title-id'),
loading: true, loading: true,
load(admin) { load(admin) {
this.isAdmin = admin; this.isAdmin = admin;
$('.tag-select').select2({ $('.tag-select').select2({
tags: true, tags: true,
placeholder: this.isAdmin ? 'Tag the title' : 'No tags found', placeholder: this.isAdmin ? 'Tag the title' : 'No tags found',
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(
a.setAttribute('class', 'uk-link-reset'); 'href',
a.onclick = event => { `${base_url}tags/${encodeURIComponent(state.text)}`,
event.stopPropagation(); );
}; a.setAttribute('class', 'uk-link-reset');
a.innerText = state.text; a.onclick = (event) => {
return a; event.stopPropagation();
} };
}); a.innerText = state.text;
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', () => {
this.onChange(); this.onChange();
}); });
$('.tag-select').trigger('change'); $('.tag-select').trigger('change');
this.loading = false; this.loading = false;
}); });
}); });
}, },
onChange() { onChange() {
this.tags = $('.tag-select').select2('data').map(o => o.text); this.tags = $('.tag-select')
}, .select2('data')
onAdd(event) { .map((o) => o.text);
const tag = event.params.data.text; },
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; onAdd(event) {
this.request(url, 'PUT'); const tag = event.params.data.text;
}, const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(
onDelete(event) { tag,
const tag = event.params.data.text; )}`;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; this.request(url, 'PUT');
this.request(url, 'DELETE'); },
}, onDelete(event) {
request(url, method, cb) { const tag = event.params.data.text;
$.ajax({ const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(
url: url, tag,
method: method, )}`;
dataType: 'json' this.request(url, 'DELETE');
}) },
.done(data => { request(url, method, cb) {
if (data.success) { $.ajax({
if (cb) cb(data); url,
} else { method,
alert('danger', data.error); dataType: 'json',
} })
}) .done((data) => {
.fail((jqXHR, status) => { if (data.success) {
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`); if (cb) cb(data);
}); } else {
} alert('danger', data.error);
}; }
})
.fail((jqXHR, status) => {
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
},
};
}; };

View File

@ -1,6 +1,6 @@
$(() => { $(() => {
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);
}); });

View File

@ -1,16 +1,17 @@
const remove = (username) => { 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) => {
}) alert(
.fail((jqXHR, status) => { 'danger',
alert('danger', `Failed to delete the user. Error: [${jqXHR.status}] ${jqXHR.statusText}`); `Failed to delete the user. Error: [${jqXHR.status}] ${jqXHR.statusText}`,
}); );
});
}; };