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:
Rajarshee Chatterjee
2025-10-20 22:54:08 +05:30
committed by GitHub
parent 252beff0bf
commit 20c2b75b01
363 changed files with 6848 additions and 3007 deletions
+188
View File
@@ -0,0 +1,188 @@
import { Plugin } from '@/types/plugin';
import { fetchApi } from '@libs/fetch';
import { CheerioAPI, load as parseHTML } from 'cheerio';
import { NovelStatus } from '@libs/novelStatus';
class FaqWikiUs implements Plugin.PluginBase {
id = 'FWK.US';
name = 'Faq Wiki';
site = 'https://faqwiki.us/';
version = '2.0.1';
icon = 'src/en/faqwikius/icon.png';
parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) {
let novels: Plugin.NovelItem[] = [];
loadedCheerio('.plt-page-item').each((index, element) => {
const name = loadedCheerio(element)
.text()
.replace('Novel – All Chapters', '')
.trim();
let cover = loadedCheerio(element).find('img').attr('data-ezsrc');
// Remove the appended query string
if (cover) {
cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
} else {
cover = loadedCheerio(element).find('img').attr('src');
}
const path = loadedCheerio(element)
.find('a')
.attr('href')
?.replace('tp:', 'tps:')
?.replace(this.site, '')
?.replace(/\/+$/, '');
if (!path) return;
novels.push({ name, cover, path });
});
if (searchTerm) {
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return novels;
}
async popularNovels(): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio);
}
async parseNovel(path: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.site + path).then(res => res.text());
const loadedCheerio = parseHTML(body);
const novel: Plugin.SourceNovel = {
path,
name: '',
};
novel.name = loadedCheerio('.entry-title')
.text()
.replace('Novel – All Chapters', '')
.trim();
const img = loadedCheerio('.wp-block-image img');
const cover = img.attr('data-ezsrc') || img.attr('src');
novel.cover = cover?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic!
const status = loadedCheerio(
"#lcp_instance_0 +:icontains('complete')",
).text();
novel.status = status ? NovelStatus.Completed : NovelStatus.Ongoing;
loadedCheerio('.entry-content strong').each((i, el) => {
const key = loadedCheerio(el).text().trim().toLowerCase();
const parent = loadedCheerio(el).parent();
const values = [parent.text().slice(key.length).trim()].concat(
parent
.nextUntil('p:has(strong)')
.map((e, ax) => loadedCheerio(ax).text().trim())
.get(),
);
let genreText = values.join(' ').trim();
const multiWordGenres = [
//add more when found
'Slice of Life',
'School Life',
];
multiWordGenres.forEach(genre => {
genreText = genreText.replace(
new RegExp(`\\b${genre}\\b`, 'g'),
genre.replace(/ /g, '_'),
);
});
const genres = genreText
.split(/\s+/)
.map(word => word.replace(/_/g, ' '))
.join(', ');
switch (key) {
case 'description:':
novel.summary = values.join('\n');
break;
case 'author(s):':
novel.author = values[0];
break;
case 'genre:':
novel.genres = genres;
}
});
const chapters: Plugin.ChapterItem[] = [];
loadedCheerio('.lcp_catlist li a').each((chapterIndex, element) => {
const name = loadedCheerio(element)
.text()
.replace(novel.name + '', '')
.replace('Novel' + '', '')
.trim();
const path = loadedCheerio(element)
.attr('href')
?.replace(this.site, '')
?.replace(/\/+$/, '');
const chapterNumber = chapterIndex + 1;
if (!path) return;
chapters.push({
name,
path,
chapterNumber,
});
});
novel.chapters = chapters;
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.site + chapterPath).then(res =>
res.text(),
);
const loadedCheerio = parseHTML(body);
const removal = ['.entry-content span', '.entry-content div', 'script'];
removal.map(e => {
loadedCheerio(e).remove();
});
const chapterText = loadedCheerio('.entry-content').html()!;
// const chapterParagraphs = loadedCheerio('.entry-content p');
// let chapterContent; // Save this code in case needed
// if (chapterParagraphs.length < 5) {
// //some chapter in this site store their whole text in 1-4 <p>,
// chapterContent = chapterParagraphs
// .map((index, element) => {
// const text = loadedCheerio(element).html();
// return text;
// })
// .get()
// .join('\n\n');
// } else {
// // Multiple paragraphs case
// chapterContent = chapterParagraphs
// .map((index, element) => {
// const text = loadedCheerio(element).text().trim();
// return `<p>${text}</p>`;
// })
// .get()
// .join('\n\n');
// }
return chapterText;
}
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
const body = await fetchApi(this.site).then(res => res.text());
const loadedCheerio = parseHTML(body);
return this.parseNovels(loadedCheerio, searchTerm);
}
}
export default new FaqWikiUs();