mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-04 00:41:33 -07:00
feat: open texthooker from cli and tray
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: TASK-317
|
||||||
|
title: Add browser open affordance for texthooker
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-05-03 02:02'
|
||||||
|
updated_date: '2026-05-03 02:21'
|
||||||
|
labels:
|
||||||
|
- feature
|
||||||
|
- texthooker
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Add a `-o` flag to the texthooker subcommand to open the texthooker page in the user's default browser, and add a tray app option that triggers the same behavior. Implement with tests and existing launcher/tray patterns.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 `texthooker -o` starts/targets the texthooker page and opens it in the default browser.
|
||||||
|
- [x] #2 Tray app exposes a menu option to open the texthooker page in the default browser.
|
||||||
|
- [x] #3 Existing texthooker behavior without `-o` remains unchanged.
|
||||||
|
- [x] #4 Relevant CLI/tray behavior covered by tests.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Implemented `subminer texthooker -o` by parsing the launcher subcommand flag, forwarding `--open-browser` to the app texthooker command, and allowing that app arg to force browser opening even when `texthooker.openBrowser` is false. Added an `Open Texthooker` tray menu item wired through the same CLI command path. Updated docs-site usage/launcher/API docs and added a changelog fragment. Verification: targeted CLI/tray tests passed; `bun run typecheck` passed; `bun run docs:test` passed; `bun run changelog:lint` passed; `bun run test:env` passed; `bun run build` passed; `bun run test:smoke:dist` passed; `bun run docs:build` passed after installing docs-site deps. `bun run test:fast` is blocked by an existing broader-suite failure in `runSubsyncManual writes deterministic _retimed filename when replace is false` (`window.electronAPI` undefined), followed by Bun nested-test cascade errors.
|
||||||
|
|
||||||
|
Follow-up fix: `subminer texthooker -o` now opens `http://127.0.0.1:5174` from the launcher after a successful texthooker app handoff, so it works even when the installed SubMiner app binary does not yet understand the app-side `--open-browser` flag. Reproduced the reported behavior; confirmed the texthooker server was running at `127.0.0.1:5174`; added a launcher regression asserting the browser URL is opened. Verification: `bun test launcher/mpv.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts src/core/services/cli-command.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts` passed; `bun run typecheck` passed; `bun run build:launcher` passed.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: added
|
||||||
|
area: texthooker
|
||||||
|
|
||||||
|
- Texthooker: Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
||||||
@@ -85,6 +85,7 @@ subminer stats -b # start background stats daemon
|
|||||||
| `subminer dictionary --candidates <path>` | List AniList candidate matches for character dictionary correction |
|
| `subminer dictionary --candidates <path>` | List AniList candidate matches for character dictionary correction |
|
||||||
| `subminer dictionary --select <id> <path>` | Pin an AniList media ID for that target series |
|
| `subminer dictionary --select <id> <path>` | Pin an AniList media ID for that target series |
|
||||||
| `subminer texthooker` | Launch texthooker-only mode |
|
| `subminer texthooker` | Launch texthooker-only mode |
|
||||||
|
| `subminer texthooker -o` | Launch texthooker and open it in the default browser |
|
||||||
| `subminer app` | Pass arguments directly to SubMiner binary |
|
| `subminer app` | Pass arguments directly to SubMiner binary |
|
||||||
|
|
||||||
Use `subminer <subcommand> -h` for command-specific help.
|
Use `subminer <subcommand> -h` for command-specific help.
|
||||||
|
|||||||
@@ -103,12 +103,14 @@ subminer dictionary /path/to/file-or-directory # Generate character dictionary
|
|||||||
subminer dictionary --candidates /path/to/file.mkv
|
subminer dictionary --candidates /path/to/file.mkv
|
||||||
subminer dictionary --select 21355 /path/to/file.mkv
|
subminer dictionary --select 21355 /path/to/file.mkv
|
||||||
subminer texthooker # Launch texthooker-only mode
|
subminer texthooker # Launch texthooker-only mode
|
||||||
|
subminer texthooker -o # Launch texthooker and open it in your browser
|
||||||
subminer app --anilist # Pass args directly to SubMiner binary (example: AniList login flow)
|
subminer app --anilist # Pass args directly to SubMiner binary (example: AniList login flow)
|
||||||
|
|
||||||
# Direct packaged app control
|
# Direct packaged app control
|
||||||
SubMiner.AppImage --background # Start in background (tray + IPC wait, minimal logs)
|
SubMiner.AppImage --background # Start in background (tray + IPC wait, minimal logs)
|
||||||
SubMiner.AppImage --start --texthooker # Start overlay with texthooker
|
SubMiner.AppImage --start --texthooker # Start overlay with texthooker
|
||||||
SubMiner.AppImage --texthooker # Launch texthooker only (no overlay window)
|
SubMiner.AppImage --texthooker # Launch texthooker only (no overlay window)
|
||||||
|
SubMiner.AppImage --texthooker --open-browser # Launch texthooker and open browser
|
||||||
SubMiner.AppImage --setup # Open first-run setup popup
|
SubMiner.AppImage --setup # Open first-run setup popup
|
||||||
SubMiner.AppImage --stop # Stop overlay
|
SubMiner.AppImage --stop # Stop overlay
|
||||||
SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility
|
SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ Start it with either:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer texthooker
|
subminer texthooker
|
||||||
|
# or open the page immediately
|
||||||
|
subminer texthooker -o
|
||||||
```
|
```
|
||||||
|
|
||||||
or by leaving `texthooker.launchAtStartup` enabled.
|
or by leaving `texthooker.launchAtStartup` enabled.
|
||||||
@@ -273,7 +275,7 @@ Examples:
|
|||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- open a media picker, then call `subminer /path/to/file.mkv`
|
- open a media picker, then call `subminer /path/to/file.mkv`
|
||||||
- launch browser-only subtitle tooling with `subminer texthooker`
|
- launch browser-only subtitle tooling with `subminer texthooker -o`
|
||||||
- disable the helper UI for a session with `subminer --no-texthooker video.mkv`
|
- disable the helper UI for a session with `subminer --no-texthooker video.mkv`
|
||||||
|
|
||||||
#### Build an overlay-adjacent client
|
#### Build an overlay-adjacent client
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function createContext(): LauncherCommandContext {
|
|||||||
useTexthooker: false,
|
useTexthooker: false,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
texthookerOnly: false,
|
texthookerOnly: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
useRofi: false,
|
useRofi: false,
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
passwordStore: '',
|
passwordStore: '',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
|
|||||||
doctorRefreshKnownWords: false,
|
doctorRefreshKnownWords: false,
|
||||||
texthookerTriggered: false,
|
texthookerTriggered: false,
|
||||||
texthookerLogLevel: null,
|
texthookerLogLevel: null,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(parsed.jellyfin, false);
|
assert.equal(parsed.jellyfin, false);
|
||||||
@@ -157,3 +158,36 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
|
|||||||
assert.equal(parsed.configShow, true);
|
assert.equal(parsed.configShow, true);
|
||||||
assert.equal(parsed.logLevel, 'warn');
|
assert.equal(parsed.logLevel, 'warn');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('applyInvocationsToArgs maps texthooker browser-open request', () => {
|
||||||
|
const parsed = createDefaultArgs({});
|
||||||
|
|
||||||
|
applyInvocationsToArgs(parsed, {
|
||||||
|
jellyfinInvocation: null,
|
||||||
|
configInvocation: null,
|
||||||
|
mpvInvocation: null,
|
||||||
|
appInvocation: null,
|
||||||
|
dictionaryTriggered: false,
|
||||||
|
dictionaryTarget: null,
|
||||||
|
dictionaryLogLevel: null,
|
||||||
|
dictionaryCandidates: false,
|
||||||
|
dictionarySelect: false,
|
||||||
|
dictionaryAnilistId: null,
|
||||||
|
statsTriggered: false,
|
||||||
|
statsBackground: false,
|
||||||
|
statsStop: false,
|
||||||
|
statsCleanup: false,
|
||||||
|
statsCleanupVocab: false,
|
||||||
|
statsCleanupLifetime: false,
|
||||||
|
statsLogLevel: null,
|
||||||
|
doctorTriggered: false,
|
||||||
|
doctorLogLevel: null,
|
||||||
|
doctorRefreshKnownWords: false,
|
||||||
|
texthookerTriggered: true,
|
||||||
|
texthookerLogLevel: null,
|
||||||
|
texthookerOpenBrowser: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(parsed.texthookerOnly, true);
|
||||||
|
assert.equal(parsed.texthookerOpenBrowser, true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ export function createDefaultArgs(
|
|||||||
useTexthooker: true,
|
useTexthooker: true,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
texthookerOnly: false,
|
texthookerOnly: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
useRofi: false,
|
useRofi: false,
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
passwordStore: '',
|
passwordStore: '',
|
||||||
@@ -247,6 +248,7 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
|||||||
if (invocations.doctorTriggered) parsed.doctor = true;
|
if (invocations.doctorTriggered) parsed.doctor = true;
|
||||||
if (invocations.doctorRefreshKnownWords) parsed.doctorRefreshKnownWords = true;
|
if (invocations.doctorRefreshKnownWords) parsed.doctorRefreshKnownWords = true;
|
||||||
if (invocations.texthookerTriggered) parsed.texthookerOnly = true;
|
if (invocations.texthookerTriggered) parsed.texthookerOnly = true;
|
||||||
|
if (invocations.texthookerOpenBrowser) parsed.texthookerOpenBrowser = true;
|
||||||
|
|
||||||
if (invocations.jellyfinInvocation) {
|
if (invocations.jellyfinInvocation) {
|
||||||
if (invocations.jellyfinInvocation.logLevel) {
|
if (invocations.jellyfinInvocation.logLevel) {
|
||||||
|
|||||||
@@ -35,3 +35,10 @@ test('parseCliPrograms routes app alias arguments through passthrough mode', ()
|
|||||||
appArgs: ['--anilist', '--log-level', 'debug'],
|
appArgs: ['--anilist', '--log-level', 'debug'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parseCliPrograms captures texthooker browser-open flag', () => {
|
||||||
|
const result = parseCliPrograms(['texthooker', '-o'], 'subminer');
|
||||||
|
|
||||||
|
assert.equal(result.invocations.texthookerTriggered, true);
|
||||||
|
assert.equal(result.invocations.texthookerOpenBrowser, true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface CliInvocations {
|
|||||||
doctorRefreshKnownWords: boolean;
|
doctorRefreshKnownWords: boolean;
|
||||||
texthookerTriggered: boolean;
|
texthookerTriggered: boolean;
|
||||||
texthookerLogLevel: string | null;
|
texthookerLogLevel: string | null;
|
||||||
|
texthookerOpenBrowser: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyRootOptions(program: Command): void {
|
function applyRootOptions(program: Command): void {
|
||||||
@@ -152,6 +153,7 @@ export function parseCliPrograms(
|
|||||||
let doctorLogLevel: string | null = null;
|
let doctorLogLevel: string | null = null;
|
||||||
let doctorRefreshKnownWords = false;
|
let doctorRefreshKnownWords = false;
|
||||||
let texthookerLogLevel: string | null = null;
|
let texthookerLogLevel: string | null = null;
|
||||||
|
let texthookerOpenBrowser = false;
|
||||||
let doctorTriggered = false;
|
let doctorTriggered = false;
|
||||||
let texthookerTriggered = false;
|
let texthookerTriggered = false;
|
||||||
|
|
||||||
@@ -313,10 +315,12 @@ export function parseCliPrograms(
|
|||||||
commandProgram
|
commandProgram
|
||||||
.command('texthooker')
|
.command('texthooker')
|
||||||
.description('Launch texthooker-only mode')
|
.description('Launch texthooker-only mode')
|
||||||
|
.option('-o, --open-browser', 'Open texthooker in the default browser')
|
||||||
.option('--log-level <level>', 'Log level')
|
.option('--log-level <level>', 'Log level')
|
||||||
.action((options: Record<string, unknown>) => {
|
.action((options: Record<string, unknown>) => {
|
||||||
texthookerTriggered = true;
|
texthookerTriggered = true;
|
||||||
texthookerLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
|
texthookerLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
|
||||||
|
texthookerOpenBrowser = options.openBrowser === true;
|
||||||
});
|
});
|
||||||
|
|
||||||
commandProgram
|
commandProgram
|
||||||
@@ -369,6 +373,7 @@ export function parseCliPrograms(
|
|||||||
doctorRefreshKnownWords,
|
doctorRefreshKnownWords,
|
||||||
texthookerTriggered,
|
texthookerTriggered,
|
||||||
texthookerLogLevel,
|
texthookerLogLevel,
|
||||||
|
texthookerOpenBrowser,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,6 +270,29 @@ test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', ()
|
|||||||
assert.equal(error.code, 1);
|
assert.equal(error.code, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('launchTexthookerOnly forwards browser-open request to app command', () => {
|
||||||
|
const { dir } = createTempSocketPath();
|
||||||
|
const appPath = path.join(dir, 'fake-subminer.sh');
|
||||||
|
const argsPath = path.join(dir, 'args.txt');
|
||||||
|
const openedUrls: string[] = [];
|
||||||
|
fs.writeFileSync(appPath, `#!/bin/sh\nprintf '%s\\n' "$@" > "${argsPath}"\nexit 0\n`);
|
||||||
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
|
const error = withProcessExitIntercept(() => {
|
||||||
|
launchTexthookerOnly(appPath, makeArgs({ logLevel: 'info', texthookerOpenBrowser: true }), {
|
||||||
|
openBrowser: (url) => openedUrls.push(url),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(error.code, 0);
|
||||||
|
assert.deepEqual(fs.readFileSync(argsPath, 'utf8').trim().split('\n'), [
|
||||||
|
'--texthooker',
|
||||||
|
'--open-browser',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(openedUrls, ['http://127.0.0.1:5174']);
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
test('launchAppCommandDetached handles child process spawn errors', async () => {
|
test('launchAppCommandDetached handles child process spawn errors', async () => {
|
||||||
let uncaughtError: Error | null = null;
|
let uncaughtError: Error | null = null;
|
||||||
const onUncaughtException = (error: Error) => {
|
const onUncaughtException = (error: Error) => {
|
||||||
@@ -399,6 +422,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
|
|||||||
useTexthooker: false,
|
useTexthooker: false,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
texthookerOnly: false,
|
texthookerOnly: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
useRofi: false,
|
useRofi: false,
|
||||||
logLevel: 'error',
|
logLevel: 'error',
|
||||||
passwordStore: '',
|
passwordStore: '',
|
||||||
|
|||||||
+30
-1
@@ -831,8 +831,30 @@ export async function startOverlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function launchTexthookerOnly(appPath: string, args: Args): never {
|
export function openUrlInDefaultBrowser(url: string, logLevel: LogLevel): void {
|
||||||
|
const target =
|
||||||
|
process.platform === 'darwin'
|
||||||
|
? { command: 'open', args: [url] }
|
||||||
|
: process.platform === 'win32'
|
||||||
|
? { command: 'cmd', args: ['/c', 'start', '', url] }
|
||||||
|
: { command: 'xdg-open', args: [url] };
|
||||||
|
const result = spawnSync(target.command, target.args, {
|
||||||
|
stdio: 'ignore',
|
||||||
|
env: process.env,
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
log('warn', logLevel, `Failed to open browser for ${url}: ${result.error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function launchTexthookerOnly(
|
||||||
|
appPath: string,
|
||||||
|
args: Args,
|
||||||
|
deps: { openBrowser?: (url: string) => void } = {},
|
||||||
|
): never {
|
||||||
const overlayArgs = ['--texthooker'];
|
const overlayArgs = ['--texthooker'];
|
||||||
|
if (args.texthookerOpenBrowser) overlayArgs.push('--open-browser');
|
||||||
if (args.logLevel !== 'info') overlayArgs.push('--log-level', args.logLevel);
|
if (args.logLevel !== 'info') overlayArgs.push('--log-level', args.logLevel);
|
||||||
|
|
||||||
log('info', args.logLevel, 'Launching texthooker mode...');
|
log('info', args.logLevel, 'Launching texthooker mode...');
|
||||||
@@ -840,6 +862,13 @@ export function launchTexthookerOnly(appPath: string, args: Args): never {
|
|||||||
if (result.error) {
|
if (result.error) {
|
||||||
fail(`Failed to launch texthooker mode: ${result.error.message}`);
|
fail(`Failed to launch texthooker mode: ${result.error.message}`);
|
||||||
}
|
}
|
||||||
|
if (args.texthookerOpenBrowser && (result.status ?? 0) === 0) {
|
||||||
|
const url = 'http://127.0.0.1:5174';
|
||||||
|
const openBrowser =
|
||||||
|
deps.openBrowser ??
|
||||||
|
((browserUrl: string) => openUrlInDefaultBrowser(browserUrl, args.logLevel));
|
||||||
|
openBrowser(url);
|
||||||
|
}
|
||||||
process.exit(result.status ?? 0);
|
process.exit(result.status ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export interface Args {
|
|||||||
useTexthooker: boolean;
|
useTexthooker: boolean;
|
||||||
autoStartOverlay: boolean;
|
autoStartOverlay: boolean;
|
||||||
texthookerOnly: boolean;
|
texthookerOnly: boolean;
|
||||||
|
texthookerOpenBrowser: boolean;
|
||||||
useRofi: boolean;
|
useRofi: boolean;
|
||||||
logLevel: LogLevel;
|
logLevel: LogLevel;
|
||||||
passwordStore: string;
|
passwordStore: string;
|
||||||
|
|||||||
@@ -124,9 +124,12 @@ test('youtube playback does not use generic overlay-runtime bootstrap classifica
|
|||||||
|
|
||||||
test('standalone texthooker classification excludes integrated start flow', () => {
|
test('standalone texthooker classification excludes integrated start flow', () => {
|
||||||
const standalone = parseArgs(['--texthooker']);
|
const standalone = parseArgs(['--texthooker']);
|
||||||
|
const standaloneOpenBrowser = parseArgs(['--texthooker', '--open-browser']);
|
||||||
const integrated = parseArgs(['--start', '--texthooker']);
|
const integrated = parseArgs(['--start', '--texthooker']);
|
||||||
|
|
||||||
assert.equal(isStandaloneTexthookerCommand(standalone), true);
|
assert.equal(isStandaloneTexthookerCommand(standalone), true);
|
||||||
|
assert.equal(standaloneOpenBrowser.texthookerOpenBrowser, true);
|
||||||
|
assert.equal(isStandaloneTexthookerCommand(standaloneOpenBrowser), true);
|
||||||
assert.equal(isStandaloneTexthookerCommand(integrated), false);
|
assert.equal(isStandaloneTexthookerCommand(integrated), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export interface CliArgs {
|
|||||||
jellyfinRemoteAnnounce: boolean;
|
jellyfinRemoteAnnounce: boolean;
|
||||||
jellyfinPreviewAuth: boolean;
|
jellyfinPreviewAuth: boolean;
|
||||||
texthooker: boolean;
|
texthooker: boolean;
|
||||||
|
texthookerOpenBrowser: boolean;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
autoStartOverlay: boolean;
|
autoStartOverlay: boolean;
|
||||||
generateConfig: boolean;
|
generateConfig: boolean;
|
||||||
@@ -164,6 +165,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
jellyfinRemoteAnnounce: false,
|
jellyfinRemoteAnnounce: false,
|
||||||
jellyfinPreviewAuth: false,
|
jellyfinPreviewAuth: false,
|
||||||
texthooker: false,
|
texthooker: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
help: false,
|
help: false,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
generateConfig: false,
|
generateConfig: false,
|
||||||
@@ -327,6 +329,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
else if (arg === '--jellyfin-remote-announce') args.jellyfinRemoteAnnounce = true;
|
else if (arg === '--jellyfin-remote-announce') args.jellyfinRemoteAnnounce = true;
|
||||||
else if (arg === '--jellyfin-preview-auth') args.jellyfinPreviewAuth = true;
|
else if (arg === '--jellyfin-preview-auth') args.jellyfinPreviewAuth = true;
|
||||||
else if (arg === '--texthooker') args.texthooker = true;
|
else if (arg === '--texthooker') args.texthooker = true;
|
||||||
|
else if (arg === '--open-browser') args.texthookerOpenBrowser = true;
|
||||||
else if (arg === '--auto-start-overlay') args.autoStartOverlay = true;
|
else if (arg === '--auto-start-overlay') args.autoStartOverlay = true;
|
||||||
else if (arg === '--generate-config') args.generateConfig = true;
|
else if (arg === '--generate-config') args.generateConfig = true;
|
||||||
else if (arg === '--backup-overwrite') args.backupOverwrite = true;
|
else if (arg === '--backup-overwrite') args.backupOverwrite = true;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ test('printHelp includes configured texthooker port', () => {
|
|||||||
assert.match(output, /default: 7777/);
|
assert.match(output, /default: 7777/);
|
||||||
assert.match(output, /--launch-mpv.*Launch mpv with SubMiner defaults and exit/);
|
assert.match(output, /--launch-mpv.*Launch mpv with SubMiner defaults and exit/);
|
||||||
assert.match(output, /--stats\s+Open the stats dashboard in your browser/);
|
assert.match(output, /--stats\s+Open the stats dashboard in your browser/);
|
||||||
|
assert.match(output, /--open-browser\s+Open texthooker in your default browser/);
|
||||||
assert.doesNotMatch(output, /--refresh-known-words/);
|
assert.doesNotMatch(output, /--refresh-known-words/);
|
||||||
assert.match(output, /--setup\s+Open first-run setup window/);
|
assert.match(output, /--setup\s+Open first-run setup window/);
|
||||||
assert.match(output, /--anilist-status/);
|
assert.match(output, /--anilist-status/);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ ${B}Session${R}
|
|||||||
--stop Stop the running instance
|
--stop Stop the running instance
|
||||||
--stats Open the stats dashboard in your browser
|
--stats Open the stats dashboard in your browser
|
||||||
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
||||||
|
--open-browser Open texthooker in your default browser
|
||||||
|
|
||||||
${B}Overlay${R}
|
${B}Overlay${R}
|
||||||
--toggle-visible-overlay Toggle subtitle overlay
|
--toggle-visible-overlay Toggle subtitle overlay
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
jellyfinRemoteAnnounce: false,
|
jellyfinRemoteAnnounce: false,
|
||||||
jellyfinPreviewAuth: false,
|
jellyfinPreviewAuth: false,
|
||||||
texthooker: false,
|
texthooker: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
help: false,
|
help: false,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
generateConfig: false,
|
generateConfig: false,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
jellyfinRemoteAnnounce: false,
|
jellyfinRemoteAnnounce: false,
|
||||||
jellyfinPreviewAuth: false,
|
jellyfinPreviewAuth: false,
|
||||||
texthooker: false,
|
texthooker: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
help: false,
|
help: false,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
generateConfig: false,
|
generateConfig: false,
|
||||||
@@ -399,6 +400,21 @@ test('handleCliCommand runs texthooker flow with browser open', () => {
|
|||||||
assert.ok(calls.includes('openTexthookerInBrowser:http://127.0.0.1:5174'));
|
assert.ok(calls.includes('openTexthookerInBrowser:http://127.0.0.1:5174'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handleCliCommand opens texthooker browser when requested even if config disables auto-open', () => {
|
||||||
|
const { deps, calls } = createDeps({
|
||||||
|
shouldOpenTexthookerBrowser: () => false,
|
||||||
|
});
|
||||||
|
const args = {
|
||||||
|
...makeArgs({ texthooker: true }),
|
||||||
|
texthookerOpenBrowser: true,
|
||||||
|
} as CliArgs;
|
||||||
|
|
||||||
|
handleCliCommand(args, 'initial', deps);
|
||||||
|
|
||||||
|
assert.ok(calls.includes('ensureTexthookerRunning:5174:'));
|
||||||
|
assert.ok(calls.includes('openTexthookerInBrowser:http://127.0.0.1:5174'));
|
||||||
|
});
|
||||||
|
|
||||||
test('handleCliCommand forwards resolved websocket url to texthooker startup', () => {
|
test('handleCliCommand forwards resolved websocket url to texthooker startup', () => {
|
||||||
const { deps, calls } = createDeps({
|
const { deps, calls } = createDeps({
|
||||||
getTexthookerWebsocketUrl: () => 'ws://127.0.0.1:6678',
|
getTexthookerWebsocketUrl: () => 'ws://127.0.0.1:6678',
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ export function handleCliCommand(
|
|||||||
} else if (args.texthooker) {
|
} else if (args.texthooker) {
|
||||||
const texthookerPort = deps.getTexthookerPort();
|
const texthookerPort = deps.getTexthookerPort();
|
||||||
deps.ensureTexthookerRunning(texthookerPort, deps.getTexthookerWebsocketUrl());
|
deps.ensureTexthookerRunning(texthookerPort, deps.getTexthookerWebsocketUrl());
|
||||||
if (deps.shouldOpenTexthookerBrowser()) {
|
if (args.texthookerOpenBrowser || deps.shouldOpenTexthookerBrowser()) {
|
||||||
deps.openTexthookerInBrowser(`http://127.0.0.1:${texthookerPort}`);
|
deps.openTexthookerInBrowser(`http://127.0.0.1:${texthookerPort}`);
|
||||||
}
|
}
|
||||||
deps.log(`Texthooker available at http://127.0.0.1:${texthookerPort}`);
|
deps.log(`Texthooker available at http://127.0.0.1:${texthookerPort}`);
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
jellyfinRemoteAnnounce: false,
|
jellyfinRemoteAnnounce: false,
|
||||||
jellyfinPreviewAuth: false,
|
jellyfinPreviewAuth: false,
|
||||||
texthooker: false,
|
texthooker: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
help: false,
|
help: false,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
generateConfig: false,
|
generateConfig: false,
|
||||||
|
|||||||
@@ -5150,6 +5150,8 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
|||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
openSessionHelpModal: () => openSessionHelpOverlay(),
|
openSessionHelpModal: () => openSessionHelpOverlay(),
|
||||||
|
openTexthookerInBrowser: () =>
|
||||||
|
handleCliCommand(parseArgs(['--texthooker', '--open-browser'])),
|
||||||
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
|
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
|
||||||
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
|
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
|
||||||
showWindowsMpvLauncherSetup: () => process.platform === 'win32',
|
showWindowsMpvLauncherSetup: () => process.platform === 'win32',
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
jellyfinRemoteAnnounce: false,
|
jellyfinRemoteAnnounce: false,
|
||||||
jellyfinPreviewAuth: false,
|
jellyfinPreviewAuth: false,
|
||||||
texthooker: false,
|
texthooker: false,
|
||||||
|
texthookerOpenBrowser: false,
|
||||||
help: false,
|
help: false,
|
||||||
autoStartOverlay: false,
|
autoStartOverlay: false,
|
||||||
generateConfig: false,
|
generateConfig: false,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ test('build tray template handler wires actions and init guards', () => {
|
|||||||
const buildTemplate = createBuildTrayMenuTemplateHandler({
|
const buildTemplate = createBuildTrayMenuTemplateHandler({
|
||||||
buildTrayMenuTemplateRuntime: (handlers) => {
|
buildTrayMenuTemplateRuntime: (handlers) => {
|
||||||
handlers.openSessionHelp();
|
handlers.openSessionHelp();
|
||||||
|
handlers.openTexthookerInBrowser();
|
||||||
handlers.openFirstRunSetup();
|
handlers.openFirstRunSetup();
|
||||||
handlers.openWindowsMpvLauncherSetup();
|
handlers.openWindowsMpvLauncherSetup();
|
||||||
handlers.openYomitanSettings();
|
handlers.openYomitanSettings();
|
||||||
@@ -57,6 +58,7 @@ test('build tray template handler wires actions and init guards', () => {
|
|||||||
},
|
},
|
||||||
isOverlayRuntimeInitialized: () => initialized,
|
isOverlayRuntimeInitialized: () => initialized,
|
||||||
openSessionHelpModal: () => calls.push('help'),
|
openSessionHelpModal: () => calls.push('help'),
|
||||||
|
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||||
showFirstRunSetup: () => true,
|
showFirstRunSetup: () => true,
|
||||||
openFirstRunSetupWindow: () => calls.push('setup'),
|
openFirstRunSetupWindow: () => calls.push('setup'),
|
||||||
showWindowsMpvLauncherSetup: () => true,
|
showWindowsMpvLauncherSetup: () => true,
|
||||||
@@ -72,6 +74,7 @@ test('build tray template handler wires actions and init guards', () => {
|
|||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
'init',
|
'init',
|
||||||
'help',
|
'help',
|
||||||
|
'texthooker',
|
||||||
'setup',
|
'setup',
|
||||||
'setup',
|
'setup',
|
||||||
'yomitan',
|
'yomitan',
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function createResolveTrayIconPathHandler(deps: {
|
|||||||
export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
||||||
buildTrayMenuTemplateRuntime: (handlers: {
|
buildTrayMenuTemplateRuntime: (handlers: {
|
||||||
openSessionHelp: () => void;
|
openSessionHelp: () => void;
|
||||||
|
openTexthookerInBrowser: () => void;
|
||||||
openFirstRunSetup: () => void;
|
openFirstRunSetup: () => void;
|
||||||
showFirstRunSetup: boolean;
|
showFirstRunSetup: boolean;
|
||||||
openWindowsMpvLauncherSetup: () => void;
|
openWindowsMpvLauncherSetup: () => void;
|
||||||
@@ -42,6 +43,7 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
|||||||
initializeOverlayRuntime: () => void;
|
initializeOverlayRuntime: () => void;
|
||||||
isOverlayRuntimeInitialized: () => boolean;
|
isOverlayRuntimeInitialized: () => boolean;
|
||||||
openSessionHelpModal: () => void;
|
openSessionHelpModal: () => void;
|
||||||
|
openTexthookerInBrowser: () => void;
|
||||||
showFirstRunSetup: () => boolean;
|
showFirstRunSetup: () => boolean;
|
||||||
openFirstRunSetupWindow: () => void;
|
openFirstRunSetupWindow: () => void;
|
||||||
showWindowsMpvLauncherSetup: () => boolean;
|
showWindowsMpvLauncherSetup: () => boolean;
|
||||||
@@ -59,6 +61,9 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
|||||||
}
|
}
|
||||||
deps.openSessionHelpModal();
|
deps.openSessionHelpModal();
|
||||||
},
|
},
|
||||||
|
openTexthookerInBrowser: () => {
|
||||||
|
deps.openTexthookerInBrowser();
|
||||||
|
},
|
||||||
openFirstRunSetup: () => {
|
openFirstRunSetup: () => {
|
||||||
deps.openFirstRunSetupWindow();
|
deps.openFirstRunSetupWindow();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ test('tray main deps builders return mapped handlers', () => {
|
|||||||
initializeOverlayRuntime: () => calls.push('init'),
|
initializeOverlayRuntime: () => calls.push('init'),
|
||||||
isOverlayRuntimeInitialized: () => false,
|
isOverlayRuntimeInitialized: () => false,
|
||||||
openSessionHelpModal: () => calls.push('help'),
|
openSessionHelpModal: () => calls.push('help'),
|
||||||
|
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||||
showFirstRunSetup: () => true,
|
showFirstRunSetup: () => true,
|
||||||
openFirstRunSetupWindow: () => calls.push('setup'),
|
openFirstRunSetupWindow: () => calls.push('setup'),
|
||||||
showWindowsMpvLauncherSetup: () => true,
|
showWindowsMpvLauncherSetup: () => true,
|
||||||
@@ -37,6 +38,7 @@ test('tray main deps builders return mapped handlers', () => {
|
|||||||
|
|
||||||
const template = menuDeps.buildTrayMenuTemplateRuntime({
|
const template = menuDeps.buildTrayMenuTemplateRuntime({
|
||||||
openSessionHelp: () => calls.push('open-help'),
|
openSessionHelp: () => calls.push('open-help'),
|
||||||
|
openTexthookerInBrowser: () => calls.push('open-texthooker'),
|
||||||
openFirstRunSetup: () => calls.push('open-setup'),
|
openFirstRunSetup: () => calls.push('open-setup'),
|
||||||
showFirstRunSetup: true,
|
showFirstRunSetup: true,
|
||||||
openWindowsMpvLauncherSetup: () => calls.push('open-windows-mpv'),
|
openWindowsMpvLauncherSetup: () => calls.push('open-windows-mpv'),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function createBuildResolveTrayIconPathMainDepsHandler(deps: {
|
|||||||
export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
|
export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
|
||||||
buildTrayMenuTemplateRuntime: (handlers: {
|
buildTrayMenuTemplateRuntime: (handlers: {
|
||||||
openSessionHelp: () => void;
|
openSessionHelp: () => void;
|
||||||
|
openTexthookerInBrowser: () => void;
|
||||||
openFirstRunSetup: () => void;
|
openFirstRunSetup: () => void;
|
||||||
showFirstRunSetup: boolean;
|
showFirstRunSetup: boolean;
|
||||||
openWindowsMpvLauncherSetup: () => void;
|
openWindowsMpvLauncherSetup: () => void;
|
||||||
@@ -41,6 +42,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
|
|||||||
initializeOverlayRuntime: () => void;
|
initializeOverlayRuntime: () => void;
|
||||||
isOverlayRuntimeInitialized: () => boolean;
|
isOverlayRuntimeInitialized: () => boolean;
|
||||||
openSessionHelpModal: () => void;
|
openSessionHelpModal: () => void;
|
||||||
|
openTexthookerInBrowser: () => void;
|
||||||
showFirstRunSetup: () => boolean;
|
showFirstRunSetup: () => boolean;
|
||||||
openFirstRunSetupWindow: () => void;
|
openFirstRunSetupWindow: () => void;
|
||||||
showWindowsMpvLauncherSetup: () => boolean;
|
showWindowsMpvLauncherSetup: () => boolean;
|
||||||
@@ -55,6 +57,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
|
|||||||
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
||||||
isOverlayRuntimeInitialized: deps.isOverlayRuntimeInitialized,
|
isOverlayRuntimeInitialized: deps.isOverlayRuntimeInitialized,
|
||||||
openSessionHelpModal: deps.openSessionHelpModal,
|
openSessionHelpModal: deps.openSessionHelpModal,
|
||||||
|
openTexthookerInBrowser: deps.openTexthookerInBrowser,
|
||||||
showFirstRunSetup: deps.showFirstRunSetup,
|
showFirstRunSetup: deps.showFirstRunSetup,
|
||||||
openFirstRunSetupWindow: deps.openFirstRunSetupWindow,
|
openFirstRunSetupWindow: deps.openFirstRunSetupWindow,
|
||||||
showWindowsMpvLauncherSetup: deps.showWindowsMpvLauncherSetup,
|
showWindowsMpvLauncherSetup: deps.showWindowsMpvLauncherSetup,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ test('tray runtime handlers compose resolve/menu/ensure/destroy handlers', () =>
|
|||||||
},
|
},
|
||||||
isOverlayRuntimeInitialized: () => overlayInitialized,
|
isOverlayRuntimeInitialized: () => overlayInitialized,
|
||||||
openSessionHelpModal: () => {},
|
openSessionHelpModal: () => {},
|
||||||
|
openTexthookerInBrowser: () => {},
|
||||||
showFirstRunSetup: () => true,
|
showFirstRunSetup: () => true,
|
||||||
openFirstRunSetupWindow: () => {},
|
openFirstRunSetupWindow: () => {},
|
||||||
showWindowsMpvLauncherSetup: () => true,
|
showWindowsMpvLauncherSetup: () => true,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ test('tray menu template contains expected entries and handlers', () => {
|
|||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const template = buildTrayMenuTemplateRuntime({
|
const template = buildTrayMenuTemplateRuntime({
|
||||||
openSessionHelp: () => calls.push('help'),
|
openSessionHelp: () => calls.push('help'),
|
||||||
|
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||||
openFirstRunSetup: () => calls.push('setup'),
|
openFirstRunSetup: () => calls.push('setup'),
|
||||||
showFirstRunSetup: true,
|
showFirstRunSetup: true,
|
||||||
openWindowsMpvLauncherSetup: () => calls.push('windows-mpv'),
|
openWindowsMpvLauncherSetup: () => calls.push('windows-mpv'),
|
||||||
@@ -41,18 +42,24 @@ test('tray menu template contains expected entries and handlers', () => {
|
|||||||
quitApp: () => calls.push('quit'),
|
quitApp: () => calls.push('quit'),
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(template.length, 9);
|
assert.equal(template.length, 10);
|
||||||
assert.equal(template.some((entry) => entry.label === 'Open Overlay'), false);
|
assert.equal(
|
||||||
|
template.some((entry) => entry.label === 'Open Overlay'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.equal(template[0]!.label, 'Open Help');
|
assert.equal(template[0]!.label, 'Open Help');
|
||||||
template[0]!.click?.();
|
template[0]!.click?.();
|
||||||
template[7]!.type === 'separator' ? calls.push('separator') : calls.push('bad');
|
assert.equal(template[1]!.label, 'Open Texthooker');
|
||||||
template[8]!.click?.();
|
template[1]!.click?.();
|
||||||
assert.deepEqual(calls, ['help', 'separator', 'quit']);
|
template[8]!.type === 'separator' ? calls.push('separator') : calls.push('bad');
|
||||||
|
template[9]!.click?.();
|
||||||
|
assert.deepEqual(calls, ['help', 'texthooker', 'separator', 'quit']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tray menu template omits first-run setup entry when setup is complete', () => {
|
test('tray menu template omits first-run setup entry when setup is complete', () => {
|
||||||
const labels = buildTrayMenuTemplateRuntime({
|
const labels = buildTrayMenuTemplateRuntime({
|
||||||
openSessionHelp: () => undefined,
|
openSessionHelp: () => undefined,
|
||||||
|
openTexthookerInBrowser: () => undefined,
|
||||||
openFirstRunSetup: () => undefined,
|
openFirstRunSetup: () => undefined,
|
||||||
showFirstRunSetup: false,
|
showFirstRunSetup: false,
|
||||||
openWindowsMpvLauncherSetup: () => undefined,
|
openWindowsMpvLauncherSetup: () => undefined,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function resolveTrayIconPathRuntime(deps: {
|
|||||||
|
|
||||||
export type TrayMenuActionHandlers = {
|
export type TrayMenuActionHandlers = {
|
||||||
openSessionHelp: () => void;
|
openSessionHelp: () => void;
|
||||||
|
openTexthookerInBrowser: () => void;
|
||||||
openFirstRunSetup: () => void;
|
openFirstRunSetup: () => void;
|
||||||
showFirstRunSetup: boolean;
|
showFirstRunSetup: boolean;
|
||||||
openWindowsMpvLauncherSetup: () => void;
|
openWindowsMpvLauncherSetup: () => void;
|
||||||
@@ -52,6 +53,10 @@ export function buildTrayMenuTemplateRuntime(handlers: TrayMenuActionHandlers):
|
|||||||
label: 'Open Help',
|
label: 'Open Help',
|
||||||
click: handlers.openSessionHelp,
|
click: handlers.openSessionHelp,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Open Texthooker',
|
||||||
|
click: handlers.openTexthookerInBrowser,
|
||||||
|
},
|
||||||
...(handlers.showFirstRunSetup
|
...(handlers.showFirstRunSetup
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user