koishi-plugin-fimtale-api 0.0.95 → 0.0.98

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 +104 -88
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -47,7 +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 标准)
50
+ // 渲染配置 (标准手机比例)
51
51
  deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
52
52
  deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
53
53
  fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)")
@@ -61,11 +61,6 @@ function apply(ctx, config) {
61
61
  lastCheck: "integer"
62
62
  }, { primary: "id", autoInc: true });
63
63
  const sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => setTimeout(resolve, ms)), "sleep");
64
- const formatDate = /* @__PURE__ */ __name((timestamp) => {
65
- if (!timestamp) return "Unknown";
66
- const date = new Date(timestamp * 1e3);
67
- return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
68
- }, "formatDate");
69
64
  const stripHtml = /* @__PURE__ */ __name((html) => {
70
65
  if (!html) return "";
71
66
  return html.replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
@@ -85,7 +80,7 @@ function apply(ctx, config) {
85
80
  if (!html) return "";
86
81
  return html.replace(/style="[^"]*"/gi, "").replace(/<p[^>]*>\s*(&nbsp;|<br\s*\/?>|\s)*\s*<\/p>/gi, "").replace(/(<br\s*\/?>\s*){2,}/gi, "<br>").trim();
87
82
  }, "cleanContent");
88
- const fontStack = '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", "WenQuanYi Micro Hei", Arial, sans-serif';
83
+ const fontStack = '"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif';
89
84
  const fontSerif = '"Noto Serif SC", "Source Han Serif SC", "SimSun", serif';
90
85
  const injectCookies = /* @__PURE__ */ __name(async (page) => {
91
86
  if (!config.cookies) return;
@@ -101,7 +96,7 @@ function apply(ctx, config) {
101
96
  const url = `${config.apiUrl}/t/${threadId}`;
102
97
  const params = { APIKey: config.apiKey, APIPass: config.apiPass };
103
98
  const res = await ctx.http.get(url, { params });
104
- if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API 返回错误" };
99
+ if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API Error" };
105
100
  return {
106
101
  valid: true,
107
102
  data: res.TopicInfo,
@@ -109,7 +104,7 @@ function apply(ctx, config) {
109
104
  menu: res.Menu || []
110
105
  };
111
106
  } catch (e) {
112
- return { valid: false, msg: "网络请求失败" };
107
+ return { valid: false, msg: "Request Failed" };
113
108
  }
114
109
  }, "fetchThread");
115
110
  const fetchRandomId = /* @__PURE__ */ __name(async () => {
@@ -149,7 +144,7 @@ function apply(ctx, config) {
149
144
  const titleEl = card.querySelector(".card-title");
150
145
  if (titleEl) title = titleEl.textContent?.trim() || "";
151
146
  else title = link.textContent?.trim() || "";
152
- let author = "未知";
147
+ let author = "Unknown";
153
148
  const authorEl = card.querySelector('a[href^="/u/"] span.grey-text');
154
149
  if (authorEl) author = authorEl.textContent?.trim() || "";
155
150
  let cover = void 0;
@@ -220,7 +215,7 @@ function apply(ctx, config) {
220
215
  const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
221
216
  let summary = stripHtml(info.Content);
222
217
  if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
223
- if (summary.length > 110) summary = summary.substring(0, 110) + "...";
218
+ if (summary.length > 150) summary = summary.substring(0, 150) + "...";
224
219
  if (!summary) summary = "暂无简介";
225
220
  const tagsArr = [];
226
221
  if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
@@ -236,39 +231,54 @@ function apply(ctx, config) {
236
231
  <style>
237
232
  body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
238
233
  .card { width: 620px; height: 360px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
234
+
239
235
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
240
236
  .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; }
241
- .info { flex: 1; padding: 24px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; }
242
237
 
243
- .header-group { display: flex; flex-direction: column; flex-shrink: 0; }
238
+ .info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
239
+
240
+ .header-group { flex-shrink: 0; margin-bottom: 10px; }
244
241
  .title {
245
- font-size: 22px; font-weight: 700; color: #333; line-height: 1.35;
242
+ font-size: 22px; font-weight: 700; color: #333; line-height: 1.4;
246
243
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
244
+ margin-bottom: 4px;
247
245
  }
248
246
  .subtitle {
249
- font-size: 15px; color: #555; margin-top: 6px; font-weight: 500;
247
+ font-size: 15px; color: #555; font-weight: 500;
250
248
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
251
249
  padding-left: 10px; border-left: 3px solid #e91e63;
250
+ margin-top: 4px;
252
251
  }
253
- .author { font-size: 13px; color: #888; margin-top: 8px; font-weight: 400; }
252
+ .author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
254
253
 
255
- .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; max-height: 56px; overflow: hidden; flex-shrink: 0;}
254
+ .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
256
255
  .tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
257
256
  .tag-imp { background: #e3f2fd; color: #1565c0; }
258
257
 
259
- /* 简介区域防截断优化 */
258
+ /* 修复:使用 Flex 自动填充剩余空间,并增加底部 padding 防止截断 */
260
259
  .summary-box {
261
- flex: 1; position: relative; margin-top: 4px; overflow: hidden;
260
+ flex: 1;
261
+ position: relative;
262
+ overflow: hidden;
263
+ min-height: 0;
262
264
  }
263
265
  .summary {
264
- font-size: 13px; color: #666;
265
- line-height: 1.5; /* 设定标准行高 */
266
- max-height: 6em; /* 4行 = 1.5 * 4 */
267
- display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
268
- padding-bottom: 2px; /* 底部缓冲 */
266
+ font-size: 13px; color: #666; line-height: 1.6;
267
+ display: -webkit-box;
268
+ -webkit-line-clamp: 4;
269
+ -webkit-box-orient: vertical;
270
+ overflow: hidden;
271
+ padding-bottom: 3px; /* 关键修复:防止下行字母(如g,j,p,q,y)被切掉 */
269
272
  }
270
273
 
271
- .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;}
274
+ .footer {
275
+ border-top: 1px solid #eee;
276
+ padding-top: 14px;
277
+ display: flex; justify-content: space-between;
278
+ font-size: 12px; color: #888;
279
+ margin-top: auto; /* 确保页脚始终在底部 */
280
+ flex-shrink: 0;
281
+ }
272
282
  .stat b { color: #555; font-weight: bold; margin-right: 2px;}
273
283
  </style>
274
284
  </head>
@@ -281,13 +291,8 @@ function apply(ctx, config) {
281
291
  ${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}
282
292
  <div class="author">@${info.UserName}</div>
283
293
  </div>
284
-
285
294
  <div class="tags">${displayTags.map((t) => `<span class="tag ${["文", "译", "R"].includes(t) ? "tag-imp" : ""}">${t}</span>`).join("")}</div>
286
-
287
- <div class="summary-box">
288
- <div class="summary">${summary}</div>
289
- </div>
290
-
295
+ <div class="summary-box"><div class="summary">${summary}</div></div>
291
296
  <div class="footer">
292
297
  <span class="stat"><b style="color:#009688">热度</b>${views}</span>
293
298
  <span class="stat"><b style="color:#673ab7">评论</b>${comments}</span>
@@ -364,76 +369,87 @@ function apply(ctx, config) {
364
369
  }, "renderSearchResults");
365
370
  const renderReadPages = /* @__PURE__ */ __name(async (info) => {
366
371
  const content = cleanContent(info.Content);
372
+ const headerHeight = 40;
373
+ const footerHeight = 30;
374
+ const paddingX = 25;
375
+ const paddingY = 20;
376
+ const lineHeightRatio = 1.8;
377
+ const contentWidth = config.deviceWidth - paddingX * 2;
378
+ const columnGap = 40;
379
+ const maxContentHeight = config.deviceHeight - headerHeight - footerHeight;
380
+ const lineHeightPx = config.fontSize * lineHeightRatio;
381
+ const linesPerPage = Math.floor((maxContentHeight - paddingY * 2) / lineHeightPx);
382
+ const optimalContentHeight = linesPerPage * lineHeightPx + paddingY * 2;
383
+ const marginTop = Math.floor((maxContentHeight - optimalContentHeight) / 2) + headerHeight;
367
384
  const html = `
368
385
  <!DOCTYPE html>
369
386
  <html>
370
387
  <head>
371
388
  <style>
372
- body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; }
373
- #source-container { display: none; }
374
- .page {
375
- width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
376
- /* 增加 padding,减少每页内容量,看起来更舒适 */
377
- padding: 40px 30px; box-sizing: border-box;
378
- position: relative; background: #f6f4ec; overflow: hidden;
379
- /* 关键:CSS多列布局,无需手动分页 */
380
- display: flex; flex-direction: column;
389
+ body { margin: 0; padding: 0; width: ${config.deviceWidth}px; height: ${config.deviceHeight}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; overflow: hidden; position: relative;}
390
+
391
+ .fixed-header {
392
+ position: absolute; top: 0; left: 0; width: 100%; height: ${headerHeight}px;
393
+ border-bottom: 1px solid #d7ccc8; box-sizing: border-box;
394
+ padding: 0 20px; display: flex; align-items: center; justify-content: space-between;
395
+ font-size: 12px; color: #8d6e63; background: #f6f4ec; z-index: 5; font-weight: bold;
381
396
  }
382
397
 
383
- /* 自定义 CSS Column 容器 */
384
- #content-wrapper {
385
- width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
386
- column-width: ${config.deviceWidth}px; column-gap: 0; column-fill: auto;
387
- overflow-x: hidden; overflow-y: hidden; /* 隐藏滚动条 */
398
+ .fixed-footer {
399
+ position: absolute; bottom: 0; left: 0; width: 100%; height: ${footerHeight}px;
400
+ display: flex; align-items: center; justify-content: center;
401
+ font-size: 12px; color: #aaa; background: #f6f4ec; z-index: 5;
388
402
  }
389
403
 
390
- /* 实际内容区 */
391
- .inner-content {
392
- padding: 50px 25px 40px 25px; /* 避让页眉页脚 */
393
- box-sizing: border-box;
394
- height: 100%;
395
- font-size: ${config.fontSize}px;
396
- line-height: 1.8;
397
- text-align: justify;
404
+ /* 视口容器:用于隐藏溢出的页面 */
405
+ #viewport {
406
+ position: absolute;
407
+ top: ${marginTop}px;
408
+ left: ${paddingX}px; /* 左边距 */
409
+ width: ${contentWidth}px; /* 限制视口只显示一页内容的宽度 */
410
+ height: ${optimalContentHeight}px;
411
+ overflow: hidden; /* 关键:隐藏其他页 */
398
412
  }
399
413
 
400
- /* 固定页眉页脚 (Overlay) */
401
- .fixed-header {
402
- position: absolute; top: 0; left: 0; width: 100%; height: 40px;
403
- background: #f6f4ec; z-index: 10;
404
- border-bottom: 1px solid #d7ccc8;
405
- display: flex; align-items: center; justify-content: space-between;
406
- padding: 0 25px; box-sizing: border-box;
407
- font-size: 12px; color: #8d6e63; font-weight: bold; font-family: ${fontStack};
408
- }
409
- .fixed-footer {
410
- position: absolute; bottom: 0; left: 0; width: 100%; height: 30px;
411
- background: #f6f4ec; z-index: 10;
412
- display: flex; align-items: center; justify-content: center;
413
- font-size: 12px; color: #aaa; font-family: ${fontStack};
414
+ /* 内容长条:实际上是超宽的 */
415
+ #content-scroller {
416
+ height: 100%;
417
+ width: 100%; /* 这里的 100% 是相对于 viewport 的 contentWidth */
418
+
419
+ /* CSS Columns 布局 */
420
+ column-width: ${contentWidth}px;
421
+ column-gap: ${columnGap}px;
422
+ column-fill: auto;
423
+
424
+ padding: ${paddingY}px 0; /* 左右不需要 padding,因为 viewport 已经定了位置 */
425
+ box-sizing: border-box;
426
+
427
+ font-size: ${config.fontSize}px;
428
+ line-height: ${lineHeightRatio};
429
+ text-align: justify;
430
+
431
+ /* 关键:使用 transform 移动,初始位置为 0 */
432
+ transform: translateX(0);
433
+ transition: none;
414
434
  }
415
-
416
- /* 排版细节 */
435
+
417
436
  p { margin: 0 0 0.8em 0; text-indent: 2em; }
418
- 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; }
419
- h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; }
437
+ img { max-width: 100%; height: auto; display: block; margin: 10px auto; border-radius: 6px; }
438
+ h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; break-inside: avoid; }
420
439
  </style>
421
440
  </head>
422
441
  <body>
423
- <div id="source-container"></div>
424
-
425
- <!-- 使用 CSS Columns 进行分页布局 -->
426
- <div id="content-wrapper">
427
- <div class="inner-content">
428
- ${content}
429
- </div>
430
- </div>
431
-
432
- <!-- 覆盖层:页眉页脚,截图时通过 JS 更新 -->
433
442
  <div class="fixed-header">
434
443
  <span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span>
435
444
  <span>${info.UserName}</span>
436
445
  </div>
446
+
447
+ <div id="viewport">
448
+ <div id="content-scroller">
449
+ ${content}
450
+ </div>
451
+ </div>
452
+
437
453
  <div class="fixed-footer" id="page-indicator">- 1 -</div>
438
454
  </body></html>`;
439
455
  const page = await ctx.puppeteer.page();
@@ -441,16 +457,16 @@ function apply(ctx, config) {
441
457
  await injectCookies(page);
442
458
  await page.setContent(html);
443
459
  await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
444
- const scrollWidth = await page.$eval("#content-wrapper", (el) => el.scrollWidth);
445
- const totalPages = Math.ceil(scrollWidth / config.deviceWidth);
460
+ const scrollWidth = await page.$eval("#content-scroller", (el) => el.scrollWidth);
461
+ const step = contentWidth + columnGap;
462
+ const totalPages = Math.ceil((scrollWidth + columnGap) / step) || 1;
446
463
  const imgs = [];
447
464
  for (let i = 0; i < totalPages; i++) {
448
- await page.evaluate((x) => {
449
- document.getElementById("content-wrapper").scrollTo(x, 0);
450
- }, i * config.deviceWidth);
451
- await page.evaluate((curr, total) => {
465
+ await page.evaluate((idx, stepPx, curr, total) => {
466
+ const offset = -(idx * stepPx);
467
+ document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
452
468
  document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
453
- }, i + 1, totalPages);
469
+ }, i, step, i + 1, totalPages);
454
470
  const img = await page.screenshot({ type: "jpeg", quality: 80 });
455
471
  imgs.push(img);
456
472
  }
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.95",
4
+ "version": "0.0.98",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [