dsh-niuma 0.2.2 → 0.2.3

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.
@@ -126,6 +126,10 @@ const zh = {
126
126
  'decimalsLabel': '小数',
127
127
  'decimalsUnit': '位',
128
128
  'turboLabel': '加速',
129
+ 'updateAvailable': '发现新版本 v{version}',
130
+ 'updateNow': '立即更新',
131
+ 'updating': '正在下载新版本…',
132
+ 'updateStaged': 'v{version} 已下载 · 重启 DSH 生效',
129
133
  'refreshRate': '刷新频率',
130
134
  'rateSecond': '按秒(1s)',
131
135
  'rateFrame': '刷新率(≈16ms)',
@@ -232,6 +236,10 @@ const en = {
232
236
  'decimalsLabel': 'Decimals',
233
237
  'decimalsUnit': 'dp',
234
238
  'turboLabel': 'Turbo',
239
+ 'updateAvailable': 'New version v{version}',
240
+ 'updateNow': 'Update now',
241
+ 'updating': 'Downloading update…',
242
+ 'updateStaged': 'v{version} downloaded · restart DSH to apply',
235
243
  'refreshRate': 'Refresh rate',
236
244
  'rateSecond': 'Per second (1s)',
237
245
  'rateFrame': 'Display rate (~16ms)',
@@ -385,6 +393,43 @@ function useRemoteConfig() {
385
393
  return { ...state, refresh };
386
394
  }
387
395
 
396
+ /** 版本 / 自动更新状态:挂载时查一次、每 6 小时刷新;点「立即更新」触发下载暂存。 */
397
+ function useUpdateInfo() {
398
+ const [state, setState] = React.useState({ status: 'loading', data: null, error: null });
399
+ const load = React.useCallback(() => {
400
+ if (typeof fetch !== 'function') {
401
+ setState({ status: 'failed', data: null, error: 'fetch unavailable' });
402
+ return;
403
+ }
404
+ fetch(`${ROUTE_BASE}/update`, { cache: 'no-store', credentials: 'same-origin' })
405
+ .then(async (response) => {
406
+ const body = await response.json();
407
+ if (!response.ok || !body || body.ok !== true) throw new Error((body && body.error) || `HTTP ${response.status}`);
408
+ return body.data || null;
409
+ })
410
+ .then((data) => setState({ status: 'ready', data, error: null }))
411
+ .catch((error) => setState({ status: 'failed', data: null, error: String((error && error.message) || error) }));
412
+ }, []);
413
+ React.useEffect(() => {
414
+ load();
415
+ const timer = setInterval(load, 6 * 3600 * 1000);
416
+ return () => clearInterval(timer);
417
+ }, [load]);
418
+ const download = React.useCallback(() => {
419
+ setState((prev) => ({ ...prev, status: 'updating' }));
420
+ if (typeof fetch !== 'function') return;
421
+ fetch(`${ROUTE_BASE}/update`, { method: 'POST', credentials: 'same-origin' })
422
+ .then(async (response) => {
423
+ const body = await response.json();
424
+ if (!response.ok || !body || body.ok !== true) throw new Error((body && body.error) || `HTTP ${response.status}`);
425
+ return body.data || null;
426
+ })
427
+ .then((data) => setState({ status: 'ready', data, error: null }))
428
+ .catch((error) => setState({ status: 'failed', data: null, error: String((error && error.message) || error) }));
429
+ }, []);
430
+ return { ...state, reload: load, download };
431
+ }
432
+
388
433
  /**
389
434
  * 当前时间:按配置的刷新间隔更新。
390
435
  * - updateMs >= 50:setInterval(按秒 / 200ms / 100ms)
@@ -493,92 +538,18 @@ function SectionTitle(props) {
493
538
 
494
539
  /** 今日已赚主卡:渐变底 + 超大金额 + 进度条(视觉焦点只有这一处)。 */
495
540
  /* ------------------------------------------------------------------ *
496
- * 滚动金额:连续的「机械里程表」
497
- * 每一位不是等金额变了才跳一格,而是按位权连续转动:
498
- * 最后一位小数转得飞快(0-9 循环),前一位慢 10 倍,整数位几乎不动。
499
- * 加速模式下每帧(rAF)更新 wheel 位置,看起来就是数字在持续往上滚。
541
+ * 主金额:毫秒级刷新时数字本身高速变化(末位小数在 60fps 下每帧都在跳),
542
+ * 视觉上就是「欻欻」往上滚;不做逐位轮盘,避免滚动裁切带来的重影。
500
543
  * ------------------------------------------------------------------ */
501
- const ROLL_BLOCKS = Array.from({ length: 11 }, (_, i) => String(i % 10));
502
-
503
- /** 单个数字轮:`position` 是连续位置(单位=1 个数字格,小数部分=滚到一半)。 */
504
- function RollingDigit(props) {
505
- const heightEm = 1.22;
506
- const widthEm = 0.62;
507
- const pos = Number.isFinite(props.position) ? props.position : 0;
508
- const wheel = ((pos % 10) + 10) % 10;
509
- return React.createElement('span', {
510
- 'aria-hidden': 'true',
511
- style: {
512
- display: 'inline-block', position: 'relative', overflow: 'hidden',
513
- width: `${widthEm}em`, height: `${heightEm}em`, verticalAlign: 'baseline',
514
- },
515
- }, [
516
- React.createElement('span', {
517
- key: 'ghost',
518
- style: { visibility: 'hidden', display: 'block', height: `${heightEm}em`, lineHeight: `${heightEm}em` },
519
- }, '0'),
520
- React.createElement('span', {
521
- key: 'strip',
522
- style: {
523
- position: 'absolute', left: 0, top: 0, display: 'flex', flexDirection: 'column',
524
- transform: `translateY(${-(wheel * heightEm)}em)`,
525
- willChange: 'transform',
526
- },
527
- }, ROLL_BLOCKS.map((value, i) => React.createElement('span', {
528
- key: i,
529
- style: { display: 'block', height: `${heightEm}em`, lineHeight: `${heightEm}em` },
530
- }, value))),
531
- ]);
532
- }
533
-
534
- /**
535
- * 金额 → 连续滚轮。`value` 是原始金额,`decimals` 决定小数位。
536
- * 每一位的轮位 = value * 10^decimals / 10^(该位右侧数字个数),因此低位快、高位慢。
537
- */
538
544
  function RollingMoney(props) {
539
545
  const decimals = Math.max(0, Math.min(6, Number.isInteger(props.decimals) ? props.decimals : 4));
540
- const text = formatMoney(props.value, decimals);
541
- const scale = 10 ** decimals;
542
- // 从右往左数「该位右侧还有几个数字」,得到每位位权
543
- const chars = text.split('');
544
- const significances = new Array(chars.length).fill(-1);
545
- let seen = 0;
546
- for (let i = chars.length - 1; i >= 0; i -= 1) {
547
- if (/[0-9]/.test(chars[i])) {
548
- significances[i] = seen;
549
- seen += 1;
550
- }
551
- }
552
546
  return React.createElement('span', {
553
547
  style: {
554
- display: 'inline-flex', alignItems: 'baseline', letterSpacing: 0, position: 'relative',
555
- // 等宽字体保证每个数字轮宽度稳定(不裁字、不抖动)
556
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
548
+ display: 'inline-block', whiteSpace: 'nowrap', letterSpacing: '-1.2px',
549
+ fontVariantNumeric: 'tabular-nums',
557
550
  ...(props.style || {}),
558
551
  },
559
- }, [
560
- React.createElement('span', {
561
- key: 'sr',
562
- style: {
563
- position: 'absolute', width: 1, height: 1, padding: 0, margin: -1,
564
- overflow: 'hidden', clip: 'rect(0,0,0,0)', whiteSpace: 'nowrap', border: 0,
565
- },
566
- }, text),
567
- React.createElement('span', {
568
- key: 'visual',
569
- 'aria-hidden': 'true',
570
- style: { display: 'inline-flex', alignItems: 'baseline' },
571
- }, chars.map((char, i) => {
572
- if (!/[0-9]/.test(char)) {
573
- return React.createElement('span', {
574
- key: `s${i}`,
575
- style: { display: 'inline-block', lineHeight: '1.22em' },
576
- }, char);
577
- }
578
- const position = (Number(props.value) || 0) * scale / 10 ** significances[i];
579
- return React.createElement(RollingDigit, { key: `d${i}`, position });
580
- })),
581
- ]);
552
+ }, formatMoney(props.value, decimals));
582
553
  }
583
554
 
584
555
  function HeroCard(props) {
@@ -635,8 +606,48 @@ function HeroCard(props) {
635
606
  : null,
636
607
  t('earnedToday'),
637
608
  ]),
638
- React.createElement('span', { key: 'b', style: { fontSize: 11, color: C.label3, ...NUM } },
639
- `${t('todayOn')} ${snap.dateKey}`),
609
+ React.createElement('div', {
610
+ key: 'b',
611
+ style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 },
612
+ }, [
613
+ React.createElement('span', { key: 'd', style: { fontSize: 11, color: C.label3, ...NUM } },
614
+ `${t('todayOn')} ${snap.dateKey}`),
615
+ React.createElement('div', { key: 'c', style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, [
616
+ React.createElement('label', {
617
+ key: 'dec', title: t('decimalsLabel'),
618
+ style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10, color: C.label3, ...NUM },
619
+ }, [
620
+ React.createElement('span', { key: 'l' }, t('decimalsLabel')),
621
+ React.createElement('select', {
622
+ key: 's', value: String(decimals), 'aria-label': t('decimalsLabel'),
623
+ onChange: (event) => onDecimalsChange && onDecimalsChange(Number(event.target.value)),
624
+ style: {
625
+ height: 22, padding: '0 2px', borderRadius: 6,
626
+ border: `1px solid ${C.border1}`, background: C.layer2, color: C.label2,
627
+ fontSize: 10.5, fontVariantNumeric: 'tabular-nums', outline: 'none', cursor: 'pointer',
628
+ },
629
+ }, [0, 1, 2, 3, 4, 5, 6].map((n) => React.createElement('option', { key: n, value: String(n) }, String(n)))),
630
+ React.createElement('span', { key: 'u' }, t('decimalsUnit')),
631
+ ]),
632
+ React.createElement('label', {
633
+ key: 'turbo', title: t('turboLabel'),
634
+ style: {
635
+ display: 'inline-flex', alignItems: 'center', gap: 3,
636
+ fontSize: 10, padding: '1px 6px', borderRadius: 999,
637
+ border: `1px solid ${turbo ? `color-mix(in srgb, ${GOLD_TINT} 60%, ${C.border1})` : C.border1}`,
638
+ background: turbo ? `color-mix(in srgb, ${GOLD_TINT} 16%, transparent)` : 'transparent',
639
+ color: turbo ? C.label2 : C.label3, cursor: 'pointer', ...NUM,
640
+ },
641
+ }, [
642
+ React.createElement('input', {
643
+ key: 'i', type: 'checkbox', checked: turbo,
644
+ onChange: (event) => onTurboChange && onTurboChange(event.target.checked),
645
+ style: { accentColor: GOLD_TINT, width: 12, height: 12, margin: 0, cursor: 'inherit' },
646
+ }),
647
+ React.createElement('span', { key: 'l' }, t('turboLabel')),
648
+ ]),
649
+ ]),
650
+ ]),
640
651
  ]),
641
652
  React.createElement('div', {
642
653
  key: 'amt',
@@ -648,7 +659,7 @@ function HeroCard(props) {
648
659
  decimals,
649
660
  style: {
650
661
  fontSize: 46, lineHeight: 1.12, fontWeight: 800,
651
- ...GOLD,
662
+ color: '#D9A441',
652
663
  ...NUM,
653
664
  ...(rest ? { opacity: 0.62 } : {}),
654
665
  },
@@ -657,46 +668,6 @@ function HeroCard(props) {
657
668
  key: 'unit',
658
669
  style: { fontSize: 18, fontWeight: 700, color: C.label2, ...NUM },
659
670
  }, '元'),
660
- React.createElement('label', {
661
- key: 'dec',
662
- title: t('decimalsLabel'),
663
- style: {
664
- marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 3,
665
- fontSize: 10, color: C.label3, ...NUM,
666
- },
667
- }, [
668
- React.createElement('span', { key: 'l' }, t('decimalsLabel')),
669
- React.createElement('select', {
670
- key: 's',
671
- value: String(decimals),
672
- 'aria-label': t('decimalsLabel'),
673
- onChange: (event) => onDecimalsChange && onDecimalsChange(Number(event.target.value)),
674
- style: {
675
- height: 22, padding: '0 2px', borderRadius: 6,
676
- border: `1px solid ${C.border1}`, background: C.layer2, color: C.label2,
677
- fontSize: 10.5, fontVariantNumeric: 'tabular-nums', outline: 'none', cursor: 'pointer',
678
- },
679
- }, [0, 1, 2, 3, 4, 5, 6].map((n) => React.createElement('option', { key: n, value: String(n) }, String(n)))),
680
- React.createElement('span', { key: 'u' }, t('decimalsUnit')),
681
- ]),
682
- React.createElement('label', {
683
- key: 'turbo',
684
- title: t('turboLabel'),
685
- style: {
686
- display: 'inline-flex', alignItems: 'center', gap: 3,
687
- fontSize: 10, padding: '1px 6px', borderRadius: 999,
688
- border: `1px solid ${turbo ? `color-mix(in srgb, ${GOLD_TINT} 60%, ${C.border1})` : C.border1}`,
689
- background: turbo ? `color-mix(in srgb, ${GOLD_TINT} 16%, transparent)` : 'transparent',
690
- color: turbo ? C.label2 : C.label3, cursor: 'pointer', ...NUM,
691
- },
692
- }, [
693
- React.createElement('input', {
694
- key: 'i', type: 'checkbox', checked: turbo,
695
- onChange: (event) => onTurboChange && onTurboChange(event.target.checked),
696
- style: { accentColor: GOLD_TINT, width: 12, height: 12, margin: 0, cursor: 'inherit' },
697
- }),
698
- React.createElement('span', { key: 'l' }, t('turboLabel')),
699
- ]),
700
671
  ]),
701
672
  React.createElement('div', {
702
673
  key: 'chips',
@@ -1193,6 +1164,7 @@ function NiumaPanel(props) {
1193
1164
  const t = props && typeof props.t === 'function' ? props.t : fallbackTranslate;
1194
1165
 
1195
1166
  const remote = useRemoteConfig();
1167
+ const update = useUpdateInfo();
1196
1168
 
1197
1169
  // 服务端配置 -> 规范化配置;坏数据/缺字段一律退回 DEFAULTS(计算不会抛)。
1198
1170
  const cfg = React.useMemo(() => {
@@ -1354,6 +1326,7 @@ function NiumaPanel(props) {
1354
1326
  const onTurboChange = (checked) => postConfig({ turbo: Boolean(checked) });
1355
1327
 
1356
1328
  const holidaySource = (remote.data && remote.data.holidaySource) || HOLIDAY_SOURCE;
1329
+ const versionText = (remote.data && remote.data.version) || (update.data && update.data.currentVersion) || '';
1357
1330
  const banner = remote.status === 'failed'
1358
1331
  ? {
1359
1332
  color: C.warn,
@@ -1451,6 +1424,51 @@ function NiumaPanel(props) {
1451
1424
  ])
1452
1425
  : null,
1453
1426
 
1427
+ // 版本 / 自动更新提示:已下载→提示重启;发现新版→一键更新
1428
+ update.data && update.data.staged
1429
+ ? React.createElement('div', {
1430
+ key: 'update',
1431
+ style: {
1432
+ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
1433
+ fontSize: 11.5, color: C.label2, padding: '6px 10px', borderRadius: 9,
1434
+ border: `1px solid color-mix(in srgb, ${GOLD_TINT} 45%, transparent)`,
1435
+ background: `color-mix(in srgb, ${GOLD_TINT} 10%, transparent)`,
1436
+ },
1437
+ }, [
1438
+ React.createElement('span', { key: 'dot', 'aria-hidden': 'true', style: { width: 6, height: 6, borderRadius: '50%', background: GOLD_TINT } }),
1439
+ React.createElement('span', { key: 'txt', style: { flex: '1 1 160px' } },
1440
+ t('updateStaged').replace('{version}', String(update.data.stagedVersion || update.data.latestVersion || ''))),
1441
+ ])
1442
+ : update.status === 'updating'
1443
+ ? React.createElement('div', {
1444
+ key: 'update',
1445
+ style: { fontSize: 11.5, color: C.label3, padding: '2px 2px' },
1446
+ }, t('updating'))
1447
+ : update.data && update.data.hasUpdate
1448
+ ? React.createElement('div', {
1449
+ key: 'update',
1450
+ style: {
1451
+ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
1452
+ fontSize: 11.5, color: C.label2, padding: '6px 10px', borderRadius: 9,
1453
+ border: `1px solid color-mix(in srgb, ${GOLD_TINT} 45%, transparent)`,
1454
+ background: `color-mix(in srgb, ${GOLD_TINT} 10%, transparent)`,
1455
+ },
1456
+ }, [
1457
+ React.createElement('span', { key: 'txt', style: { flex: '1 1 160px' } },
1458
+ t('updateAvailable').replace('{version}', String(update.data.latestVersion || ''))),
1459
+ React.createElement('button', {
1460
+ key: 'go',
1461
+ type: 'button',
1462
+ onClick: update.download,
1463
+ style: {
1464
+ border: `1px solid color-mix(in srgb, ${GOLD_TINT} 70%, rgba(0,0,0,.15))`,
1465
+ color: '#3A2A05',
1466
+ background: 'linear-gradient(180deg, #F8DC86 0%, #EFC258 52%, #D99D2C 100%)',
1467
+ borderRadius: 7, padding: '3px 12px', fontSize: 11.5, fontWeight: 700, cursor: 'pointer',
1468
+ },
1469
+ }, t('updateNow')),
1470
+ ])
1471
+ : null,
1454
1472
  React.createElement(HeroCard, { key: 'hero', t, snap, decimals: cfg.decimals, onDecimalsChange, turbo: cfg.turbo, onTurboChange }),
1455
1473
  React.createElement(CountdownRow, { key: 'countdown', t, snap }),
1456
1474
  React.createElement('div', {
@@ -1512,6 +1530,20 @@ function NiumaPanel(props) {
1512
1530
  save,
1513
1531
  onSave,
1514
1532
  }),
1533
+ // 版本信息固定放在面板最底部
1534
+ React.createElement('div', {
1535
+ key: 'version',
1536
+ style: {
1537
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
1538
+ padding: '4px 0 0', fontSize: 10.5, color: C.label3, ...NUM,
1539
+ },
1540
+ }, [
1541
+ versionText ? React.createElement('span', { key: 'v' }, `牛马加速器 v${versionText}`) : null,
1542
+ update.data && update.data.staged
1543
+ ? React.createElement('span', { key: 's', style: { color: C.label3 } },
1544
+ `· ${t('updateStaged').replace('{version}', String(update.data.stagedVersion || update.data.latestVersion || ''))}`)
1545
+ : null,
1546
+ ]),
1515
1547
  ]),
1516
1548
  ]);
1517
1549
  }