mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-03 03:15:31 -04:00
Merge branch 'dev' of https://github.com/hkalexling/Mango into feature/update-crystal-1.0
This commit is contained in:
commit
64c145cf80
@ -1,31 +0,0 @@
|
|||||||
class CreateSubscription < MG::Base
|
|
||||||
def up : String
|
|
||||||
# We allow multiple subscriptions for the same manga.
|
|
||||||
# This can be useful for example when you want to download from multiple
|
|
||||||
# groups.
|
|
||||||
<<-SQL
|
|
||||||
CREATE TABLE subscription (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
manga_id INTEGER NOT NULL,
|
|
||||||
language TEXT,
|
|
||||||
group_id INTEGER,
|
|
||||||
min_volume INTEGER,
|
|
||||||
max_volume INTEGER,
|
|
||||||
min_chapter INTEGER,
|
|
||||||
max_chapter INTEGER,
|
|
||||||
last_checked INTEGER NOT NULL,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
FOREIGN KEY (username) REFERENCES users (username)
|
|
||||||
ON UPDATE CASCADE
|
|
||||||
ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
|
|
||||||
def down : String
|
|
||||||
<<-SQL
|
|
||||||
DROP TABLE subscription;
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,379 +0,0 @@
|
|||||||
const downloadComponent = () => {
|
|
||||||
return {
|
|
||||||
chaptersLimit: 1000,
|
|
||||||
loading: false,
|
|
||||||
addingToDownload: false,
|
|
||||||
searchAvailable: false,
|
|
||||||
searchInput: '',
|
|
||||||
data: {},
|
|
||||||
chapters: [],
|
|
||||||
mangaAry: undefined, // undefined: not searching; []: searched but no result
|
|
||||||
candidateManga: {},
|
|
||||||
langChoice: 'All',
|
|
||||||
groupChoice: 'All',
|
|
||||||
chapterRange: '',
|
|
||||||
volumeRange: '',
|
|
||||||
|
|
||||||
get languages() {
|
|
||||||
const set = new Set();
|
|
||||||
if (this.data.chapters) {
|
|
||||||
this.data.chapters.forEach(chp => {
|
|
||||||
set.add(chp.language);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const ary = [...set].sort();
|
|
||||||
ary.unshift('All');
|
|
||||||
return ary;
|
|
||||||
},
|
|
||||||
|
|
||||||
get groups() {
|
|
||||||
const set = new Set();
|
|
||||||
if (this.data.chapters) {
|
|
||||||
this.data.chapters.forEach(chp => {
|
|
||||||
Object.keys(chp.groups).forEach(g => {
|
|
||||||
set.add(g);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const ary = [...set].sort();
|
|
||||||
ary.unshift('All');
|
|
||||||
return ary;
|
|
||||||
},
|
|
||||||
|
|
||||||
init() {
|
|
||||||
const tableObserver = new MutationObserver(() => {
|
|
||||||
console.log('table mutated');
|
|
||||||
$("#selectable").selectable({
|
|
||||||
filter: 'tr'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
tableObserver.observe($('table').get(0), {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
|
|
||||||
$.getJSON(`${base_url}api/admin/mangadex/expires`)
|
|
||||||
.done((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', 'Failed to check MangaDex integration status. Error: ' + data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.expires && data.expires > Math.floor(Date.now() / 1000))
|
|
||||||
this.searchAvailable = true;
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
filtersUpdated() {
|
|
||||||
if (!this.data.chapters)
|
|
||||||
this.chapters = [];
|
|
||||||
const filters = {
|
|
||||||
chapter: this.parseRange(this.chapterRange),
|
|
||||||
volume: this.parseRange(this.volumeRange),
|
|
||||||
lang: this.langChoice,
|
|
||||||
group: this.groupChoice
|
|
||||||
};
|
|
||||||
console.log('filters:', filters);
|
|
||||||
let _chapters = this.data.chapters.slice();
|
|
||||||
Object.entries(filters).forEach(([k, v]) => {
|
|
||||||
if (v === 'All') return;
|
|
||||||
if (k === 'group') {
|
|
||||||
_chapters = _chapters.filter(c => {
|
|
||||||
const unescaped_groups = Object.entries(c.groups).map(([g, id]) => this.unescapeHTML(g));
|
|
||||||
return unescaped_groups.indexOf(v) >= 0;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (k === 'lang') {
|
|
||||||
_chapters = _chapters.filter(c => c.language === v);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lb = parseFloat(v[0]);
|
|
||||||
const ub = parseFloat(v[1]);
|
|
||||||
if (isNaN(lb) && isNaN(ub)) return;
|
|
||||||
_chapters = _chapters.filter(c => {
|
|
||||||
const val = parseFloat(c[k]);
|
|
||||||
if (isNaN(val)) return false;
|
|
||||||
if (isNaN(lb))
|
|
||||||
return val <= ub;
|
|
||||||
else if (isNaN(ub))
|
|
||||||
return val >= lb;
|
|
||||||
else
|
|
||||||
return val >= lb && val <= ub;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log('filtered chapters:', _chapters);
|
|
||||||
this.chapters = _chapters;
|
|
||||||
},
|
|
||||||
|
|
||||||
search() {
|
|
||||||
if (this.loading || this.searchInput === '') return;
|
|
||||||
this.data = {};
|
|
||||||
this.mangaAry = undefined;
|
|
||||||
|
|
||||||
var int_id = -1;
|
|
||||||
try {
|
|
||||||
const path = new URL(this.searchInput).pathname;
|
|
||||||
const match = /\/(?:title|manga)\/([0-9]+)/.exec(path);
|
|
||||||
int_id = parseInt(match[1]);
|
|
||||||
} catch (e) {
|
|
||||||
int_id = parseInt(this.searchInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNaN(int_id) && int_id > 0) {
|
|
||||||
// The input is a positive integer. We treat it as an ID.
|
|
||||||
this.loading = true;
|
|
||||||
$.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`)
|
|
||||||
.done((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', 'Failed to get manga info. Error: ' + data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data = data;
|
|
||||||
this.chapters = data.chapters;
|
|
||||||
this.mangaAry = undefined;
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to get manga info. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (!this.searchAvailable) {
|
|
||||||
alert('danger', 'Please make sure you are using a valid manga ID or manga URL from Mangadex. If you are trying to search MangaDex with a search term, please log in to MangaDex first by going to "Admin -> Connect to MangaDex".');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search as a search term
|
|
||||||
this.loading = true;
|
|
||||||
$.getJSON(`${base_url}api/admin/mangadex/search?${$.param({
|
|
||||||
query: this.searchInput
|
|
||||||
})}`)
|
|
||||||
.done((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', `Failed to search MangaDex. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mangaAry = data.manga;
|
|
||||||
this.data = {};
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to search MangaDex. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
parseRange(str) {
|
|
||||||
const regex = /^[\t ]*(?:(?:(<|<=|>|>=)[\t ]*([0-9]+))|(?:([0-9]+))|(?:([0-9]+)[\t ]*-[\t ]*([0-9]+))|(?:[\t ]*))[\t ]*$/m;
|
|
||||||
const matches = str.match(regex);
|
|
||||||
var num;
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
return [null, null];
|
|
||||||
} else if (typeof matches[1] !== 'undefined' && typeof matches[2] !== 'undefined') {
|
|
||||||
// e.g., <= 30
|
|
||||||
num = parseInt(matches[2]);
|
|
||||||
if (isNaN(num)) {
|
|
||||||
return [null, null];
|
|
||||||
}
|
|
||||||
switch (matches[1]) {
|
|
||||||
case '<':
|
|
||||||
return [null, num - 1];
|
|
||||||
case '<=':
|
|
||||||
return [null, num];
|
|
||||||
case '>':
|
|
||||||
return [num + 1, null];
|
|
||||||
case '>=':
|
|
||||||
return [num, null];
|
|
||||||
}
|
|
||||||
} else if (typeof matches[3] !== 'undefined') {
|
|
||||||
// a single number
|
|
||||||
num = parseInt(matches[3]);
|
|
||||||
if (isNaN(num)) {
|
|
||||||
return [null, null];
|
|
||||||
}
|
|
||||||
return [num, num];
|
|
||||||
} else if (typeof matches[4] !== 'undefined' && typeof matches[5] !== 'undefined') {
|
|
||||||
// e.g., 10 - 23
|
|
||||||
num = parseInt(matches[4]);
|
|
||||||
const n2 = parseInt(matches[5]);
|
|
||||||
if (isNaN(num) || isNaN(n2) || num > n2) {
|
|
||||||
return [null, null];
|
|
||||||
}
|
|
||||||
return [num, n2];
|
|
||||||
} else {
|
|
||||||
// empty or space only
|
|
||||||
return [null, null];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
unescapeHTML(str) {
|
|
||||||
var elt = document.createElement("span");
|
|
||||||
elt.innerHTML = str;
|
|
||||||
return elt.innerText;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectAll() {
|
|
||||||
$('tbody > tr').each((i, e) => {
|
|
||||||
$(e).addClass('ui-selected');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
clearSelection() {
|
|
||||||
$('tbody > tr').each((i, e) => {
|
|
||||||
$(e).removeClass('ui-selected');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
download() {
|
|
||||||
const selected = $('tbody > tr.ui-selected');
|
|
||||||
if (selected.length === 0) return;
|
|
||||||
UIkit.modal.confirm(`Download ${selected.length} selected chapters?`).then(() => {
|
|
||||||
const ids = selected.map((i, e) => {
|
|
||||||
return parseInt($(e).find('td').first().text());
|
|
||||||
}).get();
|
|
||||||
const chapters = this.chapters.filter(c => ids.indexOf(c.id) >= 0);
|
|
||||||
console.log(ids);
|
|
||||||
this.addingToDownload = true;
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: `${base_url}api/admin/mangadex/download`,
|
|
||||||
data: JSON.stringify({
|
|
||||||
chapters: chapters
|
|
||||||
}),
|
|
||||||
contentType: "application/json",
|
|
||||||
dataType: 'json'
|
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const successCount = parseInt(data.success);
|
|
||||||
const failCount = parseInt(data.fail);
|
|
||||||
alert('success', `${successCount} of ${successCount + failCount} chapters added to the download queue. You can view and manage your download queue on the <a href="${base_url}admin/downloads">download manager page</a>.`);
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
this.addingToDownload = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
chooseManga(manga) {
|
|
||||||
this.candidateManga = manga;
|
|
||||||
UIkit.modal($('#modal').get(0)).show();
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmManga(id) {
|
|
||||||
UIkit.modal($('#modal').get(0)).hide();
|
|
||||||
this.searchInput = id;
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribe(langConfirmed = false, groupConfirmed = false) {
|
|
||||||
const filters = {
|
|
||||||
manga: this.data.id,
|
|
||||||
language: this.langChoice === 'All' ? null : this.langChoice,
|
|
||||||
group: this.groupChoice === 'All' ? null : this.groupChoice,
|
|
||||||
volume: this.volumeRange === '' ? null : this.volumeRange,
|
|
||||||
chapter: this.chapterRange === '' ? null : this.chapterRange
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get group ID
|
|
||||||
if (filters.group) {
|
|
||||||
this.data.chapters.forEach(chp => {
|
|
||||||
const gid = chp.groups[filters.group];
|
|
||||||
if (gid) {
|
|
||||||
filters.groupId = gid;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse range values
|
|
||||||
if (filters.volume) {
|
|
||||||
[filters.volumeMin, filters.volumeMax] = this.parseRange(filters.volume);
|
|
||||||
}
|
|
||||||
if (filters.chapter) {
|
|
||||||
[filters.chapterMin, filters.chapterMax] = this.parseRange(filters.chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filters.language && !langConfirmed) {
|
|
||||||
UIkit.modal.confirm('You didn\'t specify a language in the filtering rules. This might cause Mango to download chapters that are not in your preferred language. Are you sure you want to continue?', {
|
|
||||||
labels: {
|
|
||||||
ok: 'Yes',
|
|
||||||
cancel: 'Cancel'
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
this.subscribe(true, groupConfirmed);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filters.group && !groupConfirmed) {
|
|
||||||
UIkit.modal.confirm('You didn\'t specify a group in the filtering rules. This might cause Mango to download multiple versions of the same chapter. Are you sure you want to continue?', {
|
|
||||||
labels: {
|
|
||||||
ok: 'Yes',
|
|
||||||
cancel: 'Cancel'
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
this.subscribe(langConfirmed, true);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mangaURL = `${mangadex_base_url}/manga/${filters.manga}`;
|
|
||||||
|
|
||||||
console.log(filters);
|
|
||||||
UIkit.modal.confirm(`All <strong>FUTURE</strong> chapters matching the following filters will be downloaded:<br>
|
|
||||||
<ul>
|
|
||||||
<li>Manga ID: ${filters.manga}</li>
|
|
||||||
<li>Language: ${filters.language || 'all'}</li>
|
|
||||||
<li>Group: ${filters.group || 'all'}</li>
|
|
||||||
<li>Volume: ${filters.volume || 'all'}</li>
|
|
||||||
<li>Chapter: ${filters.chapter || 'all'}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<strong>IMPORTANT:</strong> Please make sure you are following the manga on MangaDex, otherwise Mango won't be able to receive any updates. To follow it, visit <a href="${mangaURL}">${mangaURL}</a> and click "Follow".
|
|
||||||
`, {
|
|
||||||
labels: {
|
|
||||||
ok: 'Confirm',
|
|
||||||
cancel: 'Cancel'
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: `${base_url}api/admin/mangadex/subscriptions`,
|
|
||||||
data: JSON.stringify({
|
|
||||||
subscription: filters
|
|
||||||
}),
|
|
||||||
contentType: "application/json",
|
|
||||||
dataType: 'json'
|
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', `Failed to subscribe. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
alert('success', `You've successfully subscribed to this manga! You can view and manage your subscriptions on the <a href="${base_url}download/subscription">subscription manager page</a>.`);
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to subscribe. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,61 +0,0 @@
|
|||||||
const component = () => {
|
|
||||||
return {
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
expires: undefined,
|
|
||||||
loading: true,
|
|
||||||
loggingIn: false,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.loading = true;
|
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
url: `${base_url}api/admin/mangadex/expires`,
|
|
||||||
contentType: "application/json",
|
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', `Failed to retrieve MangaDex token status. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.expires = data.expires;
|
|
||||||
this.loading = false;
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to retrieve MangaDex token status. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
login() {
|
|
||||||
if (!(this.username && this.password)) return;
|
|
||||||
this.loggingIn = true;
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: `${base_url}api/admin/mangadex/login`,
|
|
||||||
contentType: "application/json",
|
|
||||||
dataType: 'json',
|
|
||||||
data: JSON.stringify({
|
|
||||||
username: this.username,
|
|
||||||
password: this.password
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', `Failed to log in. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.expires = data.expires;
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to log in. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
this.loggingIn = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
get expired() {
|
|
||||||
return this.expires && moment().diff(moment.unix(this.expires)) > 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -6,11 +6,13 @@ const readerComponent = () => {
|
|||||||
alertClass: 'uk-alert-primary',
|
alertClass: 'uk-alert-primary',
|
||||||
items: [],
|
items: [],
|
||||||
curItem: {},
|
curItem: {},
|
||||||
|
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,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the component by fetching the page dimensions
|
* Initialize the component by fetching the page dimensions
|
||||||
@ -52,6 +54,16 @@ const readerComponent = () => {
|
|||||||
if (savedMargin) {
|
if (savedMargin) {
|
||||||
this.margin = savedMargin;
|
this.margin = savedMargin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preload Images
|
||||||
|
this.preloadLookahead = +(localStorage.getItem('preloadLookahead') ?? 3);
|
||||||
|
const limit = Math.min(page + this.preloadLookahead, this.items.length + 1);
|
||||||
|
for (let idx = page + 1; idx <= limit; idx++) {
|
||||||
|
this.preloadImage(this.items[idx - 1].url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedFlipAnimation = localStorage.getItem('enableFlipAnimation');
|
||||||
|
this.enableFlipAnimation = savedFlipAnimation === null || savedFlipAnimation === 'true';
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
const errMsg = `Failed to get the page dimensions. ${e}`;
|
const errMsg = `Failed to get the page dimensions. ${e}`;
|
||||||
@ -60,6 +72,12 @@ const readerComponent = () => {
|
|||||||
this.msg = errMsg;
|
this.msg = errMsg;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Preload an image, which is expected to be cached
|
||||||
|
*/
|
||||||
|
preloadImage(url) {
|
||||||
|
(new Image()).src = url;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Handles the `change` event for the page selector
|
* Handles the `change` event for the page selector
|
||||||
*/
|
*/
|
||||||
@ -111,12 +129,18 @@ const readerComponent = () => {
|
|||||||
|
|
||||||
if (newIdx <= 0 || newIdx > this.items.length) return;
|
if (newIdx <= 0 || newIdx > this.items.length) return;
|
||||||
|
|
||||||
|
if (newIdx + this.preloadLookahead < this.items.length + 1) {
|
||||||
|
this.preloadImage(this.items[newIdx + this.preloadLookahead - 1].url);
|
||||||
|
}
|
||||||
|
|
||||||
this.toPage(newIdx);
|
this.toPage(newIdx);
|
||||||
|
|
||||||
if (isNext)
|
if (this.enableFlipAnimation) {
|
||||||
this.flipAnimation = 'right';
|
if (isNext)
|
||||||
else
|
this.flipAnimation = 'right';
|
||||||
this.flipAnimation = 'left';
|
else
|
||||||
|
this.flipAnimation = 'left';
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.flipAnimation = null;
|
this.flipAnimation = null;
|
||||||
@ -287,6 +311,14 @@ const readerComponent = () => {
|
|||||||
marginChanged() {
|
marginChanged() {
|
||||||
localStorage.setItem('margin', this.margin);
|
localStorage.setItem('margin', this.margin);
|
||||||
this.toPage(this.selectedIndex);
|
this.toPage(this.selectedIndex);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
preloadLookaheadChanged() {
|
||||||
|
localStorage.setItem('preloadLookahead', this.preloadLookahead);
|
||||||
|
},
|
||||||
|
|
||||||
|
enableFlipAnimationChanged() {
|
||||||
|
localStorage.setItem('enableFlipAnimation', this.enableFlipAnimation);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -52,10 +52,6 @@ shards:
|
|||||||
git: https://github.com/hkalexling/koa.git
|
git: https://github.com/hkalexling/koa.git
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
|
|
||||||
mangadex:
|
|
||||||
git: https://github.com/hkalexling/mangadex.git
|
|
||||||
version: 0.11.0+git.commit.f5b0d64fbb138879fb9228b6e9ff34ec97c3e824
|
|
||||||
|
|
||||||
mg:
|
mg:
|
||||||
git: https://github.com/hkalexling/mg.git
|
git: https://github.com/hkalexling/mg.git
|
||||||
version: 0.5.0+git.commit.697e46e27cde8c3969346e228e372db2455a6264
|
version: 0.5.0+git.commit.697e46e27cde8c3969346e228e372db2455a6264
|
||||||
|
@ -41,5 +41,3 @@ dependencies:
|
|||||||
github: epoch/tallboy
|
github: epoch/tallboy
|
||||||
mg:
|
mg:
|
||||||
github: hkalexling/mg
|
github: hkalexling/mg
|
||||||
mangadex:
|
|
||||||
github: hkalexling/mangadex
|
|
||||||
|
@ -33,10 +33,8 @@ class Config
|
|||||||
"download_retries" => 4,
|
"download_retries" => 4,
|
||||||
"download_queue_db_path" => File.expand_path("~/mango/queue.db",
|
"download_queue_db_path" => File.expand_path("~/mango/queue.db",
|
||||||
home: true),
|
home: true),
|
||||||
"chapter_rename_rule" => "[Vol.{volume} ]" \
|
"chapter_rename_rule" => "[Vol.{volume} ][Ch.{chapter} ]{title|id}",
|
||||||
"[Ch.{chapter} ]{title|id}",
|
"manga_rename_rule" => "{title}",
|
||||||
"manga_rename_rule" => "{title}",
|
|
||||||
"subscription_update_interval_hours" => 24,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@singlet : Config?
|
@@singlet : Config?
|
||||||
|
@ -42,25 +42,6 @@ class Library
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
subscription_interval = Config.current
|
|
||||||
.mangadex["subscription_update_interval_hours"].as Int32
|
|
||||||
unless subscription_interval < 1
|
|
||||||
spawn do
|
|
||||||
loop do
|
|
||||||
subscriptions = Storage.default.subscriptions
|
|
||||||
Logger.info "Checking MangaDex for updates on " \
|
|
||||||
"#{subscriptions.size} subscriptions"
|
|
||||||
added_count = 0
|
|
||||||
subscriptions.each do |sub|
|
|
||||||
added_count += sub.check_for_updates
|
|
||||||
end
|
|
||||||
Logger.info "Subscription update completed. Added #{added_count} " \
|
|
||||||
"chapters to the download queue"
|
|
||||||
sleep subscription_interval.hours
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def titles
|
def titles
|
||||||
|
@ -1,172 +0,0 @@
|
|||||||
require "mangadex"
|
|
||||||
require "compress/zip"
|
|
||||||
require "../rename"
|
|
||||||
require "./ext"
|
|
||||||
|
|
||||||
module MangaDex
|
|
||||||
class PageJob
|
|
||||||
property success = false
|
|
||||||
property url : String
|
|
||||||
property filename : String
|
|
||||||
property writer : Compress::Zip::Writer
|
|
||||||
property tries_remaning : Int32
|
|
||||||
|
|
||||||
def initialize(@url, @filename, @writer, @tries_remaning)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Downloader < Queue::Downloader
|
|
||||||
@wait_seconds : Int32 = Config.current.mangadex["download_wait_seconds"]
|
|
||||||
.to_i32
|
|
||||||
@retries : Int32 = Config.current.mangadex["download_retries"].to_i32
|
|
||||||
|
|
||||||
use_default
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@client = Client.from_config
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def pop : Queue::Job?
|
|
||||||
job = nil
|
|
||||||
MainFiber.run do
|
|
||||||
DB.open "sqlite3://#{@queue.path}" do |db|
|
|
||||||
begin
|
|
||||||
db.query_one "select * from queue where id not like '%-%' " \
|
|
||||||
"and (status = 0 or status = 1) " \
|
|
||||||
"order by time limit 1" do |res|
|
|
||||||
job = Queue::Job.from_query_result res
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
job
|
|
||||||
end
|
|
||||||
|
|
||||||
private def download(job : Queue::Job)
|
|
||||||
@downloading = true
|
|
||||||
@queue.set_status Queue::JobStatus::Downloading, job
|
|
||||||
begin
|
|
||||||
chapter = @client.chapter job.id
|
|
||||||
# We must put the `.pages` call in a rescue block to handle external
|
|
||||||
# chapters.
|
|
||||||
pages = chapter.pages
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
@queue.set_status Queue::JobStatus::Error, job
|
|
||||||
unless e.message.nil?
|
|
||||||
@queue.add_message e.message.not_nil!, job
|
|
||||||
end
|
|
||||||
@downloading = false
|
|
||||||
return
|
|
||||||
end
|
|
||||||
@queue.set_pages pages.size, job
|
|
||||||
lib_dir = @library_path
|
|
||||||
rename_rule = Rename::Rule.new \
|
|
||||||
Config.current.mangadex["manga_rename_rule"].to_s
|
|
||||||
manga_dir = File.join lib_dir, chapter.manga.rename rename_rule
|
|
||||||
unless File.exists? manga_dir
|
|
||||||
Dir.mkdir_p manga_dir
|
|
||||||
end
|
|
||||||
zip_path = File.join manga_dir, "#{job.title}.cbz.part"
|
|
||||||
|
|
||||||
# Find the number of digits needed to store the number of pages
|
|
||||||
len = Math.log10(pages.size).to_i + 1
|
|
||||||
|
|
||||||
writer = Compress::Zip::Writer.new zip_path
|
|
||||||
# Create a buffered channel. It works as an FIFO queue
|
|
||||||
channel = Channel(PageJob).new pages.size
|
|
||||||
spawn do
|
|
||||||
pages.each_with_index do |url, i|
|
|
||||||
fn = Path.new(URI.parse(url).path).basename
|
|
||||||
ext = File.extname fn
|
|
||||||
fn = "#{i.to_s.rjust len, '0'}#{ext}"
|
|
||||||
page_job = PageJob.new url, fn, writer, @retries
|
|
||||||
Logger.debug "Downloading #{url}"
|
|
||||||
loop do
|
|
||||||
sleep @wait_seconds.seconds
|
|
||||||
download_page page_job
|
|
||||||
break if page_job.success ||
|
|
||||||
page_job.tries_remaning <= 0
|
|
||||||
page_job.tries_remaning -= 1
|
|
||||||
Logger.warn "Failed to download page #{url}. " \
|
|
||||||
"Retrying... Remaining retries: " \
|
|
||||||
"#{page_job.tries_remaning}"
|
|
||||||
end
|
|
||||||
|
|
||||||
channel.send page_job
|
|
||||||
break unless @queue.exists? job
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
spawn do
|
|
||||||
page_jobs = [] of PageJob
|
|
||||||
pages.size.times do
|
|
||||||
page_job = channel.receive
|
|
||||||
|
|
||||||
break unless @queue.exists? job
|
|
||||||
|
|
||||||
Logger.debug "[#{page_job.success ? "success" : "failed"}] " \
|
|
||||||
"#{page_job.url}"
|
|
||||||
page_jobs << page_job
|
|
||||||
if page_job.success
|
|
||||||
@queue.add_success job
|
|
||||||
else
|
|
||||||
@queue.add_fail job
|
|
||||||
msg = "Failed to download page #{page_job.url}"
|
|
||||||
@queue.add_message msg, job
|
|
||||||
Logger.error msg
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
unless @queue.exists? job
|
|
||||||
Logger.debug "Download cancelled"
|
|
||||||
@downloading = false
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
fail_count = page_jobs.count { |j| !j.success }
|
|
||||||
Logger.debug "Download completed. " \
|
|
||||||
"#{fail_count}/#{page_jobs.size} failed"
|
|
||||||
writer.close
|
|
||||||
filename = File.join File.dirname(zip_path), File.basename(zip_path,
|
|
||||||
".part")
|
|
||||||
File.rename zip_path, filename
|
|
||||||
Logger.debug "cbz File created at #{filename}"
|
|
||||||
|
|
||||||
zip_exception = validate_archive filename
|
|
||||||
if !zip_exception.nil?
|
|
||||||
@queue.add_message "The downloaded archive is corrupted. " \
|
|
||||||
"Error: #{zip_exception}", job
|
|
||||||
@queue.set_status Queue::JobStatus::Error, job
|
|
||||||
elsif fail_count > 0
|
|
||||||
@queue.set_status Queue::JobStatus::MissingPages, job
|
|
||||||
else
|
|
||||||
@queue.set_status Queue::JobStatus::Completed, job
|
|
||||||
end
|
|
||||||
@downloading = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private def download_page(job : PageJob)
|
|
||||||
Logger.debug "downloading #{job.url}"
|
|
||||||
headers = HTTP::Headers{
|
|
||||||
"User-agent" => "Mangadex.cr",
|
|
||||||
}
|
|
||||||
begin
|
|
||||||
HTTP::Client.get job.url, headers do |res|
|
|
||||||
unless res.success?
|
|
||||||
raise "Failed to download page #{job.url}. " \
|
|
||||||
"[#{res.status_code}] #{res.status_message}"
|
|
||||||
end
|
|
||||||
job.writer.add job.filename, res.body_io
|
|
||||||
end
|
|
||||||
job.success = true
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
job.success = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,94 +0,0 @@
|
|||||||
private macro properties_to_hash(names)
|
|
||||||
{
|
|
||||||
{% for name in names %}
|
|
||||||
"{{name.id}}" => {{name.id}}.to_s,
|
|
||||||
{% end %}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Monkey-patch the structures in the `mangadex` shard to suit our needs
|
|
||||||
module MangaDex
|
|
||||||
struct Client
|
|
||||||
@@group_cache = {} of String => Group
|
|
||||||
|
|
||||||
def self.from_config : Client
|
|
||||||
self.new base_url: Config.current.mangadex["base_url"].to_s,
|
|
||||||
api_url: Config.current.mangadex["api_url"].to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
struct Manga
|
|
||||||
def rename(rule : Rename::Rule)
|
|
||||||
rule.render properties_to_hash %w(id title author artist)
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_info_json
|
|
||||||
hash = JSON.parse(to_json).as_h
|
|
||||||
_chapters = chapters.map do |c|
|
|
||||||
JSON.parse c.to_info_json
|
|
||||||
end
|
|
||||||
hash["chapters"] = JSON::Any.new _chapters
|
|
||||||
hash.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
struct Chapter
|
|
||||||
def rename(rule : Rename::Rule)
|
|
||||||
hash = properties_to_hash %w(id title volume chapter lang_code language)
|
|
||||||
hash["groups"] = groups.join(",", &.name)
|
|
||||||
rule.render hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def full_title
|
|
||||||
rule = Rename::Rule.new \
|
|
||||||
Config.current.mangadex["chapter_rename_rule"].to_s
|
|
||||||
rename rule
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_info_json
|
|
||||||
hash = JSON.parse(to_json).as_h
|
|
||||||
hash["language"] = JSON::Any.new language
|
|
||||||
_groups = {} of String => JSON::Any
|
|
||||||
groups.each do |g|
|
|
||||||
_groups[g.name] = JSON::Any.new g.id
|
|
||||||
end
|
|
||||||
hash["groups"] = JSON::Any.new _groups
|
|
||||||
hash["full_title"] = JSON::Any.new full_title
|
|
||||||
hash.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
# We don't need to rename the manga title here. It will be renamed in
|
|
||||||
# src/mangadex/downloader.cr
|
|
||||||
def to_job : Queue::Job
|
|
||||||
Queue::Job.new(
|
|
||||||
id.to_s,
|
|
||||||
manga_id.to_s,
|
|
||||||
full_title,
|
|
||||||
manga_title,
|
|
||||||
Queue::JobStatus::Pending,
|
|
||||||
Time.unix timestamp
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
struct User
|
|
||||||
def updates_after(time : Time, &block : Chapter ->)
|
|
||||||
page = 1
|
|
||||||
stopped = false
|
|
||||||
until stopped
|
|
||||||
chapters = followed_updates(page: page).chapters
|
|
||||||
return if chapters.empty?
|
|
||||||
chapters.each do |c|
|
|
||||||
if time > Time.unix c.timestamp
|
|
||||||
stopped = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
yield c
|
|
||||||
end
|
|
||||||
page += 1
|
|
||||||
# Let's not DDOS MangaDex :)
|
|
||||||
sleep 5.seconds
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -2,7 +2,6 @@ require "./config"
|
|||||||
require "./queue"
|
require "./queue"
|
||||||
require "./server"
|
require "./server"
|
||||||
require "./main_fiber"
|
require "./main_fiber"
|
||||||
require "./mangadex/*"
|
|
||||||
require "./plugin/*"
|
require "./plugin/*"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
require "clim"
|
require "clim"
|
||||||
@ -59,7 +58,6 @@ class CLI < Clim
|
|||||||
Storage.default
|
Storage.default
|
||||||
Queue.default
|
Queue.default
|
||||||
Library.default
|
Library.default
|
||||||
MangaDex::Downloader.default
|
|
||||||
Plugin::Downloader.default
|
Plugin::Downloader.default
|
||||||
|
|
||||||
spawn do
|
spawn do
|
||||||
|
@ -73,9 +73,5 @@ struct AdminRouter
|
|||||||
get "/admin/missing" do |env|
|
get "/admin/missing" do |env|
|
||||||
layout "missing-items"
|
layout "missing-items"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/admin/mangadex" do |env|
|
|
||||||
layout "mangadex"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
require "../mangadex/*"
|
|
||||||
require "../upload"
|
require "../upload"
|
||||||
require "koa"
|
require "koa"
|
||||||
|
require "digest"
|
||||||
|
|
||||||
struct APIRouter
|
struct APIRouter
|
||||||
@@api_json : String?
|
@@api_json : String?
|
||||||
@ -56,31 +56,20 @@ struct APIRouter
|
|||||||
"error" => String?,
|
"error" => String?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Koa.schema("mdChapter", {
|
|
||||||
"id" => Int64,
|
|
||||||
"group" => {} of String => String,
|
|
||||||
}.merge(s %w(title volume chapter language full_title time
|
|
||||||
manga_title manga_id)),
|
|
||||||
desc: "A MangaDex chapter")
|
|
||||||
|
|
||||||
Koa.schema "mdManga", {
|
|
||||||
"id" => Int64,
|
|
||||||
"chapters" => ["mdChapter"],
|
|
||||||
}.merge(s %w(title description author artist cover_url)),
|
|
||||||
desc: "A MangaDex manga"
|
|
||||||
|
|
||||||
Koa.describe "Returns a page in a manga entry"
|
Koa.describe "Returns a page in a manga entry"
|
||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.path "eid", desc: "Entry ID"
|
Koa.path "eid", desc: "Entry ID"
|
||||||
Koa.path "page", schema: Int32, desc: "The page number to return (starts from 1)"
|
Koa.path "page", schema: Int32, desc: "The page number to return (starts from 1)"
|
||||||
Koa.response 200, schema: Bytes, media_type: "image/*"
|
Koa.response 200, schema: Bytes, media_type: "image/*"
|
||||||
Koa.response 500, "Page not found or not readable"
|
Koa.response 500, "Page not found or not readable"
|
||||||
|
Koa.response 304, "Page not modified (only available when `If-None-Match` is set)"
|
||||||
Koa.tag "reader"
|
Koa.tag "reader"
|
||||||
get "/api/page/:tid/:eid/:page" do |env|
|
get "/api/page/:tid/:eid/:page" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
eid = env.params.url["eid"]
|
eid = env.params.url["eid"]
|
||||||
page = env.params.url["page"].to_i
|
page = env.params.url["page"].to_i
|
||||||
|
prev_e_tag = env.request.headers["If-None-Match"]?
|
||||||
|
|
||||||
title = Library.default.get_title tid
|
title = Library.default.get_title tid
|
||||||
raise "Title ID `#{tid}` not found" if title.nil?
|
raise "Title ID `#{tid}` not found" if title.nil?
|
||||||
@ -90,7 +79,15 @@ struct APIRouter
|
|||||||
raise "Failed to load page #{page} of " \
|
raise "Failed to load page #{page} of " \
|
||||||
"`#{title.title}/#{entry.title}`" if img.nil?
|
"`#{title.title}/#{entry.title}`" if img.nil?
|
||||||
|
|
||||||
send_img env, img
|
e_tag = Digest::SHA1.hexdigest img.data
|
||||||
|
if prev_e_tag == e_tag
|
||||||
|
env.response.status_code = 304
|
||||||
|
""
|
||||||
|
else
|
||||||
|
env.response.headers["ETag"] = e_tag
|
||||||
|
env.response.headers["Cache-Control"] = "public, max-age=86400"
|
||||||
|
send_img env, img
|
||||||
|
end
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
env.response.status_code = 500
|
env.response.status_code = 500
|
||||||
@ -102,12 +99,14 @@ struct APIRouter
|
|||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.path "eid", desc: "Entry ID"
|
Koa.path "eid", desc: "Entry ID"
|
||||||
Koa.response 200, schema: Bytes, media_type: "image/*"
|
Koa.response 200, schema: Bytes, media_type: "image/*"
|
||||||
|
Koa.response 304, "Page not modified (only available when `If-None-Match` is set)"
|
||||||
Koa.response 500, "Page not found or not readable"
|
Koa.response 500, "Page not found or not readable"
|
||||||
Koa.tag "library"
|
Koa.tag "library"
|
||||||
get "/api/cover/:tid/:eid" do |env|
|
get "/api/cover/:tid/:eid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
eid = env.params.url["eid"]
|
eid = env.params.url["eid"]
|
||||||
|
prev_e_tag = env.request.headers["If-None-Match"]?
|
||||||
|
|
||||||
title = Library.default.get_title tid
|
title = Library.default.get_title tid
|
||||||
raise "Title ID `#{tid}` not found" if title.nil?
|
raise "Title ID `#{tid}` not found" if title.nil?
|
||||||
@ -118,7 +117,14 @@ struct APIRouter
|
|||||||
raise "Failed to get cover of `#{title.title}/#{entry.title}`" \
|
raise "Failed to get cover of `#{title.title}/#{entry.title}`" \
|
||||||
if img.nil?
|
if img.nil?
|
||||||
|
|
||||||
send_img env, img
|
e_tag = Digest::SHA1.hexdigest img.data
|
||||||
|
if prev_e_tag == e_tag
|
||||||
|
env.response.status_code = 304
|
||||||
|
""
|
||||||
|
else
|
||||||
|
env.response.headers["ETag"] = e_tag
|
||||||
|
send_img env, img
|
||||||
|
end
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
env.response.status_code = 500
|
env.response.status_code = 500
|
||||||
@ -323,58 +329,6 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Returns a MangaDex manga identified by `id`", <<-MD
|
|
||||||
On error, returns a JSON that contains the error message in the `error` field.
|
|
||||||
MD
|
|
||||||
Koa.tags ["admin", "mangadex"]
|
|
||||||
Koa.path "id", desc: "A MangaDex manga ID"
|
|
||||||
Koa.response 200, schema: "mdManga"
|
|
||||||
get "/api/admin/mangadex/manga/:id" do |env|
|
|
||||||
begin
|
|
||||||
id = env.params.url["id"]
|
|
||||||
manga = MangaDex::Client.from_config.manga id
|
|
||||||
send_json env, manga.to_info_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {"error" => e.message}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Koa.describe "Adds a list of MangaDex chapters to the download queue", <<-MD
|
|
||||||
On error, returns a JSON that contains the error message in the `error` field.
|
|
||||||
MD
|
|
||||||
Koa.tags ["admin", "mangadex", "downloader"]
|
|
||||||
Koa.body schema: {
|
|
||||||
"chapters" => ["mdChapter"],
|
|
||||||
}
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Int32,
|
|
||||||
"fail" => Int32,
|
|
||||||
}
|
|
||||||
post "/api/admin/mangadex/download" do |env|
|
|
||||||
begin
|
|
||||||
chapters = env.params.json["chapters"].as(Array).map &.as_h
|
|
||||||
jobs = chapters.map { |chapter|
|
|
||||||
Queue::Job.new(
|
|
||||||
chapter["id"].as_i64.to_s,
|
|
||||||
chapter["mangaId"].as_i64.to_s,
|
|
||||||
chapter["full_title"].as_s,
|
|
||||||
chapter["mangaTitle"].as_s,
|
|
||||||
Queue::JobStatus::Pending,
|
|
||||||
Time.unix chapter["timestamp"].as_i64
|
|
||||||
)
|
|
||||||
}
|
|
||||||
inserted_count = Queue.default.push jobs
|
|
||||||
send_json env, {
|
|
||||||
"success": inserted_count,
|
|
||||||
"fail": jobs.size - inserted_count,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {"error" => e.message}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ws "/api/admin/mangadex/queue" do |socket, env|
|
ws "/api/admin/mangadex/queue" do |socket, env|
|
||||||
interval_raw = env.params.query["interval"]?
|
interval_raw = env.params.query["interval"]?
|
||||||
interval = (interval_raw.to_i? if interval_raw) || 5
|
interval = (interval_raw.to_i? if interval_raw) || 5
|
||||||
@ -631,21 +585,32 @@ struct APIRouter
|
|||||||
"height" => Int32,
|
"height" => Int32,
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
Koa.response 304, "Not modified (only available when `If-None-Match` is set)"
|
||||||
get "/api/dimensions/:tid/:eid" do |env|
|
get "/api/dimensions/:tid/:eid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
eid = env.params.url["eid"]
|
eid = env.params.url["eid"]
|
||||||
|
prev_e_tag = env.request.headers["If-None-Match"]?
|
||||||
|
|
||||||
title = Library.default.get_title tid
|
title = Library.default.get_title tid
|
||||||
raise "Title ID `#{tid}` not found" if title.nil?
|
raise "Title ID `#{tid}` not found" if title.nil?
|
||||||
entry = title.get_entry eid
|
entry = title.get_entry eid
|
||||||
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
||||||
|
|
||||||
sizes = entry.page_dimensions
|
file_hash = Digest::SHA1.hexdigest (entry.zip_path + entry.mtime.to_s)
|
||||||
send_json env, {
|
e_tag = "W/#{file_hash}"
|
||||||
"success" => true,
|
if e_tag == prev_e_tag
|
||||||
"dimensions" => sizes,
|
env.response.status_code = 304
|
||||||
}.to_json
|
""
|
||||||
|
else
|
||||||
|
sizes = entry.page_dimensions
|
||||||
|
env.response.headers["ETag"] = e_tag
|
||||||
|
env.response.headers["Cache-Control"] = "public, max-age=86400"
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"dimensions" => sizes,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
@ -904,239 +869,6 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Logs the current user into their MangaDex account", <<-MD
|
|
||||||
If successful, returns the expiration date (as a unix timestamp) of the newly created token.
|
|
||||||
MD
|
|
||||||
Koa.body schema: {
|
|
||||||
"username" => String,
|
|
||||||
"password" => String,
|
|
||||||
}
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Bool,
|
|
||||||
"error" => String?,
|
|
||||||
"expires" => Int64?,
|
|
||||||
}
|
|
||||||
Koa.tags ["admin", "mangadex", "users"]
|
|
||||||
post "/api/admin/mangadex/login" do |env|
|
|
||||||
begin
|
|
||||||
username = env.params.json["username"].as String
|
|
||||||
password = env.params.json["password"].as String
|
|
||||||
mango_username = get_username env
|
|
||||||
|
|
||||||
client = MangaDex::Client.from_config
|
|
||||||
client.auth username, password
|
|
||||||
|
|
||||||
Storage.default.save_md_token mango_username, client.token.not_nil!,
|
|
||||||
client.token_expires
|
|
||||||
|
|
||||||
send_json env, {
|
|
||||||
"success" => true,
|
|
||||||
"error" => nil,
|
|
||||||
"expires" => client.token_expires.to_unix,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Koa.describe "Returns the expiration date (as a unix timestamp) of the mangadex token if it exists"
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Bool,
|
|
||||||
"error" => String?,
|
|
||||||
"expires" => Int64?,
|
|
||||||
}
|
|
||||||
Koa.tags ["admin", "mangadex", "users"]
|
|
||||||
get "/api/admin/mangadex/expires" do |env|
|
|
||||||
begin
|
|
||||||
username = get_username env
|
|
||||||
_, expires = Storage.default.get_md_token username
|
|
||||||
|
|
||||||
send_json env, {
|
|
||||||
"success" => true,
|
|
||||||
"error" => nil,
|
|
||||||
"expires" => expires.try &.to_unix,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Koa.describe "Searches MangaDex for manga matching `query`", <<-MD
|
|
||||||
Returns an empty list if the current user hasn't logged in to MangaDex.
|
|
||||||
MD
|
|
||||||
Koa.query "query"
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Bool,
|
|
||||||
"error" => String?,
|
|
||||||
"manga?" => [{
|
|
||||||
"id" => Int64,
|
|
||||||
"title" => String,
|
|
||||||
"description" => String,
|
|
||||||
"mainCover" => String,
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
Koa.tags ["admin", "mangadex"]
|
|
||||||
get "/api/admin/mangadex/search" do |env|
|
|
||||||
begin
|
|
||||||
query = env.params.query["query"]
|
|
||||||
|
|
||||||
send_json env, {
|
|
||||||
"success" => true,
|
|
||||||
"error" => nil,
|
|
||||||
"manga" => get_client(env).partial_search query,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Koa.describe "Lists all MangaDex subscriptions"
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Bool,
|
|
||||||
"error" => String?,
|
|
||||||
"subscriptions?" => [{
|
|
||||||
"id" => Int64,
|
|
||||||
"username" => String,
|
|
||||||
"manga_id" => Int64,
|
|
||||||
"language" => String?,
|
|
||||||
"group_id" => Int64?,
|
|
||||||
"min_volume" => Int64?,
|
|
||||||
"max_volume" => Int64?,
|
|
||||||
"min_chapter" => Int64?,
|
|
||||||
"max_chapter" => Int64?,
|
|
||||||
"last_checked" => Int64,
|
|
||||||
"created_at" => Int64,
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
Koa.tags ["admin", "mangadex", "subscriptions"]
|
|
||||||
get "/api/admin/mangadex/subscriptions" do |env|
|
|
||||||
begin
|
|
||||||
send_json env, {
|
|
||||||
"success" => true,
|
|
||||||
"error" => nil,
|
|
||||||
"subscriptions" => Storage.default.subscriptions,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Koa.describe "Creates a new MangaDex subscription"
|
|
||||||
Koa.body schema: {
|
|
||||||
"subscription" => {
|
|
||||||
"manga" => Int64,
|
|
||||||
"language" => String?,
|
|
||||||
"groupId" => Int64?,
|
|
||||||
"volumeMin" => Int64?,
|
|
||||||
"volumeMax" => Int64?,
|
|
||||||
"chapterMin" => Int64?,
|
|
||||||
"chapterMax" => Int64?,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Bool,
|
|
||||||
"error" => String?,
|
|
||||||
}
|
|
||||||
Koa.tags ["admin", "mangadex", "subscriptions"]
|
|
||||||
post "/api/admin/mangadex/subscriptions" do |env|
|
|
||||||
begin
|
|
||||||
json = env.params.json["subscription"].as Hash(String, JSON::Any)
|
|
||||||
sub = Subscription.new json["manga"].as_i64, get_username env
|
|
||||||
sub.language = json["language"]?.try &.as_s?
|
|
||||||
sub.group_id = json["groupId"]?.try &.as_i64?
|
|
||||||
sub.min_volume = json["volumeMin"]?.try &.as_i64?
|
|
||||||
sub.max_volume = json["volumeMax"]?.try &.as_i64?
|
|
||||||
sub.min_chapter = json["chapterMin"]?.try &.as_i64?
|
|
||||||
sub.max_chapter = json["chapterMax"]?.try &.as_i64?
|
|
||||||
|
|
||||||
Storage.default.save_subscription sub
|
|
||||||
|
|
||||||
send_json env, {
|
|
||||||
"success" => true,
|
|
||||||
"error" => nil,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Koa.describe "Deletes a MangaDex subscription identified by `id`", <<-MD
|
|
||||||
Does nothing if the subscription was not created by the current user.
|
|
||||||
MD
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Bool,
|
|
||||||
"error" => String?,
|
|
||||||
}
|
|
||||||
Koa.tags ["admin", "mangadex", "subscriptions"]
|
|
||||||
delete "/api/admin/mangadex/subscriptions/:id" do |env|
|
|
||||||
begin
|
|
||||||
id = env.params.url["id"].to_i64
|
|
||||||
Storage.default.delete_subscription id, get_username env
|
|
||||||
send_json env, {
|
|
||||||
"success" => true,
|
|
||||||
"error" => nil,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Koa.describe "Triggers an update for a MangaDex subscription identified by `id`", <<-MD
|
|
||||||
Does nothing if the subscription was not created by the current user.
|
|
||||||
MD
|
|
||||||
Koa.response 200, schema: {
|
|
||||||
"success" => Bool,
|
|
||||||
"error" => String?,
|
|
||||||
}
|
|
||||||
Koa.tags ["admin", "mangadex", "subscriptions"]
|
|
||||||
post "/api/admin/mangadex/subscriptions/check/:id" do |env|
|
|
||||||
begin
|
|
||||||
id = env.params.url["id"].to_i64
|
|
||||||
username = get_username env
|
|
||||||
sub = Storage.default.get_subscription id, username
|
|
||||||
unless sub
|
|
||||||
raise "Subscription with id #{id} not found under user #{username}"
|
|
||||||
end
|
|
||||||
spawn do
|
|
||||||
sub.check_for_updates
|
|
||||||
end
|
|
||||||
send_json env, {
|
|
||||||
"success" => true,
|
|
||||||
"error" => nil,
|
|
||||||
}.to_json
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
send_json env, {
|
|
||||||
"success" => false,
|
|
||||||
"error" => e.message,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
doc = Koa.generate
|
doc = Koa.generate
|
||||||
@@api_json = doc.to_json if doc
|
@@api_json = doc.to_json if doc
|
||||||
|
|
||||||
|
@ -72,11 +72,6 @@ struct MainRouter
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/download" do |env|
|
|
||||||
mangadex_base_url = Config.current.mangadex["base_url"]
|
|
||||||
layout "download"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/download/plugins" do |env|
|
get "/download/plugins" do |env|
|
||||||
begin
|
begin
|
||||||
id = env.params.query["plugin"]?
|
id = env.params.query["plugin"]?
|
||||||
@ -96,12 +91,6 @@ struct MainRouter
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/download/subscription" do |env|
|
|
||||||
mangadex_base_url = Config.current.mangadex["base_url"]
|
|
||||||
username = get_username env
|
|
||||||
layout "subscription"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/" do |env|
|
get "/" do |env|
|
||||||
begin
|
begin
|
||||||
username = get_username env
|
username = get_username env
|
||||||
|
@ -5,7 +5,6 @@ require "base64"
|
|||||||
require "./util/*"
|
require "./util/*"
|
||||||
require "mg"
|
require "mg"
|
||||||
require "../migration/*"
|
require "../migration/*"
|
||||||
require "./subscription"
|
|
||||||
|
|
||||||
def hash_password(pw)
|
def hash_password(pw)
|
||||||
Crypto::Bcrypt::Password.create(pw).to_s
|
Crypto::Bcrypt::Password.create(pw).to_s
|
||||||
@ -15,9 +14,6 @@ def verify_password(hash, pw)
|
|||||||
(Crypto::Bcrypt::Password.new hash).verify pw
|
(Crypto::Bcrypt::Password.new hash).verify pw
|
||||||
end
|
end
|
||||||
|
|
||||||
SUB_ATTR = %w(manga_id language group_id min_volume max_volume min_chapter
|
|
||||||
max_chapter username)
|
|
||||||
|
|
||||||
class Storage
|
class Storage
|
||||||
@@insert_entry_ids = [] of IDTuple
|
@@insert_entry_ids = [] of IDTuple
|
||||||
@@insert_title_ids = [] of IDTuple
|
@@insert_title_ids = [] of IDTuple
|
||||||
@ -549,70 +545,6 @@ class Storage
|
|||||||
{token, expires}
|
{token, expires}
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_subscription(sub : Subscription)
|
|
||||||
MainFiber.run do
|
|
||||||
get_db do |db|
|
|
||||||
{% begin %}
|
|
||||||
db.exec "insert into subscription (#{SUB_ATTR.join ","}, " \
|
|
||||||
"last_checked, created_at) values " \
|
|
||||||
"(#{Array.new(SUB_ATTR.size + 2, "?").join ","})",
|
|
||||||
{% for type in SUB_ATTR %}
|
|
||||||
sub.{{type.id}},
|
|
||||||
{% end %}
|
|
||||||
sub.last_checked.to_unix, sub.created_at.to_unix
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def subscriptions : Array(Subscription)
|
|
||||||
subs = [] of Subscription
|
|
||||||
MainFiber.run do
|
|
||||||
get_db do |db|
|
|
||||||
db.query "select * from subscription" do |rs|
|
|
||||||
subs += Subscription.from_rs rs
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
subs
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_subscription(id : Int64, username : String)
|
|
||||||
MainFiber.run do
|
|
||||||
get_db do |db|
|
|
||||||
db.exec "delete from subscription where id = (?) and username = (?)",
|
|
||||||
id, username
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_subscription(id : Int64, username : String) : Subscription?
|
|
||||||
sub = nil
|
|
||||||
MainFiber.run do
|
|
||||||
get_db do |db|
|
|
||||||
db.query "select * from subscription where id = (?) and " \
|
|
||||||
"username = (?) limit 1", id, username do |rs|
|
|
||||||
sub = Subscription.from_rs(rs).first?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
sub
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_subscription_last_checked(id : Int64? = nil)
|
|
||||||
MainFiber.run do
|
|
||||||
get_db do |db|
|
|
||||||
if id
|
|
||||||
db.exec "update subscription set last_checked = (?) where id = (?)",
|
|
||||||
Time.utc.to_unix, id
|
|
||||||
else
|
|
||||||
db.exec "update subscription set last_checked = (?)",
|
|
||||||
Time.utc.to_unix
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def close
|
def close
|
||||||
MainFiber.run do
|
MainFiber.run do
|
||||||
unless @db.nil?
|
unless @db.nil?
|
||||||
|
@ -107,25 +107,6 @@ macro get_sort_opt
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns an authorized client
|
|
||||||
def get_client(username : String) : MangaDex::Client
|
|
||||||
token, expires = Storage.default.get_md_token username
|
|
||||||
|
|
||||||
unless expires && token
|
|
||||||
raise "No token found for user #{username}"
|
|
||||||
end
|
|
||||||
|
|
||||||
client = MangaDex::Client.from_config
|
|
||||||
client.token = token
|
|
||||||
client.token_expires = expires
|
|
||||||
|
|
||||||
client
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_client(env) : MangaDex::Client
|
|
||||||
get_client get_username env
|
|
||||||
end
|
|
||||||
|
|
||||||
module HTTP
|
module HTTP
|
||||||
class Client
|
class Client
|
||||||
private def self.exec(uri : URI, tls : TLSContext = nil)
|
private def self.exec(uri : URI, tls : TLSContext = nil)
|
||||||
|
@ -33,7 +33,6 @@
|
|||||||
<option>System</option>
|
<option>System</option>
|
||||||
</select>
|
</select>
|
||||||
</li>
|
</li>
|
||||||
<li><a class="uk-link-reset" href="<%= base_url %>admin/mangadex">Connect to MangaDex</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr class="uk-divider-icon">
|
<hr class="uk-divider-icon">
|
||||||
|
@ -5,63 +5,61 @@
|
|||||||
<button class="uk-button uk-button-default" @click="load()" :disabled="loading">Refresh Queue</button>
|
<button class="uk-button uk-button-default" @click="load()" :disabled="loading">Refresh Queue</button>
|
||||||
<button class="uk-button uk-button-default" x-show="paused !== undefined" x-text="paused ? 'Resume Download' : 'Pause Download'" @click="toggle()" :disabled="toggling"></button>
|
<button class="uk-button uk-button-default" x-show="paused !== undefined" x-text="paused ? 'Resume Download' : 'Pause Download'" @click="toggle()" :disabled="toggling"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-overflow-auto">
|
<table class="uk-table uk-table-striped uk-overflow-auto">
|
||||||
<table class="uk-table uk-table-striped">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th>Chapter</th>
|
||||||
<th>Chapter</th>
|
<th>Manga</th>
|
||||||
<th>Manga</th>
|
<th>Progress</th>
|
||||||
<th>Progress</th>
|
<th>Time</th>
|
||||||
<th>Time</th>
|
<th>Status</th>
|
||||||
<th>Status</th>
|
<th>Plugin</th>
|
||||||
<th>Plugin</th>
|
<th>Actions</th>
|
||||||
<th>Actions</th>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="job in jobs" :key="job">
|
||||||
|
<tr :id="`chapter-${job.id}`">
|
||||||
|
|
||||||
|
<template x-if="job.plugin_id">
|
||||||
|
<td x-text="job.title"></td>
|
||||||
|
</template>
|
||||||
|
<template x-if="!job.plugin_id">
|
||||||
|
<td><a :href="`<%= mangadex_base_url %>/chapter/${job.id}`" x-text="job.title"></td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="job.plugin_id">
|
||||||
|
<td x-text="job.manga_title"></td>
|
||||||
|
</template>
|
||||||
|
<template x-if="!job.plugin_id">
|
||||||
|
<td><a :href="`<%= mangadex_base_url %>/manga/${job.manga_id}`" x-text="job.manga_title"></td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<td x-text="`${job.success_count}/${job.pages}`"></td>
|
||||||
|
<td x-text="`${moment(job.time).fromNow()}`"></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span :class="statusClass(job.status)" x-text="job.status"></span>
|
||||||
|
<template x-if="job.status_message.length > 0">
|
||||||
|
<div class="uk-inline">
|
||||||
|
<span uk-icon="info"></span>
|
||||||
|
<div uk-dropdown x-text="job.status_message" style="white-space: pre-line;"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td x-text="`${job.plugin_id || ''}`"></td>
|
||||||
|
<td>
|
||||||
|
<a @click="jobAction('delete', $event)" uk-icon="trash" uk-tooltip="Delete"></a>
|
||||||
|
<template x-if="job.status_message.length > 0">
|
||||||
|
<a @click="jobAction('retry', $event)" uk-icon="refresh" uk-tooltip="Retry"></a>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</template>
|
||||||
<tbody>
|
</tbody>
|
||||||
<template x-for="job in jobs" :key="job">
|
</table>
|
||||||
<tr :id="`chapter-${job.id}`">
|
</div>
|
||||||
|
|
||||||
<template x-if="job.plugin_id">
|
|
||||||
<td x-text="job.title"></td>
|
|
||||||
</template>
|
|
||||||
<template x-if="!job.plugin_id">
|
|
||||||
<td><a :href="`<%= mangadex_base_url %>/chapter/${job.id}`" x-text="job.title"></td>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="job.plugin_id">
|
|
||||||
<td x-text="job.manga_title"></td>
|
|
||||||
</template>
|
|
||||||
<template x-if="!job.plugin_id">
|
|
||||||
<td><a :href="`<%= mangadex_base_url %>/manga/${job.manga_id}`" x-text="job.manga_title"></td>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<td x-text="`${job.success_count}/${job.pages}`"></td>
|
|
||||||
<td x-text="`${moment(job.time).fromNow()}`"></td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span :class="statusClass(job.status)" x-text="job.status"></span>
|
|
||||||
<template x-if="job.status_message.length > 0">
|
|
||||||
<div class="uk-inline">
|
|
||||||
<span uk-icon="info"></span>
|
|
||||||
<div uk-dropdown x-text="job.status_message" style="white-space: pre-line;"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td x-text="`${job.plugin_id || ''}`"></td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<a @click="jobAction('delete', $event)" uk-icon="trash" uk-tooltip="Delete"></a>
|
|
||||||
<template x-if="job.status_message.length > 0">
|
|
||||||
<a @click="jobAction('retry', $event)" uk-icon="refresh" uk-tooltip="Retry"></a>
|
|
||||||
</template>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
|
@ -1,170 +1,162 @@
|
|||||||
<h2 class=uk-title>Download from MangaDex</h2>
|
<h2 class=uk-title>Download from MangaDex</h2>
|
||||||
<div x-data="downloadComponent()" x-init="init()">
|
<div x-data="downloadComponent()" x-init="init()">
|
||||||
<div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
|
<div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
|
||||||
<div class="uk-width-expand">
|
<div class="uk-width-expand">
|
||||||
<input class="uk-input" type="text" :placeholder="searchAvailable ? 'Search MangaDex or enter a manga ID/URL' : 'MangaDex manga ID or URL'" x-model="searchInput" @keydown.enter.debounce="search()">
|
<input class="uk-input" type="text" :placeholder="searchAvailable ? 'Search MangaDex or enter a manga ID/URL' : 'MangaDex manga ID or URL'" x-model="searchInput" @keydown.enter.debounce="search()">
|
||||||
</div>
|
|
||||||
<div class="uk-width-auto">
|
|
||||||
<div uk-spinner class="uk-align-center" x-show="loading" x-cloak></div>
|
|
||||||
<button class="uk-button uk-button-default" x-show="!loading" @click="search()">Search</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="uk-width-auto">
|
||||||
|
<div uk-spinner class="uk-align-center" x-show="loading" x-cloak></div>
|
||||||
|
<button class="uk-button uk-button-default" x-show="!loading" @click="search()">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template x-if="mangaAry">
|
<template x-if="mangaAry">
|
||||||
<div>
|
<div>
|
||||||
<p x-show="mangaAry.length === 0">No matching manga found.</p>
|
<p x-show="mangaAry.length === 0">No matching manga found.</p>
|
||||||
|
|
||||||
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
<template x-for="manga in mangaAry" :key="manga.id">
|
<template x-for="manga in mangaAry" :key="manga.id">
|
||||||
<div class="item" :data-id="manga.id" @click="chooseManga(manga)">
|
<div class="item" :data-id="manga.id" @click="chooseManga(manga)">
|
||||||
<div class="uk-card uk-card-default">
|
<div class="uk-card uk-card-default">
|
||||||
<div class="uk-card-media-top uk-inline">
|
<div class="uk-card-media-top uk-inline">
|
||||||
<img uk-img :data-src="manga.mainCover">
|
<img uk-img :data-src="manga.mainCover">
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-card-body">
|
<div class="uk-card-body">
|
||||||
<h3 class="uk-card-title break-word uk-margin-remove-bottom free-height" x-text="manga.title"></h3>
|
<h3 class="uk-card-title break-word uk-margin-remove-bottom free-height" x-text="manga.title"></h3>
|
||||||
<p class="uk-text-meta" x-text="`ID: ${manga.id}`"></p>
|
<p class="uk-text-meta" x-text="`ID: ${manga.id}`"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div x-show="data && data.chapters" x-cloak>
|
<div x-show="data && data.chapters" x-cloak>
|
||||||
<div class"uk-grid-small" uk-grid>
|
<div class"uk-grid-small" uk-grid>
|
||||||
<div class="uk-width-1-4@s">
|
<div class="uk-width-1-4@s">
|
||||||
<img :src="data.mainCover">
|
<img :src="data.mainCover">
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-1-4@s">
|
<div class="uk-width-1-4@s">
|
||||||
<p>Title: <a :href="`<%= mangadex_base_url %>/manga/${data.id}`" x-text="data.title"></a></p>
|
<p>Title: <a :href="`<%= mangadex_base_url %>/manga/${data.id}`" x-text="data.title"></a></p>
|
||||||
<p x-text="`Artist: ${data.artist}`"></p>
|
<p x-text="`Artist: ${data.artist}`"></p>
|
||||||
<p x-text="`Author: ${data.author}`"></p>
|
<p x-text="`Author: ${data.author}`"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-form-stacked uk-width-1-2@s" id="filters">
|
<div class="uk-form-stacked uk-width-1-2@s" id="filters">
|
||||||
<p class="uk-text-lead uk-margin-remove-bottom">
|
<p class="uk-text-lead uk-margin-remove-bottom">Filter Chapters</p>
|
||||||
<span>Filter Chapters</span>
|
<p class="uk-text-meta uk-margin-remove-top" x-text="`${chapters.length} chapters found`"></p>
|
||||||
<button class="uk-icon-button uk-align-right" uk-icon="rss" uk-tooltip="Subscribe" x-show="searchAvailable" @click="subscribe()"></button>
|
<div class="uk-margin">
|
||||||
</p>
|
<label class="uk-form-label">Language</label>
|
||||||
<p class="uk-text-meta uk-margin-remove-top" x-text="`${chapters.length} chapters found`"></p>
|
<div class="uk-form-controls">
|
||||||
<div class="uk-margin">
|
<select class="uk-select filter-field" x-model="langChoice" @change="filtersUpdated()">
|
||||||
<label class="uk-form-label">Language</label>
|
<template x-for="lang in languages" :key="lang">
|
||||||
<div class="uk-form-controls">
|
<option x-text="lang"></option>
|
||||||
<select class="uk-select filter-field" x-model="langChoice" @change="filtersUpdated()">
|
</template>
|
||||||
<template x-for="lang in languages" :key="lang">
|
</select>
|
||||||
<option x-text="lang"></option>
|
</div>
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label">Group</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<select class="uk-select filter-field" x-model="groupChoice" @change="filtersUpdated()">
|
|
||||||
<template x-for="group in groups" :key="group">
|
|
||||||
<option x-text="group"></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label">Volume</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="volumeRange" @keydown.enter="filtersUpdated()">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label">Chapter</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="chapterRange" @keydown.enter="filtersUpdated()">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<div class="uk-margin">
|
<label class="uk-form-label">Group</label>
|
||||||
<button class="uk-button uk-button-default" @click="selectAll()">Select All</button>
|
<div class="uk-form-controls">
|
||||||
<button class="uk-button uk-button-default" @click="clearSelection()">Clear Selections</button>
|
<select class="uk-select filter-field" x-model="groupChoice" @change="filtersUpdated()">
|
||||||
<button class="uk-button uk-button-primary" @click="download()" x-show="!addingToDownload">Download Selected</button>
|
<template x-for="group in groups" :key="group">
|
||||||
<div uk-spinner class="uk-margin-left" x-show="addingToDownload"></div>
|
<option x-text="group"></option>
|
||||||
</div>
|
</template>
|
||||||
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p x-text="`Mango can only list ${chaptersLimit} chapters, but we found ${chapters.length} chapters. Please use the filter options above to narrow down your search.`" x-show="chapters.length > chaptersLimit"></p>
|
|
||||||
<div class="uk-overflow-auto">
|
|
||||||
<table class="uk-table uk-table-striped" x-show="chapters.length <= chaptersLimit">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Language</th>
|
|
||||||
<th>Group</th>
|
|
||||||
<th>Volume</th>
|
|
||||||
<th>Chapter</th>
|
|
||||||
<th>Timestamp</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<template x-if="chapters.length <= chaptersLimit">
|
<div class="uk-margin">
|
||||||
<tbody id="selectable">
|
<label class="uk-form-label">Volume</label>
|
||||||
<template x-for="chp in chapters" :key="chp">
|
<div class="uk-form-controls">
|
||||||
<tr class="ui-widget-content">
|
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="volumeRange" @keydown.enter="filtersUpdated()">
|
||||||
<td><a :href="`<%= mangadex_base_url %>/chapter/${chp.id}`" x-text="chp.id"></a></td>
|
</div>
|
||||||
<td x-text="chp.title"></td>
|
</div>
|
||||||
<td x-text="chp.language"></td>
|
|
||||||
<td>
|
<div class="uk-margin">
|
||||||
<template x-for="grp in Object.entries(chp.groups)">
|
<label class="uk-form-label">Chapter</label>
|
||||||
<div>
|
<div class="uk-form-controls">
|
||||||
<a :href="`<%= mangadex_base_url %>/group/${grp[1]}`" x-text="grp[0]"></a>
|
<input class="uk-input filter-field" type="text" placeholder="e.g., 127, 10-14, >30, <=212, or leave it empty." x-model="chapterRange" @keydown.enter="filtersUpdated()">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td x-text="chp.volume"></td>
|
</div>
|
||||||
<td x-text="chp.chapter"></td>
|
|
||||||
<td x-text="`${moment.unix(chp.timestamp).fromNow()}`"></td>
|
<div class="uk-margin">
|
||||||
</tr>
|
<div class="uk-margin">
|
||||||
</template>
|
<button class="uk-button uk-button-default" @click="selectAll()">Select All</button>
|
||||||
</tbody>
|
<button class="uk-button uk-button-default" @click="clearSelection()">Clear Selections</button>
|
||||||
|
<button class="uk-button uk-button-primary" @click="download()" x-show="!addingToDownload">Download Selected</button>
|
||||||
|
<div uk-spinner class="uk-margin-left" x-show="addingToDownload"></div>
|
||||||
|
</div>
|
||||||
|
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
|
||||||
|
</div>
|
||||||
|
<p x-text="`Mango can only list ${chaptersLimit} chapters, but we found ${chapters.length} chapters. Please use the filter options above to narrow down your search.`" x-show="chapters.length > chaptersLimit"></p>
|
||||||
|
<table class="uk-table uk-table-striped uk-overflow-auto" x-show="chapters.length <= chaptersLimit">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Language</th>
|
||||||
|
<th>Group</th>
|
||||||
|
<th>Volume</th>
|
||||||
|
<th>Chapter</th>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<template x-if="chapters.length <= chaptersLimit">
|
||||||
|
<tbody id="selectable">
|
||||||
|
<template x-for="chp in chapters" :key="chp">
|
||||||
|
<tr class="ui-widget-content">
|
||||||
|
<td><a :href="`<%= mangadex_base_url %>/chapter/${chp.id}`" x-text="chp.id"></a></td>
|
||||||
|
<td x-text="chp.title"></td>
|
||||||
|
<td x-text="chp.language"></td>
|
||||||
|
<td>
|
||||||
|
<template x-for="grp in Object.entries(chp.groups)">
|
||||||
|
<div>
|
||||||
|
<a :href="`<%= mangadex_base_url %>/group/${grp[1]}`" x-text="grp[0]"></a>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</table>
|
</td>
|
||||||
</div>
|
<td x-text="chp.volume"></td>
|
||||||
</div>
|
<td x-text="chp.chapter"></td>
|
||||||
|
<td x-text="`${moment.unix(chp.timestamp).fromNow()}`"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="modal" class="uk-flex-top" uk-modal="container: false">
|
<div id="modal" class="uk-flex-top" uk-modal="container: false">
|
||||||
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
<div class="uk-modal-header">
|
<div class="uk-modal-header">
|
||||||
<h3 class="uk-modal-title break-word" x-text="candidateManga.title"></h3>
|
<h3 class="uk-modal-title break-word" x-text="candidateManga.title"></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-modal-body">
|
<div class="uk-modal-body">
|
||||||
<div class="uk-grid">
|
<div class="uk-grid">
|
||||||
<div class="uk-width-1-3@s">
|
<div class="uk-width-1-3@s">
|
||||||
<img uk-img data-width data-height :src="candidateManga.mainCover" style="width:100%;margin-bottom:10px;">
|
<img uk-img data-width data-height :src="candidateManga.mainCover" style="width:100%;margin-bottom:10px;">
|
||||||
<a :href="`<%= mangadex_base_url %>/manga/${candidateManga.id}`" x-text="`ID: ${candidateManga.id}`" class="uk-link-muted"></a>
|
<a :href="`<%= mangadex_base_url %>/manga/${candidateManga.id}`" x-text="`ID: ${candidateManga.id}`" class="uk-link-muted"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-2-3@s">
|
<div class="uk-width-2-3@s" uk-overflow-auto>
|
||||||
<p x-text="candidateManga.description"></p>
|
<p x-text="candidateManga.description"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-modal-footer">
|
|
||||||
<button class="uk-button uk-button-primary" type="button" @click="confirmManga(candidateManga.id)">Choose</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-modal-footer">
|
||||||
|
<button class="uk-button uk-button-primary" type="button" @click="confirmManga(candidateManga.id)">Choose</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "moment" %>
|
<%= render_component "moment" %>
|
||||||
<%= render_component "jquery-ui" %>
|
<%= render_component "jquery-ui" %>
|
||||||
<script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
const mangadex_base_url = "<%= mangadex_base_url %>";
|
<script src="<%= base_url %>js/download.js"></script>
|
||||||
</script>
|
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
|
||||||
<script src="<%= base_url %>js/download.js"></script>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -17,10 +17,8 @@
|
|||||||
<li class="uk-parent">
|
<li class="uk-parent">
|
||||||
<a href="#">Download</a>
|
<a href="#">Download</a>
|
||||||
<ul class="uk-nav-sub">
|
<ul class="uk-nav-sub">
|
||||||
<li><a href="<%= base_url %>download">MangaDex</a></li>
|
|
||||||
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
||||||
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
||||||
<li><a href="<%= base_url %>download/subscription">Subscription Manager</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -50,11 +48,9 @@
|
|||||||
<div class="uk-navbar-dropdown">
|
<div class="uk-navbar-dropdown">
|
||||||
<ul class="uk-nav uk-navbar-dropdown-nav">
|
<ul class="uk-nav uk-navbar-dropdown-nav">
|
||||||
<li class="uk-nav-header">Source</li>
|
<li class="uk-nav-header">Source</li>
|
||||||
<li><a href="<%= base_url %>download">MangaDex</a></li>
|
|
||||||
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
||||||
<li class="uk-nav-divider"></li>
|
<li class="uk-nav-divider"></li>
|
||||||
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
||||||
<li><a href="<%= base_url %>download/subscription">Subscription Manager</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -3,36 +3,34 @@
|
|||||||
<div x-show="!empty">
|
<div x-show="!empty">
|
||||||
<p>The following items were present in your library, but now we can't find them anymore. If you deleted them mistakenly, try to recover the files or folders, put them back to where they were, and rescan the library. Otherwise, you can safely delete them and the associated metadata using the buttons below to free up database space.</p>
|
<p>The following items were present in your library, but now we can't find them anymore. If you deleted them mistakenly, try to recover the files or folders, put them back to where they were, and rescan the library. Otherwise, you can safely delete them and the associated metadata using the buttons below to free up database space.</p>
|
||||||
<button class="uk-button uk-button-danger" @click="rmAll()">Delete All</button>
|
<button class="uk-button uk-button-danger" @click="rmAll()">Delete All</button>
|
||||||
<div class="uk-overflow-auto">
|
<table class="uk-table uk-table-striped uk-overflow-auto">
|
||||||
<table class="uk-table uk-table-striped">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th>Type</th>
|
||||||
<th>Type</th>
|
<th>Relative Path</th>
|
||||||
<th>Relative Path</th>
|
<th>ID</th>
|
||||||
<th>ID</th>
|
<th>Actions</th>
|
||||||
<th>Actions</th>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="title in titles" :key="title">
|
||||||
|
<tr :id="`title-${title.id}`">
|
||||||
|
<td>Title</td>
|
||||||
|
<td x-text="title.path"></td>
|
||||||
|
<td x-text="title.id"></td>
|
||||||
|
<td><a @click="rm($event)" uk-icon="trash"></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</template>
|
||||||
<tbody>
|
<template x-for="entry in entries" :key="entry">
|
||||||
<template x-for="title in titles" :key="title">
|
<tr :id="`entry-${entry.id}`">
|
||||||
<tr :id="`title-${title.id}`">
|
<td>Entry</td>
|
||||||
<td>Title</td>
|
<td x-text="entry.path"></td>
|
||||||
<td x-text="title.path"></td>
|
<td x-text="entry.id"></td>
|
||||||
<td x-text="title.id"></td>
|
<td><a @click="rm($event)" uk-icon="trash"></a></td>
|
||||||
<td><a @click="rm($event)" uk-icon="trash"></a></td>
|
</tr>
|
||||||
</tr>
|
</template>
|
||||||
</template>
|
</tbody>
|
||||||
<template x-for="entry in entries" :key="entry">
|
</table>
|
||||||
<tr :id="`entry-${entry.id}`">
|
|
||||||
<td>Entry</td>
|
|
||||||
<td x-text="entry.path"></td>
|
|
||||||
<td x-text="entry.id"></td>
|
|
||||||
<td><a @click="rm($event)" uk-icon="trash"></a></td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<div
|
<div
|
||||||
:class="{'uk-container': true, 'uk-container-small': mode === 'continuous', 'uk-container-expand': mode !== 'continuous'}">
|
:class="{'uk-container': true, 'uk-container-small': mode === 'continuous', 'uk-container-expand': mode !== 'continuous'}">
|
||||||
<div x-show="!loading && mode === 'continuous'" x-cloak>
|
<div x-show="!loading && mode === 'continuous'" x-cloak>
|
||||||
<template x-for="item in items">
|
<template x-if="!loading && mode === 'continuous'" x-for="item in items">
|
||||||
<img
|
<img
|
||||||
uk-img
|
uk-img
|
||||||
:class="{'uk-align-center': true, 'spine': item.width < 50}"
|
:class="{'uk-align-center': true, 'spine': item.width < 50}"
|
||||||
@ -50,6 +50,9 @@
|
|||||||
width:${mode === 'width' ? '100vw' : 'auto'};
|
width:${mode === 'width' ? '100vw' : 'auto'};
|
||||||
height:${mode === 'height' ? '100vh' : 'auto'};
|
height:${mode === 'height' ? '100vh' : 'auto'};
|
||||||
margin-bottom:0;
|
margin-bottom:0;
|
||||||
|
max-width:100%;
|
||||||
|
max-height:100%;
|
||||||
|
object-fit: contain;
|
||||||
`" />
|
`" />
|
||||||
|
|
||||||
<div style="position:absolute;z-index:1; top:0;left:0; width:30%;height:100%;" @click="flipPage(false)"></div>
|
<div style="position:absolute;z-index:1; top:0;left:0; width:30%;height:100%;" @click="flipPage(false)"></div>
|
||||||
@ -98,6 +101,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-margin uk-form-horizontal" x-show="mode !== 'continuous'">
|
||||||
|
<label class="uk-form-label" for="enable-flip-animation">Enable Flip Animation</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input id="enable-flip-animation" class="uk-checkbox" type="checkbox" x-model="enableFlipAnimation" @change="enableFlipAnimationChanged()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin uk-form-horizontal" x-show="mode !== 'continuous'">
|
||||||
|
<label class="uk-form-label" for="preload-lookahead" x-text="`Preload Image: ${preloadLookahead} page(s)`"></label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input id="preload-lookahead" class="uk-range" type="range" min="0" max="5" step="1" x-model.number="preloadLookahead" @change="preloadLookaheadChanged()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class="uk-divider-icon">
|
<hr class="uk-divider-icon">
|
||||||
|
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user