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.
- package/CHANGELOG.md +18 -0
- package/README.md +2 -1
- package/docs/preview-light-dark.png +0 -0
- package/lib/client.js +193 -161
- package/lib/client.js.map +2 -2
- package/lib/index.js +93 -3
- package/lib/update.js +247 -0
- package/package.json +4 -3
- package/src/client/index.js +154 -122
package/src/client/index.js
CHANGED
|
@@ -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-
|
|
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('
|
|
639
|
-
|
|
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
|
-
|
|
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
|
}
|