koishi-plugin-fimtale-api 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/index.js +100 -75
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -47,6 +47,7 @@ var Config = import_koishi.Schema.object({
47
47
  cookies: import_koishi.Schema.string().role("secret").description("浏览器 Cookie (用于解除安全模式,必填)"),
48
48
  pollInterval: import_koishi.Schema.number().default(60 * 1e3).description("追更轮询间隔(ms)"),
49
49
  autoParseLink: import_koishi.Schema.boolean().default(true).description("自动解析链接为预览卡片"),
50
+ // 渲染配置 (iPhone 12/13/14 标准)
50
51
  deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
51
52
  deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
52
53
  fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)")
@@ -61,7 +62,7 @@ function apply(ctx, config) {
61
62
  }, { primary: "id", autoInc: true });
62
63
  const sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => setTimeout(resolve, ms)), "sleep");
63
64
  const formatDate = /* @__PURE__ */ __name((timestamp) => {
64
- if (!timestamp) return "Unknown";
65
+ if (!timestamp) return "未知日期";
65
66
  const date = new Date(timestamp * 1e3);
66
67
  return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
67
68
  }, "formatDate");
@@ -84,7 +85,8 @@ function apply(ctx, config) {
84
85
  if (!html) return "";
85
86
  return html.replace(/<p>\s*&nbsp;\s*<\/p>/gi, "").replace(/<p>\s*<br\s*\/?>\s*<\/p>/gi, "").replace(/<p>\s*<\/p>/gi, "").replace(/(<br\s*\/?>){2,}/gi, "<br>").replace(/margin-bottom:\s*\d+px/gi, "");
86
87
  }, "cleanContent");
87
- const fontStack = '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", "WenQuanYi Micro Hei", Arial, sans-serif';
88
+ const fontStack = '"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif';
89
+ const fontSerif = '"Noto Serif SC", "Source Han Serif SC", "SimSun", serif';
88
90
  const injectCookies = /* @__PURE__ */ __name(async (page) => {
89
91
  if (!config.cookies) return;
90
92
  const cookies = config.cookies.split(";").map((pair) => {
@@ -99,7 +101,7 @@ function apply(ctx, config) {
99
101
  const url = `${config.apiUrl}/t/${threadId}`;
100
102
  const params = { APIKey: config.apiKey, APIPass: config.apiPass };
101
103
  const res = await ctx.http.get(url, { params });
102
- if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API Error" };
104
+ if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API 返回错误" };
103
105
  return {
104
106
  valid: true,
105
107
  data: res.TopicInfo,
@@ -107,7 +109,7 @@ function apply(ctx, config) {
107
109
  menu: res.Menu || []
108
110
  };
109
111
  } catch (e) {
110
- return { valid: false, msg: "Request Failed" };
112
+ return { valid: false, msg: "网络请求失败" };
111
113
  }
112
114
  }, "fetchThread");
113
115
  const fetchRandomId = /* @__PURE__ */ __name(async () => {
@@ -147,7 +149,7 @@ function apply(ctx, config) {
147
149
  const titleEl = card.querySelector(".card-title");
148
150
  if (titleEl) title = titleEl.textContent?.trim() || "";
149
151
  else title = link.textContent?.trim() || "";
150
- let author = "Unknown";
152
+ let author = "未知";
151
153
  const authorEl = card.querySelector('a[href^="/u/"] span.grey-text');
152
154
  if (authorEl) author = authorEl.textContent?.trim() || "";
153
155
  let cover = void 0;
@@ -218,7 +220,7 @@ function apply(ctx, config) {
218
220
  const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
219
221
  let summary = stripHtml(info.Content);
220
222
  if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
221
- if (summary.length > 100) summary = summary.substring(0, 100) + "...";
223
+ if (summary.length > 110) summary = summary.substring(0, 110) + "...";
222
224
  if (!summary) summary = "暂无简介";
223
225
  const tagsArr = [];
224
226
  if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
@@ -236,7 +238,15 @@ function apply(ctx, config) {
236
238
  .card { width: 620px; height: 340px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
237
239
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
238
240
  .id-tag { position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.6); color: #fff; padding: 4px 10px; border-radius: 6px; font-size: 13px; font-weight: bold; backdrop-filter: blur(4px); font-family: monospace; }
239
- .info { flex: 1; padding: 24px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; }
241
+
242
+ .info {
243
+ flex: 1; padding: 24px;
244
+ display: flex; flex-direction: column;
245
+ /* 关键:内容溢出隐藏,防止撑开 */
246
+ overflow: hidden;
247
+ }
248
+
249
+ .header-group { display: flex; flex-direction: column; flex-shrink: 0; }
240
250
 
241
251
  .title {
242
252
  font-size: 22px; font-weight: 700; color: #333; line-height: 1.35;
@@ -247,24 +257,31 @@ function apply(ctx, config) {
247
257
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
248
258
  padding-left: 10px; border-left: 3px solid #e91e63;
249
259
  }
250
-
251
260
  .author { font-size: 13px; color: #888; margin-top: 8px; font-weight: 400; }
252
261
 
253
- .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; max-height: 56px; overflow: hidden; flex-shrink: 0;}
262
+ /* 标签栏 flex-shrink 设为 0,保证标签不被压缩 */
263
+ .tags {
264
+ display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0;
265
+ max-height: 56px; overflow: hidden; flex-shrink: 0;
266
+ }
254
267
  .tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
255
268
  .tag-imp { background: #e3f2fd; color: #1565c0; }
256
269
 
270
+ /* 简介占据剩余空间 */
257
271
  .summary-box {
258
- flex: 1; overflow: hidden; position: relative; margin-top: 8px;
272
+ flex: 1; position: relative; margin-top: 4px; overflow: hidden;
259
273
  }
260
274
  .summary {
261
275
  font-size: 13px; color: #666; line-height: 1.6;
262
- /* 精确控制行数和高度防止截断 */
263
- max-height: 6.4em;
264
276
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
265
277
  }
266
278
 
267
- .footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 12px; color: #888; margin-top: 15px; flex-shrink: 0;}
279
+ /* 底部数据固定高度,防止被挤出 */
280
+ .footer {
281
+ border-top: 1px solid #eee; padding-top: 14px;
282
+ display: flex; justify-content: space-between;
283
+ font-size: 12px; color: #888; margin-top: 15px; flex-shrink: 0;
284
+ }
268
285
  .stat b { color: #555; font-weight: bold; margin-right: 2px;}
269
286
  </style>
270
287
  </head>
@@ -365,81 +382,89 @@ function apply(ctx, config) {
365
382
  <html>
366
383
  <head>
367
384
  <style>
368
- body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontStack}; }
369
- #source-container { display: none; }
370
- .page {
371
- width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
372
- padding: 35px 28px; box-sizing: border-box;
373
- position: relative; background: #f6f4ec; overflow: hidden;
374
- display: flex; flex-direction: column;
385
+ body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; }
386
+
387
+ /* 页面容器:包含页眉、内容、页脚 */
388
+ /* 固定高度,让 CSS Column 生效 */
389
+ #content-wrapper {
390
+ width: ${config.deviceWidth}px;
391
+ height: ${config.deviceHeight}px;
392
+ column-width: ${config.deviceWidth}px;
393
+ column-gap: 0;
394
+
395
+ /* 强制水平滚动 */
396
+ overflow-y: hidden;
397
+ overflow-x: scroll;
398
+ }
399
+
400
+ /* 每一页的实际内容区域 */
401
+ /* 我们需要用 padding 来模拟页眉页脚的空间 */
402
+ /* 上留 50px,下留 40px,左右 25px */
403
+ .inner-content {
404
+ padding: 50px 25px 40px 25px;
405
+ box-sizing: border-box;
406
+ height: 100%; /* 填满容器高度 */
407
+
408
+ font-size: ${config.fontSize}px;
409
+ line-height: 1.8;
410
+ text-align: justify;
375
411
  }
376
- .page-header {
377
- font-size: 12px; color: #8d6e63; border-bottom: 2px solid #d7ccc8;
378
- padding-bottom: 12px; margin-bottom: 15px; flex-shrink: 0;
379
- display: flex; justify-content: space-between; font-weight: bold;
412
+
413
+ /* 固定位置的页眉页脚 (overlay) */
414
+ .fixed-header {
415
+ position: fixed; top: 0; left: 0; width: 100%; height: 40px;
416
+ background: #f6f4ec; /* 遮挡背景 */
417
+ z-index: 10;
418
+ border-bottom: 1px solid #d7ccc8;
419
+ display: flex; align-items: center; justify-content: space-between;
420
+ padding: 0 20px; box-sizing: border-box;
421
+ font-size: 12px; color: #8d6e63; font-weight: bold; font-family: ${fontStack};
380
422
  }
381
- .page-footer {
382
- position: absolute; bottom: 15px; left: 0; right: 0; text-align: center;
383
- font-size: 12px; color: #aaa; font-family: sans-serif;
423
+ .fixed-footer {
424
+ position: fixed; bottom: 0; left: 0; width: 100%; height: 30px;
425
+ background: #f6f4ec;
426
+ z-index: 10;
427
+ display: flex; align-items: center; justify-content: center;
428
+ font-size: 12px; color: #aaa; font-family: ${fontStack};
384
429
  }
385
- .page-content { flex: 1; overflow: hidden; font-size: ${config.fontSize}px; line-height: 1.7; text-align: justify; }
386
- p { margin: 0 0 0.6em 0; text-indent: 2em; }
387
- img { max-width: 100%; height: auto; display: block; margin: 10px auto; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
388
- h1, h2, h3 { font-size: 1.1em; margin: 0.5em 0; color: #5d4037; text-indent: 0; font-weight: bold; }
430
+
431
+ p { margin: 0 0 0.8em 0; text-indent: 2em; }
432
+ /* 防止图片被切分 */
433
+ img { max-width: 100%; height: auto; display: block; margin: 15px auto; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); break-inside: avoid; }
434
+ h1, h2, h3 { font-size: 1.2em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; }
389
435
  </style>
390
436
  </head>
391
437
  <body>
392
- <div id="source-container"><div class="meta-info" data-title="${info.Title}" data-author="${info.UserName}"></div>${content}</div>
393
- <div id="output"></div>
394
- <script>
395
- function paginate() {
396
- const source = document.getElementById('source-container');
397
- const output = document.getElementById('output');
398
- const title = source.querySelector('.meta-info').dataset.title;
399
- const author = source.querySelector('.meta-info').dataset.author;
400
- let pageIndex = 1;
401
- let currentPageContent = null;
402
- function createNewPage() {
403
- const page = document.createElement('div'); page.className = 'page';
404
- const header = document.createElement('div'); header.className = 'page-header';
405
- header.innerHTML = \`<span>\${title.substring(0, 12) + (title.length>12?'...':'')}</span><span>\${author}</span>\`;
406
- page.appendChild(header);
407
- const content = document.createElement('div'); content.className = 'page-content';
408
- page.appendChild(content);
409
- const footer = document.createElement('div'); footer.className = 'page-footer';
410
- footer.id = 'footer-' + pageIndex;
411
- page.appendChild(footer);
412
- output.appendChild(page);
413
- currentPageContent = content;
414
- return page;
415
- }
416
- createNewPage();
417
- const children = Array.from(source.children);
418
- for (const child of children) {
419
- if (child.className === 'meta-info') continue;
420
- currentPageContent.appendChild(child.cloneNode(true));
421
- if (currentPageContent.scrollHeight > currentPageContent.clientHeight) {
422
- currentPageContent.removeChild(currentPageContent.lastChild);
423
- pageIndex++;
424
- createNewPage();
425
- currentPageContent.appendChild(child.cloneNode(true));
426
- }
427
- }
428
- for(let i=1; i<=pageIndex; i++) document.getElementById('footer-'+i).innerText = \`- \${i} / \${pageIndex} -\`;
429
- return pageIndex;
430
- }
431
- </script>
438
+ <div class="fixed-header">
439
+ <span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span>
440
+ <span>${info.UserName}</span>
441
+ </div>
442
+
443
+ <div id="content-wrapper">
444
+ <div class="inner-content">
445
+ ${content}
446
+ </div>
447
+ </div>
448
+
449
+ <div class="fixed-footer" id="page-indicator">- 1 -</div>
432
450
  </body></html>`;
433
451
  const page = await ctx.puppeteer.page();
434
452
  try {
435
453
  await injectCookies(page);
436
454
  await page.setContent(html);
437
455
  await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
438
- await page.evaluate("paginate()");
456
+ const scrollWidth = await page.$eval("#content-wrapper", (el) => el.scrollWidth);
457
+ const totalPages = Math.ceil(scrollWidth / config.deviceWidth);
439
458
  const imgs = [];
440
- const pages = await page.$$(".page");
441
- for (const p of pages) {
442
- imgs.push(await p.screenshot({ type: "jpeg", quality: 80 }));
459
+ for (let i = 0; i < totalPages; i++) {
460
+ await page.evaluate((x) => {
461
+ document.getElementById("content-wrapper").scrollLeft = x;
462
+ }, i * config.deviceWidth);
463
+ await page.evaluate((curr, total) => {
464
+ document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
465
+ }, i + 1, totalPages);
466
+ const img = await page.screenshot({ type: "jpeg", quality: 100 });
467
+ imgs.push(img);
443
468
  }
444
469
  return imgs;
445
470
  } finally {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-fimtale-api",
3
3
  "description": "Koishi插件,从fimtale搜索/订阅/随机获取小说/解析链接等",
4
- "version": "0.0.7",
4
+ "version": "0.0.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [