mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-26 12:15:26 -07:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ca8681dc
|
||
|
|
c2c25c0da6 | ||
|
|
556de61756
|
+16
-3
@@ -10,9 +10,22 @@
|
|||||||
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
|
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Subtitle & Karaoke Duplication**: Typeset ASS karaoke and animated signs no longer flood the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, or repeated animation events. Lines are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved. Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly. Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container. Secondary subtitles now go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines. Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
|
- **Subtitle & Karaoke Duplication**:
|
||||||
- **Character Dictionary Reliability**: Character dictionary generation, merged rebuilds, and imports no longer freeze the app on large dictionaries. Snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path. Dictionaries are reused instead of regenerated when MeCab finds no name splits, and cached portraits now restore correctly after the portrait index finishes loading post-tokenization. Desktop progress notifications on Linux AppImage installs now update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
|
- Karaoke and animated signs are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved, instead of flooding the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, and repeated animation events.
|
||||||
- **Overlay Startup & Modals**: Fixed several causes of the overlay getting stuck on "Overlay loading": the macOS window-tracking helper now targets macOS 12.0+ instead of requiring the build machine's exact macOS version (previously crashed on older systems like Ventura), and mpv IPC connection attempts now time out and retry, showing an actionable error if content still isn't ready after 30 seconds. Dedicated overlay modals are also prewarmed on macOS and Windows so shortcuts open them promptly, and on macOS reused modals and the stats window now open above fullscreen mpv on its current Space instead of jumping to another desktop.
|
- Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly.
|
||||||
|
- Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container.
|
||||||
|
- Secondary subtitles go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines.
|
||||||
|
- Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
|
||||||
|
- **Character Dictionary Reliability**:
|
||||||
|
- Generation, merged rebuilds, and imports no longer freeze the app on large dictionaries; snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path.
|
||||||
|
- Dictionaries are reused instead of regenerated when MeCab finds no name splits.
|
||||||
|
- Cached portraits restore correctly after the portrait index finishes loading post-tokenization.
|
||||||
|
- Desktop progress notifications on Linux AppImage installs update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
|
||||||
|
- **Overlay Startup & Modals**:
|
||||||
|
- The macOS window-tracking helper targets macOS 12.0+ instead of requiring the build machine's exact macOS version, fixing crashes on older systems like Ventura that left the overlay stuck on "Overlay loading".
|
||||||
|
- mpv IPC connection attempts time out and retry, showing an actionable error if content still isn't ready after 30 seconds.
|
||||||
|
- Dedicated overlay modals are prewarmed on macOS and Windows so shortcuts open them promptly.
|
||||||
|
- On macOS, reused modals and the stats window open above fullscreen mpv on its current Space instead of jumping to another desktop.
|
||||||
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
|
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
|
||||||
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
|
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
|
||||||
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
|
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ How fragments turn into a release:
|
|||||||
|
|
||||||
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
|
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
|
||||||
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
|
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
|
||||||
- GitHub release notes and prerelease notes use short top-level items with nested bullets for the change, user benefit, and any useful action note. The stable `CHANGELOG.md` can stay in compact single-line bullets.
|
- `CHANGELOG.md`, GitHub release notes, and prerelease notes all use short top-level items with one nested bullet per distinct change, instead of packing a release's worth of detail into a single paragraph bullet. An item with only one thing to say stays inline on the top-level bullet. Release notes and prerelease notes additionally cover user benefit and any useful action note in their nested bullets.
|
||||||
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
|
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
|
||||||
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Kept the Anki card update spinner visible until audio and image updates finish.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- YouTube auto-generated captions no longer disappear mid-speech: long caption rows carry a placeholder 3-second duration in YouTube's TimedText data, so converted cues now stay on screen until the next caption event, matching YouTube's own display timing.
|
||||||
+16
-3
@@ -10,9 +10,22 @@
|
|||||||
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
|
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
|
||||||
|
|
||||||
**Fixed**
|
**Fixed**
|
||||||
- **Subtitle & Karaoke Duplication**: Typeset ASS karaoke and animated signs no longer flood the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, or repeated animation events. Lines are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved. Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly. Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container. Secondary subtitles now go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines. Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
|
- **Subtitle & Karaoke Duplication**:
|
||||||
- **Character Dictionary Reliability**: Character dictionary generation, merged rebuilds, and imports no longer freeze the app on large dictionaries. Snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path. Dictionaries are reused instead of regenerated when MeCab finds no name splits, and cached portraits now restore correctly after the portrait index finishes loading post-tokenization. Desktop progress notifications on Linux AppImage installs now update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
|
- Karaoke and animated signs are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved, instead of flooding the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, and repeated animation events.
|
||||||
- **Overlay Startup & Modals**: Fixed several causes of the overlay getting stuck on "Overlay loading": the macOS window-tracking helper now targets macOS 12.0+ instead of requiring the build machine's exact macOS version (previously crashed on older systems like Ventura), and mpv IPC connection attempts now time out and retry, showing an actionable error if content still isn't ready after 30 seconds. Dedicated overlay modals are also prewarmed on macOS and Windows so shortcuts open them promptly, and on macOS reused modals and the stats window now open above fullscreen mpv on its current Space instead of jumping to another desktop.
|
- Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly.
|
||||||
|
- Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container.
|
||||||
|
- Secondary subtitles go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines.
|
||||||
|
- Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
|
||||||
|
- **Character Dictionary Reliability**:
|
||||||
|
- Generation, merged rebuilds, and imports no longer freeze the app on large dictionaries; snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path.
|
||||||
|
- Dictionaries are reused instead of regenerated when MeCab finds no name splits.
|
||||||
|
- Cached portraits restore correctly after the portrait index finishes loading post-tokenization.
|
||||||
|
- Desktop progress notifications on Linux AppImage installs update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
|
||||||
|
- **Overlay Startup & Modals**:
|
||||||
|
- The macOS window-tracking helper targets macOS 12.0+ instead of requiring the build machine's exact macOS version, fixing crashes on older systems like Ventura that left the overlay stuck on "Overlay loading".
|
||||||
|
- mpv IPC connection attempts time out and retry, showing an actionable error if content still isn't ready after 30 seconds.
|
||||||
|
- Dedicated overlay modals are prewarmed on macOS and Windows so shortcuts open them promptly.
|
||||||
|
- On macOS, reused modals and the stats window open above fullscreen mpv on its current Space instead of jumping to another desktop.
|
||||||
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
|
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
|
||||||
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
|
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
|
||||||
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
|
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
|
||||||
|
|||||||
@@ -44,14 +44,22 @@ function fragmentTypesInPrompt(input: string): string[] {
|
|||||||
.map((line) => line.slice('type: '.length).trim());
|
.map((line) => line.slice('type: '.length).trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertReleaseNotesPromptRequestsNestedBullets(input: string): void {
|
function assertPromptRequestsNestedBullets(input: string): void {
|
||||||
assert.match(input, /In MODE: release-notes, use short top-level change bullets/);
|
assert.match(input, /In both modes, split every item into one nested bullet per distinct change/);
|
||||||
assert.match(input, /Nested bullets should cover the change, user benefit, and any user action/);
|
assert.match(input, /Never stack several distinct changes into one long paragraph-shaped bullet/);
|
||||||
assert.match(input, /Do not require the exact nested labels/);
|
|
||||||
assert.match(input, /Keep nested bullets short, concrete, and readable by non-technical users/);
|
assert.match(input, /Keep nested bullets short, concrete, and readable by non-technical users/);
|
||||||
assert.match(input, /Avoid paragraph-style release-note bullets/);
|
assert.match(input, /Avoid paragraph-style release-note bullets/);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertReleaseNotesPromptRequestsNestedBullets(input: string): void {
|
||||||
|
assertPromptRequestsNestedBullets(input);
|
||||||
|
assert.match(
|
||||||
|
input,
|
||||||
|
/In MODE: release-notes, nested bullets should also cover user benefit and any user action/,
|
||||||
|
);
|
||||||
|
assert.match(input, /Do not require the exact nested labels/);
|
||||||
|
}
|
||||||
|
|
||||||
function defaultPolishedBody(input: string): string {
|
function defaultPolishedBody(input: string): string {
|
||||||
const mode = modeFromPrompt(input);
|
const mode = modeFromPrompt(input);
|
||||||
const types = fragmentTypesInPrompt(input);
|
const types = fragmentTypesInPrompt(input);
|
||||||
@@ -446,6 +454,7 @@ test('writeChangelogArtifacts prompts Claude to summarize the final stable outco
|
|||||||
prompt,
|
prompt,
|
||||||
/Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/,
|
/Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/,
|
||||||
);
|
);
|
||||||
|
assertPromptRequestsNestedBullets(prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseNotesPrompt = stub.calls.find(
|
const releaseNotesPrompt = stub.calls.find(
|
||||||
|
|||||||
@@ -480,10 +480,15 @@ You will receive a list of FRAGMENT entries below. Each fragment has metadata (t
|
|||||||
- Be merged with related bullets when possible. If five fragments all touch Windows overlay z-order/focus/restore, write one or two bullets that summarize the overall improvement instead of five.
|
- Be merged with related bullets when possible. If five fragments all touch Windows overlay z-order/focus/restore, write one or two bullets that summarize the overall improvement instead of five.
|
||||||
- Drop bullets that only describe PR housekeeping, CodeRabbit follow-ups, or test-only changes that don't affect users.
|
- Drop bullets that only describe PR housekeeping, CodeRabbit follow-ups, or test-only changes that don't affect users.
|
||||||
- Preserve the substance of breaking changes that remain breaking after applying the Release Outcome Rules. Do not soften or omit them.
|
- Preserve the substance of breaking changes that remain breaking after applying the Release Outcome Rules. Do not soften or omit them.
|
||||||
5. In MODE: changelog, each item may be a conventional single-level bullet, e.g. "- Playlist Browser: Adds faster saved-show browsing."
|
5. In both modes, split every item into one nested bullet per distinct change. Write a short bold name on the top-level bullet, then indent the details two spaces:
|
||||||
6. In MODE: release-notes, use short top-level change bullets with two or three nested bullets when an item needs explanation.
|
- **Playlist Browser**:
|
||||||
Nested bullets should cover the change, user benefit, and any user action or compatibility note when useful. Do not require the exact nested labels; natural phrasing is fine. Omit the action bullet when no action is needed.
|
- Saved shows now open without rescanning the library.
|
||||||
|
- The picker remembers the last folder you browsed between launches.
|
||||||
|
Each nested bullet covers exactly one change, behavior, or user-visible outcome. Never stack several distinct changes into one long paragraph-shaped bullet.
|
||||||
|
Aim for two to five nested bullets per item. When an item genuinely has only one thing to say, put it inline on the top-level bullet ("- **Playlist Browser**: Saved shows now open without rescanning the library.") instead of emitting a single nested bullet.
|
||||||
Keep nested bullets short, concrete, and readable by non-technical users. Avoid paragraph-style release-note bullets.
|
Keep nested bullets short, concrete, and readable by non-technical users. Avoid paragraph-style release-note bullets.
|
||||||
|
Bullets inside the Internal section may stay single-level.
|
||||||
|
6. In MODE: release-notes, nested bullets should also cover user benefit and any user action or compatibility note when useful. Do not require the exact nested labels; natural phrasing is fine. Omit the action bullet when no action is needed.
|
||||||
7. Do not invent features. Every bullet must be grounded in the input fragments.
|
7. Do not invent features. Every bullet must be grounded in the input fragments.
|
||||||
8. Do not include the version heading (## v...) — that wrapper is added by the caller.
|
8. Do not include the version heading (## v...) — that wrapper is added by the caller.
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import type { MediaInput } from './media-input';
|
|||||||
import { AnkiConnectConfig } from './types';
|
import { AnkiConnectConfig } from './types';
|
||||||
|
|
||||||
type TestOverlayNotificationPayload = {
|
type TestOverlayNotificationPayload = {
|
||||||
|
id?: string;
|
||||||
title: string;
|
title: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
|
persistent?: boolean;
|
||||||
actions?: Array<{ id: string; label: string; noteId?: number }>;
|
actions?: Array<{ id: string; label: string; noteId?: number }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1182,6 +1184,82 @@ test('AnkiIntegration embeds generated notification image on overlay mined-card
|
|||||||
assert.deepEqual(cleanupPaths, [notificationIconPath]);
|
assert.deepEqual(cleanupPaths, [notificationIconPath]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('AnkiIntegration keeps overlay card-update progress visible until the terminal notification', async () => {
|
||||||
|
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
||||||
|
const integration = new AnkiIntegration(
|
||||||
|
{
|
||||||
|
behavior: {
|
||||||
|
notificationType: 'overlay',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
(payload) => {
|
||||||
|
overlayNotifications.push(payload);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const updateNotifications = integration as unknown as {
|
||||||
|
beginUpdateProgress: (message: string) => void;
|
||||||
|
showNotification: (noteId: number, label: string | number) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateNotifications.beginUpdateProgress('Updating card');
|
||||||
|
await updateNotifications.showNotification(42, '食べる');
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
overlayNotifications.map(({ id, variant, persistent }) => ({ id, variant, persistent })),
|
||||||
|
[
|
||||||
|
{ id: 'anki-update-progress', variant: 'progress', persistent: true },
|
||||||
|
{ id: 'anki-update-progress', variant: 'success', persistent: false },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AnkiIntegration dismisses persistent overlay update progress when no terminal notification replaces it', () => {
|
||||||
|
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
||||||
|
const dismissedIds: string[] = [];
|
||||||
|
const integration = new AnkiIntegration(
|
||||||
|
{
|
||||||
|
behavior: {
|
||||||
|
notificationType: 'overlay',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
(payload) => {
|
||||||
|
overlayNotifications.push(payload);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
(id) => {
|
||||||
|
dismissedIds.push(id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const updateNotifications = integration as unknown as {
|
||||||
|
beginUpdateProgress: (message: string) => void;
|
||||||
|
endUpdateProgress: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateNotifications.beginUpdateProgress('Updating card');
|
||||||
|
updateNotifications.endUpdateProgress();
|
||||||
|
|
||||||
|
assert.equal(overlayNotifications[0]?.persistent, true);
|
||||||
|
assert.deepEqual(dismissedIds, ['anki-update-progress']);
|
||||||
|
});
|
||||||
|
|
||||||
test('AnkiIntegration keeps overlay notification image when temp icon write fails', async () => {
|
test('AnkiIntegration keeps overlay notification image when temp icon write fails', async () => {
|
||||||
const desktopNotifications: Array<{ title: string; body?: string; icon?: string }> = [];
|
const desktopNotifications: Array<{ title: string; body?: string; icon?: string }> = [];
|
||||||
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
||||||
|
|||||||
+14
-2
@@ -218,6 +218,8 @@ export class AnkiIntegration {
|
|||||||
null;
|
null;
|
||||||
private overlayNotificationCallback: ((payload: OverlayNotificationPayload) => void) | null =
|
private overlayNotificationCallback: ((payload: OverlayNotificationPayload) => void) | null =
|
||||||
null;
|
null;
|
||||||
|
private overlayNotificationDismissCallback: ((id: string) => void) | null = null;
|
||||||
|
private overlayUpdateProgressActive = false;
|
||||||
private updateInProgress = false;
|
private updateInProgress = false;
|
||||||
private uiFeedbackState: UiFeedbackState = createUiFeedbackState();
|
private uiFeedbackState: UiFeedbackState = createUiFeedbackState();
|
||||||
private parseWarningKeys = new Set<string>();
|
private parseWarningKeys = new Set<string>();
|
||||||
@@ -265,6 +267,7 @@ export class AnkiIntegration {
|
|||||||
getCachedMediaPath?: MediaGenerationInputResolverOptions['getCachedMediaPath'],
|
getCachedMediaPath?: MediaGenerationInputResolverOptions['getCachedMediaPath'],
|
||||||
shouldRequireRemoteMediaCache?: () => boolean,
|
shouldRequireRemoteMediaCache?: () => boolean,
|
||||||
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined,
|
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined,
|
||||||
|
overlayNotificationDismissCallback?: (id: string) => void,
|
||||||
) {
|
) {
|
||||||
this.config = normalizeAnkiIntegrationConfig(config);
|
this.config = normalizeAnkiIntegrationConfig(config);
|
||||||
this.aiConfig = { ...aiConfig };
|
this.aiConfig = { ...aiConfig };
|
||||||
@@ -280,6 +283,7 @@ export class AnkiIntegration {
|
|||||||
this.getCachedMediaPath = getCachedMediaPath ?? null;
|
this.getCachedMediaPath = getCachedMediaPath ?? null;
|
||||||
this.shouldRequireRemoteMediaCache = shouldRequireRemoteMediaCache ?? null;
|
this.shouldRequireRemoteMediaCache = shouldRequireRemoteMediaCache ?? null;
|
||||||
this.getYoutubeMediaSourceUrl = getYoutubeMediaSourceUrl ?? null;
|
this.getYoutubeMediaSourceUrl = getYoutubeMediaSourceUrl ?? null;
|
||||||
|
this.overlayNotificationDismissCallback = overlayNotificationDismissCallback ?? null;
|
||||||
this.pendingYoutubeMediaQueue = this.createPendingYoutubeMediaQueue();
|
this.pendingYoutubeMediaQueue = this.createPendingYoutubeMediaQueue();
|
||||||
this.knownWordCache = this.createKnownWordCache(knownWordCacheStatePath);
|
this.knownWordCache = this.createKnownWordCache(knownWordCacheStatePath);
|
||||||
this.pollingRunner = this.createPollingRunner();
|
this.pollingRunner = this.createPollingRunner();
|
||||||
@@ -1203,12 +1207,13 @@ export class AnkiIntegration {
|
|||||||
private beginUpdateProgress(initialMessage: string): void {
|
private beginUpdateProgress(initialMessage: string): void {
|
||||||
if (!this.shouldUseOsdNotifications()) {
|
if (!this.shouldUseOsdNotifications()) {
|
||||||
if (this.shouldUseOverlayNotifications()) {
|
if (this.shouldUseOverlayNotifications()) {
|
||||||
|
this.overlayUpdateProgressActive = true;
|
||||||
this.overlayNotificationCallback?.({
|
this.overlayNotificationCallback?.({
|
||||||
id: 'anki-update-progress',
|
id: 'anki-update-progress',
|
||||||
title: 'Anki update',
|
title: 'Anki update',
|
||||||
body: initialMessage,
|
body: initialMessage,
|
||||||
variant: 'progress',
|
variant: 'progress',
|
||||||
persistent: false,
|
persistent: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1220,6 +1225,10 @@ export class AnkiIntegration {
|
|||||||
|
|
||||||
private endUpdateProgress(): void {
|
private endUpdateProgress(): void {
|
||||||
if (!this.shouldUseOsdNotifications()) {
|
if (!this.shouldUseOsdNotifications()) {
|
||||||
|
if (this.overlayUpdateProgressActive) {
|
||||||
|
this.overlayUpdateProgressActive = false;
|
||||||
|
this.overlayNotificationDismissCallback?.('anki-update-progress');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
endUpdateProgress(this.uiFeedbackState, (timer) => {
|
endUpdateProgress(this.uiFeedbackState, (timer) => {
|
||||||
@@ -1243,18 +1252,20 @@ export class AnkiIntegration {
|
|||||||
if (!this.shouldUseOsdNotifications()) {
|
if (!this.shouldUseOsdNotifications()) {
|
||||||
this.updateInProgress = true;
|
this.updateInProgress = true;
|
||||||
if (this.shouldUseOverlayNotifications()) {
|
if (this.shouldUseOverlayNotifications()) {
|
||||||
|
this.overlayUpdateProgressActive = true;
|
||||||
this.overlayNotificationCallback?.({
|
this.overlayNotificationCallback?.({
|
||||||
id: 'anki-update-progress',
|
id: 'anki-update-progress',
|
||||||
title: 'Anki update',
|
title: 'Anki update',
|
||||||
body: initialMessage,
|
body: initialMessage,
|
||||||
variant: 'progress',
|
variant: 'progress',
|
||||||
persistent: false,
|
persistent: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await action();
|
return await action();
|
||||||
} finally {
|
} finally {
|
||||||
this.updateInProgress = false;
|
this.updateInProgress = false;
|
||||||
|
this.endUpdateProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return withUpdateProgress(
|
return withUpdateProgress(
|
||||||
@@ -1353,6 +1364,7 @@ export class AnkiIntegration {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (shouldShowOverlayNotification && this.overlayNotificationCallback) {
|
if (shouldShowOverlayNotification && this.overlayNotificationCallback) {
|
||||||
|
this.overlayUpdateProgressActive = false;
|
||||||
this.overlayNotificationCallback({
|
this.overlayNotificationCallback({
|
||||||
id: 'anki-update-progress',
|
id: 'anki-update-progress',
|
||||||
title: 'Anki Card Updated',
|
title: 'Anki Card Updated',
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
|||||||
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined;
|
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined;
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: () => (
|
createFieldGroupingCallback: () => (
|
||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
@@ -166,6 +167,7 @@ export function registerAnkiJimakuIpcRuntime(
|
|||||||
options.getCachedMediaPath,
|
options.getCachedMediaPath,
|
||||||
options.shouldRequireRemoteMediaCache,
|
options.shouldRequireRemoteMediaCache,
|
||||||
options.getYoutubeMediaSourceUrl,
|
options.getYoutubeMediaSourceUrl,
|
||||||
|
options.dismissOverlayNotification,
|
||||||
);
|
);
|
||||||
integration.start();
|
integration.start();
|
||||||
options.setAnkiIntegration(integration);
|
options.setAnkiIntegration(integration);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type CreateAnkiIntegrationArgs = {
|
|||||||
mpvClient: { send?: (payload: { command: string[] }) => void };
|
mpvClient: { send?: (payload: { command: string[] }) => void };
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: () => (
|
createFieldGroupingCallback: () => (
|
||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
@@ -74,6 +75,7 @@ function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiInte
|
|||||||
args.getCachedMediaPath,
|
args.getCachedMediaPath,
|
||||||
args.shouldRequireRemoteMediaCache,
|
args.shouldRequireRemoteMediaCache,
|
||||||
args.getYoutubeMediaSourceUrl,
|
args.getYoutubeMediaSourceUrl,
|
||||||
|
args.dismissOverlayNotification,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +139,7 @@ export function initializeOverlayRuntime(
|
|||||||
setAnkiIntegration: (integration: unknown | null) => void;
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: () => (
|
createFieldGroupingCallback: () => (
|
||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
@@ -177,6 +180,7 @@ export function initializeOverlayAnkiIntegration(options: {
|
|||||||
setAnkiIntegration: (integration: unknown | null) => void;
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: () => (
|
createFieldGroupingCallback: () => (
|
||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
@@ -219,6 +223,7 @@ export function initializeOverlayAnkiIntegration(options: {
|
|||||||
mpvClient,
|
mpvClient,
|
||||||
showDesktopNotification: options.showDesktopNotification,
|
showDesktopNotification: options.showDesktopNotification,
|
||||||
showOverlayNotification: options.showOverlayNotification,
|
showOverlayNotification: options.showOverlayNotification,
|
||||||
|
dismissOverlayNotification: options.dismissOverlayNotification,
|
||||||
createFieldGroupingCallback: options.createFieldGroupingCallback,
|
createFieldGroupingCallback: options.createFieldGroupingCallback,
|
||||||
knownWordCacheStatePath: options.getKnownWordCacheStatePath(),
|
knownWordCacheStatePath: options.getKnownWordCacheStatePath(),
|
||||||
...(options.getCachedMediaPath ? { getCachedMediaPath: options.getCachedMediaPath } : {}),
|
...(options.getCachedMediaPath ? { getCachedMediaPath: options.getCachedMediaPath } : {}),
|
||||||
|
|||||||
@@ -39,6 +39,36 @@ test('convertYoutubeTimedTextToVtt does not swallow text after zero-length overl
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('convertYoutubeTimedTextToVtt extends rolling captions to the next window event', () => {
|
||||||
|
// Real-world shape of YouTube's sentence-level auto captions: window-append
|
||||||
|
// filler rows (a="1", sometimes without d) mark the display timeline, while
|
||||||
|
// long text rows carry a placeholder d="3000" far shorter than the speech.
|
||||||
|
const result = convertYoutubeTimedTextToVtt(
|
||||||
|
[
|
||||||
|
'<timedtext><body>',
|
||||||
|
'<p t="98550" d="3010" w="1" a="1">\n</p>',
|
||||||
|
'<p t="98560" d="3000" w="1"><s ac="0">ありがとうって言えないよね。こんなんじゃ。</s></p>',
|
||||||
|
'<p t="106950" w="1" a="1">\n</p>',
|
||||||
|
'<p t="106960" d="3799" w="1"><s ac="0">私だったら無理だよ。</s></p>',
|
||||||
|
'</body></timedtext>',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
result,
|
||||||
|
[
|
||||||
|
'WEBVTT',
|
||||||
|
'',
|
||||||
|
'00:01:38.560 --> 00:01:46.950',
|
||||||
|
'ありがとうって言えないよね。こんなんじゃ。',
|
||||||
|
'',
|
||||||
|
'00:01:46.960 --> 00:01:50.759',
|
||||||
|
'私だったら無理だよ。',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('normalizeYoutubeAutoVtt strips cumulative rolling-caption prefixes', () => {
|
test('normalizeYoutubeAutoVtt strips cumulative rolling-caption prefixes', () => {
|
||||||
const result = normalizeYoutubeAutoVtt(
|
const result = normalizeYoutubeAutoVtt(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ interface YoutubeTimedTextRow {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface YoutubeTimedTextDocument {
|
||||||
|
rows: YoutubeTimedTextRow[];
|
||||||
|
// Start times of every <p> event, including empty window-append fillers.
|
||||||
|
// In the rolling auto-caption format YouTube displays each caption until the
|
||||||
|
// next window event and the row's own duration is often a 3000ms placeholder,
|
||||||
|
// so these timestamps are the only reliable source for cue end times.
|
||||||
|
eventStartsMs: number[];
|
||||||
|
hasRollingWindowEvents: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const YOUTUBE_TIMEDTEXT_EXTENSIONS = new Set(['srv1', 'srv2', 'srv3', 'ytsrv3']);
|
const YOUTUBE_TIMEDTEXT_EXTENSIONS = new Set(['srv1', 'srv2', 'srv3', 'ytsrv3']);
|
||||||
|
|
||||||
function decodeNumericEntity(match: string, codePoint: number): string {
|
function decodeNumericEntity(match: string, codePoint: number): string {
|
||||||
@@ -39,14 +49,24 @@ function parseAttributeMap(raw: string): Map<string, string> {
|
|||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractYoutubeTimedTextRows(xml: string): YoutubeTimedTextRow[] {
|
function extractYoutubeTimedTextDocument(xml: string): YoutubeTimedTextDocument {
|
||||||
const rows: YoutubeTimedTextRow[] = [];
|
const rows: YoutubeTimedTextRow[] = [];
|
||||||
|
const eventStartsMs: number[] = [];
|
||||||
|
let hasRollingWindowEvents = false;
|
||||||
|
|
||||||
for (const match of xml.matchAll(/<p\b([^>]*)>([\s\S]*?)<\/p>/g)) {
|
for (const match of xml.matchAll(/<p\b([^>]*)>([\s\S]*?)<\/p>/g)) {
|
||||||
const attrs = parseAttributeMap(match[1] ?? '');
|
const attrs = parseAttributeMap(match[1] ?? '');
|
||||||
const startMs = Number(attrs.get('t'));
|
const startMs = Number(attrs.get('t'));
|
||||||
|
if (!Number.isFinite(startMs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
eventStartsMs.push(startMs);
|
||||||
|
if (attrs.get('a') === '1') {
|
||||||
|
hasRollingWindowEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
const durationMs = Number(attrs.get('d'));
|
const durationMs = Number(attrs.get('d'));
|
||||||
if (!Number.isFinite(startMs) || !Number.isFinite(durationMs)) {
|
if (!Number.isFinite(durationMs)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +79,17 @@ function extractYoutubeTimedTextRows(xml: string): YoutubeTimedTextRow[] {
|
|||||||
rows.push({ startMs, durationMs, text });
|
rows.push({ startMs, durationMs, text });
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows;
|
eventStartsMs.sort((a, b) => a - b);
|
||||||
|
return { rows, eventStartsMs, hasRollingWindowEvents };
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNextEventStartMs(eventStartsMs: number[], afterMs: number): number | undefined {
|
||||||
|
for (const startMs of eventStartsMs) {
|
||||||
|
if (startMs > afterMs) {
|
||||||
|
return startMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatVttTimestamp(ms: number): string {
|
function formatVttTimestamp(ms: number): string {
|
||||||
@@ -79,7 +109,7 @@ export function isYoutubeTimedTextExtension(value: string | undefined): boolean
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function convertYoutubeTimedTextToVtt(xml: string): string {
|
export function convertYoutubeTimedTextToVtt(xml: string): string {
|
||||||
const rows = extractYoutubeTimedTextRows(xml);
|
const { rows, eventStartsMs, hasRollingWindowEvents } = extractYoutubeTimedTextDocument(xml);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return 'WEBVTT\n';
|
return 'WEBVTT\n';
|
||||||
}
|
}
|
||||||
@@ -90,10 +120,17 @@ export function convertYoutubeTimedTextToVtt(xml: string): string {
|
|||||||
const row = rows[index]!;
|
const row = rows[index]!;
|
||||||
const nextRow = rows[index + 1];
|
const nextRow = rows[index + 1];
|
||||||
const unclampedEnd = row.startMs + row.durationMs;
|
const unclampedEnd = row.startMs + row.durationMs;
|
||||||
|
// Rolling auto captions display until the next window event; the row's own
|
||||||
|
// duration is frequently a 3000ms placeholder that cuts long lines short.
|
||||||
|
const nextEventStart = hasRollingWindowEvents
|
||||||
|
? findNextEventStartMs(eventStartsMs, row.startMs)
|
||||||
|
: undefined;
|
||||||
const clampedEnd =
|
const clampedEnd =
|
||||||
nextRow && unclampedEnd > nextRow.startMs
|
nextEventStart !== undefined
|
||||||
? Math.max(row.startMs, nextRow.startMs - 1)
|
? nextEventStart
|
||||||
: unclampedEnd;
|
: nextRow && unclampedEnd > nextRow.startMs
|
||||||
|
? Math.max(row.startMs, nextRow.startMs - 1)
|
||||||
|
: unclampedEnd;
|
||||||
if (clampedEnd <= row.startMs) {
|
if (clampedEnd <= row.startMs) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5925,6 +5925,8 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
showDesktopNotification,
|
showDesktopNotification,
|
||||||
showOverlayNotification: (payload) =>
|
showOverlayNotification: (payload) =>
|
||||||
overlayNotificationsRuntime.showOverlayNotification(payload),
|
overlayNotificationsRuntime.showOverlayNotification(payload),
|
||||||
|
dismissOverlayNotification: (id) =>
|
||||||
|
overlayNotificationsRuntime.dismissOverlayNotification(id),
|
||||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||||
broadcastRuntimeOptionsChanged: () =>
|
broadcastRuntimeOptionsChanged: () =>
|
||||||
overlayVisibilityComposer.broadcastRuntimeOptionsChanged(),
|
overlayVisibilityComposer.broadcastRuntimeOptionsChanged(),
|
||||||
@@ -6415,6 +6417,8 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
|||||||
showDesktopNotification,
|
showDesktopNotification,
|
||||||
showOverlayNotification: (payload) =>
|
showOverlayNotification: (payload) =>
|
||||||
overlayNotificationsRuntime.showOverlayNotification(payload),
|
overlayNotificationsRuntime.showOverlayNotification(payload),
|
||||||
|
dismissOverlayNotification: (id) =>
|
||||||
|
overlayNotificationsRuntime.dismissOverlayNotification(id),
|
||||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||||
getCachedMediaPath: (currentVideoPath, kind) =>
|
getCachedMediaPath: (currentVideoPath, kind) =>
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
|||||||
getYoutubeMediaSourceUrl?: AnkiJimakuIpcRuntimeOptions['getYoutubeMediaSourceUrl'];
|
getYoutubeMediaSourceUrl?: AnkiJimakuIpcRuntimeOptions['getYoutubeMediaSourceUrl'];
|
||||||
showDesktopNotification: AnkiJimakuIpcRuntimeOptions['showDesktopNotification'];
|
showDesktopNotification: AnkiJimakuIpcRuntimeOptions['showDesktopNotification'];
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: AnkiJimakuIpcRuntimeOptions['createFieldGroupingCallback'];
|
createFieldGroupingCallback: AnkiJimakuIpcRuntimeOptions['createFieldGroupingCallback'];
|
||||||
broadcastRuntimeOptionsChanged: AnkiJimakuIpcRuntimeOptions['broadcastRuntimeOptionsChanged'];
|
broadcastRuntimeOptionsChanged: AnkiJimakuIpcRuntimeOptions['broadcastRuntimeOptionsChanged'];
|
||||||
getFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions['getFieldGroupingResolver'];
|
getFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions['getFieldGroupingResolver'];
|
||||||
@@ -334,6 +335,7 @@ export function createAnkiJimakuIpcRuntimeServiceDeps(
|
|||||||
: {}),
|
: {}),
|
||||||
showDesktopNotification: params.showDesktopNotification,
|
showDesktopNotification: params.showDesktopNotification,
|
||||||
showOverlayNotification: params.showOverlayNotification,
|
showOverlayNotification: params.showOverlayNotification,
|
||||||
|
dismissOverlayNotification: params.dismissOverlayNotification,
|
||||||
createFieldGroupingCallback: params.createFieldGroupingCallback,
|
createFieldGroupingCallback: params.createFieldGroupingCallback,
|
||||||
broadcastRuntimeOptionsChanged: params.broadcastRuntimeOptionsChanged,
|
broadcastRuntimeOptionsChanged: params.broadcastRuntimeOptionsChanged,
|
||||||
getFieldGroupingResolver: params.getFieldGroupingResolver,
|
getFieldGroupingResolver: params.getFieldGroupingResolver,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type InitializeOverlayRuntimeCore = (options: {
|
|||||||
} | null;
|
} | null;
|
||||||
setAnkiIntegration: (integration: unknown | null) => void;
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: () => (
|
createFieldGroupingCallback: () => (
|
||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
|||||||
getOverlayWindows: () => [],
|
getOverlayWindows: () => [],
|
||||||
getResolvedConfig: () => ({}),
|
getResolvedConfig: () => ({}),
|
||||||
showDesktopNotification: () => calls.push('notify'),
|
showDesktopNotification: () => calls.push('notify'),
|
||||||
|
showOverlayNotification: () => calls.push('show-overlay'),
|
||||||
|
dismissOverlayNotification: () => calls.push('dismiss-overlay'),
|
||||||
createFieldGroupingCallback: () => async () => ({
|
createFieldGroupingCallback: () => async () => ({
|
||||||
keepNoteId: 1,
|
keepNoteId: 1,
|
||||||
deleteNoteId: 2,
|
deleteNoteId: 2,
|
||||||
@@ -57,6 +59,8 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
|||||||
deps.refreshCurrentSubtitle?.();
|
deps.refreshCurrentSubtitle?.();
|
||||||
deps.syncOverlayShortcuts();
|
deps.syncOverlayShortcuts();
|
||||||
deps.showDesktopNotification('title', {});
|
deps.showDesktopNotification('title', {});
|
||||||
|
deps.showOverlayNotification?.({ title: 'title' });
|
||||||
|
deps.dismissOverlayNotification?.('notification-id');
|
||||||
|
|
||||||
const tracker = {
|
const tracker = {
|
||||||
close: () => {},
|
close: () => {},
|
||||||
@@ -73,6 +77,8 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
|||||||
'refresh-subtitle',
|
'refresh-subtitle',
|
||||||
'sync-shortcuts',
|
'sync-shortcuts',
|
||||||
'notify',
|
'notify',
|
||||||
|
'show-overlay',
|
||||||
|
'dismiss-overlay',
|
||||||
]);
|
]);
|
||||||
assert.equal(appState.windowTracker, tracker);
|
assert.equal(appState.windowTracker, tracker);
|
||||||
assert.deepEqual(appState.ankiIntegration, { id: 'anki' });
|
assert.deepEqual(appState.ankiIntegration, { id: 'anki' });
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
|||||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback'];
|
createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback'];
|
||||||
getKnownWordCacheStatePath: () => string;
|
getKnownWordCacheStatePath: () => string;
|
||||||
getCachedMediaPath?: OverlayRuntimeOptionsMainDeps['getCachedMediaPath'];
|
getCachedMediaPath?: OverlayRuntimeOptionsMainDeps['getCachedMediaPath'];
|
||||||
@@ -78,6 +79,7 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
|||||||
},
|
},
|
||||||
showDesktopNotification: deps.showDesktopNotification,
|
showDesktopNotification: deps.showDesktopNotification,
|
||||||
showOverlayNotification: deps.showOverlayNotification,
|
showOverlayNotification: deps.showOverlayNotification,
|
||||||
|
dismissOverlayNotification: deps.dismissOverlayNotification,
|
||||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
|
createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
|
||||||
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
|
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
|
||||||
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ test('build initialize overlay runtime options maps dependencies', () => {
|
|||||||
getRuntimeOptionsManager: () => null,
|
getRuntimeOptionsManager: () => null,
|
||||||
setAnkiIntegration: () => calls.push('set-anki'),
|
setAnkiIntegration: () => calls.push('set-anki'),
|
||||||
showDesktopNotification: () => calls.push('notify'),
|
showDesktopNotification: () => calls.push('notify'),
|
||||||
|
showOverlayNotification: () => calls.push('show-overlay'),
|
||||||
|
dismissOverlayNotification: () => calls.push('dismiss-overlay'),
|
||||||
createFieldGroupingCallback: () => async () => ({
|
createFieldGroupingCallback: () => async () => ({
|
||||||
keepNoteId: 1,
|
keepNoteId: 1,
|
||||||
deleteNoteId: 2,
|
deleteNoteId: 2,
|
||||||
@@ -47,6 +49,8 @@ test('build initialize overlay runtime options maps dependencies', () => {
|
|||||||
options.setWindowTracker(null);
|
options.setWindowTracker(null);
|
||||||
options.setAnkiIntegration(null);
|
options.setAnkiIntegration(null);
|
||||||
options.showDesktopNotification('title', {});
|
options.showDesktopNotification('title', {});
|
||||||
|
options.showOverlayNotification?.({ title: 'title' });
|
||||||
|
options.dismissOverlayNotification?.('notification-id');
|
||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
'create-main',
|
'create-main',
|
||||||
@@ -58,5 +62,7 @@ test('build initialize overlay runtime options maps dependencies', () => {
|
|||||||
'set-tracker',
|
'set-tracker',
|
||||||
'set-anki',
|
'set-anki',
|
||||||
'notify',
|
'notify',
|
||||||
|
'show-overlay',
|
||||||
|
'dismiss-overlay',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type OverlayRuntimeOptions = {
|
|||||||
setAnkiIntegration: (integration: unknown | null) => void;
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: () => (
|
createFieldGroupingCallback: () => (
|
||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
@@ -73,6 +74,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
|||||||
setAnkiIntegration: (integration: unknown | null) => void;
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification?: (id: string) => void;
|
||||||
createFieldGroupingCallback: () => (
|
createFieldGroupingCallback: () => (
|
||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
@@ -107,6 +109,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
|||||||
setAnkiIntegration: deps.setAnkiIntegration,
|
setAnkiIntegration: deps.setAnkiIntegration,
|
||||||
showDesktopNotification: deps.showDesktopNotification,
|
showDesktopNotification: deps.showDesktopNotification,
|
||||||
showOverlayNotification: deps.showOverlayNotification,
|
showOverlayNotification: deps.showOverlayNotification,
|
||||||
|
dismissOverlayNotification: deps.dismissOverlayNotification,
|
||||||
createFieldGroupingCallback: deps.createFieldGroupingCallback,
|
createFieldGroupingCallback: deps.createFieldGroupingCallback,
|
||||||
getKnownWordCacheStatePath: deps.getKnownWordCacheStatePath,
|
getKnownWordCacheStatePath: deps.getKnownWordCacheStatePath,
|
||||||
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
||||||
|
|||||||
Reference in New Issue
Block a user