refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc * feat: Overhaul Playground UI * feat: Add Filters to New UI * feat: Persist Selected Plugin * feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts [skip ci] * feat: UX Improvements * chore: Update Plugin Path in Publish Workflow * fix: Add Backward Compatibility * chore: Update Plugin Publishing Script
This commit is contained in:
committed by
GitHub
parent
252beff0bf
commit
20c2b75b01
@@ -0,0 +1,198 @@
|
||||
import { CheerioAPI, load as parseHTML } from 'cheerio';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
|
||||
class BakaInUa implements Plugin.PluginBase {
|
||||
id = 'bakainua';
|
||||
name = 'BakaInUA';
|
||||
icon = 'src/uk/bakainua/icon.png';
|
||||
site = 'https://baka.in.ua';
|
||||
version = '2.0.0';
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{
|
||||
filters,
|
||||
showLatestNovels,
|
||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const fictionIds: string[] = [];
|
||||
|
||||
const url: URL = new URL(this.site + '/fictions/alphabetical');
|
||||
|
||||
if (pageNo > 1) url.searchParams.append('page', pageNo.toString());
|
||||
if (showLatestNovels || (filters && filters.only_new.value))
|
||||
url.searchParams.append('only_new', '1');
|
||||
if (filters) {
|
||||
if (filters.longreads.value) url.searchParams.append('longreads', '1');
|
||||
if (filters.finished.value) url.searchParams.append('finished', '1');
|
||||
if (filters.genre.value !== '')
|
||||
url.searchParams.append('genre', filters.genre.value);
|
||||
}
|
||||
|
||||
const result = await fetchApi(url.toString());
|
||||
const body = await result.text();
|
||||
const $ = parseHTML(body);
|
||||
|
||||
// get the ids of the popular novels
|
||||
$('div#fiction-list-page > div > div > div > img').each((index, elem) => {
|
||||
const fictionId = $(elem).attr('data-fiction-picker-id-param');
|
||||
if (fictionId) {
|
||||
fictionIds.push(fictionId);
|
||||
}
|
||||
});
|
||||
|
||||
// fetch the details of the popular novels asynchronously
|
||||
const requests: Promise<Plugin.NovelItem>[] = fictionIds.map(async id => {
|
||||
const res = await fetchApi(`${this.site}/fictions/${id}/details`, {
|
||||
headers: {
|
||||
accept: 'text/vnd.turbo-stream.html',
|
||||
},
|
||||
});
|
||||
const $ = parseHTML(await res.text());
|
||||
const elem = $('a').first();
|
||||
|
||||
return {
|
||||
name: $('h3').text().trim(),
|
||||
path: elem.attr('href') || '',
|
||||
cover: this.site + $(elem).find('img').attr('src'),
|
||||
};
|
||||
});
|
||||
|
||||
return await Promise.all(requests);
|
||||
}
|
||||
|
||||
async parseNovel(novelUrl: string): Promise<Plugin.SourceNovel> {
|
||||
const result = await fetchApi(this.site + '/' + novelUrl);
|
||||
const body = await result.text();
|
||||
const $ = parseHTML(body);
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelUrl,
|
||||
name: $('main div > h1').text().trim(),
|
||||
author: $('button#fictions-author-search').text().trim(),
|
||||
cover: this.site + $('main div > img').attr('src'),
|
||||
summary: $('main div > h3').first().parent().find('div').text().trim(),
|
||||
genres: $('h4:contains("Жанри")')
|
||||
.last()
|
||||
.siblings()
|
||||
.last()
|
||||
.find('span')
|
||||
.map((_, el) => $(el).text().trim())
|
||||
.get()
|
||||
.join(', '),
|
||||
};
|
||||
|
||||
switch ($('div:contains("Статус")').last().siblings().text().trim()) {
|
||||
case 'Видаєт.':
|
||||
novel.status = NovelStatus.Ongoing;
|
||||
break;
|
||||
case 'Заверш.':
|
||||
novel.status = NovelStatus.Completed;
|
||||
break;
|
||||
case 'Покину.':
|
||||
novel.status = NovelStatus.OnHiatus;
|
||||
break;
|
||||
default:
|
||||
novel.status = NovelStatus.Unknown;
|
||||
break;
|
||||
}
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
$('li.group a').each((index, elem) => {
|
||||
const chapter: Plugin.ChapterItem = {
|
||||
name: $(elem).find('span').eq(1).text().trim(),
|
||||
path: $(elem).attr('href') || '',
|
||||
chapterNumber: parseInt($(elem).find('span').eq(0).text().trim()),
|
||||
releaseTime: $(elem).find('span').eq(2).text().trim(),
|
||||
};
|
||||
chapters.push(chapter);
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterUrl: string): Promise<string> {
|
||||
const result = await fetchApi(this.site + '/' + chapterUrl);
|
||||
const body = await result.text();
|
||||
const $ = parseHTML(body);
|
||||
return $('#user-content').html() || '';
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
const result = await fetchApi(
|
||||
this.site + '/search?search%5B%5D=' + searchTerm + '&only_fictions=true',
|
||||
);
|
||||
const body = await result.text();
|
||||
const $ = parseHTML(body);
|
||||
$('ul > section').each((index, elem) => {
|
||||
novels.push({
|
||||
path: $(elem).find('a').first().attr('href') || '',
|
||||
name: $(elem).find('a > h2').first().text().trim(),
|
||||
cover: this.site + $(elem).find('img').first().attr('src'),
|
||||
});
|
||||
});
|
||||
return novels;
|
||||
}
|
||||
|
||||
filters = {
|
||||
genre: {
|
||||
type: FilterTypes.Picker,
|
||||
label: 'Жанр',
|
||||
value: '',
|
||||
options: [
|
||||
{ label: 'Всі жанри', value: '' },
|
||||
{ label: 'BL', value: '19' },
|
||||
{ label: 'GL', value: '20' },
|
||||
{ label: 'Авторське', value: '32' },
|
||||
{ label: 'Бойовик', value: '2' },
|
||||
{ label: 'Вуся', value: '16' },
|
||||
{ label: 'Гарем', value: '5' },
|
||||
{ label: 'Детектив', value: '22' },
|
||||
{ label: 'Драма', value: '12' },
|
||||
{ label: 'Жахи', value: '10' },
|
||||
{ label: 'Ісекай', value: '13' },
|
||||
{ label: 'Історичне', value: '15' },
|
||||
{ label: 'Комедія', value: '11' },
|
||||
{ label: 'ЛГБТ', value: '3' },
|
||||
{ label: 'Містика', value: '18' },
|
||||
{ label: 'Омегаверс', value: '30' },
|
||||
{ label: 'Повсякденність', value: '17' },
|
||||
{ label: 'Пригоди', value: '7' },
|
||||
{ label: 'Психологія', value: '28' },
|
||||
{ label: 'Романтика', value: '1' },
|
||||
{ label: 'Спорт', value: '9' },
|
||||
{ label: 'Сюаньхвань', value: '27' },
|
||||
{ label: 'Сянься', value: '26' },
|
||||
{ label: 'Трагедія', value: '24' },
|
||||
{ label: 'Трилер', value: '21' },
|
||||
{ label: 'Фантастика', value: '8' },
|
||||
{ label: 'Фанфік', value: '23' },
|
||||
{ label: 'Фентезі', value: '4' },
|
||||
{ label: 'Школа', value: '6' },
|
||||
],
|
||||
},
|
||||
only_new: {
|
||||
type: FilterTypes.Switch,
|
||||
label: 'Новинки',
|
||||
value: false,
|
||||
},
|
||||
longreads: {
|
||||
type: FilterTypes.Switch,
|
||||
label: 'Довгочити',
|
||||
value: false,
|
||||
},
|
||||
finished: {
|
||||
type: FilterTypes.Switch,
|
||||
label: 'Завершене',
|
||||
value: false,
|
||||
},
|
||||
} satisfies Filters;
|
||||
}
|
||||
|
||||
export default new BakaInUa();
|
||||
@@ -0,0 +1,241 @@
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
class Smakolykytl implements Plugin.PluginBase {
|
||||
id = 'smakolykytl';
|
||||
name = 'Смаколики';
|
||||
site = 'https://smakolykytl.site/';
|
||||
apiSite = 'https://api.smakolykytl.site/api/user';
|
||||
version = '1.0.1';
|
||||
icon = 'src/uk/smakolykytl/icon.png';
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ showLatestNovels }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const sort = showLatestNovels ? '/updates' : '/projects';
|
||||
|
||||
const result = await fetchApi(this.apiSite + sort);
|
||||
const json = (await result.json()) as response;
|
||||
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
(json?.projects || json?.updates)?.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel.image.url,
|
||||
path: 'titles/' + novel.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const id = novelPath.split('/').pop();
|
||||
const result = await fetchApi(this.apiSite + '/projects/' + id);
|
||||
const book = (await result.json()) as response;
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
path: novelPath,
|
||||
name: book?.project?.title || '',
|
||||
cover: book?.project?.image?.url,
|
||||
summary: book?.project?.description,
|
||||
author: book?.project?.author,
|
||||
status: book?.project?.status_translate.includes('Триває')
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
};
|
||||
const tags = [book?.project?.genres, book?.project?.tags]
|
||||
.flat()
|
||||
.map(tags => tags?.title)
|
||||
.filter(tags => tags);
|
||||
|
||||
if (tags.length) {
|
||||
novel.genres = tags.join(', ');
|
||||
}
|
||||
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
const res = await fetchApi(this.apiSite + '/projects/' + id + '/books');
|
||||
const data = (await res.json()) as response;
|
||||
data?.books?.forEach(volume =>
|
||||
volume?.chapters?.forEach(chapter =>
|
||||
chapters.push({
|
||||
name: volume.title + ' ' + chapter.title,
|
||||
path: 'read/' + chapter.id,
|
||||
releaseTime: dayjs(chapter.modifiedAt).format('LLL'),
|
||||
chapterNumber: chapters.length + 1,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const id = chapterPath.split('/').pop();
|
||||
const result = await fetchApi(this.apiSite + '/chapters/' + id);
|
||||
const json = (await result.json()) as response;
|
||||
const chapterRaw: HTML[] = JSON.parse(json?.chapter?.content || '[]');
|
||||
|
||||
const chapterText = jsonToHtml(chapterRaw);
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const result = await fetchApi(this.apiSite + '/projects');
|
||||
const json = (await result.json()) as response;
|
||||
const searchTitle = searchTerm.toLowerCase();
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
json?.projects
|
||||
?.filter(
|
||||
({ title, id }) =>
|
||||
title.toLowerCase().includes(searchTitle) || String(id) == searchTerm,
|
||||
)
|
||||
?.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.title,
|
||||
cover: novel.image.url,
|
||||
path: 'titles/' + novel.id,
|
||||
}),
|
||||
);
|
||||
return novels;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Smakolykytl();
|
||||
|
||||
function jsonToHtml(json: HTML[], html = '') {
|
||||
json.forEach(element => {
|
||||
switch (element.type) {
|
||||
case 'hardBreak':
|
||||
html += '<br>';
|
||||
break;
|
||||
case 'horizontalRule':
|
||||
html += '<hr>';
|
||||
break;
|
||||
case 'image':
|
||||
if (element.attrs) {
|
||||
const attrs = Object.entries(element.attrs)
|
||||
.filter(attr => attr?.[1])
|
||||
.map(attr => `${attr[0]}="${attr[1]}"`);
|
||||
html += '<img ' + attrs.join('; ') + '>';
|
||||
}
|
||||
break;
|
||||
case 'paragraph':
|
||||
html +=
|
||||
'<p>' +
|
||||
(element.content ? jsonToHtml(element.content) : '<br>') +
|
||||
'</p>';
|
||||
break;
|
||||
case 'text':
|
||||
html += element.text;
|
||||
break;
|
||||
default:
|
||||
html += JSON.stringify(element, null, '\t'); //maybe I missed something.
|
||||
break;
|
||||
}
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
type response = {
|
||||
projects?: TopLevelProject[];
|
||||
updates?: Update[];
|
||||
project?: TopLevelProject;
|
||||
books?: BookElement[];
|
||||
chapter?: TopLevelChapter;
|
||||
};
|
||||
|
||||
type BookElement = {
|
||||
id: number;
|
||||
rank: number;
|
||||
title: string;
|
||||
chapters: PurpleChapter[];
|
||||
};
|
||||
|
||||
type PurpleChapter = {
|
||||
id: number;
|
||||
title: string;
|
||||
rank: string;
|
||||
modifiedAt: Date;
|
||||
};
|
||||
|
||||
type TopLevelChapter = {
|
||||
id: number;
|
||||
title: string;
|
||||
rank: string;
|
||||
content: string;
|
||||
modifiedAt: Date;
|
||||
book: ChapterBook;
|
||||
};
|
||||
|
||||
type ChapterBook = {
|
||||
id: number;
|
||||
rank: number;
|
||||
title: string;
|
||||
chapters: FluffyChapter[];
|
||||
project: BookProject;
|
||||
};
|
||||
|
||||
type FluffyChapter = {
|
||||
id: number;
|
||||
rank: string;
|
||||
};
|
||||
|
||||
type BookProject = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
type TopLevelProject = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
translator: string;
|
||||
modifiedAt: Date;
|
||||
alternatives: string;
|
||||
release: string;
|
||||
nation: string;
|
||||
status: string;
|
||||
status_translate: string;
|
||||
image: Image;
|
||||
tags?: Genre[];
|
||||
genres?: Genre[];
|
||||
};
|
||||
|
||||
type Genre = {
|
||||
id: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type Image = {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type Update = {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
translator: string;
|
||||
modifiedAt: Date;
|
||||
image: Image;
|
||||
};
|
||||
|
||||
type HTML = {
|
||||
type: string;
|
||||
content?: HTML[];
|
||||
attrs?: Attrs;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
type Attrs = {
|
||||
src: string;
|
||||
alt: string | null;
|
||||
title: string | null;
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
|
||||
const UA_RANOBE_ID = 'uaranobeclub' as const;
|
||||
const UA_RANOBE_URL = 'https://uaranobe.club/' as const;
|
||||
|
||||
type Writing = {
|
||||
id: string;
|
||||
title: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
updatedAt: string;
|
||||
episodes: Episode[];
|
||||
category: Category;
|
||||
genres: GenreConnection[];
|
||||
__typename: string;
|
||||
scanlators: {
|
||||
scanlator: {
|
||||
scanlatorName: string;
|
||||
username: string;
|
||||
episodes: Episode[];
|
||||
__typename: string;
|
||||
};
|
||||
__typename: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type Episode = {
|
||||
seqTitle: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
subId: string;
|
||||
text: string;
|
||||
__typename: string;
|
||||
};
|
||||
|
||||
type Category = {
|
||||
id: string;
|
||||
name: string;
|
||||
__typename: string;
|
||||
};
|
||||
|
||||
type GenreConnection = {
|
||||
genreId: string;
|
||||
genre: Genre;
|
||||
__typename: string;
|
||||
};
|
||||
|
||||
type Genre = {
|
||||
id: string;
|
||||
name: string;
|
||||
__typename: string;
|
||||
};
|
||||
|
||||
type WritingsResponse = {
|
||||
writingsCount: number;
|
||||
writings: Writing[];
|
||||
};
|
||||
|
||||
type WritingResponse = {
|
||||
data: {
|
||||
writingBySlug: Writing;
|
||||
};
|
||||
};
|
||||
|
||||
type EpisodeResponse = {
|
||||
data: {
|
||||
episodeBySlug: Episode;
|
||||
};
|
||||
};
|
||||
|
||||
class UaRanobeClubApi {
|
||||
static async fetch<TData = unknown, TVariables = unknown>(
|
||||
query: string,
|
||||
variables: TVariables,
|
||||
): Promise<TData> {
|
||||
const response = await fetchApi(`${UA_RANOBE_URL}graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
});
|
||||
|
||||
return (await response.json()) as TData;
|
||||
}
|
||||
|
||||
static async fetchChapter(
|
||||
slug: string,
|
||||
writingSlug: string,
|
||||
): Promise<string> {
|
||||
const query = `
|
||||
query EpisodeBySlug($slug: String!, $writingSlug: String!) {
|
||||
episodeBySlug(slug: $slug, writingSlug: $writingSlug) {
|
||||
text
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = {
|
||||
slug,
|
||||
writingSlug,
|
||||
} as const;
|
||||
|
||||
const response = await UaRanobeClubApi.fetch<
|
||||
EpisodeResponse,
|
||||
typeof variables
|
||||
>(query, variables);
|
||||
|
||||
return response.data.episodeBySlug.text;
|
||||
}
|
||||
|
||||
static async fetchNovel(slug: string): Promise<Writing> {
|
||||
const query = `
|
||||
query Writing($slug: String!) {
|
||||
writingBySlug(slug: $slug) {
|
||||
id
|
||||
title
|
||||
originalTitle
|
||||
image
|
||||
slug
|
||||
description
|
||||
scanlators {
|
||||
scanlator {
|
||||
scanlatorName
|
||||
username
|
||||
episodes(oldestFirst: false, slug: $slug) {
|
||||
id
|
||||
subId
|
||||
seqTitle
|
||||
title
|
||||
slug
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
genres {
|
||||
genreId
|
||||
genre {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = { slug };
|
||||
|
||||
const response = await UaRanobeClubApi.fetch<
|
||||
WritingResponse,
|
||||
typeof variables
|
||||
>(query, variables);
|
||||
|
||||
return response?.data?.writingBySlug ?? '';
|
||||
}
|
||||
|
||||
static async fetchListWithNovel(
|
||||
skip = 0,
|
||||
search = '',
|
||||
): Promise<WritingsResponse> {
|
||||
const query = `
|
||||
query Writings($skip: Int!, $search: String) {
|
||||
writingsCount(search: $search)
|
||||
writings(skip: $skip, search: $search) {
|
||||
id
|
||||
title
|
||||
image
|
||||
slug
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
const variables = {
|
||||
skip,
|
||||
search,
|
||||
} as const;
|
||||
|
||||
const response = await UaRanobeClubApi.fetch<
|
||||
{ data: WritingsResponse },
|
||||
typeof variables
|
||||
>(query, variables);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
class UaRanobeClub implements Plugin.PluginBase {
|
||||
id = UA_RANOBE_ID;
|
||||
name = 'UA Ranobe Club';
|
||||
site = UA_RANOBE_URL;
|
||||
version = '1.1.4';
|
||||
icon = `src/uk/${this.id}/icon.png`;
|
||||
|
||||
async popularNovels(page: number): Promise<Plugin.NovelItem[]> {
|
||||
const skip = (page - 1) * 10;
|
||||
|
||||
const data = await UaRanobeClubApi.fetchListWithNovel(skip, '');
|
||||
|
||||
const novelItems: Plugin.NovelItem[] = (data?.writings ?? []).map(
|
||||
({ title, image, slug }) => ({
|
||||
name: title,
|
||||
cover: image,
|
||||
path: slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novelItems;
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const slug = novelPath.split(UA_RANOBE_URL).join('');
|
||||
|
||||
const data: Writing = await UaRanobeClubApi.fetchNovel(slug);
|
||||
|
||||
const chapters: Plugin.ChapterItem[] =
|
||||
data.scanlators?.[0]?.scanlator?.episodes?.map(
|
||||
({ title, seqTitle, slug: chapterSlug, subId }) => ({
|
||||
name: `${seqTitle}. ${title}`,
|
||||
path: chapterSlug + `#${slug}`,
|
||||
chapterNumber: parseInt(subId, 10), // Предполагаем, что subId это число
|
||||
}),
|
||||
) || [];
|
||||
|
||||
const sourceNovel: Plugin.SourceNovel = {
|
||||
genres: data.genres?.map(({ genre }) => genre.name).join(',') || '',
|
||||
chapters,
|
||||
name: data.title,
|
||||
path: data.slug,
|
||||
summary: data.description,
|
||||
cover: data.image,
|
||||
};
|
||||
|
||||
return sourceNovel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const [slug, writingSlug] = chapterPath
|
||||
.split(UA_RANOBE_URL)
|
||||
.join('')
|
||||
.split('#');
|
||||
const chapterText = await UaRanobeClubApi.fetchChapter(slug, writingSlug);
|
||||
|
||||
return chapterText;
|
||||
}
|
||||
|
||||
async searchNovels(
|
||||
searchTerm: string,
|
||||
page: number,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
const skip = (page - 1) * 10;
|
||||
|
||||
const data = await UaRanobeClubApi.fetchListWithNovel(skip, searchTerm);
|
||||
const novelItems: Plugin.NovelItem[] = (data?.writings ?? []).map(
|
||||
({ title, image, slug }) => ({
|
||||
name: title,
|
||||
cover: image,
|
||||
path: slug,
|
||||
}),
|
||||
);
|
||||
|
||||
return novelItems;
|
||||
}
|
||||
}
|
||||
|
||||
export default new UaRanobeClub();
|
||||
Reference in New Issue
Block a user