Update multisrc (madara & mtlnovel) (#1023)
* Madara: - fix foxaholic infinite loop on premium chapters - genre, author and status fix for foxaholic - remove iframe from summary on foxaholic - defaultCover when viewing novels - parseData actually working * added mtlnovel multisrc for more languages
This commit is contained in:
|
Before Width: | Height: | Size: 817 B After Width: | Height: | Size: 817 B |
@@ -39,7 +39,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
this.icon = `multisrc/madara/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/madara/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
const versionIncrements = metadata.options?.versionIncrements || 0;
|
const versionIncrements = metadata.options?.versionIncrements || 0;
|
||||||
this.version = `1.0.${1 + versionIncrements}`;
|
this.version = `1.0.${2 + versionIncrements}`;
|
||||||
this.options = metadata.options;
|
this.options = metadata.options;
|
||||||
this.filters = metadata.filters;
|
this.filters = metadata.filters;
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,8 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
const novelCover =
|
const novelCover =
|
||||||
image.attr('data-src') ||
|
image.attr('data-src') ||
|
||||||
image.attr('src') ||
|
image.attr('src') ||
|
||||||
image.attr('data-lazy-srcset');
|
image.attr('data-lazy-srcset') ||
|
||||||
|
defaultCover;
|
||||||
const novel: Plugin.NovelItem = {
|
const novel: Plugin.NovelItem = {
|
||||||
name: novelName,
|
name: novelName,
|
||||||
cover: novelCover,
|
cover: novelCover,
|
||||||
@@ -182,17 +183,22 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
switch (detailName) {
|
switch (detailName) {
|
||||||
case 'Genre(s)':
|
case 'Genre(s)':
|
||||||
|
case 'Genre':
|
||||||
|
case 'Tags':
|
||||||
case 'Género(s)':
|
case 'Género(s)':
|
||||||
case 'التصنيفات':
|
case 'التصنيفات':
|
||||||
novel.genres = detail;
|
if (novel.genres) novel.genres += ', ' + detail;
|
||||||
|
else novel.genres = detail;
|
||||||
break;
|
break;
|
||||||
case 'Author(s)':
|
case 'Author(s)':
|
||||||
|
case 'Author':
|
||||||
case 'Autor(es)':
|
case 'Autor(es)':
|
||||||
case 'المؤلف':
|
case 'المؤلف':
|
||||||
case 'المؤلف (ين)':
|
case 'المؤلف (ين)':
|
||||||
novel.author = detail;
|
novel.author = detail;
|
||||||
break;
|
break;
|
||||||
case 'Status':
|
case 'Status':
|
||||||
|
case 'Novel':
|
||||||
case 'Estado':
|
case 'Estado':
|
||||||
novel.status =
|
novel.status =
|
||||||
detail.includes('OnGoing') || detail.includes('مستمرة')
|
detail.includes('OnGoing') || detail.includes('مستمرة')
|
||||||
@@ -202,7 +208,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loadedCheerio('div.summary__content .code-block,script').remove();
|
loadedCheerio('div.summary__content .code-block,script,noscript').remove();
|
||||||
const summary =
|
const summary =
|
||||||
loadedCheerio('div.summary__content') ||
|
loadedCheerio('div.summary__content') ||
|
||||||
loadedCheerio('#tab-manga-about') ||
|
loadedCheerio('#tab-manga-about') ||
|
||||||
@@ -252,12 +258,14 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
const chapterUrl = loadedCheerio(element).find('a').attr('href') || '';
|
const chapterUrl = loadedCheerio(element).find('a').attr('href') || '';
|
||||||
|
|
||||||
|
if (chapterUrl && chapterUrl != '#') {
|
||||||
chapters.push({
|
chapters.push({
|
||||||
name: chapterName,
|
name: chapterName,
|
||||||
path: chapterUrl.replace(/https?:\/\/.*?\//, '/'),
|
path: chapterUrl.replace(/https?:\/\/.*?\//, '/'),
|
||||||
releaseTime: releaseDate || null,
|
releaseTime: releaseDate || null,
|
||||||
chapterNumber: totalChapters - chapterIndex,
|
chapterNumber: totalChapters - chapterIndex,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
novel.chapters = chapters.reverse();
|
novel.chapters = chapters.reverse();
|
||||||
@@ -295,14 +303,14 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseData = (date: string) => {
|
parseData = (date: string) => {
|
||||||
const dayJSDate = dayjs(); // today
|
let dayJSDate = dayjs(); // today
|
||||||
const timeAgo = date.match(/\d+/)?.[0] || '';
|
const timeAgo = date.match(/\d+/)?.[0] || '';
|
||||||
const timeAgoInt = parseInt(timeAgo, 10);
|
const timeAgoInt = parseInt(timeAgo, 10);
|
||||||
|
|
||||||
if (!timeAgo) return date; // there is no number!
|
if (!timeAgo) return date; // there is no number!
|
||||||
|
|
||||||
if (includesAny(date, ['detik', 'segundo', 'second', 'วินาที'])) {
|
if (includesAny(date, ['detik', 'segundo', 'second', 'วินาที'])) {
|
||||||
dayJSDate.subtract(timeAgoInt, 'second'); // go back N seconds
|
dayJSDate = dayJSDate.subtract(timeAgoInt, 'second'); // go back N seconds
|
||||||
} else if (
|
} else if (
|
||||||
includesAny(date, [
|
includesAny(date, [
|
||||||
'menit',
|
'menit',
|
||||||
@@ -314,7 +322,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
'دقائق',
|
'دقائق',
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
dayJSDate.subtract(timeAgoInt, 'minute'); // go back N minute
|
dayJSDate = dayJSDate.subtract(timeAgoInt, 'minute'); // go back N minute
|
||||||
} else if (
|
} else if (
|
||||||
includesAny(date, [
|
includesAny(date, [
|
||||||
'jam',
|
'jam',
|
||||||
@@ -329,7 +337,7 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
'小时',
|
'小时',
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
|
dayJSDate = dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours
|
||||||
} else if (
|
} else if (
|
||||||
includesAny(date, [
|
includesAny(date, [
|
||||||
'hari',
|
'hari',
|
||||||
@@ -345,14 +353,17 @@ class MadaraPlugin implements Plugin.PluginBase {
|
|||||||
'天',
|
'天',
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
|
dayJSDate = dayJSDate.subtract(timeAgoInt, 'days'); // go back N days
|
||||||
} else if (includesAny(date, ['week', 'semana'])) {
|
} else if (includesAny(date, ['week', 'semana'])) {
|
||||||
dayJSDate.subtract(timeAgoInt, 'week'); // go back N a week
|
dayJSDate = dayJSDate.subtract(timeAgoInt, 'week'); // go back N a week
|
||||||
} else if (includesAny(date, ['month', 'mes'])) {
|
} else if (includesAny(date, ['month', 'mes'])) {
|
||||||
dayJSDate.subtract(timeAgoInt, 'month'); // go back N months
|
dayJSDate = dayJSDate.subtract(timeAgoInt, 'month'); // go back N months
|
||||||
} else if (includesAny(date, ['year', 'año'])) {
|
} else if (includesAny(date, ['year', 'año'])) {
|
||||||
dayJSDate.subtract(timeAgoInt, 'year'); // go back N years
|
dayJSDate = dayJSDate.subtract(timeAgoInt, 'year'); // go back N years
|
||||||
} else {
|
} else {
|
||||||
|
if (dayjs(date).format('LL') !== 'Invalid Date') {
|
||||||
|
return dayjs(date).format('LL');
|
||||||
|
}
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"order": {
|
||||||
|
"value": "view",
|
||||||
|
"label": "Order by",
|
||||||
|
"options": [
|
||||||
|
{ "label": "Date", "value": "date" },
|
||||||
|
{ "label": "Name", "value": "name" },
|
||||||
|
{ "label": "Rating", "value": "rating" },
|
||||||
|
{ "label": "View", "value": "view" }
|
||||||
|
],
|
||||||
|
"type": "Picker"
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"value": "desc",
|
||||||
|
"label": "Sort by",
|
||||||
|
"options": [
|
||||||
|
{ "label": "Descending", "value": "desc" },
|
||||||
|
{ "label": "Ascending", "value": "asc" }
|
||||||
|
],
|
||||||
|
"type": "Picker"
|
||||||
|
},
|
||||||
|
"storyStatus": {
|
||||||
|
"value": "all",
|
||||||
|
"label": "Status",
|
||||||
|
"options": [
|
||||||
|
{ "label": "All", "value": "all" },
|
||||||
|
{ "label": "Ongoing", "value": "ongoing" },
|
||||||
|
{ "label": "Complete", "value": "completed" }
|
||||||
|
],
|
||||||
|
"type": "Picker"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import list from './sources.json' with { type: 'json' };
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
export const generateAll = function () {
|
||||||
|
list.sort((a, b) => a.id.length - b.id.length);
|
||||||
|
return list.map(source => {
|
||||||
|
try {
|
||||||
|
const filters = JSON.parse(
|
||||||
|
readFileSync(`${__dirname}/filters/mtlnovel.json`, 'utf-8'),
|
||||||
|
);
|
||||||
|
source.filters = filters;
|
||||||
|
} catch (e) {}
|
||||||
|
console.log(
|
||||||
|
`[mtlnovel] Generating: ${source.id}`.padEnd(35),
|
||||||
|
source.filters ? '🔎with filters🔍' : '🚫 no filters 🚫',
|
||||||
|
);
|
||||||
|
return generator(source);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generator = function generator(source) {
|
||||||
|
const MTLNovelTemplate = readFileSync(__dirname + '/template.ts', {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pluginScript = `
|
||||||
|
${MTLNovelTemplate}
|
||||||
|
|
||||||
|
const plugin = new MTLNovelPlugin(${JSON.stringify(source)});
|
||||||
|
export default plugin;
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
lang: source.options?.lang || 'English',
|
||||||
|
filename: source.sourceName,
|
||||||
|
pluginScript,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "mtlnovel",
|
||||||
|
"sourceSite": "https://www.mtlnovel.com/",
|
||||||
|
"sourceName": "MTL Novel",
|
||||||
|
"options": {
|
||||||
|
"lang": "English"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mtlnovel-fr",
|
||||||
|
"sourceSite": "https://fr.mtlnovel.com/",
|
||||||
|
"sourceName": "MTL Novel",
|
||||||
|
"options": {
|
||||||
|
"lang": "French"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mtlnovel-es",
|
||||||
|
"sourceSite": "https://es.mtlnovel.com/",
|
||||||
|
"sourceName": "MTL Novel",
|
||||||
|
"options": {
|
||||||
|
"lang": "Spanish"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mtlnovel-id",
|
||||||
|
"sourceSite": "https://id.mtlnovel.com/",
|
||||||
|
"sourceName": "MTL Novel",
|
||||||
|
"options": {
|
||||||
|
"lang": "Indonesian"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,37 +1,85 @@
|
|||||||
import { load as parseHTML } from 'cheerio';
|
import { load as parseHTML } from 'cheerio';
|
||||||
import { fetchApi } from '@libs/fetch';
|
import { fetchApi } from '@libs/fetch';
|
||||||
import { Plugin } from '@typings/plugin';
|
import { Plugin } from '@typings/plugin';
|
||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
|
import { defaultCover } from '@libs/defaultCover';
|
||||||
|
import { Filters } from '@libs/filterInputs';
|
||||||
|
|
||||||
|
interface MTLNovelOptions {
|
||||||
|
lang?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MTLNovelMetadata {
|
||||||
|
id: string;
|
||||||
|
sourceSite: string;
|
||||||
|
sourceName: string;
|
||||||
|
options?: MTLNovelOptions;
|
||||||
|
filters?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MTLNovelPlugin implements Plugin.PluginBase {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
site: string;
|
||||||
|
version: string;
|
||||||
|
options?: MTLNovelOptions;
|
||||||
|
filters?: Filters;
|
||||||
|
|
||||||
|
constructor(metadata: MTLNovelMetadata) {
|
||||||
|
this.id = metadata.id;
|
||||||
|
this.name = metadata.sourceName;
|
||||||
|
this.icon = 'multisrc/mtlnovel/mtlnovel/icon.png';
|
||||||
|
this.site = metadata.sourceSite;
|
||||||
|
this.version = '1.1.0';
|
||||||
|
this.options = metadata.options ?? ({} as MTLNovelOptions);
|
||||||
|
this.filters = metadata.filters satisfies Filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
async safeFecth(
|
||||||
|
url: string,
|
||||||
|
headers: Headers = new Headers(),
|
||||||
|
): Promise<Response> {
|
||||||
|
headers.append('Alt-Used', 'www.mtlnovel.com');
|
||||||
|
const r = await fetchApi(url, { headers });
|
||||||
|
if (!r.ok)
|
||||||
|
throw new Error(
|
||||||
|
'Could not reach site (' + r.status + ') try to open in webview.',
|
||||||
|
);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
class MTLNovel implements Plugin.PluginBase {
|
|
||||||
id = 'mtlnovel';
|
|
||||||
name = 'MTL Novel';
|
|
||||||
version = '1.0.0';
|
|
||||||
icon = 'src/en/mtlnovel/icon.png';
|
|
||||||
site = 'https://www.mtlnovel.com/';
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{ filters }: Plugin.PopularNovelsOptions<typeof this.filters>,
|
{
|
||||||
|
filters,
|
||||||
|
showLatestNovels,
|
||||||
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let link = `${this.site}novel-list/?`;
|
let link = `${this.site}novel-list/?`;
|
||||||
|
if (filters) {
|
||||||
link += `orderby=${filters.order.value}`;
|
link += `orderby=${filters.order.value}`;
|
||||||
link += `&order=${filters.sort.value}`;
|
link += `&order=${filters.sort.value}`;
|
||||||
link += `&status=${filters.storyStatus.value}`;
|
link += `&status=${filters.storyStatus.value}`;
|
||||||
|
}
|
||||||
|
if (showLatestNovels) link += '&m_orderby=date';
|
||||||
link += `&pg=${page}`;
|
link += `&pg=${page}`;
|
||||||
|
|
||||||
const headers = new Headers();
|
const body = await this.safeFecth(link).then(r => r.text());
|
||||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
|
||||||
const body = await fetchApi(link, { headers }).then(result =>
|
|
||||||
result.text(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadedCheerio = parseHTML(body);
|
const loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
|
||||||
loadedCheerio('div.box.wide').each((i, el) => {
|
loadedCheerio('div.box.wide').each((i, el) => {
|
||||||
const novelName = loadedCheerio(el).find('a.list-title').text().slice(4);
|
const novelName = loadedCheerio(el).find('a.list-title').text().trim();
|
||||||
const novelCover = loadedCheerio(el).find('amp-img').attr('src');
|
let novelCover = loadedCheerio(el).find('amp-img').attr('src');
|
||||||
|
if (
|
||||||
|
!novelCover ||
|
||||||
|
novelCover == 'https://www.mtlnovel.net/no-image.jpg.webp'
|
||||||
|
) {
|
||||||
|
novelCover = defaultCover;
|
||||||
|
}
|
||||||
const novelUrl = loadedCheerio(el).find('a.list-title').attr('href');
|
const novelUrl = loadedCheerio(el).find('a.list-title').attr('href');
|
||||||
|
|
||||||
if (!novelUrl) return;
|
if (!novelUrl) return;
|
||||||
@@ -47,14 +95,14 @@ class MTLNovel implements Plugin.PluginBase {
|
|||||||
|
|
||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append('Referer', `${this.site}novel-list/`);
|
headers.append('Referer', `${this.site}novel-list/`);
|
||||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
|
||||||
|
|
||||||
const body = await fetchApi(this.site + novelPath, {
|
const body = await this.safeFecth(this.site + novelPath, headers).then(r =>
|
||||||
headers,
|
r.text(),
|
||||||
}).then(result => result.text());
|
);
|
||||||
|
|
||||||
let loadedCheerio = parseHTML(body);
|
let loadedCheerio = parseHTML(body);
|
||||||
|
|
||||||
@@ -72,8 +120,9 @@ class MTLNovel implements Plugin.PluginBase {
|
|||||||
const chapterListUrl = this.site + novelPath + 'chapter-list/';
|
const chapterListUrl = this.site + novelPath + 'chapter-list/';
|
||||||
|
|
||||||
const getChapters = async () => {
|
const getChapters = async () => {
|
||||||
const listResult = await fetchApi(chapterListUrl, { headers });
|
const listBody = await this.safeFecth(chapterListUrl, headers).then(r =>
|
||||||
const listBody = await listResult.text();
|
r.text(),
|
||||||
|
);
|
||||||
|
|
||||||
loadedCheerio = parseHTML(listBody);
|
loadedCheerio = parseHTML(listBody);
|
||||||
|
|
||||||
@@ -101,9 +150,7 @@ class MTLNovel implements Plugin.PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseChapter(chapterPath: string): Promise<string> {
|
async parseChapter(chapterPath: string): Promise<string> {
|
||||||
const headers = new Headers();
|
const body = await this.safeFecth(this.site + chapterPath).then(r =>
|
||||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
|
||||||
const body = await fetchApi(this.site + chapterPath, { headers }).then(r =>
|
|
||||||
r.text(),
|
r.text(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -114,13 +161,15 @@ class MTLNovel implements Plugin.PluginBase {
|
|||||||
return chapterText;
|
return chapterText;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
async searchNovels(
|
||||||
const headers = new Headers();
|
searchTerm: string,
|
||||||
headers.append('Alt-Used', 'www.mtlnovel.com');
|
pageNo: number,
|
||||||
|
): Promise<Plugin.NovelItem[]> {
|
||||||
|
if (pageNo !== 1) return [];
|
||||||
const searchUrl =
|
const searchUrl =
|
||||||
this.site + 'wp-admin/admin-ajax.php?action=autosuggest&q=' + searchTerm;
|
this.site + 'wp-admin/admin-ajax.php?action=autosuggest&q=' + searchTerm;
|
||||||
|
|
||||||
const res = await fetchApi(searchUrl, { headers });
|
const res = await this.safeFecth(searchUrl);
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = [];
|
const novels: Plugin.NovelItem[] = [];
|
||||||
@@ -142,37 +191,9 @@ class MTLNovel implements Plugin.PluginBase {
|
|||||||
return novels;
|
return novels;
|
||||||
}
|
}
|
||||||
|
|
||||||
filters = {
|
imageRequestInit: Plugin.ImageRequestInit = {
|
||||||
order: {
|
headers: {
|
||||||
value: 'view',
|
'Alt-Used': 'www.mtlnovel.com',
|
||||||
label: 'Order by',
|
|
||||||
options: [
|
|
||||||
{ label: 'Date', value: 'date' },
|
|
||||||
{ label: 'Name', value: 'name' },
|
|
||||||
{ label: 'Rating', value: 'rating' },
|
|
||||||
{ label: 'View', value: 'view' },
|
|
||||||
],
|
|
||||||
type: FilterTypes.Picker,
|
|
||||||
},
|
},
|
||||||
sort: {
|
};
|
||||||
value: 'desc',
|
|
||||||
label: 'Sort by',
|
|
||||||
options: [
|
|
||||||
{ label: 'Descending', value: 'desc' },
|
|
||||||
{ label: 'Ascending', value: 'asc' },
|
|
||||||
],
|
|
||||||
type: FilterTypes.Picker,
|
|
||||||
},
|
|
||||||
storyStatus: {
|
|
||||||
value: 'all',
|
|
||||||
label: 'Status',
|
|
||||||
options: [
|
|
||||||
{ label: 'All', value: 'all' },
|
|
||||||
{ label: 'Ongoing', value: 'ongoing' },
|
|
||||||
{ label: 'Complete', value: 'completed' },
|
|
||||||
],
|
|
||||||
type: FilterTypes.Picker,
|
|
||||||
},
|
|
||||||
} satisfies Filters;
|
|
||||||
}
|
}
|
||||||
export default new MTLNovel();
|
|
||||||
Reference in New Issue
Block a user