cda9a0baac
* update lastestchapter logic * update version
1617 lines
49 KiB
JavaScript
1617 lines
49 KiB
JavaScript
// @ts-check
|
|
/* Imports */
|
|
/** @typedef {import("@libs/filterInputs").Filters} Filters */
|
|
/** @typedef {import("@typings/types").PluginList} PluginList */
|
|
/** @typedef {import("@typings/plugin").Plugin.PluginItem} PluginItem */
|
|
/** @typedef {import("@typings/plugin").Plugin.ChapterItem} ChapterItem */
|
|
/** @typedef {import("@typings/plugin").Plugin.NovelItem} NovelItem */
|
|
/** @typedef {import("@typings/plugin").Plugin.SourceNovel & {totalPages?: number}} SourceNovel */
|
|
/** @typedef {import("@typings/plugin").Plugin.SourcePage} SourcePage */
|
|
/** @typedef {import("@libs/filterInputs").FilterTypes} FilterTypes */
|
|
/** @typedef {import("@libs/filterInputs").FilterTypes.CheckboxGroup} CheckboxGroup */
|
|
/**
|
|
* @template {FilterTypes} T
|
|
* @typedef {import("@libs/filterInputs").ValueOfFilter<T>} ValueOfFilter<T> */
|
|
/**
|
|
* @template {Filters} T
|
|
* @typedef {import("@libs/filterInputs").FilterToValues<T>} FilterToValues<T>
|
|
*/
|
|
/** @typedef {{backgroundColor:string, textColor:string}} Theme */
|
|
/** @typedef {Theme[]} Themes */
|
|
/** @typedef {import("./accordion").AccordionBox} AccordionBox */
|
|
/// <reference path="./types.d.ts" />
|
|
|
|
// #region DOM Queries
|
|
const nav = $('nav');
|
|
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const headerChanger = $('#headerChanger');
|
|
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const popularNovels = $('#popularNovels');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const popularNovels_page_select = $('#popularNovels .page-select');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const popularNovels_fetch_btn = $('#popularNovels .fetch-btn');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const popularNovels_spinner = $('#popularNovels .spinner-border');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const popularNovels_novel_list = $('#popularNovels .novel-list');
|
|
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const searchNovels = $('#searchNovels');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const seacrhNovels_novel_list = $('#searchNovels .novel-list');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const searchNovels_page_select = $('#searchNovels .page-select');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const searchNovels_fetch_btn = $('#searchNovels .fetch-btn');
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const searchNovels_searchbar = $('#searchNovels .searchbar input');
|
|
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const parseNovel = $('#parseNovel');
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const parsePage = $('#parsePage');
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const parseChapter = $('#parseChapter');
|
|
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const fetchImage = $('#fetchImage');
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const resolveUrl = $('#resolveUrl');
|
|
|
|
/** @type {JQuery<HTMLSelectElement>}*/
|
|
const plugin_language_selection = $('#language');
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const plugin_search = $('#plugin');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const plugin_search_results = $('#search-results');
|
|
/** @type {JQuery<HTMLButtonElement>} */
|
|
const clear_plugin_search = $('#clear-search');
|
|
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const previewSwitch = $('#raw-preview-switch');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const chapterViewer = $('#chapter-viewer');
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const filtersModal = $('#filtersModal .modal-body');
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const latestSwitch = $('#latest-switch');
|
|
|
|
// #endregion
|
|
|
|
// #region pagination
|
|
class NovelListPagination {
|
|
constructor(
|
|
/** @type {boolean} */ sL,
|
|
/** @type {JQuery<HTMLDivElement>} */ sE,
|
|
/** @type {JQuery<HTMLDivElement>} */ fC,
|
|
/** @type {JQuery<HTMLDivElement>} */ nL,
|
|
) {
|
|
/** @type {Record<number,NovelItem[]>} */
|
|
this.pageStore = {};
|
|
this._showlatest = sL;
|
|
this.lastFetchedPage = 0;
|
|
this.currentPage = 0;
|
|
this.canFetchMore = true;
|
|
this.selectElement = sE;
|
|
this.fetchContainer = fC;
|
|
this.resetStoreOnFetch = true;
|
|
this.novelList = nL;
|
|
}
|
|
get showLatest() {
|
|
return this._showlatest;
|
|
}
|
|
set showLatest(val) {
|
|
this._showlatest = val;
|
|
this.reset();
|
|
}
|
|
get fetchBtn() {
|
|
return this.fetchContainer.children('button')[0];
|
|
}
|
|
get numSelect() {
|
|
return this.selectElement.children('select')[0];
|
|
}
|
|
lockFetch(cause = 0) {
|
|
this.canFetchMore = false;
|
|
this.fetchBtn.disabled = true;
|
|
if (cause === 1) this.fetchBtn.innerText = 'no more to fetch';
|
|
if (cause === 2) this.fetchBtn.innerText = 'fetching';
|
|
if (cause === 3) this.fetchBtn.innerText = 'error';
|
|
}
|
|
unlockFetch() {
|
|
this.canFetchMore = true;
|
|
this.fetchBtn.disabled = false;
|
|
this.fetchBtn.innerText = 'fetch';
|
|
}
|
|
async fetchNovels(
|
|
/** @type {(page:number, latest: boolean)=>Promise<NovelItem[]>} */ fn,
|
|
) {
|
|
this.error = undefined;
|
|
if (this.resetStoreOnFetch) {
|
|
this.resetStoreOnFetch = false;
|
|
this.pageStore = {};
|
|
}
|
|
try {
|
|
this.lockFetch(2);
|
|
const lfp = ++this.lastFetchedPage;
|
|
console.log('fetching ', lfp, 'page');
|
|
const novels = await fn(lfp, this.showLatest);
|
|
if (novels.length === 0) {
|
|
this.lastFetchedPage--;
|
|
this.lockFetch(1);
|
|
if (this.lastFetchedPage === 0) {
|
|
this.error = 'No novels found!';
|
|
this.render();
|
|
}
|
|
return false;
|
|
}
|
|
this.loadNovels(lfp, novels);
|
|
this.unlockFetch();
|
|
return true;
|
|
} catch (e) {
|
|
this.lastFetchedPage--;
|
|
this.lockFetch(3);
|
|
this.error = `${e}`;
|
|
this.render();
|
|
return false;
|
|
}
|
|
}
|
|
showNextPage() {
|
|
console.log('lf', this.lastFetchedPage, 'cp', this.currentPage);
|
|
if (this.lastFetchedPage > this.currentPage) {
|
|
this.currentPage = this.lastFetchedPage;
|
|
this.render();
|
|
}
|
|
this.numSelect.value = `${this.currentPage}`;
|
|
}
|
|
arePagesTheSame(
|
|
/** @type {NovelItem[]} */ p1,
|
|
/** @type {NovelItem[]} */ p2,
|
|
) {
|
|
if (p1.length !== p2.length) return false;
|
|
let i = 0;
|
|
for (const novel of p1) {
|
|
if (novel.path !== p2[i].path) return false;
|
|
i++;
|
|
}
|
|
return true;
|
|
}
|
|
loadNovels(/** @type {number} */ page, /** @type {NovelItem[]} */ list) {
|
|
this.pageStore[page] = list;
|
|
if (page - 1 > 0 && this.pageStore[page - 1]) {
|
|
if (
|
|
this.arePagesTheSame(this.pageStore[page], this.pageStore[page - 1])
|
|
) {
|
|
if (this.numSelect.nextElementSibling)
|
|
this.numSelect.nextElementSibling.innerHTML =
|
|
"Pages are the same! It's possible that pagination is not implemented in the source!";
|
|
}
|
|
}
|
|
}
|
|
reset() {
|
|
this.hideSelectBox();
|
|
this.currentPage = 0;
|
|
this.lastFetchedPage = 0;
|
|
this.resetStoreOnFetch = true;
|
|
this.error = undefined;
|
|
if (this.numSelect.nextElementSibling) {
|
|
this.numSelect.nextElementSibling.innerHTML = '';
|
|
}
|
|
this.unlockFetch();
|
|
}
|
|
setupSelectBox() {
|
|
this.selectElement[0].style.display = 'inline';
|
|
this.numSelect.innerHTML = '';
|
|
for (let i = 1; i <= this.lastFetchedPage; i++) {
|
|
const option = $('<option>').text(`${i}`).val(`${i}`);
|
|
this.numSelect.append(option[0]);
|
|
}
|
|
this.numSelect.value = `${this.currentPage}`;
|
|
this.numSelect.onchange = () => {
|
|
this.currentPage = parseInt(this.numSelect.value);
|
|
this.render();
|
|
};
|
|
}
|
|
hideSelectBox() {
|
|
this.selectElement[0].style.display = '';
|
|
}
|
|
render() {
|
|
if (this.lastFetchedPage > 1) {
|
|
if (!this.pageStore[this.currentPage]) {
|
|
console.log('no page store, erroring');
|
|
this.reset();
|
|
this.error =
|
|
'Please request new pages after previous one loaded! Try again!';
|
|
}
|
|
}
|
|
if (this.error) {
|
|
this.novelList.html('');
|
|
this.novelList.append(this.error);
|
|
return;
|
|
}
|
|
if (this.lastFetchedPage > 0) {
|
|
this.setupSelectBox();
|
|
this.fetchBtn.innerText = 'fetch next';
|
|
}
|
|
this.novelList.html('');
|
|
this.novelList.append(
|
|
...this.pageStore[this.currentPage].map(novel =>
|
|
PluginWrapper.createNovelItem(novel, {
|
|
target: popularNovels[0],
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
// #endregion
|
|
|
|
// #region state
|
|
const state = {
|
|
/** @type {PluginList} */
|
|
all_plugins: {},
|
|
/** @type {Array<PluginItem>} */
|
|
plugin_infos: [],
|
|
/** @type {{language: string, keyword:null|string}} */
|
|
plugin_search: {
|
|
language: '',
|
|
keyword: null,
|
|
},
|
|
/** @type {Array<PluginItem>} */
|
|
plugins_in_search: [],
|
|
/** @type {PluginWrapper | undefined} */
|
|
current_plugin: undefined,
|
|
/** @type {FilterToValues<Filters>} */
|
|
filterValues: {},
|
|
/** popularNovels pagination states */
|
|
popular_pages: new NovelListPagination(
|
|
false,
|
|
popularNovels_page_select,
|
|
popularNovels_fetch_btn,
|
|
popularNovels_novel_list,
|
|
),
|
|
search_pages: new NovelListPagination(
|
|
false,
|
|
searchNovels_page_select,
|
|
searchNovels_fetch_btn,
|
|
seacrhNovels_novel_list,
|
|
),
|
|
};
|
|
// #endregion
|
|
|
|
// #region util functions
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {RequestInit | undefined} init
|
|
*/
|
|
const fetchFromAPI = async (path, init = undefined) => {
|
|
if (init) {
|
|
init.headers = {
|
|
...init.headers,
|
|
'x-custom-headers': JSON.stringify(getHeaders()),
|
|
};
|
|
}
|
|
return fetch(path, init);
|
|
};
|
|
// #endregion
|
|
|
|
class PluginWrapper {
|
|
/**
|
|
* @param {string} requirePath
|
|
*/
|
|
constructor(requirePath) {
|
|
/** @type {string} */
|
|
parsePage.addClass('d-none');
|
|
this.requirePath = requirePath;
|
|
this.currentView = previewSwitch?.[0]?.checked ? 'preview' : 'raw';
|
|
fetchFromAPI('/hasParsePage/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
}),
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.hasParsePage) {
|
|
parsePage.removeClass('d-none');
|
|
}
|
|
});
|
|
}
|
|
|
|
static previewSettings = {
|
|
htmlTemplate: '',
|
|
fillJS: '',
|
|
fillCSS: '',
|
|
customJS: '',
|
|
customCSS: '',
|
|
readerSettings: {
|
|
theme: '#292832',
|
|
textColor: '#CCCCCC',
|
|
textSize: 16,
|
|
textAlign: 'left',
|
|
padding: 5,
|
|
fontFamily: '',
|
|
lineHeight: 1.5,
|
|
},
|
|
iframeSize: {
|
|
width: 800,
|
|
height: 1200,
|
|
},
|
|
/** @type {Theme} */
|
|
theme: window.themes[window.themes.length - 2],
|
|
layoutHeight: 0,
|
|
novel: { pluginId: 0 },
|
|
chapter: { novelId: 0, id: 0 },
|
|
html: '',
|
|
sanitizedHTML: '',
|
|
sanitize: true,
|
|
StatusBar: { currentHeight: 0 },
|
|
};
|
|
|
|
/** @type {"raw" | "preview"} */
|
|
currentView = 'raw';
|
|
|
|
/** @type {NodeJS.Timeout[]} */
|
|
static clocks = [];
|
|
|
|
/**
|
|
* @param {[string,Filters[string]]} flt
|
|
* @returns {JQuery<HTMLDivElement>}
|
|
*/
|
|
static createFilterElement(flt) {
|
|
/** @param {string} f */
|
|
const getSafeFilterID = f => f.replace(/\s/g, '_').toLowerCase();
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const retDiv = $('<div>');
|
|
const [key, filter] = flt;
|
|
retDiv.addClass(`${filter.type}Filter`);
|
|
switch (filter.type) {
|
|
case 'Picker':
|
|
{
|
|
const id = `${key}_picker`;
|
|
// add label
|
|
$('<span>')
|
|
.addClass('filter-label')
|
|
.text(`${filter.label}:`)
|
|
.appendTo(retDiv);
|
|
|
|
/** @type {JQuery<HTMLSelectElement>} */
|
|
const select = $('<select>');
|
|
// add select and its attributes
|
|
select
|
|
.attr('title', `key: ${key}`)
|
|
.addClass('custom-select')
|
|
.on('change', e => {
|
|
state.popular_pages.reset();
|
|
state.filterValues[key].value = e.target.selectedOptions[0].value;
|
|
})
|
|
.appendTo(retDiv);
|
|
// add all the options
|
|
for (const option of filter.options) {
|
|
select.append(
|
|
`<option ${
|
|
option.value === filter.value ? 'selected' : ''
|
|
} value='${option.value}'>${option.label}</option>`,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
case 'Checkbox':
|
|
{
|
|
// add label
|
|
$('<span>')
|
|
.addClass('filter-label')
|
|
.text(filter.label)
|
|
.appendTo(retDiv);
|
|
|
|
// create box for checkboxes
|
|
const box = $('<div>').appendTo(retDiv).addClass('checkbox-groupbox');
|
|
|
|
// add all checkboxes
|
|
for (let cb of filter.options) {
|
|
const id = `checkbox_${key}_${getSafeFilterID(cb.value)}`;
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
let inp = $('<input>', { type: 'checkbox' });
|
|
// set default value
|
|
inp[0].checked = filter.value.includes(cb.value);
|
|
// set checkbox attributes and listeners
|
|
inp.attr('id', id).on('change', e => {
|
|
const { checked } = e.target;
|
|
state.popular_pages.reset();
|
|
const fV = state.filterValues[key];
|
|
if (Array.isArray(fV.value)) {
|
|
fV.value = checked
|
|
? [...fV.value, cb.value]
|
|
: fV.value.filter(r => r !== cb.value);
|
|
}
|
|
});
|
|
// add checkbox label
|
|
const label = $('<label>', { for: id })
|
|
.text(cb.label)
|
|
.prepend(inp)
|
|
.attr('title', `${key}: ${cb.value}`);
|
|
$('<div>').append(label).appendTo(box);
|
|
}
|
|
}
|
|
break;
|
|
case 'Switch': {
|
|
const id = `switch_${key}`;
|
|
// add the checkbox
|
|
const slider_checkbox = $('<div>');
|
|
slider_checkbox.addClass('slider-checkbox');
|
|
const label = $('<label>');
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const checkbox = $('<input>', { type: 'checkbox' });
|
|
checkbox.attr('id', id);
|
|
// set default value
|
|
checkbox[0].checked = filter.value;
|
|
label
|
|
.append(checkbox, $('<span>').text(filter.label))
|
|
.appendTo(slider_checkbox);
|
|
slider_checkbox.appendTo(retDiv);
|
|
break;
|
|
}
|
|
case 'Text':
|
|
{
|
|
const id = `text_${key}`;
|
|
$('<label>', { for: id })
|
|
.text(filter.label)
|
|
.addClass('filter-label')
|
|
.appendTo(retDiv);
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const inp = $('<input>', { type: 'text' });
|
|
inp
|
|
.attr('id', id)
|
|
.on('change', e => {
|
|
state.popular_pages.reset();
|
|
const fV = state.filterValues[key];
|
|
if (typeof fV.value === 'string') {
|
|
fV.value = e.target.value;
|
|
}
|
|
})
|
|
.appendTo(retDiv);
|
|
// set default value
|
|
inp.val(filter.value);
|
|
}
|
|
break;
|
|
case 'XCheckbox':
|
|
{
|
|
// add label
|
|
$('<span>')
|
|
.addClass('filter-label')
|
|
.text(filter.label)
|
|
.appendTo(retDiv);
|
|
|
|
// create box for checkboxes
|
|
const box = $('<div>')
|
|
.appendTo(retDiv)
|
|
.addClass('xcheckbox-groupbox');
|
|
|
|
// add all checkboxes
|
|
for (let cb of filter.options) {
|
|
const id = `checkbox_${key}_${getSafeFilterID(cb.value)}`;
|
|
|
|
const getCurrentState = () => {
|
|
const fV = state.filterValues[key];
|
|
if (typeof fV.value === 'object' && !Array.isArray(fV.value)) {
|
|
return (
|
|
(fV.value.exclude?.includes(cb.value) && 'x') ||
|
|
(fV.value.include?.includes(cb.value) && 'i') ||
|
|
false
|
|
);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// default State
|
|
const checkedState =
|
|
(filter.value.exclude?.includes(cb.value) && 'x') ||
|
|
(filter.value.include?.includes(cb.value) && 'i') ||
|
|
false;
|
|
|
|
/** @type {JQuery<HTMLLabelElement>} */
|
|
const label = $('<label>', { for: id });
|
|
label.addClass('xcheckbox').attr('data-state', checkedState || '');
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const xchb = $('<input>', { type: 'checkbox' });
|
|
if (checkedState) xchb[0].checked = true;
|
|
xchb.attr('id', id);
|
|
label.text(cb.label).append(xchb).append($('<span>'));
|
|
xchb.on('change', e => {
|
|
state.popular_pages.reset();
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
e.stopPropagation();
|
|
const fV = state.filterValues[key];
|
|
if (fV.value instanceof Array || typeof fV.value !== 'object')
|
|
return;
|
|
const curState = getCurrentState();
|
|
switch (curState) {
|
|
case 'i':
|
|
if (fV.value.include)
|
|
fV.value.include = fV.value.include.filter(
|
|
f => f !== cb.value,
|
|
);
|
|
if (fV.value.exclude) fV.value.exclude.push(cb.value);
|
|
else fV.value.exclude = [cb.value];
|
|
e.target.checked = true;
|
|
label[0].dataset.state = 'x';
|
|
break;
|
|
case 'x':
|
|
// set to false
|
|
if (fV.value.exclude)
|
|
fV.value.exclude = fV.value.exclude.filter(
|
|
f => f !== cb.value,
|
|
);
|
|
e.target.checked = false;
|
|
label[0].dataset.state = '';
|
|
break;
|
|
default:
|
|
// set to "e"
|
|
if (!fV.value.include) fV.value.include = [cb.value];
|
|
else fV.value.include.push(cb.value);
|
|
e.target.checked = true;
|
|
label[0].dataset.state = 'i';
|
|
break;
|
|
}
|
|
});
|
|
box.append(label);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
return retDiv;
|
|
}
|
|
return retDiv;
|
|
}
|
|
|
|
async getFilters() {
|
|
try {
|
|
/** @type {Filters} */
|
|
const filters = await (
|
|
await fetchFromAPI('/filters', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
}),
|
|
})
|
|
).json();
|
|
filtersModal.html('');
|
|
state.filterValues = {};
|
|
for (const fKey in filters) {
|
|
filtersModal.append(
|
|
PluginWrapper.createFilterElement([fKey, filters[fKey]]),
|
|
$('<hr>'),
|
|
);
|
|
state.filterValues[fKey] = {
|
|
type: filters[fKey].type,
|
|
value: filters[fKey].value,
|
|
};
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
/**
|
|
* @param {string} name
|
|
* @param {string | undefined} data
|
|
*/
|
|
static createInfoItem = (name, data) => {
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const info_item = $('<div>');
|
|
info_item.addClass('info-item');
|
|
// many data but one datum
|
|
const datum = data || 'undefined';
|
|
$('<div>')
|
|
.addClass('info-name btn btn-info disabled')
|
|
.text(name)
|
|
.appendTo(info_item);
|
|
$('<samp>')
|
|
.addClass('info-value btn-light')
|
|
.text(datum.slice(0, 50))
|
|
.appendTo(info_item);
|
|
$('<div>')
|
|
.addClass('info-copy btn btn-primary')
|
|
.text('Copy')
|
|
.on('click', () => {
|
|
navigator.clipboard.writeText(datum);
|
|
})
|
|
.appendTo(info_item);
|
|
return info_item;
|
|
};
|
|
|
|
/**
|
|
* @param {NovelItem} novel
|
|
* @param {{target:AccordionBox}|undefined} parsable
|
|
* @returns
|
|
*/
|
|
static createNovelItem(novel, parsable = undefined) {
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const novel_item = $(`<div>`);
|
|
novel_item.addClass('novel-item');
|
|
const novel_image = $('<div>').addClass('novel-img').appendTo(novel_item);
|
|
$('<img>')
|
|
.attr('src', novel.cover || '')
|
|
.attr('alt', 'No cover found')
|
|
.appendTo(novel_image);
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const novel_info = $('<div>');
|
|
novel_info.addClass('novel-info').appendTo(novel_item);
|
|
this.createInfoItem('name', novel.name).appendTo(novel_info);
|
|
this.createInfoItem('path', novel.path).appendTo(novel_info);
|
|
this.createInfoItem('cover', novel.cover || 'undefined').appendTo(
|
|
novel_info,
|
|
);
|
|
if (parsable)
|
|
novel_info.append(
|
|
$('<div>')
|
|
.addClass('btn btn-primary parse-novel-btn')
|
|
.text('Parse')
|
|
.on('click', () => {
|
|
// parsable.target.toggle();
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const pnc = $('#parseNovel');
|
|
const box = pnc[0];
|
|
$('#parse-novel-path').val(novel.path);
|
|
$('#parse-novel-btn').trigger('click');
|
|
if (box.collapsed) {
|
|
box.toggle();
|
|
setTimeout(() => {
|
|
// scroll after it un-collapses
|
|
box.scrollIntoView({
|
|
behavior: 'smooth',
|
|
});
|
|
}, 500);
|
|
} else box.scrollIntoView({ behavior: 'smooth' });
|
|
}),
|
|
);
|
|
return novel_item;
|
|
}
|
|
|
|
/** @param {ChapterItem} chapter */
|
|
static createChapterItem(chapter) {
|
|
/**
|
|
* @type {JQuery<HTMLDivElement>}
|
|
*/
|
|
const chapter_item = $('<div>');
|
|
chapter_item.addClass('chapter-item');
|
|
$('<samp>')
|
|
.addClass('info-value btn-light ci-topleft')
|
|
.text(chapter.name?.slice(0, 26))
|
|
.appendTo(chapter_item);
|
|
$('<samp>')
|
|
.addClass('info-value btn-light ci-bottomleft')
|
|
.text(chapter.releaseTime || 'undefined')
|
|
.appendTo(chapter_item);
|
|
$('<div>')
|
|
.addClass('info-copy btn btn-primary ci-btn1')
|
|
.attr('data', chapter.path)
|
|
.text('Copy path')
|
|
.on('click', () => {
|
|
navigator.clipboard.writeText(chapter.path);
|
|
})
|
|
.appendTo(chapter_item);
|
|
$('<div>')
|
|
.addClass('info-copy btn btn-primary ci-btn2')
|
|
.attr('data', chapter.path)
|
|
.text('Parse')
|
|
.on('click', () => {
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const parseChapter = $('#parseChapter');
|
|
const box = parseChapter[0];
|
|
$('#chapter-parse-path').val(chapter.path);
|
|
$('#chapter-parse-btn').trigger('click');
|
|
if (box.collapsed) {
|
|
box.toggle();
|
|
setTimeout(() => {
|
|
// scroll after it un-collapses
|
|
box.scrollIntoView({
|
|
behavior: 'smooth',
|
|
});
|
|
}, 500);
|
|
} else box.scrollIntoView({ behavior: 'smooth' });
|
|
})
|
|
.appendTo(chapter_item);
|
|
return chapter_item;
|
|
}
|
|
|
|
async getPopularNovels() {
|
|
popularNovels_spinner.show();
|
|
/** @type {FilterToValues<Filters>} */
|
|
const filters = { ...state.filterValues };
|
|
try {
|
|
await state.popular_pages.fetchNovels(async (page, latest) => {
|
|
console.log('Asking for page #', page);
|
|
const novels = await (
|
|
await fetchFromAPI(`/popularNovels/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
showLatestNovels: latest,
|
|
pluginRequirePath: this.requirePath,
|
|
filters: filters,
|
|
page,
|
|
}),
|
|
})
|
|
).json();
|
|
if ('error' in novels)
|
|
throw `popularNovels threw an error:<div class='error'>${novels.error}</div>`;
|
|
return novels;
|
|
});
|
|
state.popular_pages.showNextPage();
|
|
} catch (/** @type {unknown}*/ e) {
|
|
console.error(e);
|
|
if (e)
|
|
popularNovels_novel_list.html(
|
|
`${typeof e === 'object' && 'message' in e ? e.message : e}`,
|
|
);
|
|
} finally {
|
|
popularNovels_spinner.hide();
|
|
}
|
|
}
|
|
|
|
async getSearchedNovels() {
|
|
/** @type {JQuery<AccordionBox>} */
|
|
const accordionBox = $('#searchNovels');
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const searchBox = $('#searchNovels input');
|
|
const searchTerm = searchBox.val();
|
|
const spinner = $('#searchNovels .spinner-border');
|
|
spinner.show();
|
|
try {
|
|
await state.search_pages.fetchNovels(async (n, _) => {
|
|
const novels = await (
|
|
await fetchFromAPI(`/searchNovels/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
searchTerm: searchTerm,
|
|
page: n,
|
|
}),
|
|
})
|
|
).json();
|
|
if ('error' in novels)
|
|
throw `searchNovels threw an error:<div class='error'>${novels.error}</div>`;
|
|
return novels;
|
|
});
|
|
state.search_pages.showNextPage();
|
|
} catch (/** @type {unknown} */ e) {
|
|
console.error(e);
|
|
if (e)
|
|
seacrhNovels_novel_list.html(
|
|
`${typeof e === 'object' && 'message' in e ? e.message : e}`,
|
|
);
|
|
} finally {
|
|
spinner.hide();
|
|
}
|
|
}
|
|
|
|
async getNovel() {
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const novelPathInput = $('#parseNovel input');
|
|
const novel_item = $('#parseNovel .novel-item');
|
|
const chapter_list = $('#parseNovel .chapter-list');
|
|
const novelPath = novelPathInput.val();
|
|
const spinner = $('#parseNovel .spinner-border');
|
|
spinner.show();
|
|
try {
|
|
/** @type {SourceNovel | {error:string}} */
|
|
const sourceNovel = await (
|
|
await fetchFromAPI(`/parseNovel/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
novelPath,
|
|
}),
|
|
})
|
|
).json();
|
|
if ('error' in sourceNovel)
|
|
throw `Error getting the novel ${sourceNovel.error}`;
|
|
novel_item.empty();
|
|
const novel_data = PluginWrapper.createNovelItem({
|
|
name: sourceNovel.name || 'undefined',
|
|
path: sourceNovel.path,
|
|
cover: sourceNovel.cover,
|
|
});
|
|
|
|
novel_data
|
|
.children('div + div')
|
|
.append(
|
|
PluginWrapper.createInfoItem('summary', sourceNovel.summary),
|
|
PluginWrapper.createInfoItem('author', sourceNovel.author),
|
|
PluginWrapper.createInfoItem('artist', sourceNovel.artist),
|
|
PluginWrapper.createInfoItem('status', sourceNovel.status),
|
|
PluginWrapper.createInfoItem('genres', sourceNovel.genres),
|
|
);
|
|
if (sourceNovel.totalPages) {
|
|
novel_data
|
|
.children('div + div')
|
|
.append(
|
|
PluginWrapper.createInfoItem(
|
|
'totalPages',
|
|
sourceNovel.totalPages.toString(),
|
|
),
|
|
);
|
|
}
|
|
if (sourceNovel.latestChapter) {
|
|
novel_data
|
|
.children('div + div')
|
|
.append(PluginWrapper.createChapterItem(sourceNovel.latestChapter));
|
|
}
|
|
novel_item.replaceWith(novel_data);
|
|
|
|
if (
|
|
sourceNovel?.chapters?.length !==
|
|
new Set(sourceNovel?.chapters?.map(r => r.path) || []).size
|
|
) {
|
|
alert('Chapter paths are the same!');
|
|
}
|
|
|
|
chapter_list.html('');
|
|
if (sourceNovel.chapters)
|
|
for (const chapter of sourceNovel.chapters) {
|
|
chapter_list.append(PluginWrapper.createChapterItem(chapter));
|
|
}
|
|
} catch (/** @type {unknown} */ e) {
|
|
} finally {
|
|
spinner.hide();
|
|
}
|
|
}
|
|
async getPage() {
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const novelPathInput = $('#parsePage #parse-page-novel-path');
|
|
const pageInput = $('#parsePage #parse-page-index');
|
|
const chapter_list = $('#parsePage .chapter-list');
|
|
const novelPath = novelPathInput.val();
|
|
const page = pageInput.val();
|
|
const spinner = $('#parsePage .spinner-border');
|
|
spinner.show();
|
|
try {
|
|
/** @type {SourcePage | {error:string}} */
|
|
const sourcePage = await (
|
|
await fetchFromAPI(`/parsePage/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
novelPath,
|
|
page,
|
|
}),
|
|
})
|
|
).json();
|
|
if ('error' in sourcePage)
|
|
throw `Error getting the novel ${sourcePage.error}`;
|
|
if (
|
|
sourcePage?.chapters?.length !==
|
|
new Set(sourcePage?.chapters?.map(r => r.path) || []).size
|
|
) {
|
|
alert('Chapter paths are the same!');
|
|
}
|
|
|
|
chapter_list.html('');
|
|
if (sourcePage.chapters)
|
|
for (const chapter of sourcePage.chapters) {
|
|
chapter_list.append(PluginWrapper.createChapterItem(chapter));
|
|
}
|
|
} catch (/** @type {unknown} */ e) {
|
|
} finally {
|
|
spinner.hide();
|
|
}
|
|
}
|
|
async getChapter() {
|
|
const chapterPath = $('#parseChapter input').val();
|
|
const chapterRawTextarea = $('#parseChapter textarea');
|
|
const spinner = $('#parseChapter .spinner-border');
|
|
spinner.show();
|
|
try {
|
|
const chapterText = await (
|
|
await fetchFromAPI(`/parseChapter/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
chapterPath,
|
|
}),
|
|
})
|
|
).text();
|
|
PluginWrapper.previewSettings.html = chapterText;
|
|
PluginWrapper.previewSettings.sanitizedHTML =
|
|
window.sanitizeChapterText(chapterText);
|
|
this.refreshPreview();
|
|
} catch (e) {
|
|
console.error(e);
|
|
if (e)
|
|
chapterRawTextarea.text(
|
|
`${typeof e === 'object' && 'message' in e ? e.message : e}`,
|
|
);
|
|
} finally {
|
|
spinner.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Window | null} iframeWindow
|
|
*/
|
|
loadHTMLToIFrame = async iframeWindow => {
|
|
const text = PluginWrapper.previewSettings.htmlTemplate.replace(
|
|
/\/\*\{(.*)\}\*\//gi,
|
|
function (s, p1) {
|
|
/** @type {[(keyof PluginWrapper.previewSettings), (keyof PluginWrapper.previewSettings[keyof PluginWrapper.previewSettings])?]} */
|
|
const dotted = p1.split('.').map((/** @type {string} */ r) => r.trim());
|
|
const [key1, key2] = dotted;
|
|
if (dotted.length === 1) {
|
|
if (key1 === 'html') {
|
|
return `${
|
|
PluginWrapper.previewSettings.sanitize
|
|
? PluginWrapper.previewSettings.sanitizedHTML
|
|
: PluginWrapper.previewSettings.html
|
|
}`;
|
|
}
|
|
return `${PluginWrapper.previewSettings[key1]}`;
|
|
} else if (dotted.length === 2 && key2) {
|
|
return `${PluginWrapper.previewSettings[key1][key2]}`;
|
|
}
|
|
return '';
|
|
},
|
|
);
|
|
PluginWrapper.clocks.push(
|
|
setTimeout(() => {
|
|
if (iframeWindow) {
|
|
const html = iframeWindow.document.querySelector('html');
|
|
if (html) {
|
|
PluginWrapper.clocks.push(
|
|
setTimeout(() => {
|
|
// @ts-ignore
|
|
iframeWindow?.eval(PluginWrapper.previewSettings.customJS);
|
|
}, 10),
|
|
);
|
|
html.innerHTML = text;
|
|
} else {
|
|
console.error('No html object inside the iframe');
|
|
}
|
|
} else console.error('No iframe window yet!');
|
|
}, 10),
|
|
);
|
|
};
|
|
|
|
showChapterPreview() {
|
|
PluginWrapper.clocks.forEach(c => {
|
|
clearTimeout(c);
|
|
});
|
|
this.currentView = 'preview';
|
|
chapterViewer.empty();
|
|
// create the iframe
|
|
/** @type {JQuery<HTMLIFrameElement>} */
|
|
const iframe = $('<iframe>');
|
|
iframe.attr('id', 'preview');
|
|
iframe.css({
|
|
width: `${PluginWrapper.previewSettings.iframeSize.width}px`,
|
|
height: `${PluginWrapper.previewSettings.iframeSize.height}px`,
|
|
});
|
|
$('#scaled-info').text('');
|
|
$('#parseChapter').css({ 'max-height': 'unset' });
|
|
// the thing is too big!
|
|
if (
|
|
PluginWrapper.previewSettings.iframeSize.width >
|
|
window.innerWidth - 75
|
|
) {
|
|
const factor =
|
|
(window.innerWidth - 75) /
|
|
PluginWrapper.previewSettings.iframeSize.width;
|
|
iframe.css({
|
|
transform: `scale(${factor})`,
|
|
'transform-origin': 'top left',
|
|
});
|
|
$('#scaled-info').text(
|
|
`Scaled ${(factor * 100).toFixed(2)}% to fit on screen!`,
|
|
);
|
|
$('#parseChapter').css({
|
|
'max-height': `${
|
|
PluginWrapper.previewSettings.iframeSize.height * factor + 150
|
|
}px`,
|
|
});
|
|
}
|
|
chapterViewer.append(iframe);
|
|
const iframeWindow = iframe[0].contentWindow;
|
|
this.loadHTMLToIFrame(iframeWindow);
|
|
}
|
|
|
|
showRawChapterText() {
|
|
chapterViewer.empty();
|
|
/**
|
|
* @type {JQuery<HTMLTextAreaElement>}
|
|
*/
|
|
const chapterRawTextarea = $('<textarea>');
|
|
chapterRawTextarea
|
|
.attr('rows', '10')
|
|
.attr('maxlength', 10000000)
|
|
.appendTo(chapterViewer);
|
|
chapterRawTextarea.addClass('form-control');
|
|
const rawHTML = PluginWrapper.previewSettings.sanitize
|
|
? PluginWrapper.previewSettings.sanitizedHTML
|
|
: PluginWrapper.previewSettings.html;
|
|
if (rawHTML) {
|
|
chapterRawTextarea.text(rawHTML);
|
|
} else chapterRawTextarea.text('');
|
|
this.currentView = 'raw';
|
|
}
|
|
|
|
toggleChapterView(nochange = false) {
|
|
if (!nochange) {
|
|
this.currentView = this.currentView === 'raw' ? 'preview' : 'raw';
|
|
}
|
|
if (this.currentView === 'preview') this.showChapterPreview();
|
|
else this.showRawChapterText();
|
|
previewSwitch
|
|
.siblings('span')
|
|
.text(this.currentView === 'preview' ? 'Preview' : 'Raw');
|
|
}
|
|
|
|
/**
|
|
* @param {boolean | undefined} forcedValue
|
|
*/
|
|
static toggleHTMLSanitize(forcedValue) {
|
|
PluginWrapper.previewSettings.sanitize =
|
|
forcedValue ?? !PluginWrapper.previewSettings.sanitize;
|
|
currentWrapper().refreshPreview();
|
|
$('#sanitize-preview-html')
|
|
.siblings('span')
|
|
.text(
|
|
PluginWrapper.previewSettings.sanitize
|
|
? 'Sanitized HTML'
|
|
: 'Unsanitized HTML',
|
|
);
|
|
}
|
|
|
|
refreshPreview() {
|
|
this.toggleChapterView(true);
|
|
}
|
|
|
|
async fetchImage() {
|
|
const url = $('#fetchImage input').val();
|
|
const spinner = $('#fetchImage .spinner-border');
|
|
spinner.show();
|
|
try {
|
|
const base64 = await (
|
|
await fetchFromAPI(`/fetchImage/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
url: url,
|
|
}),
|
|
})
|
|
).text();
|
|
$('#fetchImage img').attr('src', `data:image/jpg;base64,${base64}`);
|
|
} catch (e) {
|
|
console.error(e);
|
|
if (e)
|
|
$('#fetchImage').text(
|
|
`${typeof e === 'object' && 'message' in e ? e.message : e}`,
|
|
);
|
|
} finally {
|
|
spinner.hide();
|
|
}
|
|
}
|
|
|
|
async resolveUrl() {
|
|
const type = $('#resolveUrl select').val();
|
|
const path = $('#resolveUrl input').val();
|
|
const spinner = $('#resolveUrl .spinner-border');
|
|
spinner.show();
|
|
try {
|
|
const fullURL = await (
|
|
await fetchFromAPI('/resolveUrl/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
pluginRequirePath: this.requirePath,
|
|
isNovel: type === 'novel',
|
|
path,
|
|
}),
|
|
})
|
|
).text();
|
|
$('#resolveUrl input[readonly]').val(fullURL);
|
|
} catch (e) {
|
|
console.error(e);
|
|
if (e)
|
|
$('#resolveUrl').text(
|
|
`${typeof e === 'object' && 'message' in e ? e.message : e}`,
|
|
);
|
|
} finally {
|
|
spinner.hide();
|
|
}
|
|
}
|
|
}
|
|
const currentWrapper = () => state.current_plugin || emptyPluginWrapper;
|
|
const emptyPluginWrapper = new PluginWrapper('');
|
|
|
|
// #region headers
|
|
/** @type {Record<string,string>} */
|
|
let lastCopiedHeaders = {};
|
|
|
|
const getHeaders = () => {
|
|
/** @type {Record<string,string>} */
|
|
const headers = {};
|
|
const els = document.querySelectorAll('.headerinfo').forEach(el => {
|
|
/** @type {HTMLSpanElement | null} */
|
|
const nameSpan = el.querySelector('.headername');
|
|
/** @type {HTMLSpanElement | null} */
|
|
const valueSpan = el.querySelector('.headervalue');
|
|
if (nameSpan && valueSpan) {
|
|
const { innerText: name } = nameSpan;
|
|
const { innerText: value } = valueSpan;
|
|
if (
|
|
typeof name === 'string' &&
|
|
typeof value === 'string' &&
|
|
value.trim()
|
|
) {
|
|
headers[name] = value.trim();
|
|
}
|
|
}
|
|
});
|
|
return headers;
|
|
};
|
|
|
|
const checkIfHeadersChanged = () => {
|
|
const currentHeaders = getHeaders();
|
|
|
|
const headers = new Set(Object.keys(currentHeaders));
|
|
// quick exit if all custom headers removed
|
|
if (headers.size <= 2) {
|
|
$('#headersnotcopied').css({ display: 'none' });
|
|
return;
|
|
}
|
|
|
|
// get a list of all headers
|
|
for (const headername of Object.keys(lastCopiedHeaders)) {
|
|
headers.add(headername);
|
|
}
|
|
|
|
headers.delete('Cookie');
|
|
headers.delete('User-Agent');
|
|
|
|
let headersChanged = false;
|
|
// check if they're all the same
|
|
for (const headername of Array.from(headers)) {
|
|
if (currentHeaders[headername] !== lastCopiedHeaders[headername])
|
|
headersChanged = true;
|
|
}
|
|
|
|
if (headersChanged) $('#headersnotcopied').css({ display: 'block' });
|
|
else $('#headersnotcopied').css({ display: 'none' });
|
|
};
|
|
// #endregion
|
|
|
|
// #region preview
|
|
|
|
const loadThemePreset = (/** @type {Theme} */ t) => {
|
|
PluginWrapper.previewSettings.theme = t;
|
|
PluginWrapper.previewSettings.readerSettings = {
|
|
...PluginWrapper.previewSettings.readerSettings,
|
|
textColor: t.textColor,
|
|
theme: t.backgroundColor,
|
|
};
|
|
};
|
|
|
|
const refreshPreviewSettings = () => {
|
|
/**
|
|
* @param {number} w
|
|
* @param {number} h
|
|
*/
|
|
const setPreviewSize = (w, h) => {
|
|
const { iframeSize } = PluginWrapper.previewSettings;
|
|
[iframeSize.width, iframeSize.height] = [w, h];
|
|
width[0].value = `${iframeSize.width}`;
|
|
height[0].value = `${iframeSize.height}`;
|
|
currentWrapper().refreshPreview();
|
|
};
|
|
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const width = $('#width-input');
|
|
if (width[0])
|
|
width[0].value = `${PluginWrapper.previewSettings.iframeSize.width}`;
|
|
width.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.iframeSize.width = parseFloat(value);
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const height = $('#height-input');
|
|
if (height[0])
|
|
height[0].value = `${PluginWrapper.previewSettings.iframeSize.height}`;
|
|
height.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.iframeSize.height = parseFloat(value);
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
$('#swap-iframe-size')
|
|
.off('click')
|
|
.on('click', () => {
|
|
const { width, height } = PluginWrapper.previewSettings.iframeSize;
|
|
setPreviewSize(height, width);
|
|
});
|
|
|
|
$('#load-preset-size')
|
|
.off('click')
|
|
.on('click', () => {
|
|
/** @type {JQuery<HTMLSelectElement>} */
|
|
const select = $('#size-preset');
|
|
const [wStr, hStr] = select[0].value.split(',');
|
|
if (wStr && hStr && parseInt(wStr) && parseInt(hStr))
|
|
setPreviewSize(parseInt(wStr), parseInt(hStr));
|
|
});
|
|
|
|
/** @type {JQuery<HTMLInputElement>}*/
|
|
const shouldSanitizeHTML = $('#sanitize-preview-html');
|
|
if (shouldSanitizeHTML[0])
|
|
PluginWrapper.toggleHTMLSanitize(shouldSanitizeHTML[0].checked);
|
|
shouldSanitizeHTML.off('click').on('click', ({ target: { checked } }) => {
|
|
PluginWrapper.toggleHTMLSanitize(checked);
|
|
});
|
|
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const lineHeight = $('#lh-input');
|
|
if (lineHeight[0])
|
|
lineHeight[0].value = `${PluginWrapper.previewSettings.readerSettings.lineHeight}`;
|
|
lineHeight.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.readerSettings.lineHeight = parseFloat(value);
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const backgroundColor = $('#bgc-input');
|
|
if (backgroundColor[0])
|
|
backgroundColor[0].value = `${PluginWrapper.previewSettings.readerSettings.theme}`;
|
|
backgroundColor.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.readerSettings.theme = value;
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const textColor = $('#txt-input');
|
|
if (textColor[0])
|
|
textColor[0].value = `${PluginWrapper.previewSettings.readerSettings.textColor}`;
|
|
textColor.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.readerSettings.textColor = value;
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const fontSize = $('#fs-input');
|
|
if (fontSize[0])
|
|
fontSize[0].value = `${PluginWrapper.previewSettings.readerSettings.textSize}`;
|
|
fontSize.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.readerSettings.textSize = parseFloat(value);
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
/** @type {JQuery<HTMLSelectElement>} */
|
|
const textAlign = $('#ta-input');
|
|
textAlign.off('change').on('change', ({ target: { selectedOptions } }) => {
|
|
PluginWrapper.previewSettings.readerSettings.textAlign =
|
|
selectedOptions[0].value;
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
/** @type {JQuery<HTMLTextAreaElement>} */
|
|
const customCSS = $('#css-ta');
|
|
if (customCSS[0])
|
|
customCSS[0].value = PluginWrapper.previewSettings.customCSS;
|
|
customCSS.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.customCSS = value;
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
|
|
/** @type {JQuery<HTMLTextAreaElement>} */
|
|
const customJS = $('#js-ta');
|
|
if (customJS[0]) customJS[0].value = PluginWrapper.previewSettings.customJS;
|
|
customJS.off('change').on('change', ({ target: { value } }) => {
|
|
PluginWrapper.previewSettings.customJS = value;
|
|
currentWrapper().refreshPreview();
|
|
});
|
|
};
|
|
|
|
// #endregion
|
|
|
|
// load plugin
|
|
|
|
const load_plugin = (/** @type {HTMLButtonElement} */ btn) => {
|
|
const ele = $(btn);
|
|
const plugin_requirePath = ele.attr('data-require');
|
|
if (!plugin_requirePath) return;
|
|
state.current_plugin = new PluginWrapper(plugin_requirePath);
|
|
plugin_search.attr('data-require', plugin_requirePath);
|
|
plugin_search.val(ele.text().split('/')[1]);
|
|
state.popular_pages.reset();
|
|
state.search_pages.reset();
|
|
destroySearchTurnOffBox();
|
|
state.current_plugin.getFilters().then(() => {
|
|
$('#loadedplugin').text('Loaded: ' + btn.innerText);
|
|
});
|
|
};
|
|
|
|
// #region plugin search
|
|
|
|
const loadPluginsIntoSearchBar = () => {
|
|
const { language, keyword } = state.plugin_search;
|
|
const plugins = language ? state.all_plugins[language] : state.plugin_infos;
|
|
state.plugins_in_search = keyword
|
|
? plugins.filter(p =>
|
|
keyword.charAt(0) === '-'
|
|
? !p.name.toLowerCase().includes(keyword.toLowerCase().substring(1))
|
|
: p.name.toLowerCase().includes(keyword.toLowerCase()),
|
|
)
|
|
: plugins;
|
|
};
|
|
|
|
fetchFromAPI('/all_plugins')
|
|
.then(res => res.json())
|
|
.then((/** @type {PluginList} */ all) => {
|
|
state.all_plugins = all;
|
|
for (let lang in all) {
|
|
plugin_language_selection.append(`<option>${lang}</option>`);
|
|
state.plugin_infos = state.plugin_infos.concat(all[lang]);
|
|
}
|
|
});
|
|
|
|
const destroySearchTurnOffBox = () => {
|
|
$('#turnoff-catch').trigger('click');
|
|
};
|
|
|
|
const createSearchTurnOffBox = () => {
|
|
// if there is no unclick box
|
|
if (!$('#turnoff-catch').length) {
|
|
// create it
|
|
const turnoff_catch = $('<div>').attr('id', 'turnoff-catch');
|
|
$('body').append(turnoff_catch);
|
|
turnoff_catch.on('click', () => {
|
|
plugin_search_results.hide();
|
|
turnoff_catch.remove();
|
|
});
|
|
}
|
|
};
|
|
|
|
const display_plugin_search = () => {
|
|
plugin_search_results.hide();
|
|
plugin_search_results.empty();
|
|
createSearchTurnOffBox();
|
|
state.plugins_in_search.forEach(plugin => {
|
|
/** @type {JQuery<HTMLImageElement>} */
|
|
const img = $('<img>', { src: `./icons/${plugin.icon}` });
|
|
img.css({
|
|
height: '100%',
|
|
'object-fit': 'contains',
|
|
'margin-right': '10px',
|
|
});
|
|
/** @type {JQuery<HTMLButtonElement>} */
|
|
const btn = $('<button>', {
|
|
type: 'button',
|
|
class: 'search-item btn btn-light btn-outline-primary btn-md btn-block',
|
|
});
|
|
btn
|
|
.css({
|
|
display: 'flex',
|
|
'align-items': 'center',
|
|
height: '30px',
|
|
'flex-shrink': 0,
|
|
})
|
|
.attr('data-require', `${plugin.requirePath}`)
|
|
.on('click', e => {
|
|
load_plugin(e.target);
|
|
});
|
|
/** @type {JQuery<HTMLDivElement>} */
|
|
const div = $('<div>');
|
|
div.css({
|
|
display: 'flex',
|
|
'align-items': 'center',
|
|
});
|
|
btn.append(img).append(`${plugin.lang}/${plugin.name}`);
|
|
div.append(btn);
|
|
plugin_search_results.append(div);
|
|
});
|
|
plugin_search_results.show();
|
|
};
|
|
// #endregion
|
|
|
|
// #region event handlers
|
|
plugin_language_selection.on('change', e => {
|
|
state.plugin_search.language = e.target.value;
|
|
loadPluginsIntoSearchBar();
|
|
});
|
|
|
|
plugin_search.on('focus', () => {
|
|
if (!state.all_plugins) return;
|
|
plugin_search[0].selectionStart = 0;
|
|
plugin_search[0].selectionEnd = plugin_search.val()?.length || 0;
|
|
loadPluginsIntoSearchBar();
|
|
display_plugin_search();
|
|
});
|
|
|
|
plugin_search.on('keyup', e => {
|
|
state.plugin_search.keyword = e.target.value;
|
|
loadPluginsIntoSearchBar();
|
|
display_plugin_search();
|
|
});
|
|
|
|
clear_plugin_search.on('click', e => {
|
|
plugin_search_results.hide();
|
|
plugin_search.val('');
|
|
plugin_search.attr('data-require', '');
|
|
});
|
|
|
|
previewSwitch.on('change', () => {
|
|
currentWrapper().toggleChapterView();
|
|
});
|
|
|
|
// get all header inputs and check if headers changed after
|
|
$(".headerinfo > span[contenteditable='true']").each((_, inputElement) => {
|
|
$(inputElement).on('input', checkIfHeadersChanged);
|
|
});
|
|
|
|
latestSwitch.on('change', () => {
|
|
const label = latestSwitch.siblings('span');
|
|
/** @type {JQuery<HTMLInputElement>} */
|
|
const filtersBtn = $('#filters-btn');
|
|
if (latestSwitch[0].checked) {
|
|
state.popular_pages.showLatest = true;
|
|
filtersBtn[0].disabled = true;
|
|
label.text('Latest');
|
|
} else {
|
|
state.popular_pages.showLatest = false;
|
|
filtersBtn[0].disabled = false;
|
|
label.text('Popular');
|
|
}
|
|
});
|
|
|
|
searchNovels_searchbar.on('keyup', () => {
|
|
state.search_pages.reset();
|
|
});
|
|
|
|
$('#addheaderbtn').on('click', () => {
|
|
/**
|
|
<div class="headerinfo" data-headername="Cookie">
|
|
<span class="headername" onclick="this.nextElementSibling.nextElementSibling.focus()">Cookie</span><b>: </b>
|
|
<span class="headervalue" contenteditable="true" ></span>
|
|
</div>
|
|
*/
|
|
const nameSpan = $('<span>').addClass('headername');
|
|
const valueSpan = $('<span>').addClass('headervalue');
|
|
const separator = $('<b>').html(': ');
|
|
const clearButton = $('<button>')
|
|
.text('Clear')
|
|
.addClass('headereditbutton')
|
|
.on('click', () => {
|
|
valueSpan.text('');
|
|
checkIfHeadersChanged();
|
|
});
|
|
const removeButton = $('<button>')
|
|
.text('Remove')
|
|
.addClass('headereditbutton');
|
|
nameSpan[0].contentEditable = 'true';
|
|
valueSpan[0].contentEditable = 'true';
|
|
nameSpan.on('input', checkIfHeadersChanged);
|
|
valueSpan.on('input', checkIfHeadersChanged);
|
|
const headerinfo = $('<div>')
|
|
.append(nameSpan, separator, valueSpan, clearButton, removeButton)
|
|
.attr('class', 'headerinfo');
|
|
removeButton.on('click', () => {
|
|
headerinfo.remove();
|
|
checkIfHeadersChanged();
|
|
});
|
|
$('#headerinfos').append(headerinfo);
|
|
});
|
|
|
|
$('#copyascode').on('click', async () => {
|
|
const headers = getHeaders();
|
|
const strippedHeaders = {
|
|
...headers,
|
|
};
|
|
delete strippedHeaders['Cookie'];
|
|
delete strippedHeaders['User-Agent'];
|
|
await navigator.clipboard.writeText(JSON.stringify(strippedHeaders, null, 4));
|
|
lastCopiedHeaders = headers;
|
|
checkIfHeadersChanged();
|
|
$('#headerscopied').css({ display: 'block' });
|
|
setTimeout(() => $('#headerscopied').css({ display: 'none' }), 1000);
|
|
});
|
|
|
|
// buttons:
|
|
$('.popularNovels-btn').on('click', () =>
|
|
state.current_plugin?.getPopularNovels(),
|
|
);
|
|
|
|
$('.searchNovels-btn').on('click', () =>
|
|
state.current_plugin?.getSearchedNovels(),
|
|
);
|
|
|
|
$('.parseNovel-btn').on('click', () => state.current_plugin?.getNovel());
|
|
$('.parsePage-btn').on('click', () => state.current_plugin?.getPage());
|
|
|
|
$('.parseChapter-btn').on('click', () => state.current_plugin?.getChapter());
|
|
$('.fetchImage-btn').on('click', () => state.current_plugin?.fetchImage());
|
|
$('.resolveUrl-btn').on('click', () => state.current_plugin?.resolveUrl());
|
|
|
|
// #endregion
|
|
|
|
// JQuery document.body.onload
|
|
$(() => {
|
|
/** @type {JQuery<HTMLSpanElement>} */
|
|
const userAgentInput = $('[data-headername="User-Agent"] .headervalue');
|
|
if (userAgentInput.length) {
|
|
userAgentInput.text(navigator.userAgent);
|
|
}
|
|
// TODO read the values from localstorage
|
|
state.plugin_search.keyword = plugin_search[0].value;
|
|
PluginWrapper.previewSettings.sanitize = false;
|
|
Promise.allSettled([
|
|
fetchFromAPI('static/html/template.html')
|
|
.then(r => r.text())
|
|
.then(t => {
|
|
PluginWrapper.previewSettings.htmlTemplate = t;
|
|
}),
|
|
fetchFromAPI(
|
|
'https://raw.githubusercontent.com/LNReader/lnreader/plugins/android/app/src/main/assets/css/index.css',
|
|
)
|
|
.then(r => r.text())
|
|
.then(t => (PluginWrapper.previewSettings.fillCSS = t)),
|
|
fetchFromAPI(
|
|
'https://raw.githubusercontent.com/LNReader/lnreader/plugins/android/app/src/main/assets/js/index.js',
|
|
)
|
|
.then(r => r.text())
|
|
.then(t => (PluginWrapper.previewSettings.fillJS = t)),
|
|
]).then(() => {
|
|
emptyPluginWrapper.refreshPreview();
|
|
});
|
|
latestSwitch.trigger('change');
|
|
for (const key in window.themes) {
|
|
const t = window.themes[key];
|
|
const picker = $('#theme-picker');
|
|
picker.append(
|
|
$('<div>')
|
|
.text(PluginWrapper.previewSettings.theme === t ? '✓' : 'A')
|
|
.addClass('theme')
|
|
.css({
|
|
'background-color': t.backgroundColor,
|
|
color: t.textColor,
|
|
})
|
|
.attr('id', key)
|
|
.on('click', x => {
|
|
loadThemePreset(t);
|
|
refreshPreviewSettings();
|
|
currentWrapper().refreshPreview();
|
|
$('#theme-picker .theme').each((e, el) => {
|
|
el.innerText = el.id === key ? '✓' : 'A';
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
refreshPreviewSettings();
|
|
lastCopiedHeaders = getHeaders();
|
|
checkIfHeadersChanged();
|
|
});
|