From 1ff4c995ec4e3d8b93ef0206ed6cb3ab2e9bb27a Mon Sep 17 00:00:00 2001 From: NovaKn1ght Date: Mon, 10 Aug 2026 20:55:45 -0500 Subject: [PATCH] feat: added Chikari english plugin --- plugins/english/chikari.ts | 242 ++++++++++++++++++++++++++ public/static/src/en/chikari/icon.png | Bin 0 -> 3309 bytes 2 files changed, 242 insertions(+) create mode 100644 plugins/english/chikari.ts create mode 100644 public/static/src/en/chikari/icon.png diff --git a/plugins/english/chikari.ts b/plugins/english/chikari.ts new file mode 100644 index 000000000..4d373eecf --- /dev/null +++ b/plugins/english/chikari.ts @@ -0,0 +1,242 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class ChikariPlugin implements Plugin.PluginBase { + id = 'chikari'; + name = 'Chikari'; + icon = 'src/en/chikari/icon.png'; + site = 'https://chikari.moe'; + version = '1.0.0'; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + // Define the filters for the app's filter menu + filters = { + sort: { + type: FilterTypes.Picker, + label: 'Sort By', + value: 'popular', + options: [ + { label: 'Popular', value: 'popular' }, + { label: 'Trending', value: 'trending' }, + { label: 'Top Rated', value: 'top_rated' }, + { label: 'Recently Updated', value: 'updated' }, + { label: 'Recently Added', value: 'added' }, + { label: 'Most Bookmarked', value: 'most_bookmarked' }, + ], + }, + genres: { + type: FilterTypes.ExcludableCheckboxGroup, + label: 'Genres', + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Gender Bender', value: 'gender_bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Martial Arts', value: 'martial_arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school_life' }, + { label: 'Sci-Fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shoujo Ai', value: 'shoujo_ai' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Shounen Ai', value: 'shounen_ai' }, + { label: 'Slice of Life', value: 'slice_of_life' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + }, + } satisfies Filters; + +async popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise { + const limit = 60; + const offset = (pageNo - 1) * limit; + + let sort = 'popular'; + let genreQuery = ''; + + if (showLatestNovels) { + sort = 'updated'; + } else if (filters && filters.sort) { + // Safely extract string value if wrapped in an object ({ type, value }) + sort = typeof filters.sort === 'object' ? filters.sort.value : filters.sort; + } + + // Safely extract the genres payload + const genresFilter = filters?.genres; + const genreValues = typeof genresFilter === 'object' && 'value' in genresFilter + ? genresFilter.value + : genresFilter; + + if (genreValues) { + // 1. If passed as an Array + if (Array.isArray(genreValues)) { + genreValues.forEach((genre: string) => { + if (genre.startsWith('-')) { + genreQuery += `&genre_exclude=${genre.substring(1)}`; + } else { + genreQuery += `&genre=${genre}`; + } + }); + } + // 2. If passed as an Object ({ include: [], exclude: [] }) + else if (typeof genreValues === 'object') { + if (Array.isArray(genreValues.include)) { + genreValues.include.forEach((genre: string) => { + genreQuery += `&genre=${genre}`; + }); + } + if (Array.isArray(genreValues.exclude)) { + genreValues.exclude.forEach((genre: string) => { + genreQuery += `&genre_exclude=${genre}`; + }); + } + } + } + + const url = `${this.site}/api/novels?sort=${sort}${genreQuery}&limit=${limit}&offset=${offset}`; + + // console.log('Fetching URL:', url); + + const response = await fetchApi(url); + const json = await response.json(); + + const novels: Plugin.NovelItem[] = (json.items || []).map((item: any) => ({ + name: item.title, + path: item.slug, + cover: item.cover_url || defaultCover, + })); + + return novels; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const limit = 36; + const offset = (pageNo - 1) * limit; + const url = `${this.site}/api/novels?sort=popular&q=${encodeURIComponent(searchTerm)}&limit=${limit}&offset=${offset}`; + + const response = await fetchApi(url); + const json = await response.json(); + + const novels: Plugin.NovelItem[] = (json.items || []).map((item: any) => ({ + name: item.title, + path: item.slug, + cover: item.cover_url || defaultCover, + })); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const detailUrl = `${this.site}/api/novels/${novelPath}`; + const detailResponse = await fetchApi(detailUrl); + const detailJson = await detailResponse.json(); + + const authorName = detailJson.authors && detailJson.authors.length > 0 + ? detailJson.authors.map((a: any) => a.name).join(', ') + : 'Unknown'; + + // Parse the genres array into a comma-separated string + const genres = detailJson.genres && detailJson.genres.length > 0 + ? detailJson.genres.map((g: any) => g.name).join(', ') + : ''; + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: detailJson.title || 'Untitled', + cover: detailJson.cover_url || defaultCover, + author: authorName, + summary: detailJson.description || '', + genres: genres, // Handled here! + status: detailJson.status === 'releasing' ? NovelStatus.Ongoing : NovelStatus.Completed, + }; + + const chapters: Plugin.ChapterItem[] = []; + + let offset = 0; + const limit = 500; + let hasMoreChapters = true; + + while (hasMoreChapters) { + const chaptersUrl = `${this.site}/api/novels/${novelPath}/chapters?order=asc&limit=${limit}&offset=${offset}`; + const chaptersResponse = await fetchApi(chaptersUrl); + const chaptersJson = await chaptersResponse.json(); + + const fetchedChapters = chaptersJson.items; + + if (!fetchedChapters || fetchedChapters.length === 0) { + hasMoreChapters = false; + break; + } + + fetchedChapters.forEach((ch: any) => { + chapters.push({ + name: ch.title || `Chapter ${ch.number}`, + path: `api/novels/${novelPath}/chapters/${ch.number}/read`, + releaseTime: ch.created_at || '', + chapterNumber: ch.number, + }); + }); + + if (fetchedChapters.length === limit) { + offset += limit; + } else { + hasMoreChapters = false; + } + } + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const url = `${this.site}/${chapterPath}`; + + const response = await fetchApi(url); + const json = await response.json(); + + const rawText = json.body || ''; + + const chapterHtml = rawText + .split('\n') + .map((paragraph: string) => `

${paragraph.trim()}

`) + .filter((paragraph: string) => paragraph !== '

') + .join(''); + + return chapterHtml; + } + + resolveUrl = (path: string, isNovel?: boolean) => { + if (isNovel) return `${this.site}/novel/${path}`; + return `${this.site}/${path}`; + }; +} + +export default new ChikariPlugin(); \ No newline at end of file diff --git a/public/static/src/en/chikari/icon.png b/public/static/src/en/chikari/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..682022db2dd44c8be78b204ba0fb18ac189f7356 GIT binary patch literal 3309 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