mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-03 11:25:29 -04:00
WIP
This commit is contained in:
parent
87c479bf42
commit
59bcb4db3b
@ -1,257 +0,0 @@
|
|||||||
// TODO: limit the number of chapters to list
|
|
||||||
|
|
||||||
const component = () => {
|
|
||||||
return {
|
|
||||||
plugins: [],
|
|
||||||
info: undefined,
|
|
||||||
pid: undefined,
|
|
||||||
chapters: undefined, // undefined: not searched yet, []: empty
|
|
||||||
allChapters: [],
|
|
||||||
query: '',
|
|
||||||
mangaTitle: '',
|
|
||||||
searching: false,
|
|
||||||
adding: false,
|
|
||||||
sortOptions: [],
|
|
||||||
showFilters: false,
|
|
||||||
appliedFilters: [],
|
|
||||||
chaptersLimit: 500,
|
|
||||||
init() {
|
|
||||||
const tableObserver = new MutationObserver(() => {
|
|
||||||
console.log('table mutated');
|
|
||||||
$('#selectable').selectable({
|
|
||||||
filter: 'tr'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
tableObserver.observe($('table').get(0), {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
fetch(`${base_url}api/admin/plugin`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.success)
|
|
||||||
throw new Error(data.error);
|
|
||||||
this.plugins = data.plugins;
|
|
||||||
|
|
||||||
const pid = localStorage.getItem('plugin');
|
|
||||||
if (pid && this.plugins.map(p => p.id).includes(pid))
|
|
||||||
return this.loadPlugin(pid);
|
|
||||||
|
|
||||||
if (this.plugins.length > 0)
|
|
||||||
this.loadPlugin(this.plugins[0].id);
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
alert('danger', `Failed to list the available plugins. Error: ${e}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
loadPlugin(pid) {
|
|
||||||
fetch(`${base_url}api/admin/plugin/info?${new URLSearchParams({
|
|
||||||
plugin: pid
|
|
||||||
})}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.success)
|
|
||||||
throw new Error(data.error);
|
|
||||||
this.info = data.info;
|
|
||||||
this.pid = pid;
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
alert('danger', `Failed to get plugin metadata. Error: ${e}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
pluginChanged() {
|
|
||||||
this.loadPlugin(this.pid);
|
|
||||||
localStorage.setItem('plugin', this.pid);
|
|
||||||
},
|
|
||||||
get chapterKeys() {
|
|
||||||
if (this.allChapters.length < 1) return [];
|
|
||||||
return Object.keys(this.allChapters[0]).filter(k => !['manga_title'].includes(k));
|
|
||||||
},
|
|
||||||
search() {
|
|
||||||
this.searching = true;
|
|
||||||
this.allChapters = [];
|
|
||||||
this.chapters = undefined;
|
|
||||||
fetch(`${base_url}api/admin/plugin/list?${new URLSearchParams({
|
|
||||||
plugin: this.pid,
|
|
||||||
query: this.query
|
|
||||||
})}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.success)
|
|
||||||
throw new Error(data.error);
|
|
||||||
try {
|
|
||||||
this.mangaTitle = data.chapters[0].manga_title;
|
|
||||||
if (!this.mangaTitle) throw new Error();
|
|
||||||
} catch (e) {
|
|
||||||
this.mangaTitle = data.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.chapters = Array(10).fill(data.chapters).flat();
|
|
||||||
|
|
||||||
this.allChapters = data.chapters;
|
|
||||||
this.chapters = data.chapters;
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
alert('danger', `Failed to list chapters. Error: ${e}`);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.searching = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selectAll() {
|
|
||||||
$('tbody > tr').each((i, e) => {
|
|
||||||
$(e).addClass('ui-selected');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
clearSelection() {
|
|
||||||
$('tbody > tr').each((i, e) => {
|
|
||||||
$(e).removeClass('ui-selected');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
download() {
|
|
||||||
const selected = $('tbody > tr.ui-selected').get();
|
|
||||||
if (selected.length === 0) return;
|
|
||||||
|
|
||||||
UIkit.modal.confirm(`Download ${selected.length} selected chapters?`).then(() => {
|
|
||||||
const ids = selected.map(e => e.id);
|
|
||||||
const chapters = this.chapters.filter(c => ids.includes(c.id));
|
|
||||||
console.log(chapters);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
thClicked(event) {
|
|
||||||
const idx = parseInt(event.currentTarget.id.split('-')[1]);
|
|
||||||
if (idx === undefined || isNaN(idx)) return;
|
|
||||||
const curOption = this.sortOptions[idx];
|
|
||||||
let option;
|
|
||||||
this.sortOptions = [];
|
|
||||||
switch (curOption) {
|
|
||||||
case 1:
|
|
||||||
option = -1;
|
|
||||||
break;
|
|
||||||
case -1:
|
|
||||||
option = 0;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
option = 1;
|
|
||||||
}
|
|
||||||
this.sortOptions[idx] = option;
|
|
||||||
this.sort(this.chapterKeys[idx], option)
|
|
||||||
},
|
|
||||||
// Returns an array of filtered but unsorted chapters. Useful when
|
|
||||||
// reseting the sort options.
|
|
||||||
get filteredChapters() {
|
|
||||||
let ary = this.allChapters.slice();
|
|
||||||
|
|
||||||
console.log('initial size:', ary.length);
|
|
||||||
for (let filter of this.appliedFilters) {
|
|
||||||
if (!filter.value) continue;
|
|
||||||
if (filter.type === 'array' && filter.value === 'all') continue;
|
|
||||||
|
|
||||||
console.log('applying filter:', filter);
|
|
||||||
|
|
||||||
if (filter.type === 'string') {
|
|
||||||
ary = ary.filter(ch => ch[filter.key].toLowerCase().includes(filter.value.toLowerCase()));
|
|
||||||
}
|
|
||||||
if (filter.type === 'number-min') {
|
|
||||||
ary = ary.filter(ch => Number(ch[filter.key]) >= Number(filter.value));
|
|
||||||
}
|
|
||||||
if (filter.type === 'number-max') {
|
|
||||||
ary = ary.filter(ch => Number(ch[filter.key]) <= Number(filter.value));
|
|
||||||
}
|
|
||||||
if (filter.type === 'date-min') {
|
|
||||||
ary = ary.filter(ch => Date.parse(ch[filter.key]) >= Date.parse(filter.value));
|
|
||||||
}
|
|
||||||
if (filter.type === 'date-max') {
|
|
||||||
ary = ary.filter(ch => Date.parse(ch[filter.key]) <= Date.parse(filter.value));
|
|
||||||
}
|
|
||||||
if (filter.type === 'array') {
|
|
||||||
ary = ary.filter(ch => ch[filter.key].map(s => typeof s === 'string' ? s.toLowerCase() : s).includes(filter.value.toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('filtered size:', ary.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ary;
|
|
||||||
},
|
|
||||||
// option:
|
|
||||||
// - 1: asending
|
|
||||||
// - -1: desending
|
|
||||||
// - 0: unsorted
|
|
||||||
sort(key, option) {
|
|
||||||
if (option === 0) {
|
|
||||||
this.chapters = this.filteredChapters;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chapters = this.filteredChapters.sort((a, b) => {
|
|
||||||
const comp = this.compare(a[key], b[key]);
|
|
||||||
return option < 0 ? comp * -1 : comp;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
compare(a, b) {
|
|
||||||
if (a === b) return 0;
|
|
||||||
|
|
||||||
// try numbers
|
|
||||||
// this must come before the date checks, because any integer would
|
|
||||||
// also be parsed as a date.
|
|
||||||
if (!isNaN(a) && !isNaN(b))
|
|
||||||
return Number(a) - Number(b);
|
|
||||||
|
|
||||||
// try dates
|
|
||||||
if (!isNaN(Date.parse(a)) && !isNaN(Date.parse(b)))
|
|
||||||
return Date.parse(a) - Date.parse(b);
|
|
||||||
|
|
||||||
const preprocessString = (val) => {
|
|
||||||
if (typeof val !== 'string') return val;
|
|
||||||
return val.toLowerCase().replace(/\s\s/g, ' ').trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
return preprocessString(a) > preprocessString(b) ? 1 : -1;
|
|
||||||
},
|
|
||||||
fieldType(values) {
|
|
||||||
if (values.every(v => !isNaN(v))) return 'number'; // display input for number range
|
|
||||||
if (values.every(v => !isNaN(Date.parse(v)))) return 'date'; // display input for date range
|
|
||||||
if (values.every(v => Array.isArray(v))) return 'array'; // display input for contains
|
|
||||||
return 'string'; // display input for string searching.
|
|
||||||
// for the last two, if the number of options is small enough (say < 50), display a multi-select2
|
|
||||||
},
|
|
||||||
get filters() {
|
|
||||||
if (this.allChapters.length < 1) return [];
|
|
||||||
const keys = Object.keys(this.allChapters[0]).filter(k => !['manga_title', 'id'].includes(k));
|
|
||||||
return keys.map(k => {
|
|
||||||
let values = this.allChapters.map(c => c[k]);
|
|
||||||
const type = this.fieldType(values);
|
|
||||||
|
|
||||||
if (type === 'array') {
|
|
||||||
// if the type is an array, return the list of available elements
|
|
||||||
// example: an array of groups or authors
|
|
||||||
values = Array.from(new Set(values.flat().map(v => {
|
|
||||||
if (typeof v === 'string') return v.toLowerCase();
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: k,
|
|
||||||
type: type,
|
|
||||||
values: values
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
applyFilters() {
|
|
||||||
const values = $('#filter-form input, #filter-form select')
|
|
||||||
.get()
|
|
||||||
.map(i => ({
|
|
||||||
key: i.getAttribute('data-filter-key'),
|
|
||||||
value: i.value.trim(),
|
|
||||||
type: i.getAttribute('data-filter-type')
|
|
||||||
}));
|
|
||||||
this.appliedFilters = values;
|
|
||||||
this.chapters = this.filteredChapters;
|
|
||||||
},
|
|
||||||
clearFilters() {
|
|
||||||
$('#filter-form input').get().forEach(i => i.value = '');
|
|
||||||
this.appliedFilters = [];
|
|
||||||
this.chapters = this.filteredChapters;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,139 +1,326 @@
|
|||||||
const loadPlugin = id => {
|
const component = () => {
|
||||||
localStorage.setItem('plugin', id);
|
return {
|
||||||
const url = `${location.protocol}//${location.host}${location.pathname}`;
|
plugins: [],
|
||||||
const newURL = `${url}?${$.param({
|
info: undefined,
|
||||||
plugin: id
|
pid: undefined,
|
||||||
})}`;
|
chapters: undefined, // undefined: not searched yet, []: empty
|
||||||
window.location.href = newURL;
|
manga: undefined, // undefined: not searched yet, []: empty
|
||||||
};
|
allChapters: [],
|
||||||
|
query: '',
|
||||||
$(() => {
|
mangaTitle: '',
|
||||||
var storedID = localStorage.getItem('plugin');
|
searching: false,
|
||||||
if (storedID && storedID !== pid) {
|
adding: false,
|
||||||
loadPlugin(storedID);
|
sortOptions: [],
|
||||||
} else {
|
showFilters: false,
|
||||||
$('#controls').removeAttr('hidden');
|
appliedFilters: [],
|
||||||
}
|
chaptersLimit: 500,
|
||||||
|
listManga: false,
|
||||||
$('#search-input').keypress(event => {
|
|
||||||
if (event.which === 13) {
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#plugin-select').val(pid);
|
|
||||||
$('#plugin-select').change(() => {
|
|
||||||
const id = $('#plugin-select').val();
|
|
||||||
loadPlugin(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let mangaTitle = "";
|
|
||||||
let searching = false;
|
|
||||||
const search = () => {
|
|
||||||
if (searching)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const query = $.param({
|
|
||||||
query: $('#search-input').val(),
|
|
||||||
plugin: pid
|
|
||||||
});
|
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
url: `${base_url}api/admin/plugin/list?${query}`,
|
|
||||||
contentType: "application/json",
|
|
||||||
dataType: 'json'
|
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', `Search failed. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mangaTitle = data.title;
|
|
||||||
$('#title-text').text(data.title);
|
|
||||||
buildTable(data.chapters);
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Search failed. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
})
|
|
||||||
.always(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildTable = (chapters) => {
|
|
||||||
$('#table').attr('hidden', '');
|
|
||||||
$('table').empty();
|
|
||||||
|
|
||||||
const keys = Object.keys(chapters[0]).map(k => `<th>${k}</th>`).join('');
|
|
||||||
const thead = `<thead><tr>${keys}</tr></thead>`;
|
|
||||||
$('table').append(thead);
|
|
||||||
|
|
||||||
const rows = chapters.map(ch => {
|
|
||||||
const tds = Object.values(ch).map(v => `<td>${v}</td>`).join('');
|
|
||||||
return `<tr data-id="${ch.id}" data-title="${ch.title}">${tds}</tr>`;
|
|
||||||
});
|
|
||||||
const tbody = `<tbody id="selectable">${rows}</tbody>`;
|
|
||||||
$('table').append(tbody);
|
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const tableObserver = new MutationObserver(() => {
|
||||||
|
console.log('table mutated');
|
||||||
$('#selectable').selectable({
|
$('#selectable').selectable({
|
||||||
filter: 'tr'
|
filter: 'tr'
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
tableObserver.observe($('table').get(0), {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
fetch(`${base_url}api/admin/plugin`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success)
|
||||||
|
throw new Error(data.error);
|
||||||
|
this.plugins = data.plugins;
|
||||||
|
|
||||||
$('#table table').tablesorter();
|
const pid = localStorage.getItem('plugin');
|
||||||
$('#table').removeAttr('hidden');
|
if (pid && this.plugins.map(p => p.id).includes(pid))
|
||||||
};
|
return this.loadPlugin(pid);
|
||||||
|
|
||||||
const selectAll = () => {
|
if (this.plugins.length > 0)
|
||||||
|
this.loadPlugin(this.plugins[0].id);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
alert('danger', `Failed to list the available plugins. Error: ${e}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadPlugin(pid) {
|
||||||
|
fetch(`${base_url}api/admin/plugin/info?${new URLSearchParams({
|
||||||
|
plugin: pid
|
||||||
|
})}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success)
|
||||||
|
throw new Error(data.error);
|
||||||
|
this.info = data.info;
|
||||||
|
this.pid = pid;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
alert('danger', `Failed to get plugin metadata. Error: ${e}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pluginChanged() {
|
||||||
|
this.loadPlugin(this.pid);
|
||||||
|
localStorage.setItem('plugin', this.pid);
|
||||||
|
},
|
||||||
|
get chapterKeys() {
|
||||||
|
if (this.allChapters.length < 1) return [];
|
||||||
|
return Object.keys(this.allChapters[0]).filter(k => !['manga_title'].includes(k));
|
||||||
|
},
|
||||||
|
searchChapters(query) {
|
||||||
|
this.searching = true;
|
||||||
|
this.allChapters = [];
|
||||||
|
this.chapters = undefined;
|
||||||
|
this.listManga = false;
|
||||||
|
fetch(`${base_url}api/admin/plugin/list?${new URLSearchParams({
|
||||||
|
plugin: this.pid,
|
||||||
|
query: query
|
||||||
|
})}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success)
|
||||||
|
throw new Error(data.error);
|
||||||
|
try {
|
||||||
|
this.mangaTitle = data.chapters[0].manga_title;
|
||||||
|
if (!this.mangaTitle) throw new Error();
|
||||||
|
} catch (e) {
|
||||||
|
this.mangaTitle = data.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allChapters = data.chapters;
|
||||||
|
this.chapters = data.chapters;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
alert('danger', `Failed to list chapters. Error: ${e}`);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.searching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
searchManga() {
|
||||||
|
this.searching = true;
|
||||||
|
this.allChapters = [];
|
||||||
|
this.chapters = undefined;
|
||||||
|
this.manga = undefined;
|
||||||
|
fetch(`${base_url}api/admin/plugin/search?${new URLSearchParams({
|
||||||
|
plugin: this.pid,
|
||||||
|
query: this.query
|
||||||
|
})}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success)
|
||||||
|
throw new Error(data.error);
|
||||||
|
this.manga = data.manga;
|
||||||
|
this.listManga = true;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
alert('danger', `Search failed. Error: ${e}`);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.searching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
search() {
|
||||||
|
this.manga = undefined;
|
||||||
|
if (this.info.version === 1) {
|
||||||
|
this.searchChapters(this.query);
|
||||||
|
} else {
|
||||||
|
this.searchManga();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectAll() {
|
||||||
$('tbody > tr').each((i, e) => {
|
$('tbody > tr').each((i, e) => {
|
||||||
$(e).addClass('ui-selected');
|
$(e).addClass('ui-selected');
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
clearSelection() {
|
||||||
const unselect = () => {
|
|
||||||
$('tbody > tr').each((i, e) => {
|
$('tbody > tr').each((i, e) => {
|
||||||
$(e).removeClass('ui-selected');
|
$(e).removeClass('ui-selected');
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
download() {
|
||||||
const download = () => {
|
const selected = $('tbody > tr.ui-selected').get();
|
||||||
const selected = $('tbody > tr.ui-selected');
|
|
||||||
if (selected.length === 0) return;
|
if (selected.length === 0) return;
|
||||||
|
|
||||||
UIkit.modal.confirm(`Download ${selected.length} selected chapters?`).then(() => {
|
UIkit.modal.confirm(`Download ${selected.length} selected chapters?`).then(() => {
|
||||||
$('#download-btn').attr('hidden', '');
|
const ids = selected.map(e => e.id);
|
||||||
$('#download-spinner').removeAttr('hidden');
|
const chapters = this.chapters.filter(c => ids.includes(c.id));
|
||||||
const chapters = selected.map((i, e) => {
|
|
||||||
return {
|
|
||||||
id: $(e).attr('data-id'),
|
|
||||||
title: $(e).attr('data-title')
|
|
||||||
}
|
|
||||||
}).get();
|
|
||||||
console.log(chapters);
|
console.log(chapters);
|
||||||
$.ajax({
|
this.adding = true;
|
||||||
type: 'POST',
|
fetch(`${base_url}api/admin/plugin/download`, {
|
||||||
url: base_url + 'api/admin/plugin/download',
|
method: 'POST',
|
||||||
data: JSON.stringify({
|
body: JSON.stringify({
|
||||||
plugin: pid,
|
chapters,
|
||||||
chapters: chapters,
|
plugin: this.pid,
|
||||||
title: mangaTitle
|
title: this.mangaTitle
|
||||||
}),
|
}),
|
||||||
contentType: "application/json",
|
headers: {
|
||||||
dataType: 'json'
|
"Content-Type": "application/json"
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success)
|
||||||
|
throw new Error(data.error);
|
||||||
const successCount = parseInt(data.success);
|
const successCount = parseInt(data.success);
|
||||||
const failCount = parseInt(data.fail);
|
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>.`);
|
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) => {
|
.catch(e => {
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to add chapters to the download queue. Error: ${e}`);
|
||||||
})
|
})
|
||||||
.always(() => {
|
.finally(() => {
|
||||||
$('#download-spinner').attr('hidden', '');
|
this.adding = false;
|
||||||
$('#download-btn').removeAttr('hidden');
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
thClicked(event) {
|
||||||
|
const idx = parseInt(event.currentTarget.id.split('-')[1]);
|
||||||
|
if (idx === undefined || isNaN(idx)) return;
|
||||||
|
const curOption = this.sortOptions[idx];
|
||||||
|
let option;
|
||||||
|
this.sortOptions = [];
|
||||||
|
switch (curOption) {
|
||||||
|
case 1:
|
||||||
|
option = -1;
|
||||||
|
break;
|
||||||
|
case -1:
|
||||||
|
option = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
option = 1;
|
||||||
|
}
|
||||||
|
this.sortOptions[idx] = option;
|
||||||
|
this.sort(this.chapterKeys[idx], option)
|
||||||
|
},
|
||||||
|
// Returns an array of filtered but unsorted chapters. Useful when
|
||||||
|
// reseting the sort options.
|
||||||
|
get filteredChapters() {
|
||||||
|
let ary = this.allChapters.slice();
|
||||||
|
|
||||||
|
console.log('initial size:', ary.length);
|
||||||
|
for (let filter of this.appliedFilters) {
|
||||||
|
if (!filter.value) continue;
|
||||||
|
if (filter.type === 'array' && filter.value === 'all') continue;
|
||||||
|
|
||||||
|
console.log('applying filter:', filter);
|
||||||
|
|
||||||
|
if (filter.type === 'string') {
|
||||||
|
ary = ary.filter(ch => ch[filter.key].toLowerCase().includes(filter.value.toLowerCase()));
|
||||||
|
}
|
||||||
|
if (filter.type === 'number-min') {
|
||||||
|
ary = ary.filter(ch => Number(ch[filter.key]) >= Number(filter.value));
|
||||||
|
}
|
||||||
|
if (filter.type === 'number-max') {
|
||||||
|
ary = ary.filter(ch => Number(ch[filter.key]) <= Number(filter.value));
|
||||||
|
}
|
||||||
|
if (filter.type === 'date-min') {
|
||||||
|
ary = ary.filter(ch => this.parseDate(ch[filter.key]) >= this.parseDate(filter.value));
|
||||||
|
}
|
||||||
|
if (filter.type === 'date-max') {
|
||||||
|
ary = ary.filter(ch => this.parseDate(ch[filter.key]) <= this.parseDate(filter.value));
|
||||||
|
}
|
||||||
|
if (filter.type === 'array') {
|
||||||
|
ary = ary.filter(ch => ch[filter.key].map(s => typeof s === 'string' ? s.toLowerCase() : s).includes(filter.value.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('filtered size:', ary.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ary;
|
||||||
|
},
|
||||||
|
// option:
|
||||||
|
// - 1: asending
|
||||||
|
// - -1: desending
|
||||||
|
// - 0: unsorted
|
||||||
|
sort(key, option) {
|
||||||
|
if (option === 0) {
|
||||||
|
this.chapters = this.filteredChapters;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chapters = this.filteredChapters.sort((a, b) => {
|
||||||
|
const comp = this.compare(a[key], b[key]);
|
||||||
|
return option < 0 ? comp * -1 : comp;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
compare(a, b) {
|
||||||
|
if (a === b) return 0;
|
||||||
|
|
||||||
|
// try numbers
|
||||||
|
// this must come before the date checks, because any integer would
|
||||||
|
// also be parsed as a date.
|
||||||
|
if (!isNaN(a) && !isNaN(b))
|
||||||
|
return Number(a) - Number(b);
|
||||||
|
|
||||||
|
// try dates
|
||||||
|
if (!isNaN(this.parseDate(a)) && !isNaN(this.parseDate(b)))
|
||||||
|
return this.parseDate(a) - this.parseDate(b);
|
||||||
|
|
||||||
|
const preprocessString = (val) => {
|
||||||
|
if (typeof val !== 'string') return val;
|
||||||
|
return val.toLowerCase().replace(/\s\s/g, ' ').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
return preprocessString(a) > preprocessString(b) ? 1 : -1;
|
||||||
|
},
|
||||||
|
fieldType(values) {
|
||||||
|
if (values.every(v => !isNaN(v))) return 'number'; // display input for number range
|
||||||
|
if (values.every(v => !isNaN(this.parseDate(v)))) return 'date'; // display input for date range
|
||||||
|
if (values.every(v => Array.isArray(v))) return 'array'; // display input for contains
|
||||||
|
return 'string'; // display input for string searching.
|
||||||
|
// for the last two, if the number of options is small enough (say < 50), display a multi-select2
|
||||||
|
},
|
||||||
|
get filters() {
|
||||||
|
if (this.allChapters.length < 1) return [];
|
||||||
|
const keys = Object.keys(this.allChapters[0]).filter(k => !['manga_title', 'id'].includes(k));
|
||||||
|
return keys.map(k => {
|
||||||
|
let values = this.allChapters.map(c => c[k]);
|
||||||
|
const type = this.fieldType(values);
|
||||||
|
|
||||||
|
if (type === 'array') {
|
||||||
|
// if the type is an array, return the list of available elements
|
||||||
|
// example: an array of groups or authors
|
||||||
|
values = Array.from(new Set(values.flat().map(v => {
|
||||||
|
if (typeof v === 'string') return v.toLowerCase();
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: k,
|
||||||
|
type: type,
|
||||||
|
values: values
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyFilters() {
|
||||||
|
const values = $('#filter-form input, #filter-form select')
|
||||||
|
.get()
|
||||||
|
.map(i => ({
|
||||||
|
key: i.getAttribute('data-filter-key'),
|
||||||
|
value: i.value.trim(),
|
||||||
|
type: i.getAttribute('data-filter-type')
|
||||||
|
}));
|
||||||
|
this.appliedFilters = values;
|
||||||
|
this.chapters = this.filteredChapters;
|
||||||
|
},
|
||||||
|
clearFilters() {
|
||||||
|
$('#filter-form input').get().forEach(i => i.value = '');
|
||||||
|
this.appliedFilters = [];
|
||||||
|
this.chapters = this.filteredChapters;
|
||||||
|
},
|
||||||
|
mangaSelected(event) {
|
||||||
|
const mid = event.currentTarget.getAttribute('data-id');
|
||||||
|
this.searchChapters(mid);
|
||||||
|
},
|
||||||
|
parseDate(str) {
|
||||||
|
const regex = /([0-9]+[/\-,\ ][0-9]+[/\-,\ ][0-9]+)|([A-Za-z]+)[/\-,\ ]+[0-9]+(st|nd|rd|th)?[/\-,\ ]+[0-9]+/g;
|
||||||
|
// Basic sanity check to make sure it's an actual date.
|
||||||
|
// We need this because Date.parse thinks 'Chapter 1' is a date.
|
||||||
|
if (!regex.test(str))
|
||||||
|
return NaN;
|
||||||
|
return Date.parse(str);
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -44,7 +44,7 @@ class Plugin
|
|||||||
@{{name.id}} = @json[{{name}}].as_s
|
@{{name.id}} = @json[{{name}}].as_s
|
||||||
{% end %}
|
{% end %}
|
||||||
@wait_seconds = @json["wait_seconds"].as_i.to_u64
|
@wait_seconds = @json["wait_seconds"].as_i.to_u64
|
||||||
@version = @json["api_verson"]?.try(&.as_i.to_u64) || 1u64
|
@version = @json["api_version"]?.try(&.as_i.to_u64) || 1u64
|
||||||
|
|
||||||
if @version > 1 && (settings_hash = @json["settings"]?.try &.as_h?)
|
if @version > 1 && (settings_hash = @json["settings"]?.try &.as_h?)
|
||||||
settings_hash.each do |k, v|
|
settings_hash.each do |k, v|
|
||||||
|
@ -639,7 +639,7 @@ struct APIRouter
|
|||||||
"error" => String?,
|
"error" => String?,
|
||||||
"chapters?" => [{
|
"chapters?" => [{
|
||||||
"id" => String,
|
"id" => String,
|
||||||
"title" => String,
|
"title?" => String,
|
||||||
}],
|
}],
|
||||||
"title" => String?,
|
"title" => String?,
|
||||||
}
|
}
|
||||||
@ -649,8 +649,14 @@ struct APIRouter
|
|||||||
plugin = Plugin.new env.params.query["plugin"].as String
|
plugin = Plugin.new env.params.query["plugin"].as String
|
||||||
|
|
||||||
json = plugin.list_chapters query
|
json = plugin.list_chapters query
|
||||||
|
|
||||||
|
if plugin.info.version == 1
|
||||||
chapters = json["chapters"]
|
chapters = json["chapters"]
|
||||||
title = json["title"]
|
title = json["title"]
|
||||||
|
else
|
||||||
|
chapters = json
|
||||||
|
title = nil
|
||||||
|
end
|
||||||
|
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => true,
|
"success" => true,
|
||||||
|
@ -79,16 +79,6 @@ struct MainRouter
|
|||||||
|
|
||||||
get "/download/plugins" do |env|
|
get "/download/plugins" do |env|
|
||||||
begin
|
begin
|
||||||
id = env.params.query["plugin"]?
|
|
||||||
plugins = Plugin.list
|
|
||||||
plugin = nil
|
|
||||||
|
|
||||||
if id
|
|
||||||
plugin = Plugin.new id
|
|
||||||
elsif !plugins.empty?
|
|
||||||
plugin = Plugin.new plugins[0][:id]
|
|
||||||
end
|
|
||||||
|
|
||||||
layout "plugin-download"
|
layout "plugin-download"
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
@ -96,15 +86,6 @@ struct MainRouter
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/download/plugins2" do |env|
|
|
||||||
begin
|
|
||||||
layout "plugin-download-2"
|
|
||||||
rescue e
|
|
||||||
Logger.error e
|
|
||||||
env.response.status_code = 500
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/download/subscription" do |env|
|
get "/download/subscription" do |env|
|
||||||
mangadex_base_url = Config.current.mangadex["base_url"]
|
mangadex_base_url = Config.current.mangadex["base_url"]
|
||||||
username = get_username env
|
username = get_username env
|
||||||
|
@ -1,155 +0,0 @@
|
|||||||
<div x-data="component()" x-init="init()" x-cloak>
|
|
||||||
<div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
|
|
||||||
<div class="uk-container uk-text-center" x-show="plugins.length === 0" style="width:100%">
|
|
||||||
<h2>No Plugins Found</h2>
|
|
||||||
<p>We could't find any plugins in the directory <code><%= Config.current.plugin_path %></code>.</p>
|
|
||||||
<p>You can download official plugins from the <a href="https://github.com/hkalexling/mango-plugins">Mango plugins repository</a>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-show="plugins.length > 0" style="width:100%">
|
|
||||||
<h2 class=uk-title>Download with Plugins
|
|
||||||
<span x-show="searching" uk-spinner class="uk-margin-left"></span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<template x-if="info !== undefined">
|
|
||||||
<div>
|
|
||||||
<div class="uk-grid-small" uk-grid>
|
|
||||||
<div class="uk-width-3-4@m uk-child-width-1-1">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<label class="uk-form-label"> </label>
|
|
||||||
<input class="uk-input" type="text" :placeholder="info.placeholder" x-model="query" @keydown.enter="search()">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-width-expand">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label">Choose a plugin</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<select class="uk-select" x-model="pid" @change="pluginChanged()">
|
|
||||||
<template x-for="p in plugins" :key="p">
|
|
||||||
<option :value="p.id" x-text="p.title"></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="uk-width-auto">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label"> </label>
|
|
||||||
<div class="uk-form-controls" style="padding-top: 10px;">
|
|
||||||
<span uk-icon="info" uk-toggle="target: #toggle"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template x-for="entry, idx in Object.entries(info).filter(tp => !['id', 'settings'].includes(tp[0]))" :key="idx">
|
|
||||||
<dl class="uk-description-list" id="toggle" hidden>
|
|
||||||
<dt x-text="entry[0]"></dt>
|
|
||||||
<dd x-text="entry[1]"></dd>
|
|
||||||
</dl>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="uk-margin-large-top" x-show="chapters !== undefined">
|
|
||||||
<h3 x-text="mangaTitle"></h3>
|
|
||||||
<p x-text="`${chapters ? chapters.length : 0} chapters found`"></p>
|
|
||||||
|
|
||||||
<div class="uk-margin">
|
|
||||||
<div x-show="chapters && chapters.length > 0 && chapters.length <= chaptersLimit">
|
|
||||||
<button class="uk-button uk-button-default" @click="selectAll()">Select All</button>
|
|
||||||
<button class="uk-button uk-button-default" @click="clearSelection()">Clear Selections</button>
|
|
||||||
<button class="uk-button uk-button-primary" @click="download()">Download Selected</button>
|
|
||||||
<button class="uk-icon-button uk-margin-small-left" uk-icon="settings" @click="showFilters = !showFilters"></button>
|
|
||||||
</div>
|
|
||||||
<div uk-spinner class="uk-margin-left" x-show="adding"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form x-show="showFilters || (chapters && chapters.length > chaptersLimit)" class="uk-form-stacked uk-margin-bottom" id="filter-form">
|
|
||||||
<template x-for="field in filters">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<label class="uk-form-label">
|
|
||||||
<span x-text="field.key"></span>
|
|
||||||
<template x-if="field.type === 'number'">
|
|
||||||
<span class="uk-text-meta" x-text="`(between ${Math.min(...field.values)} and ${Math.max(...field.values)})`"></span>
|
|
||||||
</template>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div x-show="field.type === 'number'" class="uk-grid-small" uk-grid>
|
|
||||||
<div class="uk-width-1-2@s">
|
|
||||||
<input class="uk-input" placeholder="minimum value" :data-filter-key="field.key" data-filter-type="number-min">
|
|
||||||
</div>
|
|
||||||
<div class="uk-width-1-2@s">
|
|
||||||
<input class="uk-input" placeholder="maximum value" :data-filter-key="field.key" data-filter-type="number-max">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-show="field.type === 'date'" class="uk-grid-small" uk-grid>
|
|
||||||
<div class="uk-width-1-2@s">
|
|
||||||
<input class="uk-input" placeholder="minimum date, e.g., Jan 1 1970" :data-filter-key="field.key" data-filter-type="date-min">
|
|
||||||
</div>
|
|
||||||
<div class="uk-width-1-2@s">
|
|
||||||
<input class="uk-input" placeholder="maximum date, e.g., Jan 1 1970" :data-filter-key="field.key" data-filter-type="date-max">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input x-show="field.type === 'string'" class="uk-input" placeholder="filter text" :data-filter-key="field.key" data-filter-type="string">
|
|
||||||
|
|
||||||
<select class="uk-select" x-show="field.type === 'array'" :data-filter-key="field.key" data-filter-type="array">
|
|
||||||
<option value="all">All</option>
|
|
||||||
<template x-for="v in field.values" :key="v">
|
|
||||||
<option x-text="v" :value="v"></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<button class="uk-button uk-button-primary" @click.prevent="applyFilters()">Apply</button>
|
|
||||||
<button class="uk-button uk-button-default" @click.prevent="clearFilters()">Clear</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p class="uk-text-meta" x-show="chapters && chapters.length > chaptersLimit" x-text="`The manga has ${chapters ? chapters.length : 0} chapters, but Mango can only list up to ${chaptersLimit}. Please use the filters to narrow down your search.`"></p>
|
|
||||||
<p x-show="chapters && chapters.length === 0" class="uk-text-meta">No chapters found.</p>
|
|
||||||
|
|
||||||
<div x-show="chapters && chapters.length > 0 && chapters.length <= chaptersLimit">
|
|
||||||
<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 class="uk-overflow-auto">
|
|
||||||
<table class="uk-table uk-table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<template x-for="(k, idx) in chapterKeys" :key="k">
|
|
||||||
<th :id="`th-${idx}`" @click="thClicked($event)">
|
|
||||||
<span x-text="k"></span>
|
|
||||||
<i class="fas fa-sort" x-show="![1, -1].includes(sortOptions[idx])"></i>
|
|
||||||
<i class="fas fa-sort-up" x-show="sortOptions[idx] === 1"></i>
|
|
||||||
<i class="fas fa-sort-down" x-show="sortOptions[idx] === -1"></i>
|
|
||||||
</th>
|
|
||||||
</template>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="selectable">
|
|
||||||
<template x-if="chapters !== undefined && chapters.length < chaptersLimit">
|
|
||||||
<template x-for="ch in chapters" :key="ch">
|
|
||||||
<tr class="ui-widget-content" :id="ch.id">
|
|
||||||
<template x-for="k in chapterKeys" :key="k">
|
|
||||||
<td x-text="ch[k]"></td>
|
|
||||||
</template>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% content_for "script" do %>
|
|
||||||
<%= render_component "moment" %>
|
|
||||||
<%= render_component "jquery-ui" %>
|
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
|
||||||
<script src="<%= base_url %>js/plugin-download-2.js"></script>
|
|
||||||
<% end %>
|
|
@ -1,37 +1,42 @@
|
|||||||
<% if plugins.empty? %>
|
<div x-data="component()" x-init="init()" x-cloak>
|
||||||
<div class="uk-container uk-text-center">
|
<div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
|
||||||
|
<div class="uk-container uk-text-center" x-show="plugins.length === 0" style="width:100%">
|
||||||
<h2>No Plugins Found</h2>
|
<h2>No Plugins Found</h2>
|
||||||
<p>We could't find any plugins in the directory <code><%= Config.current.plugin_path %></code>.</p>
|
<p>We could't find any plugins in the directory <code><%= Config.current.plugin_path %></code>.</p>
|
||||||
<p>You can download official plugins from the <a href="https://github.com/hkalexling/mango-plugins">Mango plugins repository</a>.</p>
|
<p>You can download official plugins from the <a href="https://github.com/hkalexling/mango-plugins">Mango plugins repository</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% else %>
|
<div x-show="plugins.length > 0" style="width:100%">
|
||||||
<h2 class=uk-title>Download with Plugins</h2>
|
<h2 class=uk-title>Download with Plugins
|
||||||
|
<span x-show="searching" uk-spinner class="uk-margin-left"></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div id="controls" class="uk-grid-small" uk-grid hidden>
|
<template x-if="info !== undefined">
|
||||||
|
<div>
|
||||||
|
<div class="uk-grid-small" uk-grid>
|
||||||
<div class="uk-width-3-4@m uk-child-width-1-1">
|
<div class="uk-width-3-4@m uk-child-width-1-1">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="search-input"> </label>
|
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<input id="search-input" class="uk-input" type="text" placeholder="<%= plugin.not_nil!.info.placeholder %>">
|
<label class="uk-form-label"> </label>
|
||||||
|
<input class="uk-input" type="text" :placeholder="info.placeholder" x-model="query" @keydown.enter="search()">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-expand">
|
<div class="uk-width-expand">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="plugin-select">Choose a plugin</label>
|
<label class="uk-form-label">Choose a plugin</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<select id="plugin-select" class="uk-select">
|
<select class="uk-select" x-model="pid" @change="pluginChanged()">
|
||||||
<% plugins.each do |p| %>
|
<template x-for="p in plugins" :key="p">
|
||||||
<option value="<%= p[:id] %>"><%= p[:title] %></option>
|
<option :value="p.id" x-text="p.title"></option>
|
||||||
<% end %>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-auto">
|
<div class="uk-width-auto">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="search-input"> </label>
|
<label class="uk-form-label"> </label>
|
||||||
<div class="uk-form-controls" style="padding-top: 10px;">
|
<div class="uk-form-controls" style="padding-top: 10px;">
|
||||||
<span uk-icon="info" uk-toggle="target: #toggle"></span>
|
<span uk-icon="info" uk-toggle="target: #toggle"></span>
|
||||||
</div>
|
</div>
|
||||||
@ -39,39 +44,137 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template x-for="entry, idx in Object.entries(info).filter(tp => !['id', 'settings'].includes(tp[0]))" :key="idx">
|
||||||
<dl class="uk-description-list" id="toggle" hidden>
|
<dl class="uk-description-list" id="toggle" hidden>
|
||||||
<% plugin.not_nil!.info.each do |k, v| %>
|
<dt x-text="entry[0]"></dt>
|
||||||
<dt><%= k %></dt>
|
<dd x-text="entry[1]"></dd>
|
||||||
<dd><%= v.to_s %></dd>
|
|
||||||
<% end %>
|
|
||||||
</dl>
|
</dl>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div id="table" class="uk-margin-large-top" hidden>
|
<template x-if="manga">
|
||||||
<h3 id="title-text"></h3>
|
<div class="uk-margin">
|
||||||
|
<p x-show="manga.length === 0">No matching manga found.</p>
|
||||||
|
<p x-show="manga.length > 0">
|
||||||
|
<span x-text="`${manga.length} manga found`"></span>
|
||||||
|
<span :uk-icon="listManga ? 'chevron-down' : 'chevron-right'" @click="listManga = !listManga"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid x-show="listManga">
|
||||||
|
<template x-for="m in manga" :key="m.id">
|
||||||
|
<div class="item" :data-id="m.id" @click="mangaSelected($event)">
|
||||||
|
<div class="uk-card uk-card-default">
|
||||||
|
<div class="uk-card-media-top uk-inline">
|
||||||
|
<img uk-img :data-src="m.cover_url">
|
||||||
|
</div>
|
||||||
|
<div class="uk-card-body">
|
||||||
|
<h3 class="uk-card-title break-word uk-margin-remove-bottom free-height" x-text="m.title"></h3>
|
||||||
|
<p class="uk-text-meta" x-text="`ID: ${m.id}`"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="uk-margin-large-top" x-show="chapters !== undefined">
|
||||||
|
<h3 x-text="mangaTitle"></h3>
|
||||||
|
<p x-text="`${chapters ? chapters.length : 0} chapters found`"></p>
|
||||||
|
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<button class="uk-button uk-button-default" onclick="selectAll()">Select All</button>
|
<div x-show="chapters && chapters.length > 0 && chapters.length <= chaptersLimit">
|
||||||
<button class="uk-button uk-button-default" onclick="unselect()">Clear Selections</button>
|
<button class="uk-button uk-button-default" @click="selectAll()">Select All</button>
|
||||||
<button class="uk-button uk-button-primary" id="download-btn" onclick="download()">Download Selected</button>
|
<button class="uk-button uk-button-default" @click="clearSelection()">Clear Selections</button>
|
||||||
<div id="download-spinner" uk-spinner class="uk-margin-left" hidden></div>
|
<button class="uk-button uk-button-primary" @click="download()">Download Selected</button>
|
||||||
|
<button class="uk-icon-button uk-margin-small-left" uk-icon="settings" @click="showFilters = !showFilters"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div uk-spinner class="uk-margin-left" x-show="adding"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form x-show="showFilters || (chapters && chapters.length > chaptersLimit)" class="uk-form-stacked uk-margin-bottom" id="filter-form">
|
||||||
|
<template x-for="field in filters">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label">
|
||||||
|
<span x-text="field.key"></span>
|
||||||
|
<template x-if="field.type === 'number'">
|
||||||
|
<span class="uk-text-meta" x-text="`(between ${Math.min(...field.values)} and ${Math.max(...field.values)})`"></span>
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div x-show="field.type === 'number'" class="uk-grid-small" uk-grid>
|
||||||
|
<div class="uk-width-1-2@s">
|
||||||
|
<input class="uk-input" placeholder="minimum value" :data-filter-key="field.key" data-filter-type="number-min">
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-2@s">
|
||||||
|
<input class="uk-input" placeholder="maximum value" :data-filter-key="field.key" data-filter-type="number-max">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="field.type === 'date'" class="uk-grid-small" uk-grid>
|
||||||
|
<div class="uk-width-1-2@s">
|
||||||
|
<input class="uk-input" placeholder="minimum date, e.g., Jan 1 1970" :data-filter-key="field.key" data-filter-type="date-min">
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-2@s">
|
||||||
|
<input class="uk-input" placeholder="maximum date, e.g., Jan 1 1970" :data-filter-key="field.key" data-filter-type="date-max">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input x-show="field.type === 'string'" class="uk-input" placeholder="filter text" :data-filter-key="field.key" data-filter-type="string">
|
||||||
|
|
||||||
|
<select class="uk-select" x-show="field.type === 'array'" :data-filter-key="field.key" data-filter-type="array">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<template x-for="v in field.values" :key="v">
|
||||||
|
<option x-text="v" :value="v"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<button class="uk-button uk-button-primary" @click.prevent="applyFilters()">Apply</button>
|
||||||
|
<button class="uk-button uk-button-default" @click.prevent="clearFilters()">Clear</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="uk-text-meta" x-show="chapters && chapters.length > chaptersLimit" x-text="`The manga has ${chapters ? chapters.length : 0} chapters, but Mango can only list up to ${chaptersLimit}. Please use the filters to narrow down your search.`"></p>
|
||||||
|
<p x-show="chapters && chapters.length === 0" class="uk-text-meta">No chapters found.</p>
|
||||||
|
|
||||||
|
<div x-show="chapters && chapters.length > 0 && chapters.length <= chaptersLimit">
|
||||||
<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>
|
<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 class="uk-overflow-auto">
|
<div class="uk-overflow-auto">
|
||||||
<table class="uk-table uk-table-striped tablesorter">
|
<table class="uk-table uk-table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<template x-for="(k, idx) in chapterKeys" :key="k">
|
||||||
|
<th :id="`th-${idx}`" @click="thClicked($event)">
|
||||||
|
<span x-text="k"></span>
|
||||||
|
<i class="fas fa-sort" x-show="![1, -1].includes(sortOptions[idx])"></i>
|
||||||
|
<i class="fas fa-sort-up" x-show="sortOptions[idx] === 1"></i>
|
||||||
|
<i class="fas fa-sort-down" x-show="sortOptions[idx] === -1"></i>
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="selectable">
|
||||||
|
<template x-if="chapters !== undefined && chapters.length < chaptersLimit">
|
||||||
|
<template x-for="ch in chapters" :key="ch">
|
||||||
|
<tr class="ui-widget-content" :id="ch.id">
|
||||||
|
<template x-for="k in chapterKeys" :key="k">
|
||||||
|
<td x-text="ch[k]"></td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<% if plugin %>
|
|
||||||
<script>
|
|
||||||
var pid = "<%= plugin.not_nil!.info.id %>";
|
|
||||||
</script>
|
|
||||||
<% end %>
|
|
||||||
<%= render_component "jquery-ui" %>
|
<%= render_component "jquery-ui" %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.combined.min.js"></script>
|
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/plugin-download.js"></script>
|
<script src="<%= base_url %>js/plugin-download.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user