mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-18 03:13:31 -07:00
fix(linux): auto-install managed plugin copy; include in asset updates (#127)
This commit is contained in:
@@ -243,6 +243,9 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
runUpdateCommand: async (args) => {
|
||||
calls.push(`runUpdateCommand:${args.updateLauncherPath ?? ''}`);
|
||||
},
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: async () => {
|
||||
calls.push('runEnsureLinuxRuntimePluginAssetsCommand');
|
||||
},
|
||||
printHelp: () => {
|
||||
calls.push('printHelp');
|
||||
},
|
||||
@@ -624,6 +627,7 @@ test('createCliCommandDepsRuntime reconnects MPV client when reconnect hook exis
|
||||
stop: () => {},
|
||||
hasMainWindow: () => true,
|
||||
runUpdateCommand: async () => {},
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: async () => {},
|
||||
runYoutubePlaybackFlow: async () => {},
|
||||
},
|
||||
dispatchSessionAction: async () => {},
|
||||
|
||||
@@ -97,6 +97,10 @@ export interface CliCommandServiceDeps {
|
||||
runStatsCommand: (args: CliArgs, source: CliCommandSource) => Promise<void>;
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
runUpdateCommand: (args: CliArgs, source: CliCommandSource) => Promise<void>;
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: (
|
||||
args: CliArgs,
|
||||
source: CliCommandSource,
|
||||
) => Promise<void>;
|
||||
runYoutubePlaybackFlow: (request: {
|
||||
url: string;
|
||||
mode: NonNullable<CliArgs['youtubeMode']>;
|
||||
@@ -182,6 +186,7 @@ interface AppCliRuntime {
|
||||
stop: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
runUpdateCommand: CliCommandServiceDeps['runUpdateCommand'];
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: CliCommandServiceDeps['runEnsureLinuxRuntimePluginAssetsCommand'];
|
||||
runYoutubePlaybackFlow: CliCommandServiceDeps['runYoutubePlaybackFlow'];
|
||||
}
|
||||
|
||||
@@ -292,6 +297,7 @@ export function createCliCommandDepsRuntime(
|
||||
runStatsCommand: options.jellyfin.runStatsCommand,
|
||||
runJellyfinCommand: options.jellyfin.runCommand,
|
||||
runUpdateCommand: options.app.runUpdateCommand,
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: options.app.runEnsureLinuxRuntimePluginAssetsCommand,
|
||||
runYoutubePlaybackFlow: options.app.runYoutubePlaybackFlow,
|
||||
printHelp: options.ui.printHelp,
|
||||
hasMainWindow: options.app.hasMainWindow,
|
||||
@@ -454,6 +460,19 @@ export function handleCliCommand(
|
||||
deps.stopApp();
|
||||
}
|
||||
});
|
||||
} else if (args.ensureLinuxRuntimePluginAssets) {
|
||||
const shouldStopAfterRun = source === 'initial' && !deps.hasMainWindow();
|
||||
deps
|
||||
.runEnsureLinuxRuntimePluginAssetsCommand(args, source)
|
||||
.catch((err) => {
|
||||
deps.error('runEnsureLinuxRuntimePluginAssetsCommand failed:', err);
|
||||
deps.showMpvOsd(`Linux runtime plugin install failed: ${(err as Error).message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
if (shouldStopAfterRun) {
|
||||
deps.stopApp();
|
||||
}
|
||||
});
|
||||
} else if (args.toggleSecondarySub) {
|
||||
deps.cycleSecondarySubMode();
|
||||
} else if (args.triggerFieldGrouping) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { resolveDefaultNotificationIconPath } from './notification';
|
||||
|
||||
test('default notification icon resolves packaged SubMiner asset when no per-notification icon is provided', () => {
|
||||
const path = resolveDefaultNotificationIconPath({
|
||||
platform: 'linux',
|
||||
resourcesPath: '/opt/SubMiner/resources',
|
||||
appPath: '/opt/SubMiner/resources/app.asar',
|
||||
dirname: '/opt/SubMiner/resources/app.asar/dist/core/utils',
|
||||
cwd: '/opt/SubMiner',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
fileExists: (candidate) => candidate === '/opt/SubMiner/resources/assets/SubMiner.png',
|
||||
});
|
||||
|
||||
assert.equal(path, '/opt/SubMiner/resources/assets/SubMiner.png');
|
||||
});
|
||||
|
||||
test('default notification icon prefers the square app icon when bundled images are available', () => {
|
||||
const path = resolveDefaultNotificationIconPath({
|
||||
platform: 'linux',
|
||||
resourcesPath: '/opt/SubMiner/resources',
|
||||
appPath: '/opt/SubMiner/resources/app.asar',
|
||||
dirname: '/opt/SubMiner/resources/app.asar/dist/core/utils',
|
||||
cwd: '/opt/SubMiner',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
fileExists: (candidate) =>
|
||||
candidate === '/opt/SubMiner/resources/assets/SubMiner.png' ||
|
||||
candidate === '/opt/SubMiner/resources/assets/SubMiner-square.png',
|
||||
});
|
||||
|
||||
assert.equal(path, '/opt/SubMiner/resources/assets/SubMiner-square.png');
|
||||
});
|
||||
|
||||
test('default notification icon avoids macOS tray template assets', () => {
|
||||
const seen: string[] = [];
|
||||
const path = resolveDefaultNotificationIconPath({
|
||||
platform: 'darwin',
|
||||
resourcesPath: '/Applications/SubMiner.app/Contents/Resources',
|
||||
appPath: '/Applications/SubMiner.app/Contents/Resources/app.asar',
|
||||
dirname: '/Applications/SubMiner.app/Contents/Resources/app.asar/dist/core/utils',
|
||||
cwd: '/Applications/SubMiner.app/Contents/Resources',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
fileExists: (candidate) => {
|
||||
seen.push(candidate);
|
||||
return candidate.endsWith('/assets/SubMiner-square.png');
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(path, '/Applications/SubMiner.app/Contents/Resources/assets/SubMiner-square.png');
|
||||
assert.equal(
|
||||
seen.some((candidate) => candidate.includes('SubMinerTemplate')),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('default notification icon resolves cwd fallback through injected deps', () => {
|
||||
const resolvedPath = resolveDefaultNotificationIconPath({
|
||||
platform: 'linux',
|
||||
resourcesPath: '/missing/resources',
|
||||
appPath: '/missing/app',
|
||||
dirname: '/missing/dist/core/utils',
|
||||
cwd: '/portable/SubMiner',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
fileExists: (candidate) => candidate === '/portable/SubMiner/assets/SubMiner-square.png',
|
||||
});
|
||||
|
||||
assert.equal(resolvedPath, '/portable/SubMiner/assets/SubMiner-square.png');
|
||||
});
|
||||
@@ -1,10 +1,57 @@
|
||||
import electron from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { createLogger } from '../../logger';
|
||||
|
||||
const { Notification, nativeImage } = electron;
|
||||
const logger = createLogger('core:notification');
|
||||
|
||||
export function resolveDefaultNotificationIconPath(deps: {
|
||||
platform: string;
|
||||
resourcesPath: string;
|
||||
appPath: string;
|
||||
dirname: string;
|
||||
cwd: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
fileExists: (path: string) => boolean;
|
||||
}): string | null {
|
||||
const iconNames =
|
||||
deps.platform === 'win32'
|
||||
? ['SubMiner.ico', 'SubMiner-square.png', 'SubMiner.png']
|
||||
: ['SubMiner-square.png', 'SubMiner.png'];
|
||||
|
||||
const baseDirs = [
|
||||
deps.joinPath(deps.resourcesPath, 'assets'),
|
||||
deps.joinPath(deps.appPath, 'assets'),
|
||||
deps.joinPath(deps.dirname, '..', 'assets'),
|
||||
deps.joinPath(deps.dirname, '..', '..', 'assets'),
|
||||
deps.joinPath(deps.cwd, 'assets'),
|
||||
];
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const iconName of iconNames) {
|
||||
const candidate = deps.joinPath(baseDir, iconName);
|
||||
if (deps.fileExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveRuntimeDefaultNotificationIconPath(): string | null {
|
||||
return resolveDefaultNotificationIconPath({
|
||||
platform: process.platform,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: electron.app?.getAppPath?.() ?? process.cwd(),
|
||||
dirname: __dirname,
|
||||
cwd: process.cwd(),
|
||||
joinPath: (...parts) => path.join(...parts),
|
||||
fileExists: (candidate) => fs.existsSync(candidate),
|
||||
});
|
||||
}
|
||||
|
||||
export function showDesktopNotification(
|
||||
title: string,
|
||||
options: { body?: string; icon?: string },
|
||||
@@ -19,19 +66,20 @@ export function showDesktopNotification(
|
||||
notificationOptions.body = options.body;
|
||||
}
|
||||
|
||||
if (options.icon) {
|
||||
const icon = options.icon ?? resolveRuntimeDefaultNotificationIconPath() ?? undefined;
|
||||
|
||||
if (icon) {
|
||||
const isFilePath =
|
||||
typeof options.icon === 'string' &&
|
||||
(options.icon.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(options.icon));
|
||||
typeof icon === 'string' && (icon.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(icon));
|
||||
|
||||
if (isFilePath) {
|
||||
if (fs.existsSync(options.icon)) {
|
||||
notificationOptions.icon = options.icon;
|
||||
if (fs.existsSync(icon)) {
|
||||
notificationOptions.icon = icon;
|
||||
} else {
|
||||
logger.warn('Notification icon file not found', options.icon);
|
||||
logger.warn('Notification icon file not found', icon);
|
||||
}
|
||||
} else if (typeof options.icon === 'string' && options.icon.startsWith('data:image/')) {
|
||||
const base64Data = options.icon.replace(/^data:image\/\w+;base64,/, '');
|
||||
} else if (typeof icon === 'string' && icon.startsWith('data:image/')) {
|
||||
const base64Data = icon.replace(/^data:image\/\w+;base64,/, '');
|
||||
try {
|
||||
const image = nativeImage.createFromBuffer(Buffer.from(base64Data, 'base64'));
|
||||
if (image.isEmpty()) {
|
||||
@@ -45,7 +93,7 @@ export function showDesktopNotification(
|
||||
logger.error('Failed to create notification icon from base64', err);
|
||||
}
|
||||
} else {
|
||||
notificationOptions.icon = options.icon;
|
||||
notificationOptions.icon = icon;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user