koishi-plugin-cfmrmod 1.1.3 → 1.1.4
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.
- package/dist/mcmod/cards/mod-card.js +179 -109
- package/package.json +1 -1
|
@@ -12,27 +12,26 @@ async function drawModCard(url) {
|
|
|
12
12
|
const html = await (0, http_1.fetchMcmodText)(url, { headers: (0, http_1.getHeaders)(url) });
|
|
13
13
|
const $ = cheerio.load(html);
|
|
14
14
|
// --- 1. 数据抓取 (保持原逻辑,确保稳定性) ---
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
coverUrl = iconUrl;
|
|
27
|
-
if (!iconUrl && coverUrl)
|
|
28
|
-
iconUrl = coverUrl;
|
|
15
|
+
const titleRoot = $('.class-title').first();
|
|
16
|
+
const title = (0, utils_1.cleanText)(titleRoot.find('h1,h2,h3').first().text()) ||
|
|
17
|
+
(0, utils_1.cleanText)(($('meta[property="og:title"]').attr('content') || $('title').text()).split('-')[0]);
|
|
18
|
+
const subTitle = titleRoot.find('h4,small,.sub-title,.subtitle').map((_, el) => (0, utils_1.cleanText)($(el).text())).get()
|
|
19
|
+
.filter(text => text && text !== title)
|
|
20
|
+
.join(' ');
|
|
21
|
+
const coverNode = $('.class-cover-image img, .class-banner img').first()[0];
|
|
22
|
+
const iconNode = $('.class-icon img, .class-logo img, .class-cover-icon img').first()[0];
|
|
23
|
+
const hasDedicatedIcon = !!iconNode;
|
|
24
|
+
let coverUrl = (0, utils_1.fixUrl)((0, utils_1.extractImageUrl)(coverNode));
|
|
25
|
+
let iconUrl = (0, utils_1.fixUrl)((0, utils_1.extractImageUrl)(iconNode)) || coverUrl;
|
|
29
26
|
// 标签
|
|
30
27
|
const tags = [];
|
|
31
28
|
const officialTags = new Set();
|
|
32
|
-
|
|
29
|
+
const seenTags = new Set();
|
|
30
|
+
$('.class-title .class-status, .class-official-group div').each((i, el) => {
|
|
33
31
|
const txt = (0, utils_1.cleanText)($(el).text());
|
|
34
|
-
if (!txt || txt.length > 20)
|
|
32
|
+
if (!txt || txt.length > 20 || seenTags.has(txt))
|
|
35
33
|
return;
|
|
34
|
+
seenTags.add(txt);
|
|
36
35
|
officialTags.add(txt);
|
|
37
36
|
let color = '#999', bg = '#eee';
|
|
38
37
|
if (txt.includes('开源') || txt.includes('活跃') || txt.includes('稳定')) {
|
|
@@ -51,8 +50,9 @@ async function drawModCard(url) {
|
|
|
51
50
|
});
|
|
52
51
|
$('.class-label-list a').each((i, el) => {
|
|
53
52
|
const labelText = (0, utils_1.cleanText)($(el).text());
|
|
54
|
-
if (!labelText || officialTags.has(labelText))
|
|
53
|
+
if (!labelText || officialTags.has(labelText) || seenTags.has(labelText))
|
|
55
54
|
return;
|
|
55
|
+
seenTags.add(labelText);
|
|
56
56
|
const cls = $(el).attr('class') || '';
|
|
57
57
|
let bg = '#e3f2fd', c = '#3498db';
|
|
58
58
|
if (cls.includes('c_1')) {
|
|
@@ -112,10 +112,10 @@ async function drawModCard(url) {
|
|
|
112
112
|
const subNum = getSocialNum('subscribe');
|
|
113
113
|
// 作者
|
|
114
114
|
const authors = [];
|
|
115
|
-
$('.author-list li, .author li').each((i, el) => {
|
|
116
|
-
const n = (0, utils_1.cleanText)($(el).find('.name').text());
|
|
115
|
+
$('.author li, .author-list li, .class-author-list li, .common-class-author li').each((i, el) => {
|
|
116
|
+
const n = (0, utils_1.cleanText)($(el).find('.name a, .name, .member a').first().text()) || (0, utils_1.cleanText)($(el).attr('title'));
|
|
117
117
|
const r = (0, utils_1.cleanText)($(el).find('.position').text());
|
|
118
|
-
const iurl = (0, utils_1.fixUrl)($(el).find('img').
|
|
118
|
+
const iurl = (0, utils_1.fixUrl)((0, utils_1.extractImageUrl)($(el).find('.avatar img, img').first()[0]));
|
|
119
119
|
if (n)
|
|
120
120
|
authors.push({ n, r, i: iurl });
|
|
121
121
|
});
|
|
@@ -296,6 +296,29 @@ async function drawModCard(url) {
|
|
|
296
296
|
if (metaDesc)
|
|
297
297
|
descNodes.push({ type: 't', val: metaDesc, tag: 'p' });
|
|
298
298
|
}
|
|
299
|
+
const loadOptionalImage = async (src) => {
|
|
300
|
+
if (!src)
|
|
301
|
+
return null;
|
|
302
|
+
try {
|
|
303
|
+
return await (0, rendering_1.loadImageWithHeaders)(src, url, 18000);
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
try {
|
|
307
|
+
return await (0, rendering_1.loadImageWithHeaders)(src, constants_1.BASE_URL, 18000);
|
|
308
|
+
}
|
|
309
|
+
catch (e2) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const coverImg = await loadOptionalImage(coverUrl);
|
|
315
|
+
let iconImg = await loadOptionalImage(iconUrl);
|
|
316
|
+
if (!iconImg && iconUrl === coverUrl && coverImg)
|
|
317
|
+
iconImg = coverImg;
|
|
318
|
+
const showCover = !!coverImg && (hasDedicatedIcon || coverUrl !== iconUrl);
|
|
319
|
+
await Promise.all(authors.slice(0, 3).map(async (author) => {
|
|
320
|
+
author.imgCache = await loadOptionalImage(author.i);
|
|
321
|
+
}));
|
|
299
322
|
// --- 2. 布局计算 (macOS 风格) ---
|
|
300
323
|
const width = 800;
|
|
301
324
|
const font = rendering_1.GLOBAL_FONT_FAMILY;
|
|
@@ -307,17 +330,30 @@ async function drawModCard(url) {
|
|
|
307
330
|
const dummy = dummyC.getContext('2d');
|
|
308
331
|
dummy.font = `bold 32px "${font}"`;
|
|
309
332
|
// 头部区域 (Header)
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
333
|
+
const iconSize = 88;
|
|
334
|
+
const titleAreaW = contentW - iconSize - 24;
|
|
335
|
+
const titleLinesNum = (0, rendering_1.wrapText)(dummy, title, 0, 0, titleAreaW, 40, 10, false) / 40;
|
|
336
|
+
let headerH = Math.max(iconSize, titleLinesNum * 40 + (subTitle ? 26 : 0) + (authors.length ? 32 : 0) + 8);
|
|
313
337
|
// 标签区域
|
|
314
338
|
let tagsH = 0;
|
|
315
|
-
if (tags.length)
|
|
316
|
-
|
|
339
|
+
if (tags.length) {
|
|
340
|
+
dummy.font = `12px "${font}"`;
|
|
341
|
+
let rowW = 0;
|
|
342
|
+
let rows = 1;
|
|
343
|
+
for (const tag of tags) {
|
|
344
|
+
const tagW = dummy.measureText(tag.t).width + 20;
|
|
345
|
+
if (rowW && rowW + tagW > contentW) {
|
|
346
|
+
rows++;
|
|
347
|
+
rowW = 0;
|
|
348
|
+
}
|
|
349
|
+
rowW += tagW + 10;
|
|
350
|
+
}
|
|
351
|
+
tagsH = rows * 28;
|
|
352
|
+
}
|
|
317
353
|
// 封面图 (Cover)
|
|
318
354
|
let coverH = 0;
|
|
319
|
-
if (
|
|
320
|
-
coverH =
|
|
355
|
+
if (showCover)
|
|
356
|
+
coverH = 220;
|
|
321
357
|
// 统计数据 (Stats Grid)
|
|
322
358
|
// 布局:每行4个数据
|
|
323
359
|
const statsItems = [
|
|
@@ -351,8 +387,20 @@ async function drawModCard(url) {
|
|
|
351
387
|
extraH += lines * 20 + 10;
|
|
352
388
|
});
|
|
353
389
|
}
|
|
354
|
-
if (links.length)
|
|
355
|
-
|
|
390
|
+
if (links.length) {
|
|
391
|
+
dummy.font = `bold 12px "${font}"`;
|
|
392
|
+
let rowW = 0;
|
|
393
|
+
let rows = 1;
|
|
394
|
+
for (const link of links) {
|
|
395
|
+
const linkW = dummy.measureText(link).width + 20;
|
|
396
|
+
if (rowW && rowW + linkW > contentW) {
|
|
397
|
+
rows++;
|
|
398
|
+
rowW = 0;
|
|
399
|
+
}
|
|
400
|
+
rowW += linkW + 10;
|
|
401
|
+
}
|
|
402
|
+
extraH += rows * 30 + 12;
|
|
403
|
+
}
|
|
356
404
|
// 简介 (Desc)
|
|
357
405
|
let descH = 0;
|
|
358
406
|
dummy.font = `16px "${font}"`;
|
|
@@ -421,8 +469,8 @@ async function drawModCard(url) {
|
|
|
421
469
|
// 总高度
|
|
422
470
|
let cursorY = margin + 40; // Top traffic lights area
|
|
423
471
|
const components = [
|
|
424
|
-
{ h:
|
|
425
|
-
{ h:
|
|
472
|
+
{ h: headerH, gap: 16 },
|
|
473
|
+
{ h: tagsH, gap: 20 },
|
|
426
474
|
{ h: coverH, gap: 25 },
|
|
427
475
|
{ h: statsH, gap: 25 },
|
|
428
476
|
{ h: propsH, gap: 25 },
|
|
@@ -432,7 +480,7 @@ async function drawModCard(url) {
|
|
|
432
480
|
components.forEach(c => { if (c.h > 0)
|
|
433
481
|
cursorY += c.h + c.gap; });
|
|
434
482
|
const windowH = cursorY;
|
|
435
|
-
const totalH = windowH + margin * 2;
|
|
483
|
+
const totalH = windowH + margin * 2 + 24;
|
|
436
484
|
// --- 3. 开始绘制 ---
|
|
437
485
|
const canvas = (0, rendering_1.createCanvas)(width, totalH);
|
|
438
486
|
const ctx = canvas.getContext('2d');
|
|
@@ -479,49 +527,50 @@ async function drawModCard(url) {
|
|
|
479
527
|
// --- 内容绘制 ---
|
|
480
528
|
let dy = winY + 50;
|
|
481
529
|
const cx = winX + winPadding;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
ctx.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
530
|
+
const getInitials = (text) => {
|
|
531
|
+
var _a;
|
|
532
|
+
const value = (0, utils_1.cleanText)(text).replace(/^\[[^\]]+\]\s*/, '');
|
|
533
|
+
if (!value)
|
|
534
|
+
return 'MOD';
|
|
535
|
+
const ascii = (_a = value.match(/[A-Za-z0-9]+/g)) === null || _a === void 0 ? void 0 : _a.join('').slice(0, 2);
|
|
536
|
+
if (ascii)
|
|
537
|
+
return ascii.toUpperCase();
|
|
538
|
+
return Array.from(value).slice(0, 2).join('');
|
|
539
|
+
};
|
|
540
|
+
const drawImageCover = (img, x, y, w, h, radius) => {
|
|
541
|
+
const scale = Math.max(w / img.width, h / img.height);
|
|
542
|
+
ctx.save();
|
|
543
|
+
(0, rendering_1.roundRect)(ctx, x, y, w, h, radius);
|
|
544
|
+
ctx.clip();
|
|
545
|
+
ctx.drawImage(img, x + (w - img.width * scale) / 2, y + (h - img.height * scale) / 2, img.width * scale, img.height * scale);
|
|
546
|
+
ctx.restore();
|
|
547
|
+
};
|
|
548
|
+
const drawInitialTile = (x, y, w, h, label, radius = 12) => {
|
|
549
|
+
const grad = ctx.createLinearGradient(x, y, x + w, y + h);
|
|
550
|
+
grad.addColorStop(0, '#4b5563');
|
|
551
|
+
grad.addColorStop(1, '#111827');
|
|
552
|
+
ctx.fillStyle = grad;
|
|
553
|
+
(0, rendering_1.roundRect)(ctx, x, y, w, h, radius);
|
|
554
|
+
ctx.fill();
|
|
555
|
+
ctx.fillStyle = '#fff';
|
|
556
|
+
ctx.font = `bold ${Math.max(18, Math.floor(w * 0.32))}px "${font}"`;
|
|
557
|
+
ctx.textAlign = 'center';
|
|
558
|
+
ctx.textBaseline = 'middle';
|
|
559
|
+
ctx.fillText(getInitials(label), x + w / 2, y + h / 2);
|
|
560
|
+
ctx.textAlign = 'left';
|
|
561
|
+
ctx.textBaseline = 'alphabetic';
|
|
562
|
+
};
|
|
563
|
+
// 1. Header
|
|
564
|
+
if (iconImg)
|
|
565
|
+
drawImageCover(iconImg, cx, dy, iconSize, iconSize, 12);
|
|
566
|
+
else
|
|
567
|
+
drawInitialTile(cx, dy, iconSize, iconSize, title, 12);
|
|
519
568
|
// Title
|
|
520
|
-
const titleX = cx + iconSize +
|
|
569
|
+
const titleX = cx + iconSize + 24;
|
|
521
570
|
ctx.fillStyle = '#333';
|
|
522
571
|
ctx.font = `bold 32px "${font}"`;
|
|
523
572
|
ctx.textBaseline = 'top';
|
|
524
|
-
const titleDrawnH = (0, rendering_1.wrapText)(ctx, title, titleX, dy -
|
|
573
|
+
const titleDrawnH = (0, rendering_1.wrapText)(ctx, title, titleX, dy - 4, titleAreaW, 40, 3, true);
|
|
525
574
|
// SubTitle
|
|
526
575
|
let subY = titleDrawnH + 5;
|
|
527
576
|
if (subTitle) {
|
|
@@ -538,44 +587,60 @@ async function drawModCard(url) {
|
|
|
538
587
|
ctx.beginPath();
|
|
539
588
|
ctx.arc(ax + 12, subY + 12, 12, 0, Math.PI * 2);
|
|
540
589
|
ctx.clip();
|
|
541
|
-
if (a.
|
|
542
|
-
|
|
543
|
-
const img = await (0, rendering_1.loadImageWithHeaders)(a.i, constants_1.BASE_URL);
|
|
544
|
-
ctx.drawImage(img, ax, subY, 24, 24);
|
|
545
|
-
}
|
|
546
|
-
catch (e) {
|
|
547
|
-
ctx.fillStyle = '#ccc';
|
|
548
|
-
ctx.fill();
|
|
549
|
-
}
|
|
550
|
-
}
|
|
590
|
+
if (a.imgCache)
|
|
591
|
+
ctx.drawImage(a.imgCache, ax, subY, 24, 24);
|
|
551
592
|
else {
|
|
552
|
-
ctx.fillStyle = '#
|
|
553
|
-
ctx.
|
|
593
|
+
ctx.fillStyle = '#d1d5db';
|
|
594
|
+
ctx.fillRect(ax, subY, 24, 24);
|
|
554
595
|
}
|
|
555
596
|
ctx.restore();
|
|
597
|
+
if (!a.imgCache) {
|
|
598
|
+
ctx.fillStyle = '#6b7280';
|
|
599
|
+
ctx.font = `bold 11px "${font}"`;
|
|
600
|
+
ctx.textAlign = 'center';
|
|
601
|
+
ctx.textBaseline = 'middle';
|
|
602
|
+
ctx.fillText(getInitials(a.n).slice(0, 1), ax + 12, subY + 12);
|
|
603
|
+
ctx.textAlign = 'left';
|
|
604
|
+
ctx.textBaseline = 'alphabetic';
|
|
605
|
+
}
|
|
556
606
|
ctx.fillStyle = '#666';
|
|
557
607
|
ctx.font = `14px "${font}"`;
|
|
558
|
-
|
|
559
|
-
|
|
608
|
+
let name = a.n;
|
|
609
|
+
while (ctx.measureText(name).width > 130 && name.length > 2)
|
|
610
|
+
name = `${name.slice(0, -2)}...`;
|
|
611
|
+
if (ax + 30 + ctx.measureText(name).width > cx + contentW)
|
|
612
|
+
break;
|
|
613
|
+
ctx.fillText(name, ax + 30, subY + 5);
|
|
614
|
+
ax += ctx.measureText(name).width + 45;
|
|
560
615
|
}
|
|
561
616
|
}
|
|
562
|
-
dy +=
|
|
617
|
+
dy += headerH + 16;
|
|
618
|
+
// 2. Tags
|
|
619
|
+
if (tags.length) {
|
|
620
|
+
let tx = cx;
|
|
621
|
+
let ty = dy;
|
|
622
|
+
ctx.textBaseline = 'middle';
|
|
623
|
+
tags.forEach(t => {
|
|
624
|
+
ctx.font = `12px "${font}"`;
|
|
625
|
+
const tw = ctx.measureText(t.t).width + 20;
|
|
626
|
+
if (tx !== cx && tx + tw > cx + contentW) {
|
|
627
|
+
tx = cx;
|
|
628
|
+
ty += 28;
|
|
629
|
+
}
|
|
630
|
+
ctx.fillStyle = t.bg;
|
|
631
|
+
(0, rendering_1.roundRect)(ctx, tx, ty, tw, 24, 6);
|
|
632
|
+
ctx.fill();
|
|
633
|
+
ctx.fillStyle = t.c;
|
|
634
|
+
ctx.fillText(t.t, tx + 10, ty + 12);
|
|
635
|
+
tx += tw + 10;
|
|
636
|
+
});
|
|
637
|
+
ctx.textBaseline = 'alphabetic';
|
|
638
|
+
dy += tagsH + 20;
|
|
639
|
+
}
|
|
563
640
|
// 3. Cover Image
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const coverW = contentW;
|
|
568
|
-
const coverH_Actual = 280;
|
|
569
|
-
// Crop fit
|
|
570
|
-
const r = Math.max(coverW / img.width, coverH_Actual / img.height);
|
|
571
|
-
ctx.save();
|
|
572
|
-
(0, rendering_1.roundRect)(ctx, cx, dy, coverW, coverH_Actual, 12);
|
|
573
|
-
ctx.clip();
|
|
574
|
-
ctx.drawImage(img, (coverW - img.width * r) / 2 + cx, (coverH_Actual - img.height * r) / 2 + dy, img.width * r, img.height * r);
|
|
575
|
-
ctx.restore();
|
|
576
|
-
dy += coverH_Actual + 25;
|
|
577
|
-
}
|
|
578
|
-
catch (e) { }
|
|
641
|
+
if (showCover) {
|
|
642
|
+
drawImageCover(coverImg, cx, dy, contentW, coverH, 12);
|
|
643
|
+
dy += coverH + 25;
|
|
579
644
|
}
|
|
580
645
|
// 4. Stats Grid
|
|
581
646
|
if (statsItems.length) {
|
|
@@ -644,19 +709,24 @@ async function drawModCard(url) {
|
|
|
644
709
|
}
|
|
645
710
|
if (links.length) {
|
|
646
711
|
let lx = cx;
|
|
712
|
+
let ly = dy;
|
|
713
|
+
ctx.textBaseline = 'middle';
|
|
647
714
|
links.forEach(l => {
|
|
648
715
|
ctx.font = `bold 12px "${font}"`;
|
|
649
716
|
const w = ctx.measureText(l).width + 20;
|
|
650
|
-
if (lx + w
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
ctx.fill();
|
|
654
|
-
ctx.fillStyle = '#fff';
|
|
655
|
-
ctx.fillText(l, lx + 10, dy + 6);
|
|
656
|
-
lx += w + 10;
|
|
717
|
+
if (lx !== cx && lx + w > cx + contentW) {
|
|
718
|
+
lx = cx;
|
|
719
|
+
ly += 30;
|
|
657
720
|
}
|
|
721
|
+
ctx.fillStyle = '#333';
|
|
722
|
+
(0, rendering_1.roundRect)(ctx, lx, ly, w, 24, 12);
|
|
723
|
+
ctx.fill();
|
|
724
|
+
ctx.fillStyle = '#fff';
|
|
725
|
+
ctx.fillText(l, lx + 10, ly + 12);
|
|
726
|
+
lx += w + 10;
|
|
658
727
|
});
|
|
659
|
-
|
|
728
|
+
ctx.textBaseline = 'alphabetic';
|
|
729
|
+
dy += Math.ceil((ly - dy + 24) / 30) * 30 + 12;
|
|
660
730
|
}
|
|
661
731
|
// 7. Description
|
|
662
732
|
if (descNodes.length) {
|