mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract mining and clipboard runtime service
This commit is contained in:
179
src/core/services/mining-runtime-service.ts
Normal file
179
src/core/services/mining-runtime-service.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
interface SubtitleTimingTrackerLike {
|
||||||
|
getRecentBlocks: (count: number) => string[];
|
||||||
|
getCurrentSubtitle: () => string | null;
|
||||||
|
findTiming: (text: string) => { startTime: number; endTime: number } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnkiIntegrationLike {
|
||||||
|
updateLastAddedFromClipboard: (clipboardText: string) => Promise<void>;
|
||||||
|
triggerFieldGroupingForLastAddedCard: () => Promise<void>;
|
||||||
|
markLastCardAsAudioCard: () => Promise<void>;
|
||||||
|
createSentenceCard: (
|
||||||
|
sentence: string,
|
||||||
|
startTime: number,
|
||||||
|
endTime: number,
|
||||||
|
secondarySub?: string,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MpvClientLike {
|
||||||
|
connected: boolean;
|
||||||
|
currentSubText: string;
|
||||||
|
currentSubStart: number;
|
||||||
|
currentSubEnd: number;
|
||||||
|
currentSecondarySubText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleMultiCopyDigitService(
|
||||||
|
count: number,
|
||||||
|
deps: {
|
||||||
|
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
|
||||||
|
writeClipboardText: (text: string) => void;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
if (!deps.subtitleTimingTracker) return;
|
||||||
|
|
||||||
|
const availableCount = Math.min(count, 200);
|
||||||
|
const blocks = deps.subtitleTimingTracker.getRecentBlocks(availableCount);
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
deps.showMpvOsd("No subtitle history available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualCount = blocks.length;
|
||||||
|
deps.writeClipboardText(blocks.join("\n\n"));
|
||||||
|
if (actualCount < count) {
|
||||||
|
deps.showMpvOsd(`Only ${actualCount} lines available, copied ${actualCount}`);
|
||||||
|
} else {
|
||||||
|
deps.showMpvOsd(`Copied ${actualCount} lines`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyCurrentSubtitleService(deps: {
|
||||||
|
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
|
||||||
|
writeClipboardText: (text: string) => void;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}): void {
|
||||||
|
if (!deps.subtitleTimingTracker) {
|
||||||
|
deps.showMpvOsd("Subtitle tracker not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentSubtitle = deps.subtitleTimingTracker.getCurrentSubtitle();
|
||||||
|
if (!currentSubtitle) {
|
||||||
|
deps.showMpvOsd("No current subtitle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.writeClipboardText(currentSubtitle);
|
||||||
|
deps.showMpvOsd("Copied subtitle");
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAnkiIntegration(
|
||||||
|
ankiIntegration: AnkiIntegrationLike | null,
|
||||||
|
showMpvOsd: (text: string) => void,
|
||||||
|
): AnkiIntegrationLike | null {
|
||||||
|
if (!ankiIntegration) {
|
||||||
|
showMpvOsd("AnkiConnect integration not enabled");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ankiIntegration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateLastCardFromClipboardService(deps: {
|
||||||
|
ankiIntegration: AnkiIntegrationLike | null;
|
||||||
|
readClipboardText: () => string;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}): Promise<void> {
|
||||||
|
const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd);
|
||||||
|
if (!anki) return;
|
||||||
|
await anki.updateLastAddedFromClipboard(deps.readClipboardText());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function triggerFieldGroupingService(deps: {
|
||||||
|
ankiIntegration: AnkiIntegrationLike | null;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}): Promise<void> {
|
||||||
|
const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd);
|
||||||
|
if (!anki) return;
|
||||||
|
await anki.triggerFieldGroupingForLastAddedCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markLastCardAsAudioCardService(deps: {
|
||||||
|
ankiIntegration: AnkiIntegrationLike | null;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}): Promise<void> {
|
||||||
|
const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd);
|
||||||
|
if (!anki) return;
|
||||||
|
await anki.markLastCardAsAudioCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function mineSentenceCardService(deps: {
|
||||||
|
ankiIntegration: AnkiIntegrationLike | null;
|
||||||
|
mpvClient: MpvClientLike | null;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}): Promise<void> {
|
||||||
|
const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd);
|
||||||
|
if (!anki) return;
|
||||||
|
|
||||||
|
const mpvClient = deps.mpvClient;
|
||||||
|
if (!mpvClient || !mpvClient.connected) {
|
||||||
|
deps.showMpvOsd("MPV not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mpvClient.currentSubText) {
|
||||||
|
deps.showMpvOsd("No current subtitle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await anki.createSentenceCard(
|
||||||
|
mpvClient.currentSubText,
|
||||||
|
mpvClient.currentSubStart,
|
||||||
|
mpvClient.currentSubEnd,
|
||||||
|
mpvClient.currentSecondarySubText || undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleMineSentenceDigitService(
|
||||||
|
count: number,
|
||||||
|
deps: {
|
||||||
|
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
|
||||||
|
ankiIntegration: AnkiIntegrationLike | null;
|
||||||
|
getCurrentSecondarySubText: () => string | undefined;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
logError: (message: string, err: unknown) => void;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
if (!deps.subtitleTimingTracker || !deps.ankiIntegration) return;
|
||||||
|
|
||||||
|
const blocks = deps.subtitleTimingTracker.getRecentBlocks(count);
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
deps.showMpvOsd("No subtitle history available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timings: { startTime: number; endTime: number }[] = [];
|
||||||
|
for (const block of blocks) {
|
||||||
|
const timing = deps.subtitleTimingTracker.findTiming(block);
|
||||||
|
if (timing) timings.push(timing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timings.length === 0) {
|
||||||
|
deps.showMpvOsd("Subtitle timing not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeStart = Math.min(...timings.map((t) => t.startTime));
|
||||||
|
const rangeEnd = Math.max(...timings.map((t) => t.endTime));
|
||||||
|
const sentence = blocks.join(" ");
|
||||||
|
deps.ankiIntegration
|
||||||
|
.createSentenceCard(
|
||||||
|
sentence,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd,
|
||||||
|
deps.getCurrentSecondarySubText(),
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
deps.logError("mineSentenceMultiple failed:", err);
|
||||||
|
deps.showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
155
src/main.ts
155
src/main.ts
@@ -118,6 +118,15 @@ import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-sh
|
|||||||
import { createNumericShortcutSessionService } from "./core/services/numeric-shortcut-session-service";
|
import { createNumericShortcutSessionService } from "./core/services/numeric-shortcut-session-service";
|
||||||
import { handleCliCommandService } from "./core/services/cli-command-service";
|
import { handleCliCommandService } from "./core/services/cli-command-service";
|
||||||
import { cycleSecondarySubModeService } from "./core/services/secondary-subtitle-service";
|
import { cycleSecondarySubModeService } from "./core/services/secondary-subtitle-service";
|
||||||
|
import {
|
||||||
|
copyCurrentSubtitleService,
|
||||||
|
handleMineSentenceDigitService,
|
||||||
|
handleMultiCopyDigitService,
|
||||||
|
markLastCardAsAudioCardService,
|
||||||
|
mineSentenceCardService,
|
||||||
|
triggerFieldGroupingService,
|
||||||
|
updateLastCardFromClipboardService,
|
||||||
|
} from "./core/services/mining-runtime-service";
|
||||||
import { showDesktopNotification } from "./core/utils/notification";
|
import { showDesktopNotification } from "./core/utils/notification";
|
||||||
import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
|
import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
|
||||||
import { tokenizeSubtitleService } from "./core/services/tokenizer-service";
|
import { tokenizeSubtitleService } from "./core/services/tokenizer-service";
|
||||||
@@ -928,96 +937,49 @@ function startPendingMultiCopy(timeoutMs: number): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMultiCopyDigit(count: number): void {
|
function handleMultiCopyDigit(count: number): void {
|
||||||
if (!subtitleTimingTracker) return;
|
handleMultiCopyDigitService(count, {
|
||||||
|
subtitleTimingTracker,
|
||||||
const availableCount = Math.min(count, 200); // Max history size
|
writeClipboardText: (text) => clipboard.writeText(text),
|
||||||
const blocks = subtitleTimingTracker.getRecentBlocks(availableCount);
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
|
});
|
||||||
if (blocks.length === 0) {
|
|
||||||
showMpvOsd("No subtitle history available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualCount = blocks.length;
|
|
||||||
const clipboardText = blocks.join("\n\n");
|
|
||||||
clipboard.writeText(clipboardText);
|
|
||||||
|
|
||||||
if (actualCount < count) {
|
|
||||||
showMpvOsd(`Only ${actualCount} lines available, copied ${actualCount}`);
|
|
||||||
} else {
|
|
||||||
showMpvOsd(`Copied ${actualCount} lines`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyCurrentSubtitle(): void {
|
function copyCurrentSubtitle(): void {
|
||||||
if (!subtitleTimingTracker) {
|
copyCurrentSubtitleService({
|
||||||
showMpvOsd("Subtitle tracker not available");
|
subtitleTimingTracker,
|
||||||
return;
|
writeClipboardText: (text) => clipboard.writeText(text),
|
||||||
}
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
|
});
|
||||||
const currentSubtitle = subtitleTimingTracker.getCurrentSubtitle();
|
|
||||||
if (!currentSubtitle) {
|
|
||||||
showMpvOsd("No current subtitle");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clipboard.writeText(currentSubtitle);
|
|
||||||
showMpvOsd("Copied subtitle");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateLastCardFromClipboard(): Promise<void> {
|
async function updateLastCardFromClipboard(): Promise<void> {
|
||||||
if (!ankiIntegration) {
|
await updateLastCardFromClipboardService({
|
||||||
showMpvOsd("AnkiConnect integration not enabled");
|
ankiIntegration,
|
||||||
return;
|
readClipboardText: () => clipboard.readText(),
|
||||||
}
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
|
});
|
||||||
const clipboardText = clipboard.readText();
|
|
||||||
await ankiIntegration.updateLastAddedFromClipboard(clipboardText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function triggerFieldGrouping(): Promise<void> {
|
async function triggerFieldGrouping(): Promise<void> {
|
||||||
if (!ankiIntegration) {
|
await triggerFieldGroupingService({
|
||||||
showMpvOsd("AnkiConnect integration not enabled");
|
ankiIntegration,
|
||||||
return;
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
}
|
});
|
||||||
await ankiIntegration.triggerFieldGroupingForLastAddedCard();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markLastCardAsAudioCard(): Promise<void> {
|
async function markLastCardAsAudioCard(): Promise<void> {
|
||||||
if (!ankiIntegration) {
|
await markLastCardAsAudioCardService({
|
||||||
showMpvOsd("AnkiConnect integration not enabled");
|
ankiIntegration,
|
||||||
return;
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
}
|
});
|
||||||
await ankiIntegration.markLastCardAsAudioCard();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mineSentenceCard(): Promise<void> {
|
async function mineSentenceCard(): Promise<void> {
|
||||||
if (!ankiIntegration) {
|
await mineSentenceCardService({
|
||||||
showMpvOsd("AnkiConnect integration not enabled");
|
ankiIntegration,
|
||||||
return;
|
mpvClient,
|
||||||
}
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
|
});
|
||||||
if (!mpvClient || !mpvClient.connected) {
|
|
||||||
showMpvOsd("MPV not connected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = mpvClient.currentSubText;
|
|
||||||
if (!text) {
|
|
||||||
showMpvOsd("No current subtitle");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = mpvClient.currentSubStart;
|
|
||||||
const endTime = mpvClient.currentSubEnd;
|
|
||||||
const secondarySub = mpvClient.currentSecondarySubText || undefined;
|
|
||||||
|
|
||||||
await ankiIntegration.createSentenceCard(
|
|
||||||
text,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
secondarySub,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPendingMineSentenceMultiple(): void {
|
function cancelPendingMineSentenceMultiple(): void {
|
||||||
@@ -1037,40 +999,15 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMineSentenceDigit(count: number): void {
|
function handleMineSentenceDigit(count: number): void {
|
||||||
if (!subtitleTimingTracker || !ankiIntegration)
|
handleMineSentenceDigitService(count, {
|
||||||
return;
|
subtitleTimingTracker,
|
||||||
|
ankiIntegration,
|
||||||
const blocks = subtitleTimingTracker.getRecentBlocks(count);
|
getCurrentSecondarySubText: () => mpvClient?.currentSecondarySubText || undefined,
|
||||||
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
if (blocks.length === 0) {
|
logError: (message, err) => {
|
||||||
showMpvOsd("No subtitle history available");
|
console.error(message, err);
|
||||||
return;
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
const timings: { startTime: number; endTime: number }[] = [];
|
|
||||||
for (const block of blocks) {
|
|
||||||
const timing = subtitleTimingTracker.findTiming(block);
|
|
||||||
if (timing) {
|
|
||||||
timings.push(timing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timings.length === 0) {
|
|
||||||
showMpvOsd("Subtitle timing not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rangeStart = Math.min(...timings.map((t) => t.startTime));
|
|
||||||
const rangeEnd = Math.max(...timings.map((t) => t.endTime));
|
|
||||||
const sentence = blocks.join(" ");
|
|
||||||
|
|
||||||
const secondarySub = mpvClient?.currentSecondarySubText || undefined;
|
|
||||||
ankiIntegration
|
|
||||||
.createSentenceCard(sentence, rangeStart, rangeEnd, secondarySub)
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("mineSentenceMultiple failed:", err);
|
|
||||||
showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerOverlayShortcuts(): void {
|
function registerOverlayShortcuts(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user