From 1a1f2f4170cdfa49fdae582c554cd7454ded2422 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 15 May 2026 15:36:37 +0000 Subject: [PATCH 01/13] Add TruyenSS Vietnamese source for truyenss.com Implements novel pages, hash-style chapter list, POST chapter loader (/layout/xem-chuong.php), genre filters for popular listings, and multi-pattern search. Includes saved HTML fixtures used for selectors. --- README.md | 2 + .../truyenss-samples/chapter-content.html | 401 ++++++++++++++++++ fixtures/truyenss-samples/chapter-select.html | 401 ++++++++++++++++++ plugins/index.ts | 2 + plugins/vietnamese/truyenss.ts | 260 ++++++++++++ public/static/src/vi/truyenss/icon.png | Bin 0 -> 624 bytes 6 files changed, 1066 insertions(+) create mode 100644 fixtures/truyenss-samples/chapter-content.html create mode 100644 fixtures/truyenss-samples/chapter-select.html create mode 100644 plugins/vietnamese/truyenss.ts create mode 100644 public/static/src/vi/truyenss/icon.png diff --git a/README.md b/README.md index 02e3116fd..c572c19ba 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Community-driven plugin repository for [LNReader](https://github.com/LNReader/lnreader). This repository hosts plugins and manages related issues and requests. +Includes the custom Vietnamese source **TruyenSS** (`truyenss.com`). HTML snapshots used while implementing it are in `fixtures/truyenss-samples/`. + ## Quick Start **Prerequisites:** Node.js >= 22 diff --git a/fixtures/truyenss-samples/chapter-content.html b/fixtures/truyenss-samples/chapter-content.html new file mode 100644 index 000000000..ef0a2a26a --- /dev/null +++ b/fixtures/truyenss-samples/chapter-content.html @@ -0,0 +1,401 @@ + + + + + + + + + + + Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch] + + + + + + + + + + + + + + + + + + + + + +
+
Loading...
+
+ + + +
+ +
+ +
+

Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch]

+

Tags: Tiên HiệpXuyên KhôngĐông PhươngHệ ThốngHuyền HuyễnDị NăngGóc Nhìn Nam

+ + + + +
+

Giới Thiệu: Ta Mô Phỏng Con Đường Trường Sinh của tác giả Phẫn Nộ Đích Ô Tặc.



Tiên đạo sao mà khó!

Huống chi tu tiên giới đã bị một trận ôn dịch thay đổi hoàn toàn!

Phàm nhân thân mang dịch bệnh, tiên nhân một khi tiếp xúc, nhẹ thì giảm tu vi, nặng thì hoàn đạo thiên vu, sau đó Tiên Phàm cách biệt.

Tiên pháp không thể đồng tu luyện, toàn bộ tu tiên giới đã trở thành chốn rừng sâu nước thẳm;

...

Lý Phàm xuyên qua, tuy có hùng tâm vạn trượng, nhưng lại chỉ có thể an phận chốn phàm trần, phí phạm thời gian cả đời.

Cũng may lúc lâm chung, cuối cùng hắn cũng thức tỉnh được dị bảo, có thể chuyển cuộc đời hắn thành một giấc mộng, trở lại lúc vừa chuyển kiếp!

Vì vậy, Lý Phàm bắt đầu bước lên con đường trường sinh!

Đời thứ hai, chỉ 50 năm Lý Phàm đã nắm cả thiên hạ trong tay, nhưng tìm khắp chốn nhân gian vẫn không thấy được tung tích tiên. Chỉ đến cuối đời thì hắn mới thấy được vết tích tiên nhân.

Đời thứ ba, Lý Phàm dù hết lòng hết dạ, mưu đồ mọi cách nhưng cuối cùng cũng không đỡ nổi một kiếm của tiên nhân!

Đời thứ tư...

...

Ta, Lý Phàm, một kẻ phàm nhân, muôn đời không hối hận, nhưng cầu trường sinh!

+
+
+ + + +

Danh Sách Chương

+
+
+
+
+ + + + +
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + diff --git a/fixtures/truyenss-samples/chapter-select.html b/fixtures/truyenss-samples/chapter-select.html new file mode 100644 index 000000000..fc72901c8 --- /dev/null +++ b/fixtures/truyenss-samples/chapter-select.html @@ -0,0 +1,401 @@ + + + + + + + + + + + Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch] + + + + + + + + + + + + + + + + + + + + + +
+
Loading...
+
+ + + +
+ +
+ +
+

Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch]

+

Tags: Tiên HiệpXuyên KhôngĐông PhươngHệ ThốngHuyền HuyễnDị NăngGóc Nhìn Nam

+ + + + +
+

Giới Thiệu: Ta Mô Phỏng Con Đường Trường Sinh của tác giả Phẫn Nộ Đích Ô Tặc.



Tiên đạo sao mà khó!

Huống chi tu tiên giới đã bị một trận ôn dịch thay đổi hoàn toàn!

Phàm nhân thân mang dịch bệnh, tiên nhân một khi tiếp xúc, nhẹ thì giảm tu vi, nặng thì hoàn đạo thiên vu, sau đó Tiên Phàm cách biệt.

Tiên pháp không thể đồng tu luyện, toàn bộ tu tiên giới đã trở thành chốn rừng sâu nước thẳm;

...

Lý Phàm xuyên qua, tuy có hùng tâm vạn trượng, nhưng lại chỉ có thể an phận chốn phàm trần, phí phạm thời gian cả đời.

Cũng may lúc lâm chung, cuối cùng hắn cũng thức tỉnh được dị bảo, có thể chuyển cuộc đời hắn thành một giấc mộng, trở lại lúc vừa chuyển kiếp!

Vì vậy, Lý Phàm bắt đầu bước lên con đường trường sinh!

Đời thứ hai, chỉ 50 năm Lý Phàm đã nắm cả thiên hạ trong tay, nhưng tìm khắp chốn nhân gian vẫn không thấy được tung tích tiên. Chỉ đến cuối đời thì hắn mới thấy được vết tích tiên nhân.

Đời thứ ba, Lý Phàm dù hết lòng hết dạ, mưu đồ mọi cách nhưng cuối cùng cũng không đỡ nổi một kiếm của tiên nhân!

Đời thứ tư...

...

Ta, Lý Phàm, một kẻ phàm nhân, muôn đời không hối hận, nhưng cầu trường sinh!

+
+
+ + + +

Danh Sách Chương

+
+
+
+
+ + + + +
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + diff --git a/plugins/index.ts b/plugins/index.ts index c152366d6..1384ad90c 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -245,6 +245,7 @@ import p_242 from '@plugins/ukrainian/smakolykytl'; import p_243 from '@plugins/vietnamese/LNHako'; import p_244 from '@plugins/vietnamese/lightnovelvn'; import p_245 from '@plugins/vietnamese/nettruyen'; +import p_246 from '@plugins/vietnamese/truyenss'; const PLUGINS: Plugin.PluginBase[] = [ p_0, @@ -493,5 +494,6 @@ const PLUGINS: Plugin.PluginBase[] = [ p_243, p_244, p_245, + p_246, ]; export default PLUGINS; diff --git a/plugins/vietnamese/truyenss.ts b/plugins/vietnamese/truyenss.ts new file mode 100644 index 000000000..e2477f583 --- /dev/null +++ b/plugins/vietnamese/truyenss.ts @@ -0,0 +1,260 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; +import { Plugin } from '@/types/plugin'; + +const CHAPTER_PATH = /^\/truyen\/([^/]+)\/chuong-(\d+)$/; + +class TruyenSS implements Plugin.PluginBase { + id = 'truyenss.com'; + name = 'TruyenSS'; + icon = 'src/vi/truyenss/icon.png'; + site = 'https://truyenss.com'; + version = '1.0.0'; + + imageRequestInit: Plugin.ImageRequestInit = { + headers: { Referer: this.site + '/' }, + }; + + filters = { + genre: { + type: FilterTypes.Picker, + label: 'Thể loại', + value: 'tien-hiep', + options: [ + { label: 'Tiên Hiệp', value: 'tien-hiep' }, + { label: 'Nữ Cường', value: 'nu-cuong' }, + { label: 'Xuyên Không', value: 'xuyen-khong' }, + { label: 'Điền Văn', value: 'dien-van' }, + { label: 'Thám Hiểm', value: 'tham-hiem' }, + { label: 'Linh Dị', value: 'linh-di' }, + { label: 'Truyện Ngược', value: 'truyen-nguoc' }, + { label: 'Truyện Sủng', value: 'truyen-sung' }, + { label: 'Đông Phương', value: 'dong-phuong' }, + { label: 'Hài Hước', value: 'hai-huoc' }, + { label: 'Hiện Đại', value: 'hien-dai' }, + { label: 'Quân Sự', value: 'quan-su' }, + { label: 'Mạt Thế', value: 'mat-the' }, + { label: 'Trọng Sinh', value: 'trong-sinh' }, + { label: 'Đồng Nhân', value: 'dong-nhan' }, + { label: 'Quan Trường', value: 'quan-truong' }, + { label: 'Cổ Đại', value: 'co-dai' }, + { label: 'Hệ Thống', value: 'he-thong' }, + { label: 'Phương Tây', value: 'phuong-tay' }, + { label: 'Lịch Sử', value: 'lich-su' }, + { label: 'Ngôn Tình', value: 'ngon-tinh' }, + { label: 'Huyền Huyễn', value: 'huyen-huyen' }, + { label: 'Kiếm Hiệp', value: 'kiem-hiep' }, + { label: 'Võng Du', value: 'vong-du' }, + { label: 'Trinh Thám', value: 'trinh-tham' }, + { label: 'Khoa Huyễn', value: 'khoa-huyen' }, + { label: 'Dị Năng', value: 'di-nang' }, + { label: 'Gia Đấu Cung Đấu', value: 'gia-dau-cung-dau' }, + { label: 'Góc Nhìn Nữ', value: 'goc-nhin-nu' }, + { label: 'Góc Nhìn Nam', value: 'goc-nhin-nam' }, + ], + }, + } satisfies Filters; + + private collectTruyenLinks(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + const seen = new Set(); + loadedCheerio('a[href^="/truyen/"]').each((_, el) => { + const href = el.attribs['href']; + if (!href || href.split('/').length !== 3) return; + const path = href.split('?')[0]!; + if (seen.has(path)) return; + seen.add(path); + const name = loadedCheerio(el).text().replace(/\s+/g, ' ').trim(); + if (!name) return; + novels.push({ path, name }); + }); + return novels; + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + if (showLatestNovels) { + if (pageNo > 1) return []; + const body = await fetchApi(this.site + '/').then(r => r.text()); + return this.collectTruyenLinks(parseHTML(body)); + } + const genre = filters?.genre.value ?? 'tien-hiep'; + const url = + pageNo <= 1 + ? `${this.site}/${genre}` + : `${this.site}/${genre}?page=${pageNo}`; + const body = await fetchApi(url).then(r => r.text()); + return this.collectTruyenLinks(parseHTML(body)); + } + + private parseStatusLine(raw: string): string { + const t = raw.toLowerCase(); + if (t.includes('hoàn') || t.includes('full')) return NovelStatus.Completed; + if (t.includes('đang') || t.includes('ra chương')) + return NovelStatus.Ongoing; + return NovelStatus.Unknown; + } + + private parseChapters( + loadedCheerio: CheerioAPI, + novelPath: string, + ): Plugin.ChapterItem[] { + const chapters: Plugin.ChapterItem[] = []; + const h2 = loadedCheerio('h2') + .filter((_, el) => loadedCheerio(el).text().includes('Danh Sách Chương')) + .first(); + const container = h2.next('div.position-relative'); + const anchors = container.length + ? container.find('a[href^="#"]') + : loadedCheerio('#inner-page a[href^="#"]'); + + anchors.each((_, el) => { + const href = el.attribs['href']; + if (!href?.startsWith('#')) return; + const num = Number(href.slice(1)); + if (!Number.isFinite(num) || num <= 0) return; + const name = loadedCheerio(el).text().replace(/\s+/g, ' ').trim(); + chapters.push({ + name: name || `Chương ${num}`, + path: `${novelPath}/chuong-${num}`, + chapterNumber: num, + }); + }); + chapters.sort((a, b) => (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0)); + return chapters; + } + + async parseNovel(novelPath: string): Promise { + let path = novelPath; + if (path.startsWith('http')) { + try { + path = new URL(path).pathname; + } catch { + /* keep path */ + } + } + const url = this.site + path; + const body = await fetchApi(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path, + name: + loadedCheerio('#inner-page > h1').first().text().trim() || + loadedCheerio('main#main h1').first().text().trim() || + 'Không có tiêu đề', + chapters: [], + }; + + const cover = loadedCheerio('.info_truyen img.avatar').attr('src'); + if (cover) { + novel.cover = cover.startsWith('http') ? cover : this.site + cover; + } + + const infoBlock = loadedCheerio('.info_truyen').first(); + const infoText = infoBlock.text(); + const authorMatch = infoText.match(/Tác\s*Giả:\s*([^\n\r]+)/i); + if (authorMatch) novel.author = authorMatch[1]!.trim(); + + const statusMatch = infoText.match(/Tình\s*Trạng:\s*([^\n\r]+)/i); + if (statusMatch) novel.status = this.parseStatusLine(statusMatch[1]!); + + novel.genres = loadedCheerio('p.tags a.badge') + .toArray() + .map(a => loadedCheerio(a).text().trim()) + .filter(Boolean) + .join(', '); + + const intro = loadedCheerio( + '#inner-page .position-relative.mt-4 .line-height-3', + ).first(); + if (intro.length) { + novel.summary = intro.text().replace(/\s+/g, ' ').trim(); + } + + novel.chapters = this.parseChapters(loadedCheerio, path); + return novel; + } + + private extractChapterBody($: CheerioAPI): string { + $('script, style').remove(); + let best = ''; + let bestP = 0; + $('div').each((_, el) => { + const div = $(el); + const pCount = div.find('p').length; + if (pCount > bestP) { + bestP = pCount; + best = div.html() ?? ''; + } + }); + if (bestP >= 2) return best; + const fallback = $('body').html() ?? $.root().html() ?? ''; + return fallback; + } + + async parseChapter(chapterPath: string): Promise { + let rel = chapterPath; + if (rel.startsWith(this.site)) { + rel = rel.slice(this.site.length); + } + const m = rel.match(CHAPTER_PATH); + if (!m) throw new Error(`TruyenSS: invalid chapter path: ${rel}`); + const folder = m[1]!; + const chuong = m[2]!; + const referer = `${this.site}/truyen/${folder}`; + + const body = await fetchApi(`${this.site}/layout/xem-chuong.php`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + Referer: referer, + }, + body: new URLSearchParams({ folder, chuong }).toString(), + }).then(r => r.text()); + + if (!body.trim()) { + throw new Error('TruyenSS: empty chapter response'); + } + + return this.extractChapterBody(parseHTML(body)); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const q = encodeURIComponent(searchTerm.trim()); + if (!q) return []; + + const tryUrls = [ + `${this.site}/tim-kiem?q=${q}&page=${pageNo}`, + `${this.site}/tim-kiem/${q}?page=${pageNo}`, + `${this.site}/tim-truyen?tu-khoa=${q}&page=${pageNo}`, + ]; + + for (const tryUrl of tryUrls) { + const body = await fetchApi(tryUrl).then(r => r.text()); + const novels = this.collectTruyenLinks(parseHTML(body)); + if (novels.length) return novels; + } + return []; + } + + resolveUrl = (path: string): string => { + const m = path.match(CHAPTER_PATH); + if (m) return `${this.site}/truyen/${m[1]}#${m[2]}`; + if (path.startsWith('http')) return path; + return this.site + path; + }; +} + +export default new TruyenSS(); diff --git a/public/static/src/vi/truyenss/icon.png b/public/static/src/vi/truyenss/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db5d0538f92a6c1e01b4fff648676566da1035af GIT binary patch literal 624 zcmV-$0+0QPP)F1DMG=cx|NnanjnhahnM`|m=nVHU5;v+xLkV>p#W_SZ~uPSzy?w7>E$Sc0a^6dk%tC2`CND~pq6uYQo#m>oEIlGO5ooO zhI1LwfNn*S2N$ph-Si0_4p8Ex>IA(-x|PX@ZN~0_|Nd3Qw>emb!vGZVld3lWlsx Date: Fri, 15 May 2026 16:52:25 +0000 Subject: [PATCH 02/13] fix(truyenss): fetch chapter HTML via GET like the site TruyenSS loads chapters with jQuery $.ajax, which defaults to GET. POST to xem-chuong.php returns an empty body, causing empty chapter errors in the app while WebView (GET + hash) still worked. Bump plugin version to 1.0.1. --- plugins/vietnamese/truyenss.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/vietnamese/truyenss.ts b/plugins/vietnamese/truyenss.ts index e2477f583..3f197fc28 100644 --- a/plugins/vietnamese/truyenss.ts +++ b/plugins/vietnamese/truyenss.ts @@ -11,7 +11,7 @@ class TruyenSS implements Plugin.PluginBase { name = 'TruyenSS'; icon = 'src/vi/truyenss/icon.png'; site = 'https://truyenss.com'; - version = '1.0.0'; + version = '1.0.1'; imageRequestInit: Plugin.ImageRequestInit = { headers: { Referer: this.site + '/' }, @@ -211,14 +211,12 @@ class TruyenSS implements Plugin.PluginBase { const chuong = m[2]!; const referer = `${this.site}/truyen/${folder}`; - const body = await fetchApi(`${this.site}/layout/xem-chuong.php`, { - method: 'POST', + const qs = new URLSearchParams({ folder, chuong }).toString(); + const body = await fetchApi(`${this.site}/layout/xem-chuong.php?${qs}`, { headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', Referer: referer, }, - body: new URLSearchParams({ folder, chuong }).toString(), }).then(r => r.text()); if (!body.trim()) { From 69313224972384e06bf8819c4de05566b0ed3e7a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 15:54:16 +0000 Subject: [PATCH 03/13] =?UTF-8?q?fix(truyenss):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20version=201.0.0,=20defaultCover,=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove README note and truyenss HTML fixtures. Drop unused resolveUrl and full-URL handling in parseNovel. Use defaultCover for listing items and when novel page has no cover image. --- README.md | 2 - .../truyenss-samples/chapter-content.html | 401 ------------------ fixtures/truyenss-samples/chapter-select.html | 401 ------------------ plugins/vietnamese/truyenss.ts | 29 +- 4 files changed, 9 insertions(+), 824 deletions(-) delete mode 100644 fixtures/truyenss-samples/chapter-content.html delete mode 100644 fixtures/truyenss-samples/chapter-select.html diff --git a/README.md b/README.md index c572c19ba..02e3116fd 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ Community-driven plugin repository for [LNReader](https://github.com/LNReader/lnreader). This repository hosts plugins and manages related issues and requests. -Includes the custom Vietnamese source **TruyenSS** (`truyenss.com`). HTML snapshots used while implementing it are in `fixtures/truyenss-samples/`. - ## Quick Start **Prerequisites:** Node.js >= 22 diff --git a/fixtures/truyenss-samples/chapter-content.html b/fixtures/truyenss-samples/chapter-content.html deleted file mode 100644 index ef0a2a26a..000000000 --- a/fixtures/truyenss-samples/chapter-content.html +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - - - - - - Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch] - - - - - - - - - - - - - - - - - - - - - -
-
Loading...
-
- - - -
- -
- -
-

Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch]

-

Tags: Tiên HiệpXuyên KhôngĐông PhươngHệ ThốngHuyền HuyễnDị NăngGóc Nhìn Nam

- - - - -
-

Giới Thiệu: Ta Mô Phỏng Con Đường Trường Sinh của tác giả Phẫn Nộ Đích Ô Tặc.



Tiên đạo sao mà khó!

Huống chi tu tiên giới đã bị một trận ôn dịch thay đổi hoàn toàn!

Phàm nhân thân mang dịch bệnh, tiên nhân một khi tiếp xúc, nhẹ thì giảm tu vi, nặng thì hoàn đạo thiên vu, sau đó Tiên Phàm cách biệt.

Tiên pháp không thể đồng tu luyện, toàn bộ tu tiên giới đã trở thành chốn rừng sâu nước thẳm;

...

Lý Phàm xuyên qua, tuy có hùng tâm vạn trượng, nhưng lại chỉ có thể an phận chốn phàm trần, phí phạm thời gian cả đời.

Cũng may lúc lâm chung, cuối cùng hắn cũng thức tỉnh được dị bảo, có thể chuyển cuộc đời hắn thành một giấc mộng, trở lại lúc vừa chuyển kiếp!

Vì vậy, Lý Phàm bắt đầu bước lên con đường trường sinh!

Đời thứ hai, chỉ 50 năm Lý Phàm đã nắm cả thiên hạ trong tay, nhưng tìm khắp chốn nhân gian vẫn không thấy được tung tích tiên. Chỉ đến cuối đời thì hắn mới thấy được vết tích tiên nhân.

Đời thứ ba, Lý Phàm dù hết lòng hết dạ, mưu đồ mọi cách nhưng cuối cùng cũng không đỡ nổi một kiếm của tiên nhân!

Đời thứ tư...

...

Ta, Lý Phàm, một kẻ phàm nhân, muôn đời không hối hận, nhưng cầu trường sinh!

-
-
- - - -

Danh Sách Chương

-
-
-
-
- - - - -
- - - - -
- - -
- - - - - - - - - - - - - - - - - - - diff --git a/fixtures/truyenss-samples/chapter-select.html b/fixtures/truyenss-samples/chapter-select.html deleted file mode 100644 index fc72901c8..000000000 --- a/fixtures/truyenss-samples/chapter-select.html +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - - - - - - Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch] - - - - - - - - - - - - - - - - - - - - - -
-
Loading...
-
- - - -
- -
- -
-

Ta Mô Phỏng Trường Sinh Lộ [Bản Dịch]

-

Tags: Tiên HiệpXuyên KhôngĐông PhươngHệ ThốngHuyền HuyễnDị NăngGóc Nhìn Nam

- - - - -
-

Giới Thiệu: Ta Mô Phỏng Con Đường Trường Sinh của tác giả Phẫn Nộ Đích Ô Tặc.



Tiên đạo sao mà khó!

Huống chi tu tiên giới đã bị một trận ôn dịch thay đổi hoàn toàn!

Phàm nhân thân mang dịch bệnh, tiên nhân một khi tiếp xúc, nhẹ thì giảm tu vi, nặng thì hoàn đạo thiên vu, sau đó Tiên Phàm cách biệt.

Tiên pháp không thể đồng tu luyện, toàn bộ tu tiên giới đã trở thành chốn rừng sâu nước thẳm;

...

Lý Phàm xuyên qua, tuy có hùng tâm vạn trượng, nhưng lại chỉ có thể an phận chốn phàm trần, phí phạm thời gian cả đời.

Cũng may lúc lâm chung, cuối cùng hắn cũng thức tỉnh được dị bảo, có thể chuyển cuộc đời hắn thành một giấc mộng, trở lại lúc vừa chuyển kiếp!

Vì vậy, Lý Phàm bắt đầu bước lên con đường trường sinh!

Đời thứ hai, chỉ 50 năm Lý Phàm đã nắm cả thiên hạ trong tay, nhưng tìm khắp chốn nhân gian vẫn không thấy được tung tích tiên. Chỉ đến cuối đời thì hắn mới thấy được vết tích tiên nhân.

Đời thứ ba, Lý Phàm dù hết lòng hết dạ, mưu đồ mọi cách nhưng cuối cùng cũng không đỡ nổi một kiếm của tiên nhân!

Đời thứ tư...

...

Ta, Lý Phàm, một kẻ phàm nhân, muôn đời không hối hận, nhưng cầu trường sinh!

-
-
- - - -

Danh Sách Chương

-
-
-
-
- - - - -
- - - - -
- - -
- - - - - - - - - - - - - - - - - - - diff --git a/plugins/vietnamese/truyenss.ts b/plugins/vietnamese/truyenss.ts index 3f197fc28..9a6a4407d 100644 --- a/plugins/vietnamese/truyenss.ts +++ b/plugins/vietnamese/truyenss.ts @@ -1,4 +1,5 @@ import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; import { fetchApi } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { NovelStatus } from '@libs/novelStatus'; @@ -11,7 +12,7 @@ class TruyenSS implements Plugin.PluginBase { name = 'TruyenSS'; icon = 'src/vi/truyenss/icon.png'; site = 'https://truyenss.com'; - version = '1.0.1'; + version = '1.0.0'; imageRequestInit: Plugin.ImageRequestInit = { headers: { Referer: this.site + '/' }, @@ -68,7 +69,7 @@ class TruyenSS implements Plugin.PluginBase { seen.add(path); const name = loadedCheerio(el).text().replace(/\s+/g, ' ').trim(); if (!name) return; - novels.push({ path, name }); + novels.push({ path, name, cover: defaultCover }); }); return novels; } @@ -132,14 +133,7 @@ class TruyenSS implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - let path = novelPath; - if (path.startsWith('http')) { - try { - path = new URL(path).pathname; - } catch { - /* keep path */ - } - } + const path = novelPath; const url = this.site + path; const body = await fetchApi(url).then(r => r.text()); const loadedCheerio = parseHTML(body); @@ -154,9 +148,11 @@ class TruyenSS implements Plugin.PluginBase { }; const cover = loadedCheerio('.info_truyen img.avatar').attr('src'); - if (cover) { - novel.cover = cover.startsWith('http') ? cover : this.site + cover; - } + novel.cover = cover + ? cover.startsWith('http') + ? cover + : this.site + cover + : defaultCover; const infoBlock = loadedCheerio('.info_truyen').first(); const infoText = infoBlock.text(); @@ -246,13 +242,6 @@ class TruyenSS implements Plugin.PluginBase { } return []; } - - resolveUrl = (path: string): string => { - const m = path.match(CHAPTER_PATH); - if (m) return `${this.site}/truyen/${m[1]}#${m[2]}`; - if (path.startsWith('http')) return path; - return this.site + path; - }; } export default new TruyenSS(); From 908c3502eb228753a316f6f49d1c176567576e79 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 16:23:17 +0000 Subject: [PATCH 04/13] =?UTF-8?q?fix(truyenss):=20browse=20list=20covers?= =?UTF-8?q?=20=E2=80=94=20scrape=20thumbs,=20stable=20placeholder=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LNReader applies plugin Referer headers to browse tiles; external blob placeholders can fail. List layout omits defaultCover when cover is unset. Prefer real listing images; use raw GitHub placeholder URL aligned with the app. --- plugins/vietnamese/truyenss.ts | 43 ++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/plugins/vietnamese/truyenss.ts b/plugins/vietnamese/truyenss.ts index 9a6a4407d..16008f4d6 100644 --- a/plugins/vietnamese/truyenss.ts +++ b/plugins/vietnamese/truyenss.ts @@ -1,5 +1,5 @@ -import { CheerioAPI, load as parseHTML } from 'cheerio'; -import { defaultCover } from '@libs/defaultCover'; +import { CheerioAPI, load as parseHTML, type Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; import { fetchApi } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { NovelStatus } from '@libs/novelStatus'; @@ -58,6 +58,40 @@ class TruyenSS implements Plugin.PluginBase { }, } satisfies Filters; + /** Matches LNReader's built-in placeholder file; raw URL tends to load more reliably with plugin Referer headers than blob URLs. */ + private readonly browseFallbackCover = + 'https://raw.githubusercontent.com/lnreader/lnreader-plugins/master/public/static/coverNotAvailable.webp'; + + private absolutizeCoverUrl(raw: string): string | undefined { + const u = raw.trim(); + if (!u || u.startsWith('data:')) return undefined; + if (u.startsWith('//')) return 'https:' + u; + if (u.startsWith('http')) return u; + return this.site + (u.startsWith('/') ? u : '/' + u); + } + + private coverFromTruyenAnchor( + loadedCheerio: CheerioAPI, + el: Element, + ): string { + const $a = loadedCheerio(el); + const fromImg = (img: Cheerio) => { + const src = + img.attr('data-src') || img.attr('data-lazy-src') || img.attr('src'); + if (!src) return undefined; + return this.absolutizeCoverUrl(src); + }; + + const inner = fromImg($a.find('img')); + if (inner) return inner; + + const parentImg = $a.parent().children('img').first(); + const sibling = fromImg(parentImg); + if (sibling) return sibling; + + return this.browseFallbackCover; + } + private collectTruyenLinks(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { const novels: Plugin.NovelItem[] = []; const seen = new Set(); @@ -69,7 +103,8 @@ class TruyenSS implements Plugin.PluginBase { seen.add(path); const name = loadedCheerio(el).text().replace(/\s+/g, ' ').trim(); if (!name) return; - novels.push({ path, name, cover: defaultCover }); + const cover = this.coverFromTruyenAnchor(loadedCheerio, el); + novels.push({ path, name, cover }); }); return novels; } @@ -152,7 +187,7 @@ class TruyenSS implements Plugin.PluginBase { ? cover.startsWith('http') ? cover : this.site + cover - : defaultCover; + : this.browseFallbackCover; const infoBlock = loadedCheerio('.info_truyen').first(); const infoText = infoBlock.text(); From 066249327341479643a1e4d6ae1adb4c5087d99a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 16:35:33 +0000 Subject: [PATCH 05/13] fix(truyenss): resolve listing covers (.card sibling img, ../ URLs) Genre/search cards put the thumbnail in col-2 beside the stretched link, not inside the anchor. Image src uses ../user-upload/... which must be resolved against the list page URL. Use the site no_avatar.jpg placeholder so browse tiles load with the plugin Referer. --- plugins/vietnamese/truyenss.ts | 68 +++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/plugins/vietnamese/truyenss.ts b/plugins/vietnamese/truyenss.ts index 16008f4d6..8a1659f49 100644 --- a/plugins/vietnamese/truyenss.ts +++ b/plugins/vietnamese/truyenss.ts @@ -58,41 +58,60 @@ class TruyenSS implements Plugin.PluginBase { }, } satisfies Filters; - /** Matches LNReader's built-in placeholder file; raw URL tends to load more reliably with plugin Referer headers than blob URLs. */ - private readonly browseFallbackCover = - 'https://raw.githubusercontent.com/lnreader/lnreader-plugins/master/public/static/coverNotAvailable.webp'; + /** Host-local placeholder from the site (og:image); works with plugin Referer headers. */ + private get sitePlaceholderCover(): string { + return `${this.site}/images/no_avatar.jpg`; + } - private absolutizeCoverUrl(raw: string): string | undefined { + private resolveCoverUrl( + raw: string | undefined, + pageUrl: string, + ): string | undefined { + if (!raw) return undefined; const u = raw.trim(); if (!u || u.startsWith('data:')) return undefined; - if (u.startsWith('//')) return 'https:' + u; - if (u.startsWith('http')) return u; - return this.site + (u.startsWith('/') ? u : '/' + u); + try { + if (u.startsWith('//')) return 'https:' + u; + if (u.startsWith('http')) return u; + return new URL(u, pageUrl).href; + } catch { + return undefined; + } } private coverFromTruyenAnchor( loadedCheerio: CheerioAPI, el: Element, + pageUrl: string, ): string { const $a = loadedCheerio(el); const fromImg = (img: Cheerio) => { const src = - img.attr('data-src') || img.attr('data-lazy-src') || img.attr('src'); - if (!src) return undefined; - return this.absolutizeCoverUrl(src); + img.attr('data-src') || + img.attr('data-lazy-src') || + img.attr('data-original') || + img.attr('src'); + return this.resolveCoverUrl(src, pageUrl); }; - const inner = fromImg($a.find('img')); + const inner = fromImg($a.find('img').first()); if (inner) return inner; - const parentImg = $a.parent().children('img').first(); - const sibling = fromImg(parentImg); - if (sibling) return sibling; + const cardImg = $a.closest('.card').find('img').first(); + const fromCard = fromImg(cardImg); + if (fromCard) return fromCard; - return this.browseFallbackCover; + const rowImg = $a.closest('.row').find('img').first(); + const fromRow = fromImg(rowImg); + if (fromRow) return fromRow; + + return this.sitePlaceholderCover; } - private collectTruyenLinks(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { + private collectTruyenLinks( + loadedCheerio: CheerioAPI, + pageUrl: string, + ): Plugin.NovelItem[] { const novels: Plugin.NovelItem[] = []; const seen = new Set(); loadedCheerio('a[href^="/truyen/"]').each((_, el) => { @@ -103,7 +122,7 @@ class TruyenSS implements Plugin.PluginBase { seen.add(path); const name = loadedCheerio(el).text().replace(/\s+/g, ' ').trim(); if (!name) return; - const cover = this.coverFromTruyenAnchor(loadedCheerio, el); + const cover = this.coverFromTruyenAnchor(loadedCheerio, el, pageUrl); novels.push({ path, name, cover }); }); return novels; @@ -119,7 +138,7 @@ class TruyenSS implements Plugin.PluginBase { if (showLatestNovels) { if (pageNo > 1) return []; const body = await fetchApi(this.site + '/').then(r => r.text()); - return this.collectTruyenLinks(parseHTML(body)); + return this.collectTruyenLinks(parseHTML(body), `${this.site}/`); } const genre = filters?.genre.value ?? 'tien-hiep'; const url = @@ -127,7 +146,7 @@ class TruyenSS implements Plugin.PluginBase { ? `${this.site}/${genre}` : `${this.site}/${genre}?page=${pageNo}`; const body = await fetchApi(url).then(r => r.text()); - return this.collectTruyenLinks(parseHTML(body)); + return this.collectTruyenLinks(parseHTML(body), url); } private parseStatusLine(raw: string): string { @@ -182,12 +201,9 @@ class TruyenSS implements Plugin.PluginBase { chapters: [], }; - const cover = loadedCheerio('.info_truyen img.avatar').attr('src'); - novel.cover = cover - ? cover.startsWith('http') - ? cover - : this.site + cover - : this.browseFallbackCover; + const coverSrc = loadedCheerio('.info_truyen img.avatar').attr('src'); + novel.cover = + this.resolveCoverUrl(coverSrc, url) ?? this.sitePlaceholderCover; const infoBlock = loadedCheerio('.info_truyen').first(); const infoText = infoBlock.text(); @@ -272,7 +288,7 @@ class TruyenSS implements Plugin.PluginBase { for (const tryUrl of tryUrls) { const body = await fetchApi(tryUrl).then(r => r.text()); - const novels = this.collectTruyenLinks(parseHTML(body)); + const novels = this.collectTruyenLinks(parseHTML(body), tryUrl); if (novels.length) return novels; } return []; From 093668b091a29574260c885e5d5c93700cad0cd5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 17:54:16 +0000 Subject: [PATCH 06/13] fix(playground): readable novel summary + TruyenSS paragraph breaks - Parse playground: normalize CR/LF and stray spaces; use whitespace-pre-line, max-w-prose, leading-7, break-words, explicit font-sans for robust wrapping when webfonts misbehave. - TruyenSS: preserve
/

structure like novelfire (join with newlines) instead of collapsing the whole intro to one line. --- plugins/vietnamese/truyenss.ts | 13 ++++++++++++- src/components/parse-novel.tsx | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/plugins/vietnamese/truyenss.ts b/plugins/vietnamese/truyenss.ts index 8a1659f49..e9415ff75 100644 --- a/plugins/vietnamese/truyenss.ts +++ b/plugins/vietnamese/truyenss.ts @@ -223,7 +223,18 @@ class TruyenSS implements Plugin.PluginBase { '#inner-page .position-relative.mt-4 .line-height-3', ).first(); if (intro.length) { - novel.summary = intro.text().replace(/\s+/g, ' ').trim(); + const block = intro.clone(); + block.find('script, style').remove(); + block.find('br').replaceWith('\n'); + block.find('p').before('\n').after('\n\n'); + novel.summary = block + .text() + .split('\n') + .map(line => line.replace(/\s+/g, ' ').trim()) + .filter(Boolean) + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); } novel.chapters = this.parseChapters(loadedCheerio, path); diff --git a/src/components/parse-novel.tsx b/src/components/parse-novel.tsx index 4a5dbe779..f3d4ccead 100644 --- a/src/components/parse-novel.tsx +++ b/src/components/parse-novel.tsx @@ -27,6 +27,18 @@ type ParseNovelSectionProps = { onNavigateToParseChapter?: () => void; }; +/** Normalize line breaks / spaces for multi-line summary display (playground + long single-line strings). */ +function formatSummaryForDisplay(raw: string): string { + return raw + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/[\t\f\v]+/g, ' ') + .replace(/ *\n */g, '\n') + .replace(/[ \u00a0]+/g, ' ') + .replace(/\n{3,}/g, '\n\n') + .trim(); +} + const ParseNovelSection = React.memo(function ParseNovelSection({ onNavigateToParseChapter, }: ParseNovelSectionProps) { @@ -360,8 +372,8 @@ const ParseNovelSection = React.memo(function ParseNovelSection({

Summary

-

- {sourceNovel.summary} +

+ {formatSummaryForDisplay(sourceNovel.summary)}

)} From 58ddf6c6003fa3828a5a6627f77b4ea3aa3cfca4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 17:58:44 +0000 Subject: [PATCH 07/13] fix(proxy): avoid static zstd import so Vite starts on older Node zstdDecompressSync is not exported from zlib on Node versions before it was added. Importing it at load time broke vite.config (and the dev server) with SyntaxError. Only call zstd at runtime when content-encoding is zstd and throw a clear error if the runtime lacks support. --- proxy.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/proxy.ts b/proxy.ts index 1e55e8639..7f8d479d3 100644 --- a/proxy.ts +++ b/proxy.ts @@ -4,7 +4,22 @@ import { FetchMode, ServerSetting } from './src/types/types'; import { Connect } from 'vite'; import httpProxy from 'http-proxy'; import { exec } from 'child_process'; -import { brotliDecompressSync, gunzipSync, zstdDecompressSync } from 'zlib'; +import { brotliDecompressSync, gunzipSync } from 'node:zlib'; +import * as zlibNs from 'node:zlib'; + +type ZlibWithZstd = typeof zlibNs & { + zstdDecompressSync?: (buffer: Buffer) => Buffer; +}; + +function zstdDecompressSync(buffer: Buffer): Buffer { + const fn = (zlibNs as ZlibWithZstd).zstdDecompressSync; + if (typeof fn !== 'function') { + throw new Error( + 'Response uses zstd encoding but this Node.js build has no zlib.zstdDecompressSync. Use Node 22.12+ (or newer current LTS with zstd) or switch fetch mode in settings.', + ); + } + return fn(buffer); +} const proxy = httpProxy.createProxyServer({}); From ab5c4f0f6b0059589956693da534294a49500be0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 17:58:54 +0000 Subject: [PATCH 08/13] fix(proxy): correct decompress error message for zstd --- proxy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy.ts b/proxy.ts index 7f8d479d3..e390a797d 100644 --- a/proxy.ts +++ b/proxy.ts @@ -295,7 +295,8 @@ proxy.on('proxyRes', function (proxyRes, req, res) { } catch (err) { console.error(err); res.statusCode = 500; - res.end(`Error decompressing ${isBrotli ? 'Brotli' : 'GZIP'} content`); + const kind = isBrotli ? 'Brotli' : isZstd ? 'zstd' : 'gzip'; + res.end(`Error decompressing ${kind} content`); } }); } else { From 2c08ddb92ceed3a21c9fca7865b0ebf004f2486c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 18:00:21 +0000 Subject: [PATCH 09/13] Revert "fix(proxy): correct decompress error message for zstd" This reverts commit ab5c4f0f6b0059589956693da534294a49500be0. --- proxy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxy.ts b/proxy.ts index e390a797d..7f8d479d3 100644 --- a/proxy.ts +++ b/proxy.ts @@ -295,8 +295,7 @@ proxy.on('proxyRes', function (proxyRes, req, res) { } catch (err) { console.error(err); res.statusCode = 500; - const kind = isBrotli ? 'Brotli' : isZstd ? 'zstd' : 'gzip'; - res.end(`Error decompressing ${kind} content`); + res.end(`Error decompressing ${isBrotli ? 'Brotli' : 'GZIP'} content`); } }); } else { From 309cc99c958d15fd814339aa32b892a7232e423a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 18:00:21 +0000 Subject: [PATCH 10/13] Revert "fix(proxy): avoid static zstd import so Vite starts on older Node" This reverts commit 58ddf6c6003fa3828a5a6627f77b4ea3aa3cfca4. --- proxy.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/proxy.ts b/proxy.ts index 7f8d479d3..1e55e8639 100644 --- a/proxy.ts +++ b/proxy.ts @@ -4,22 +4,7 @@ import { FetchMode, ServerSetting } from './src/types/types'; import { Connect } from 'vite'; import httpProxy from 'http-proxy'; import { exec } from 'child_process'; -import { brotliDecompressSync, gunzipSync } from 'node:zlib'; -import * as zlibNs from 'node:zlib'; - -type ZlibWithZstd = typeof zlibNs & { - zstdDecompressSync?: (buffer: Buffer) => Buffer; -}; - -function zstdDecompressSync(buffer: Buffer): Buffer { - const fn = (zlibNs as ZlibWithZstd).zstdDecompressSync; - if (typeof fn !== 'function') { - throw new Error( - 'Response uses zstd encoding but this Node.js build has no zlib.zstdDecompressSync. Use Node 22.12+ (or newer current LTS with zstd) or switch fetch mode in settings.', - ); - } - return fn(buffer); -} +import { brotliDecompressSync, gunzipSync, zstdDecompressSync } from 'zlib'; const proxy = httpProxy.createProxyServer({}); From b0de564fca75e756ef11899e77038fc594f18f97 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 18:08:14 +0000 Subject: [PATCH 11/13] fix(dev): proxy URL for https targets + disable auto-open by default - Reconstruct target URL when the dev server path is /https://... (from window.fetch prefixing localhost); the old + path form produced an invalid URL and broke all proxied plugin requests. - Only open a browser when VITE_OPEN=true so missing xdg-open does not error. --- proxy.ts | 7 ++++++- vite.config.ts | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/proxy.ts b/proxy.ts index 1e55e8639..33de868d5 100644 --- a/proxy.ts +++ b/proxy.ts @@ -57,7 +57,12 @@ const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => { }; const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { - const rawUrl = 'https:' + req.url; + const path = req.url ?? ''; + /** Browser fetch becomes GET /https://host/... — must not use `https:` + path (breaks URL). */ + const rawUrl = + path.startsWith('/https://') || path.startsWith('/http://') + ? path.slice(1) + : `https:${path}`; if (req.headers['access-control-request-method']) { res.setHeader( 'access-control-allow-methods', diff --git a/vite.config.ts b/vite.config.ts index be796289f..35d6b1895 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ }, server: { port: 3000, - open: true, + // Opt-in: avoids `spawn xdg-open ENOENT` on minimal Linux; open http://localhost:3000 manually. + open: process.env.VITE_OPEN === '1' || process.env.VITE_OPEN === 'true', }, }); From bbedd4e17c50b6bbfaf1914bee1283d4e125da15 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 18:16:04 +0000 Subject: [PATCH 12/13] Revert "fix(dev): proxy URL for https targets + disable auto-open by default" This reverts commit b0de564fca75e756ef11899e77038fc594f18f97. --- proxy.ts | 7 +------ vite.config.ts | 3 +-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/proxy.ts b/proxy.ts index 33de868d5..1e55e8639 100644 --- a/proxy.ts +++ b/proxy.ts @@ -57,12 +57,7 @@ const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => { }; const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { - const path = req.url ?? ''; - /** Browser fetch becomes GET /https://host/... — must not use `https:` + path (breaks URL). */ - const rawUrl = - path.startsWith('/https://') || path.startsWith('/http://') - ? path.slice(1) - : `https:${path}`; + const rawUrl = 'https:' + req.url; if (req.headers['access-control-request-method']) { res.setHeader( 'access-control-allow-methods', diff --git a/vite.config.ts b/vite.config.ts index 35d6b1895..be796289f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -31,7 +31,6 @@ export default defineConfig({ }, server: { port: 3000, - // Opt-in: avoids `spawn xdg-open ENOENT` on minimal Linux; open http://localhost:3000 manually. - open: process.env.VITE_OPEN === '1' || process.env.VITE_OPEN === 'true', + open: true, }, }); From ff42ee1fee30da406d320e5f8e314b23ebcc89dd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 19:10:31 +0000 Subject: [PATCH 13/13] revert: drop out-of-scope parse-novel playground summary changes Restore src/components/parse-novel.tsx to pre-093668b state; TruyenSS summary parsing in the plugin is unchanged. --- src/components/parse-novel.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/parse-novel.tsx b/src/components/parse-novel.tsx index f3d4ccead..4a5dbe779 100644 --- a/src/components/parse-novel.tsx +++ b/src/components/parse-novel.tsx @@ -27,18 +27,6 @@ type ParseNovelSectionProps = { onNavigateToParseChapter?: () => void; }; -/** Normalize line breaks / spaces for multi-line summary display (playground + long single-line strings). */ -function formatSummaryForDisplay(raw: string): string { - return raw - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n') - .replace(/[\t\f\v]+/g, ' ') - .replace(/ *\n */g, '\n') - .replace(/[ \u00a0]+/g, ' ') - .replace(/\n{3,}/g, '\n\n') - .trim(); -} - const ParseNovelSection = React.memo(function ParseNovelSection({ onNavigateToParseChapter, }: ParseNovelSectionProps) { @@ -372,8 +360,8 @@ const ParseNovelSection = React.memo(function ParseNovelSection({

Summary

-

- {formatSummaryForDisplay(sourceNovel.summary)} +

+ {sourceNovel.summary}

)}