mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
fix: align texthooker and stats formatting with CI expectations
This commit is contained in:
@@ -319,7 +319,7 @@
|
|||||||
"SubMiner"
|
"SubMiner"
|
||||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
"fields": {
|
"fields": {
|
||||||
"word": "Expression", // Word setting.
|
"word": "Expression", // Card field for the mined word or expression text.
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Audio setting.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Image setting.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Sentence setting.
|
||||||
|
|||||||
@@ -319,7 +319,7 @@
|
|||||||
"SubMiner"
|
"SubMiner"
|
||||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
"fields": {
|
"fields": {
|
||||||
"word": "Expression", // Word setting.
|
"word": "Expression", // Card field for the mined word or expression text.
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Audio setting.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Image setting.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Sentence setting.
|
||||||
|
|||||||
@@ -1,23 +1,72 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { injectTexthookerBootstrapHtml } from './texthooker';
|
import { injectTexthookerBootstrapHtml, type TexthookerBootstrapSettings } from './texthooker';
|
||||||
|
|
||||||
test('injectTexthookerBootstrapHtml injects websocket bootstrap before head close', () => {
|
test('injectTexthookerBootstrapHtml injects websocket bootstrap before head close', () => {
|
||||||
const html = '<html><head><title>Texthooker</title></head><body></body></html>';
|
const html = '<html><head><title>Texthooker</title></head><body></body></html>';
|
||||||
|
const settings: TexthookerBootstrapSettings = {
|
||||||
const actual = injectTexthookerBootstrapHtml(html, 'ws://127.0.0.1:6678');
|
enableKnownWordColoring: true,
|
||||||
|
enableNPlusOneColoring: true,
|
||||||
|
enableNameMatchColoring: true,
|
||||||
|
enableFrequencyColoring: true,
|
||||||
|
enableJlptColoring: true,
|
||||||
|
characterDictionaryEnabled: true,
|
||||||
|
knownWordColor: '#a6da95',
|
||||||
|
nPlusOneColor: '#c6a0f6',
|
||||||
|
nameMatchColor: '#f5bde6',
|
||||||
|
hoverTokenColor: '#f4dbd6',
|
||||||
|
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||||
|
jlptColors: {
|
||||||
|
N1: '#ed8796',
|
||||||
|
N2: '#f5a97f',
|
||||||
|
N3: '#f9e2af',
|
||||||
|
N4: '#a6e3a1',
|
||||||
|
N5: '#8aadf4',
|
||||||
|
},
|
||||||
|
frequencyDictionary: {
|
||||||
|
singleColor: '#f5a97f',
|
||||||
|
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#8bd5ca', '#8aadf4'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const actual = injectTexthookerBootstrapHtml(html, 'ws://127.0.0.1:6678', settings);
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
actual,
|
actual,
|
||||||
/window\.localStorage\.setItem\('bannou-texthooker-websocketUrl', "ws:\/\/127\.0\.0\.1:6678"\)/,
|
/window\.localStorage\.setItem\('bannou-texthooker-websocketUrl', "ws:\/\/127\.0\.0\.1:6678"\)/,
|
||||||
);
|
);
|
||||||
|
assert.match(
|
||||||
|
actual,
|
||||||
|
/window\.localStorage\.setItem\('bannou-texthooker-enableKnownWordColoring', "1"\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
actual,
|
||||||
|
/window\.localStorage\.setItem\('bannou-texthooker-enableNPlusOneColoring', "1"\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
actual,
|
||||||
|
/window\.localStorage\.setItem\('bannou-texthooker-enableNameMatchColoring', "1"\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
actual,
|
||||||
|
/window\.localStorage\.setItem\('bannou-texthooker-enableFrequencyColoring', "1"\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
actual,
|
||||||
|
/window\.localStorage\.setItem\('bannou-texthooker-enableJlptColoring', "1"\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
actual,
|
||||||
|
/window\.localStorage\.setItem\('bannou-texthooker-characterDictionaryEnabled', "1"\)/,
|
||||||
|
);
|
||||||
|
assert.match(actual, /--subminer-known-word-color:\s*#a6da95;/);
|
||||||
|
assert.match(actual, /--subminer-n-plus-one-color:\s*#c6a0f6;/);
|
||||||
|
assert.match(actual, /--subminer-name-match-color:\s*#f5bde6;/);
|
||||||
|
assert.match(actual, /--subminer-jlpt-n1-color:\s*#ed8796;/);
|
||||||
|
assert.match(actual, /--subminer-frequency-band-4-color:\s*#8bd5ca;/);
|
||||||
|
assert.match(actual, /--sm-token-hover-bg:\s*rgba\(54, 58, 79, 0\.84\);/);
|
||||||
|
assert.match(actual, /p \.word\.word-known\s*\{\s*color:\s*var\(--subminer-known-word-color\);/);
|
||||||
assert.ok(actual.indexOf('</script></head>') !== -1);
|
assert.ok(actual.indexOf('</script></head>') !== -1);
|
||||||
assert.ok(actual.includes('bannou-texthooker-websocketUrl'));
|
assert.ok(actual.includes('bannou-texthooker-websocketUrl'));
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableKnownWordColoring'));
|
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableNPlusOneColoring'));
|
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableNameMatchColoring'));
|
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableFrequencyColoring'));
|
|
||||||
assert.ok(!actual.includes('bannou-texthooker-enableJlptColoring'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('injectTexthookerBootstrapHtml leaves html unchanged without websocketUrl', () => {
|
test('injectTexthookerBootstrapHtml leaves html unchanged without websocketUrl', () => {
|
||||||
|
|||||||
@@ -5,23 +5,92 @@ import { createLogger } from '../../logger';
|
|||||||
|
|
||||||
const logger = createLogger('main:texthooker');
|
const logger = createLogger('main:texthooker');
|
||||||
|
|
||||||
export function injectTexthookerBootstrapHtml(html: string, websocketUrl?: string): string {
|
export type TexthookerBootstrapSettings = {
|
||||||
if (!websocketUrl) {
|
enableKnownWordColoring: boolean;
|
||||||
|
enableNPlusOneColoring: boolean;
|
||||||
|
enableNameMatchColoring: boolean;
|
||||||
|
enableFrequencyColoring: boolean;
|
||||||
|
enableJlptColoring: boolean;
|
||||||
|
characterDictionaryEnabled: boolean;
|
||||||
|
knownWordColor: string;
|
||||||
|
nPlusOneColor: string;
|
||||||
|
nameMatchColor: string;
|
||||||
|
hoverTokenColor: string;
|
||||||
|
hoverTokenBackgroundColor: string;
|
||||||
|
jlptColors: {
|
||||||
|
N1: string;
|
||||||
|
N2: string;
|
||||||
|
N3: string;
|
||||||
|
N4: string;
|
||||||
|
N5: string;
|
||||||
|
};
|
||||||
|
frequencyDictionary: {
|
||||||
|
singleColor: string;
|
||||||
|
bandedColors: readonly [string, string, string, string, string];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildTexthookerBootstrapScript(
|
||||||
|
websocketUrl?: string,
|
||||||
|
settings?: TexthookerBootstrapSettings,
|
||||||
|
): string {
|
||||||
|
const statements: string[] = [];
|
||||||
|
|
||||||
|
if (websocketUrl) {
|
||||||
|
statements.push(
|
||||||
|
`window.localStorage.setItem('bannou-texthooker-websocketUrl', ${JSON.stringify(websocketUrl)});`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings) {
|
||||||
|
const booleanStorageValue = (enabled: boolean): '"1"' | '"0"' => (enabled ? '"1"' : '"0"');
|
||||||
|
statements.push(
|
||||||
|
`window.localStorage.setItem('bannou-texthooker-enableKnownWordColoring', ${booleanStorageValue(settings.enableKnownWordColoring)});`,
|
||||||
|
`window.localStorage.setItem('bannou-texthooker-enableNPlusOneColoring', ${booleanStorageValue(settings.enableNPlusOneColoring)});`,
|
||||||
|
`window.localStorage.setItem('bannou-texthooker-enableNameMatchColoring', ${booleanStorageValue(settings.enableNameMatchColoring)});`,
|
||||||
|
`window.localStorage.setItem('bannou-texthooker-enableFrequencyColoring', ${booleanStorageValue(settings.enableFrequencyColoring)});`,
|
||||||
|
`window.localStorage.setItem('bannou-texthooker-enableJlptColoring', ${booleanStorageValue(settings.enableJlptColoring)});`,
|
||||||
|
`window.localStorage.setItem('bannou-texthooker-characterDictionaryEnabled', ${booleanStorageValue(settings.characterDictionaryEnabled)});`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return statements.length > 0 ? `<script>${statements.join('')}</script>` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTexthookerBootstrapStyle(settings?: TexthookerBootstrapSettings): string {
|
||||||
|
if (!settings) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const [band1, band2, band3, band4, band5] = settings.frequencyDictionary.bandedColors;
|
||||||
|
|
||||||
|
return `<style id="subminer-texthooker-bootstrap-style">:root{--subminer-known-word-color:${settings.knownWordColor};--subminer-n-plus-one-color:${settings.nPlusOneColor};--subminer-name-match-color:${settings.nameMatchColor};--subminer-jlpt-n1-color:${settings.jlptColors.N1};--subminer-jlpt-n2-color:${settings.jlptColors.N2};--subminer-jlpt-n3-color:${settings.jlptColors.N3};--subminer-jlpt-n4-color:${settings.jlptColors.N4};--subminer-jlpt-n5-color:${settings.jlptColors.N5};--subminer-frequency-single-color:${settings.frequencyDictionary.singleColor};--subminer-frequency-band-1-color:${band1};--subminer-frequency-band-2-color:${band2};--subminer-frequency-band-3-color:${band3};--subminer-frequency-band-4-color:${band4};--subminer-frequency-band-5-color:${band5};--sm-token-hover-bg:${settings.hoverTokenBackgroundColor};--sm-token-hover-text:${settings.hoverTokenColor};}p .word.word-known{color:var(--subminer-known-word-color);}p .word.word-n-plus-one{color:var(--subminer-n-plus-one-color);}p .word.word-name-match{color:var(--subminer-name-match-color);}p .word.word-jlpt-n1{text-decoration-color:var(--subminer-jlpt-n1-color);}p .word.word-jlpt-n1[data-jlpt-level]::after{color:var(--subminer-jlpt-n1-color);}p .word.word-jlpt-n2{text-decoration-color:var(--subminer-jlpt-n2-color);}p .word.word-jlpt-n2[data-jlpt-level]::after{color:var(--subminer-jlpt-n2-color);}p .word.word-jlpt-n3{text-decoration-color:var(--subminer-jlpt-n3-color);}p .word.word-jlpt-n3[data-jlpt-level]::after{color:var(--subminer-jlpt-n3-color);}p .word.word-jlpt-n4{text-decoration-color:var(--subminer-jlpt-n4-color);}p .word.word-jlpt-n4[data-jlpt-level]::after{color:var(--subminer-jlpt-n4-color);}p .word.word-jlpt-n5{text-decoration-color:var(--subminer-jlpt-n5-color);}p .word.word-jlpt-n5[data-jlpt-level]::after{color:var(--subminer-jlpt-n5-color);}p .word.word-frequency-single{color:var(--subminer-frequency-single-color);}p .word.word-frequency-band-1{color:var(--subminer-frequency-band-1-color);}p .word.word-frequency-band-2{color:var(--subminer-frequency-band-2-color);}p .word.word-frequency-band-3{color:var(--subminer-frequency-band-3-color);}p .word.word-frequency-band-4{color:var(--subminer-frequency-band-4-color);}p .word.word-frequency-band-5{color:var(--subminer-frequency-band-5-color);}</style>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function injectTexthookerBootstrapHtml(
|
||||||
|
html: string,
|
||||||
|
websocketUrl?: string,
|
||||||
|
settings?: TexthookerBootstrapSettings,
|
||||||
|
): string {
|
||||||
|
const bootstrapStyle = buildTexthookerBootstrapStyle(settings);
|
||||||
|
const bootstrapScript = buildTexthookerBootstrapScript(websocketUrl, settings);
|
||||||
|
|
||||||
|
if (!bootstrapStyle && !bootstrapScript) {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bootstrapScript = `<script>window.localStorage.setItem('bannou-texthooker-websocketUrl', ${JSON.stringify(
|
|
||||||
websocketUrl,
|
|
||||||
)});</script>`;
|
|
||||||
|
|
||||||
if (html.includes('</head>')) {
|
if (html.includes('</head>')) {
|
||||||
return html.replace('</head>', `${bootstrapScript}</head>`);
|
return html.replace('</head>', `${bootstrapStyle}${bootstrapScript}</head>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${bootstrapScript}${html}`;
|
return `${bootstrapStyle}${bootstrapScript}${html}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Texthooker {
|
export class Texthooker {
|
||||||
|
constructor(
|
||||||
|
private readonly getBootstrapSettings?: () => TexthookerBootstrapSettings | undefined,
|
||||||
|
) {}
|
||||||
|
|
||||||
private server: http.Server | null = null;
|
private server: http.Server | null = null;
|
||||||
|
|
||||||
public isRunning(): boolean {
|
public isRunning(): boolean {
|
||||||
@@ -62,9 +131,16 @@ export class Texthooker {
|
|||||||
res.end('Not found');
|
res.end('Not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const bootstrapSettings = this.getBootstrapSettings?.();
|
||||||
const responseData =
|
const responseData =
|
||||||
urlPath === '/' || urlPath === '/index.html'
|
urlPath === '/' || urlPath === '/index.html'
|
||||||
? Buffer.from(injectTexthookerBootstrapHtml(data.toString('utf-8'), websocketUrl))
|
? Buffer.from(
|
||||||
|
injectTexthookerBootstrapHtml(
|
||||||
|
data.toString('utf-8'),
|
||||||
|
websocketUrl,
|
||||||
|
bootstrapSettings,
|
||||||
|
),
|
||||||
|
)
|
||||||
: data;
|
: data;
|
||||||
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'text/plain' });
|
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'text/plain' });
|
||||||
res.end(responseData);
|
res.end(responseData);
|
||||||
|
|||||||
35
src/main.ts
35
src/main.ts
@@ -568,7 +568,40 @@ const anilistUpdateQueue = createAnilistUpdateQueue(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const isDev = process.argv.includes('--dev') || process.argv.includes('--debug');
|
const isDev = process.argv.includes('--dev') || process.argv.includes('--debug');
|
||||||
const texthookerService = new Texthooker();
|
const texthookerService = new Texthooker(() => {
|
||||||
|
const config = getResolvedConfig();
|
||||||
|
const characterDictionaryEnabled =
|
||||||
|
config.anilist.characterDictionary.enabled && yomitanProfilePolicy.isCharacterDictionaryEnabled();
|
||||||
|
const knownAndNPlusOneEnabled = getRuntimeBooleanOption(
|
||||||
|
'subtitle.annotation.nPlusOne',
|
||||||
|
config.ankiConnect.knownWords.highlightEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enableKnownWordColoring: knownAndNPlusOneEnabled,
|
||||||
|
enableNPlusOneColoring: knownAndNPlusOneEnabled,
|
||||||
|
enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled,
|
||||||
|
enableFrequencyColoring: getRuntimeBooleanOption(
|
||||||
|
'subtitle.annotation.frequency',
|
||||||
|
config.subtitleStyle.frequencyDictionary.enabled,
|
||||||
|
),
|
||||||
|
enableJlptColoring: getRuntimeBooleanOption(
|
||||||
|
'subtitle.annotation.jlpt',
|
||||||
|
config.subtitleStyle.enableJlpt,
|
||||||
|
),
|
||||||
|
characterDictionaryEnabled,
|
||||||
|
knownWordColor: config.ankiConnect.knownWords.color,
|
||||||
|
nPlusOneColor: config.ankiConnect.nPlusOne.nPlusOne,
|
||||||
|
nameMatchColor: config.subtitleStyle.nameMatchColor,
|
||||||
|
hoverTokenColor: config.subtitleStyle.hoverTokenColor,
|
||||||
|
hoverTokenBackgroundColor: config.subtitleStyle.hoverTokenBackgroundColor,
|
||||||
|
jlptColors: config.subtitleStyle.jlptColors,
|
||||||
|
frequencyDictionary: {
|
||||||
|
singleColor: config.subtitleStyle.frequencyDictionary.singleColor,
|
||||||
|
bandedColors: config.subtitleStyle.frequencyDictionary.bandedColors,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
const subtitleWsService = new SubtitleWebSocket();
|
const subtitleWsService = new SubtitleWebSocket();
|
||||||
const annotationSubtitleWsService = new SubtitleWebSocket();
|
const annotationSubtitleWsService = new SubtitleWebSocket();
|
||||||
const logger = createLogger('main');
|
const logger = createLogger('main');
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export function MediaHeader({ detail, initialKnownWordsSummary = null }: MediaHe
|
|||||||
<div className="text-xs text-ctp-overlay2">total watch time</div>
|
<div className="text-xs text-ctp-overlay2">total watch time</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-ctp-cards-mined font-medium">{formatNumber(detail.totalCards)}</div>
|
<div className="text-ctp-cards-mined font-medium">
|
||||||
|
{formatNumber(detail.totalCards)}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-ctp-overlay2">cards mined</div>
|
<div className="text-xs text-ctp-overlay2">cards mined</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -245,13 +245,13 @@ function AnimeGroupRow({
|
|||||||
{group.sessions.length} sessions · {formatDuration(group.totalActiveMs)} active
|
{group.sessions.length} sessions · {formatDuration(group.totalActiveMs)} active
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 text-xs text-center shrink-0">
|
<div className="flex gap-4 text-xs text-center shrink-0">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-ctp-cards-mined font-medium font-mono tabular-nums">
|
<div className="text-ctp-cards-mined font-medium font-mono tabular-nums">
|
||||||
{formatNumber(group.totalCards)}
|
{formatNumber(group.totalCards)}
|
||||||
|
</div>
|
||||||
|
<div className="text-ctp-overlay2">cards</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ctp-overlay2">cards</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-ctp-mauve font-medium font-mono tabular-nums">
|
<div className="text-ctp-mauve font-medium font-mono tabular-nums">
|
||||||
{formatNumber(group.totalWords)}
|
{formatNumber(group.totalWords)}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { formatEventSeconds, type SessionChartMarker, type SessionEventNoteInfo } from '../../lib/session-events';
|
import {
|
||||||
|
formatEventSeconds,
|
||||||
|
type SessionChartMarker,
|
||||||
|
type SessionEventNoteInfo,
|
||||||
|
} from '../../lib/session-events';
|
||||||
|
|
||||||
interface SessionEventPopoverProps {
|
interface SessionEventPopoverProps {
|
||||||
marker: SessionChartMarker;
|
marker: SessionChartMarker;
|
||||||
@@ -83,7 +87,8 @@ export function SessionEventPopover({
|
|||||||
{marker.kind === 'seek' && (
|
{marker.kind === 'seek' && (
|
||||||
<div className="space-y-1 text-xs text-ctp-subtext0">
|
<div className="space-y-1 text-xs text-ctp-subtext0">
|
||||||
<div>
|
<div>
|
||||||
From <span className="text-ctp-teal">{formatEventSeconds(marker.fromMs) ?? '\u2014'}</span>{' '}
|
From{' '}
|
||||||
|
<span className="text-ctp-teal">{formatEventSeconds(marker.fromMs) ?? '\u2014'}</span>{' '}
|
||||||
to <span className="text-ctp-teal">{formatEventSeconds(marker.toMs) ?? '\u2014'}</span>
|
to <span className="text-ctp-teal">{formatEventSeconds(marker.toMs) ?? '\u2014'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -120,7 +125,9 @@ export function SessionEventPopover({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{info?.expression ? (
|
{info?.expression ? (
|
||||||
<div className="mb-1 text-sm font-medium text-ctp-text">{info.expression}</div>
|
<div className="mb-1 text-sm font-medium text-ctp-text">
|
||||||
|
{info.expression}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{info?.context ? (
|
{info?.context ? (
|
||||||
<div className="mb-1 text-xs text-ctp-subtext0">{info.context}</div>
|
<div className="mb-1 text-xs text-ctp-subtext0">{info.context}</div>
|
||||||
|
|||||||
@@ -177,7 +177,12 @@ export function TrendsTab() {
|
|||||||
color="#8aadf4"
|
color="#8aadf4"
|
||||||
type="bar"
|
type="bar"
|
||||||
/>
|
/>
|
||||||
<TrendChart title="Cards Mined" data={data.activity.cards} color={cardsMinedColor} type="bar" />
|
<TrendChart
|
||||||
|
title="Cards Mined"
|
||||||
|
data={data.activity.cards}
|
||||||
|
color={cardsMinedColor}
|
||||||
|
type="bar"
|
||||||
|
/>
|
||||||
<TrendChart title="Tokens Seen" data={data.activity.words} color="#8bd5ca" type="bar" />
|
<TrendChart title="Tokens Seen" data={data.activity.words} color="#8bd5ca" type="bar" />
|
||||||
<TrendChart title="Sessions" data={data.activity.sessions} color="#b7bdf8" type="bar" />
|
<TrendChart title="Sessions" data={data.activity.sessions} color="#b7bdf8" type="bar" />
|
||||||
|
|
||||||
@@ -196,7 +201,12 @@ export function TrendsTab() {
|
|||||||
color="#c6a0f6"
|
color="#c6a0f6"
|
||||||
type="line"
|
type="line"
|
||||||
/>
|
/>
|
||||||
<TrendChart title="Cards Mined" data={data.progress.cards} color={cardsMinedColor} type="line" />
|
<TrendChart
|
||||||
|
title="Cards Mined"
|
||||||
|
data={data.progress.cards}
|
||||||
|
color={cardsMinedColor}
|
||||||
|
type="line"
|
||||||
|
/>
|
||||||
<TrendChart
|
<TrendChart
|
||||||
title="Episodes Watched"
|
title="Episodes Watched"
|
||||||
data={data.progress.episodes}
|
data={data.progress.episodes}
|
||||||
|
|||||||
@@ -146,15 +146,18 @@ test('extractSessionEventNoteInfo ignores malformed notes without a numeric note
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('mergeSessionEventNoteInfos keys previews by both requested and returned note ids', () => {
|
test('mergeSessionEventNoteInfos keys previews by both requested and returned note ids', () => {
|
||||||
const noteInfos = mergeSessionEventNoteInfos([111], [
|
const noteInfos = mergeSessionEventNoteInfos(
|
||||||
{
|
[111],
|
||||||
noteId: 222,
|
[
|
||||||
fields: {
|
{
|
||||||
Expression: { value: '呪い' },
|
noteId: 222,
|
||||||
Sentence: { value: 'この剣は呪いだ' },
|
fields: {
|
||||||
|
Expression: { value: '呪い' },
|
||||||
|
Sentence: { value: 'この剣は呪いだ' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
]);
|
);
|
||||||
|
|
||||||
assert.deepEqual(noteInfos.get(111), {
|
assert.deepEqual(noteInfos.get(111), {
|
||||||
noteId: 222,
|
noteId: 222,
|
||||||
|
|||||||
@@ -237,17 +237,16 @@ export function collectPendingSessionEventNoteIds(
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSessionEventCardRequest(
|
export function getSessionEventCardRequest(marker: SessionChartMarker | null): {
|
||||||
marker: SessionChartMarker | null,
|
noteIds: number[];
|
||||||
): { noteIds: number[]; requestKey: string | null } {
|
requestKey: string | null;
|
||||||
|
} {
|
||||||
if (!marker || marker.kind !== 'card' || marker.noteIds.length === 0) {
|
if (!marker || marker.kind !== 'card' || marker.noteIds.length === 0) {
|
||||||
return { noteIds: [], requestKey: null };
|
return { noteIds: [], requestKey: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteIds = Array.from(
|
const noteIds = Array.from(
|
||||||
new Set(
|
new Set(marker.noteIds.filter((noteId) => Number.isInteger(noteId) && noteId > 0)),
|
||||||
marker.noteIds.filter((noteId) => Number.isInteger(noteId) && noteId > 0),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user