mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 19:21:34 -07:00
feat(anime): add anime browser powered by Aniyomi extensions
- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached - Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config - Support searching all sources at once with streaming, per-source results and status - Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese - Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon - Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
This commit is contained in:
@@ -17,6 +17,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
yomitan: false,
|
||||
settings: false,
|
||||
syncWindow: false,
|
||||
animeBrowser: false,
|
||||
setup: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
|
||||
@@ -186,7 +186,8 @@ export function startAppLifecycle(initialArgs: CliArgs, deps: AppLifecycleServic
|
||||
(!deps.isDarwinPlatform() ||
|
||||
initialArgs.settings ||
|
||||
initialArgs.setup ||
|
||||
initialArgs.syncWindow)
|
||||
initialArgs.syncWindow ||
|
||||
initialArgs.animeBrowser)
|
||||
) {
|
||||
deps.quitApp();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
yomitan: false,
|
||||
settings: false,
|
||||
syncWindow: false,
|
||||
animeBrowser: false,
|
||||
setup: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
@@ -142,6 +143,9 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
openSyncUiWindow: () => {
|
||||
calls.push('openSyncUiWindow');
|
||||
},
|
||||
openAnimeBrowserWindow: () => {
|
||||
calls.push('openAnimeBrowserWindow');
|
||||
},
|
||||
openFirstRunSetup: (force?: boolean) => {
|
||||
calls.push(`openFirstRunSetup:${force === true ? 'force' : 'default'}`);
|
||||
},
|
||||
@@ -665,6 +669,7 @@ test('createCliCommandDepsRuntime reconnects MPV client when reconnect hook exis
|
||||
openYomitanSettings: () => {},
|
||||
openConfigSettingsWindow: () => {},
|
||||
openSyncUiWindow: () => {},
|
||||
openAnimeBrowserWindow: () => {},
|
||||
cycleSecondarySubMode: () => {},
|
||||
openRuntimeOptionsPalette: () => {},
|
||||
printHelp: () => {},
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface CliCommandServiceDeps {
|
||||
openYomitanSettingsDelayed: (delayMs: number) => void;
|
||||
openConfigSettingsWindow: () => void;
|
||||
openSyncUiWindow: () => void;
|
||||
openAnimeBrowserWindow: () => void;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
copyCurrentSubtitle: () => void;
|
||||
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||
@@ -172,6 +173,7 @@ interface UiCliRuntime {
|
||||
openYomitanSettings: () => void;
|
||||
openConfigSettingsWindow: () => void;
|
||||
openSyncUiWindow: () => void;
|
||||
openAnimeBrowserWindow: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
printHelp: () => void;
|
||||
@@ -277,6 +279,7 @@ export function createCliCommandDepsRuntime(
|
||||
},
|
||||
openConfigSettingsWindow: options.ui.openConfigSettingsWindow,
|
||||
openSyncUiWindow: options.ui.openSyncUiWindow,
|
||||
openAnimeBrowserWindow: options.ui.openAnimeBrowserWindow,
|
||||
setVisibleOverlayVisible: options.overlay.setVisible,
|
||||
copyCurrentSubtitle: options.mining.copyCurrentSubtitle,
|
||||
startPendingMultiCopy: options.mining.startPendingMultiCopy,
|
||||
@@ -422,6 +425,8 @@ export function handleCliCommand(
|
||||
deps.openConfigSettingsWindow();
|
||||
} else if (args.syncWindow) {
|
||||
deps.openSyncUiWindow();
|
||||
} else if (args.animeBrowser) {
|
||||
deps.openAnimeBrowserWindow();
|
||||
} else if (args.show || args.showVisibleOverlay) {
|
||||
deps.setVisibleOverlayVisible(true);
|
||||
} else if (args.hide || args.hideVisibleOverlay) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test, { beforeEach } from 'node:test';
|
||||
import {
|
||||
isDockIconRetained,
|
||||
releaseDockIcon,
|
||||
resetDockIconRetentionForTests,
|
||||
retainDockIcon,
|
||||
} from './dock-icon-visibility';
|
||||
|
||||
function createDockRecorder() {
|
||||
const calls: string[] = [];
|
||||
return {
|
||||
calls,
|
||||
dock: {
|
||||
show: () => {
|
||||
calls.push('show');
|
||||
},
|
||||
hide: () => {
|
||||
calls.push('hide');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetDockIconRetentionForTests();
|
||||
});
|
||||
|
||||
test('retainDockIcon shows the dock once and marks retention on darwin', () => {
|
||||
const recorder = createDockRecorder();
|
||||
|
||||
retainDockIcon({ dock: recorder.dock, platform: 'darwin' });
|
||||
retainDockIcon({ dock: recorder.dock, platform: 'darwin' });
|
||||
|
||||
assert.deepEqual(recorder.calls, ['show']);
|
||||
assert.equal(isDockIconRetained(), true);
|
||||
});
|
||||
|
||||
test('releaseDockIcon rehides only when the last retainer releases and overlay still needs it', () => {
|
||||
const recorder = createDockRecorder();
|
||||
|
||||
retainDockIcon({ dock: recorder.dock, platform: 'darwin' });
|
||||
retainDockIcon({ dock: recorder.dock, platform: 'darwin' });
|
||||
releaseDockIcon({ dock: recorder.dock, platform: 'darwin', shouldRehide: () => true });
|
||||
assert.deepEqual(recorder.calls, ['show']);
|
||||
assert.equal(isDockIconRetained(), true);
|
||||
|
||||
releaseDockIcon({ dock: recorder.dock, platform: 'darwin', shouldRehide: () => true });
|
||||
assert.deepEqual(recorder.calls, ['show', 'hide']);
|
||||
assert.equal(isDockIconRetained(), false);
|
||||
});
|
||||
|
||||
test('releaseDockIcon leaves the dock visible when no overlay needs the accessory transform', () => {
|
||||
const recorder = createDockRecorder();
|
||||
|
||||
retainDockIcon({ dock: recorder.dock, platform: 'darwin' });
|
||||
releaseDockIcon({ dock: recorder.dock, platform: 'darwin', shouldRehide: () => false });
|
||||
|
||||
assert.deepEqual(recorder.calls, ['show']);
|
||||
assert.equal(isDockIconRetained(), false);
|
||||
});
|
||||
|
||||
test('release without retain and non-darwin platforms are no-ops', () => {
|
||||
const recorder = createDockRecorder();
|
||||
|
||||
releaseDockIcon({ dock: recorder.dock, platform: 'darwin', shouldRehide: () => true });
|
||||
retainDockIcon({ dock: recorder.dock, platform: 'linux' });
|
||||
releaseDockIcon({ dock: recorder.dock, platform: 'win32', shouldRehide: () => true });
|
||||
|
||||
assert.deepEqual(recorder.calls, []);
|
||||
assert.equal(isDockIconRetained(), false);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// macOS-only: Electron's setVisibleOnAllWorkspaces(..., { visibleOnFullScreen: true })
|
||||
// transforms the whole app into a UIElement (accessory) process so overlay windows can
|
||||
// float above fullscreen Spaces. That side effect removes the Dock icon and the app's
|
||||
// Cmd+Tab entry, making regular windows (anime browser, settings) unreachable.
|
||||
//
|
||||
// While at least one regular window "retains" the Dock icon, overlay/stats level
|
||||
// assertions must pass skipTransformProcessType so they don't yank the app back to
|
||||
// accessory mode behind the user's back.
|
||||
|
||||
type DockLike = {
|
||||
show: () => Promise<void> | void;
|
||||
hide: () => void;
|
||||
};
|
||||
|
||||
let dockIconRetainCount = 0;
|
||||
|
||||
export function isDockIconRetained(): boolean {
|
||||
return dockIconRetainCount > 0;
|
||||
}
|
||||
|
||||
export function retainDockIcon(options: {
|
||||
dock: DockLike | null | undefined;
|
||||
platform?: NodeJS.Platform;
|
||||
}): void {
|
||||
if ((options.platform ?? process.platform) !== 'darwin') return;
|
||||
dockIconRetainCount += 1;
|
||||
if (dockIconRetainCount === 1) {
|
||||
void options.dock?.show();
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseDockIcon(options: {
|
||||
dock: DockLike | null | undefined;
|
||||
platform?: NodeJS.Platform;
|
||||
// True when an overlay window still needs the accessory transform to float
|
||||
// above fullscreen Spaces; false leaves the Dock icon visible.
|
||||
shouldRehide: () => boolean;
|
||||
}): void {
|
||||
if ((options.platform ?? process.platform) !== 'darwin') return;
|
||||
if (dockIconRetainCount === 0) return;
|
||||
dockIconRetainCount -= 1;
|
||||
if (dockIconRetainCount === 0 && options.shouldRehide()) {
|
||||
options.dock?.hide();
|
||||
}
|
||||
}
|
||||
|
||||
export function resetDockIconRetentionForTests(): void {
|
||||
dockIconRetainCount = 0;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type HyprlandPlacementStatus,
|
||||
} from './hyprland-window-placement';
|
||||
import { buildOverlayWindowOptions, OVERLAY_WINDOW_TITLES } from './overlay-window-options';
|
||||
import { isDockIconRetained } from './dock-icon-visibility';
|
||||
import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds';
|
||||
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||
export { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||
@@ -79,7 +80,12 @@ export function updateOverlayWindowBounds(
|
||||
export function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
||||
if (process.platform === 'darwin') {
|
||||
window.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||
// While a regular window (anime browser, ...) holds the Dock icon, skip the
|
||||
// accessory process transform so the app stays in the Dock and Cmd+Tab.
|
||||
window.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
skipTransformProcessType: isDockIconRetained(),
|
||||
});
|
||||
window.setFullScreenable(false);
|
||||
window.moveTop();
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
yomitan: false,
|
||||
settings: false,
|
||||
syncWindow: false,
|
||||
animeBrowser: false,
|
||||
setup: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
MessageBoxSyncOptions,
|
||||
} from 'electron';
|
||||
import type { WindowGeometry } from '../../types';
|
||||
import { isDockIconRetained } from './dock-icon-visibility';
|
||||
|
||||
const DEFAULT_STATS_WINDOW_WIDTH = 900;
|
||||
const DEFAULT_STATS_WINDOW_HEIGHT = 700;
|
||||
@@ -114,7 +115,10 @@ export function promoteStatsWindowLevel(
|
||||
): void {
|
||||
if (platform === 'darwin') {
|
||||
window.setAlwaysOnTop(true, 'screen-saver', 2);
|
||||
window.setVisibleOnAllWorkspaces?.(true, { visibleOnFullScreen: true });
|
||||
window.setVisibleOnAllWorkspaces?.(true, {
|
||||
visibleOnFullScreen: true,
|
||||
skipTransformProcessType: isDockIconRetained(),
|
||||
});
|
||||
window.setFullScreenable?.(false);
|
||||
window.moveTop();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user