Compare commits

..

12 Commits
master ... dev

Author SHA1 Message Date
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
23 changed files with 1800 additions and 1683 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

@@ -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

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,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"
} }
} }

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#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);
); 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;
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.list(pid); this.pid = pid;
}) this.list(pid);
.catch((e) => { })
alert( .catch((e) => {
"danger", alert('danger', `Failed to list the available plugins. Error: ${e}`);
`Failed to list the available plugins. Error: ${e}` });
); },
}); pluginChanged() {
}, localStorage.setItem('plugin', this.pid);
pluginChanged() { this.list(this.pid);
localStorage.setItem("plugin", this.pid); },
this.list(this.pid); list(pid) {
}, if (!pid) return;
list(pid) { fetch(
if (!pid) return; `${base_url}api/admin/plugin/subscriptions?${new URLSearchParams({
fetch( plugin: pid,
`${base_url}api/admin/plugin/subscriptions?${new URLSearchParams( })}`,
{ {
plugin: pid, method: 'GET',
} },
)}`, )
{ .then((response) => response.json())
method: "GET", .then((data) => {
} if (!data.success) throw new Error(data.error);
) this.subscriptions = data.subscriptions;
.then((response) => response.json()) })
.then((data) => { .catch((e) => {
if (!data.success) throw new Error(data.error); alert('danger', `Failed to list subscriptions. Error: ${e}`);
this.subscriptions = data.subscriptions; });
}) },
.catch((e) => { renderStrCell(str) {
alert( const maxLength = 40;
"danger", if (str.length > maxLength)
`Failed to list subscriptions. Error: ${e}` return `<td><span>${str.substring(
); 0,
}); maxLength,
}, )}...</span><div uk-dropdown>${str}</div></td>`;
renderStrCell(str) { return `<td>${str}</td>`;
const maxLength = 40; },
if (str.length > maxLength) renderDateCell(timestamp) {
return `<td><span>${str.substring( return `<td>${moment
0, .duration(moment.unix(timestamp).diff(moment()))
maxLength .humanize(true)}</td>`;
)}...</span><div uk-dropdown>${str}</div></td>`; },
return `<td>${str}</td>`; selected(event, modal) {
}, const id = event.currentTarget.getAttribute('sid');
renderDateCell(timestamp) { this.subscription = this.subscriptions.find((s) => s.id === id);
return `<td>${moment UIkit.modal(modal).show();
.duration(moment.unix(timestamp).diff(moment())) },
.humanize(true)}</td>`; renderFilterRow(ft) {
}, const key = ft.key;
selected(event, modal) { let type = ft.type;
const id = event.currentTarget.getAttribute("sid"); switch (type) {
this.subscription = this.subscriptions.find((s) => s.id === id); case 'number-min':
UIkit.modal(modal).show(); type = 'number (minimum value)';
}, break;
renderFilterRow(ft) { case 'number-max':
const key = ft.key; type = 'number (maximum value)';
let type = ft.type; break;
switch (type) { case 'date-min':
case "number-min": type = 'minimum date';
type = "number (minimum value)"; break;
break; case 'date-max':
case "number-max": type = 'maximum date';
type = "number (maximum value)"; break;
break; }
case "date-min": let value = ft.value;
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}`,
}); );
});
}; };

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,

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