koishi-plugin-fimtale-api 0.0.8 → 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 +101 -94
  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 Pro 比例)
50
+ // 渲染配置 (iPhone 12/13/14 标准)
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)")
@@ -85,7 +85,8 @@ function apply(ctx, config) {
85
85
  if (!html) return "";
86
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, "");
87
87
  }, "cleanContent");
88
- 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';
89
90
  const injectCookies = /* @__PURE__ */ __name(async (page) => {
90
91
  if (!config.cookies) return;
91
92
  const cookies = config.cookies.split(";").map((pair) => {
@@ -234,12 +235,19 @@ function apply(ctx, config) {
234
235
  <head>
235
236
  <style>
236
237
  body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
237
- .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; }
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; }
238
239
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
239
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; }
240
- .info { flex: 1; padding: 24px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; }
241
241
 
242
- .header-group { display: flex; flex-direction: column; }
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; }
250
+
243
251
  .title {
244
252
  font-size: 22px; font-weight: 700; color: #333; line-height: 1.35;
245
253
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
@@ -251,20 +259,29 @@ function apply(ctx, config) {
251
259
  }
252
260
  .author { font-size: 13px; color: #888; margin-top: 8px; font-weight: 400; }
253
261
 
254
- .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
+ }
255
267
  .tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
256
268
  .tag-imp { background: #e3f2fd; color: #1565c0; }
257
269
 
270
+ /* 简介占据剩余空间 */
258
271
  .summary-box {
259
- flex: 1; overflow: hidden; position: relative; margin-top: 8px;
272
+ flex: 1; position: relative; margin-top: 4px; overflow: hidden;
260
273
  }
261
274
  .summary {
262
275
  font-size: 13px; color: #666; line-height: 1.6;
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>
@@ -277,8 +294,13 @@ function apply(ctx, config) {
277
294
  ${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}
278
295
  <div class="author">@${info.UserName}</div>
279
296
  </div>
297
+
280
298
  <div class="tags">${displayTags.map((t) => `<span class="tag ${["文", "译", "R"].includes(t) ? "tag-imp" : ""}">${t}</span>`).join("")}</div>
281
- <div class="summary-box"><div class="summary">${summary}</div></div>
299
+
300
+ <div class="summary-box">
301
+ <div class="summary">${summary}</div>
302
+ </div>
303
+
282
304
  <div class="footer">
283
305
  <span class="stat"><b style="color:#009688">热度</b>${views}</span>
284
306
  <span class="stat"><b style="color:#673ab7">评论</b>${comments}</span>
@@ -338,7 +360,7 @@ function apply(ctx, config) {
338
360
  <div class="top-row"><div class="title">${r.title}</div><div class="id-badge">ID: ${r.id}</div></div>
339
361
  <div class="author">By ${r.author} ${r.status ? ` · ${r.status}` : ""}</div>
340
362
  <div class="tags">${r.tags.map((t) => `<span class="tag">${t}</span>`).join("")}</div>
341
- <div class="meta-row">${stats || "No Data"}</div>
363
+ <div class="meta-row">${stats || "暂无数据"}</div>
342
364
  </div></div>`;
343
365
  }).join("")}
344
366
  </div>
@@ -360,104 +382,89 @@ function apply(ctx, config) {
360
382
  <html>
361
383
  <head>
362
384
  <style>
363
- body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontStack}; }
364
- #source-container { display: none; }
385
+ body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; }
365
386
 
366
- /* 使用 Flex 布局,让页脚自然占据底部空间,不覆盖正文 */
367
- .page {
368
- width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
369
- padding: 35px 28px; box-sizing: border-box;
370
- position: relative; background: #f6f4ec; overflow: hidden;
371
- display: flex; flex-direction: column;
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;
372
398
  }
373
-
374
- .page-header {
375
- font-size: 12px; color: #8d6e63; border-bottom: 2px solid #d7ccc8;
376
- padding-bottom: 12px; margin-bottom: 15px; flex-shrink: 0;
377
- display: flex; justify-content: space-between; font-weight: bold;
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;
378
411
  }
379
-
380
- /* 页脚改为相对定位,并给予上边距 */
381
- .page-footer {
382
- text-align: center; font-size: 12px; color: #aaa; font-family: sans-serif;
383
- flex-shrink: 0; padding-top: 10px; margin-top: auto;
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};
384
422
  }
385
-
386
- .page-content {
387
- flex: 1; /* 占据剩余所有空间 */
388
- overflow: hidden; /* 隐藏溢出部分 */
389
- font-size: ${config.fontSize}px;
390
- line-height: 1.7;
391
- text-align: justify;
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};
392
429
  }
393
-
394
- p { margin: 0 0 0.6em 0; text-indent: 2em; }
395
- 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); }
396
- 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; }
397
435
  </style>
398
436
  </head>
399
437
  <body>
400
- <div id="source-container"><div class="meta-info" data-title="${info.Title}" data-author="${info.UserName}"></div>${content}</div>
401
- <div id="output"></div>
402
- <script>
403
- function paginate() {
404
- const source = document.getElementById('source-container');
405
- const output = document.getElementById('output');
406
- const title = source.querySelector('.meta-info').dataset.title;
407
- const author = source.querySelector('.meta-info').dataset.author;
408
- let pageIndex = 1;
409
- let currentPageContent = null;
410
-
411
- function createNewPage() {
412
- const page = document.createElement('div'); page.className = 'page';
413
- const header = document.createElement('div'); header.className = 'page-header';
414
- header.innerHTML = \`<span>\${title.substring(0, 12) + (title.length>12?'...':'')}</span><span>\${author}</span>\`;
415
- page.appendChild(header);
416
-
417
- const content = document.createElement('div'); content.className = 'page-content';
418
- page.appendChild(content);
419
-
420
- const footer = document.createElement('div'); footer.className = 'page-footer';
421
- footer.id = 'footer-' + pageIndex;
422
- page.appendChild(footer);
423
-
424
- output.appendChild(page);
425
- currentPageContent = content;
426
- return page;
427
- }
428
-
429
- createNewPage();
430
- const children = Array.from(source.children);
431
-
432
- for (const child of children) {
433
- if (child.className === 'meta-info') continue;
434
-
435
- currentPageContent.appendChild(child.cloneNode(true));
436
-
437
- // 核心修复:增加 10px 的安全缓冲,防止 descenders 被切
438
- if (currentPageContent.scrollHeight > currentPageContent.clientHeight - 10) {
439
- currentPageContent.removeChild(currentPageContent.lastChild);
440
- pageIndex++;
441
- createNewPage();
442
- currentPageContent.appendChild(child.cloneNode(true));
443
- }
444
- }
445
-
446
- for(let i=1; i<=pageIndex; i++) document.getElementById('footer-'+i).innerText = \`- \${i} / \${pageIndex} -\`;
447
- return pageIndex;
448
- }
449
- </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>
450
450
  </body></html>`;
451
451
  const page = await ctx.puppeteer.page();
452
452
  try {
453
453
  await injectCookies(page);
454
454
  await page.setContent(html);
455
455
  await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
456
- await page.evaluate("paginate()");
456
+ const scrollWidth = await page.$eval("#content-wrapper", (el) => el.scrollWidth);
457
+ const totalPages = Math.ceil(scrollWidth / config.deviceWidth);
457
458
  const imgs = [];
458
- const pages = await page.$$(".page");
459
- for (const p of pages) {
460
- 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);
461
468
  }
462
469
  return imgs;
463
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.8",
4
+ "version": "0.0.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [