Compare commits

..

1 Commits

Author SHA1 Message Date
sudacode 89723e2ccb fix(updater): handle unsupported macOS app updates 2026-05-16 02:05:28 -07:00
8 changed files with 112 additions and 18 deletions
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: updater
- Fixed tray update checks for builds that cannot install native app updates, showing a manual install message instead of a restart prompt that cannot apply the update.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "subminer",
"productName": "SubMiner",
"desktopName": "SubMiner.desktop",
"version": "0.15.0-beta.1",
"version": "0.15.0-beta.2",
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
"packageManager": "bun@1.3.5",
"main": "dist/main-entry.js",
+16 -12
View File
@@ -3,21 +3,25 @@
## Highlights
### Added
- **Auto-Update:** Tray and `subminer -u` command-line update checks for new SubMiner releases, with app and launcher update prompts, checksum verification, configurable update notifications, and an opt-in prerelease channel for beta and RC builds.
- **First-Run Setup:** Guided setup flow to install Bun and the `subminer` command-line launcher on Linux, macOS, and Windows. On Windows, a `subminer.cmd` PATH shim is installed so you can type `subminer` in any terminal without adding the main executable to PATH.
**Auto-Update:** The tray and `subminer -u` now check for SubMiner releases and prompt you to install updates, covering the app itself, the command-line launcher, and Linux rofi themes. Update notifications are configurable, downloads are checksum-verified, and an opt-in prerelease channel lets you receive beta and RC builds.
**First-Run Setup:** The app can now install Bun and the `subminer` command-line launcher during first run on Linux, macOS, and Windows. On Windows, a `subminer.cmd` PATH shim lets you type `subminer` in any terminal without manually adding `SubMiner.exe` to PATH.
### Fixed
- **macOS Overlay:** Transient mpv window appearances no longer incorrectly hide the subtitle overlay; minimizing mpv still hides it as expected. mpv controls are also now clickable before hovering a subtitle bar.
- **Subtitle Sync Modal:** Opening the subtitle sync panel on macOS no longer flashes and dismisses on the first attempt, and no longer leaves stale modal state after syncing.
- **Updater Stability:** Linux tray and background update checks now use GitHub release metadata instead of the native Electron updater, preventing crashes. Unsafe native updater paths are avoided on all platforms.
- **Linux Launcher Update:** `subminer -u` on Linux now performs release updates directly from the launcher without requiring the tray app to be running. When already on the latest version it reports up to date without downloading assets. Support asset updates are limited to the Linux rofi theme.
- **Linux Launcher Install:** First-run launcher installs on Linux now use a valid Bun shebang so the installed launcher executes correctly.
- **macOS Setup:** First-run setup now correctly recognizes launchers already installed via Homebrew or user PATH directories, and manual installs avoid writing to Homebrew-managed locations.
- **Update Dialog:** macOS update dialogs are brought to the front when `subminer --update` is run from the command line.
- **Setup Flow:** `subminer app --setup` now correctly opens the setup window when SubMiner is already running in the background. The standalone setup process also quits after first-run completes, returning the terminal prompt instead of leaving the app open.
- **Build:** One-shot `make clean build install` flows now correctly pick up the AppImage produced by the current build rather than a stale previous one.
- **Tray Settings:** Closing Yomitan settings launched from the tray no longer quits the tray app, and loading settings no longer blocks other tray actions. A close button is shown within the Yomitan settings page on Hyprland where native window controls are unavailable. The embedded Yomitan popup preview is disabled in the tray settings window to prevent renderer hangs. Extension refreshes are now serialized to prevent startup race conditions, and session help modals can close correctly without mpv running.
**macOS Overlay:** Controls on the mpv window are now clickable before you hover the subtitle bars. Transient mpv window focus misses no longer hide the overlay; minimizing mpv still hides it as expected.
**Subtitle Sync:** The subtitle sync modal on macOS now opens reliably: no more flash-and-hide on the first attempt or stale modal state left after syncing.
**Updater:** Update checks on Linux now use GitHub release metadata instead of the native Electron updater, preventing tray app crashes. macOS builds that cannot auto-install updates show a manual install prompt instead of a non-functional restart prompt. macOS update dialogs are brought to the front when `subminer --update` is run from the launcher.
**Linux Command-Line Updater:** `subminer -u` now performs release updates directly from the launcher without requiring the tray app, and reports "up to date" without downloading assets when already on the latest version.
**Launcher Setup:** Linux first-run launcher installs now build with a valid Bun shebang. `subminer app --setup` correctly opens the setup flow even when SubMiner is already running in the background. On macOS, first-run setup recognizes existing launchers in Homebrew or user PATH directories, and manual installs avoid Homebrew-managed locations. The setup window now quits automatically after first-run setup completes, returning control to the terminal.
**Tray App:** Fixed several issues with tray-launched Yomitan settings: closing the window no longer quits the tray app, a close-only menu replaces the default native menu, and settings loading no longer blocks other tray actions. An in-page close button is now available on Hyprland, where native window controls may be absent. Disabled the embedded popup preview in the settings window to prevent renderer hangs. Fixed a startup race condition that could leave extension loading in an error state, and corrected focus handling for the session help modal when mpv is not running.
**Build:** One-shot `make clean build install` flows now correctly pick up the AppImage built in the same invocation.
## Installation
+2
View File
@@ -4697,6 +4697,8 @@ function getUpdateService() {
showUpdateAvailableDialog: (version) =>
updateDialogPresenter.showUpdateAvailableDialog(version),
showUpdateFailedDialog: (message) => updateDialogPresenter.showUpdateFailedDialog(message),
showManualUpdateRequiredDialog: (version) =>
updateDialogPresenter.showManualUpdateRequiredDialog(version),
downloadAppUpdate: () => appUpdater.downloadUpdate(),
showRestartDialog: () => updateDialogPresenter.showRestartDialog(),
quitAndInstall: () => appUpdater.quitAndInstall(),
+28 -1
View File
@@ -1,6 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createUpdateDialogPresenter, type ShowMessageBox } from './update-dialogs';
import {
createUpdateDialogPresenter,
showManualUpdateRequiredDialog,
type ShowMessageBox,
} from './update-dialogs';
test('update dialog presenter focuses app before showing macOS dialogs', async () => {
const calls: string[] = [];
@@ -35,3 +39,26 @@ test('update dialog presenter does not focus app before showing non-macOS dialog
assert.deepEqual(calls, ['dialog:SubMiner is up to date (v0.14.0)']);
});
test('manual update required dialog explains that automatic install is unavailable', async () => {
let shown:
| {
type?: string;
title?: string;
message: string;
detail?: string;
buttons?: string[];
}
| undefined;
const showMessageBox: ShowMessageBox = async (options) => {
shown = options;
return { response: 0 };
};
await showManualUpdateRequiredDialog(showMessageBox, '0.15.0-beta.1');
assert.equal(shown?.type, 'warning');
assert.equal(shown?.message, 'Manual install required');
assert.match(shown?.detail ?? '', /SubMiner v0\.15\.0-beta\.1 is available/);
assert.match(shown?.detail ?? '', /cannot install app updates automatically/);
});
+15
View File
@@ -50,6 +50,8 @@ export function createUpdateDialogPresenter(deps: UpdateDialogPresenterDeps) {
showUpdateAvailableDialog(showFocusedMessageBox, version),
showUpdateFailedDialog: (message: string) =>
showUpdateFailedDialog(showFocusedMessageBox, message),
showManualUpdateRequiredDialog: (version: string) =>
showManualUpdateRequiredDialog(showFocusedMessageBox, version),
showRestartDialog: () => showRestartDialog(showFocusedMessageBox),
};
}
@@ -81,6 +83,19 @@ export async function showRestartDialog(showMessageBox: ShowMessageBox): Promise
return result.response === 0 ? 'restart' : 'later';
}
export async function showManualUpdateRequiredDialog(
showMessageBox: ShowMessageBox,
version: string,
): Promise<void> {
await showMessageBox({
type: 'warning',
title: 'SubMiner Updates',
message: 'Manual install required',
detail: `SubMiner v${version} is available, but this build cannot install app updates automatically. Download and install the latest release, then reopen SubMiner.`,
buttons: ['Close'],
});
}
export async function showUpdateFailedDialog(
showMessageBox: ShowMessageBox,
message: string,
+41 -1
View File
@@ -37,6 +37,9 @@ function createDeps(overrides: Partial<UpdateServiceDeps> = {}) {
showUpdateFailedDialog: async (message) => {
calls.push(`failed:${message}`);
},
showManualUpdateRequiredDialog: async (version) => {
calls.push(`manual-install:${version}`);
},
downloadAppUpdate: async () => {
calls.push('download');
},
@@ -115,7 +118,44 @@ test('manual update check reports available when no update asset was applied', a
const result = await service.checkForUpdates({ source: 'manual' });
assert.equal(result.status, 'update-available');
assert.deepEqual(calls, ['available-dialog:0.15.0', 'launcher:stable']);
assert.deepEqual(calls, ['available-dialog:0.15.0', 'launcher:stable', 'manual-install:0.15.0']);
});
test('manual update check does not prompt restart when only launcher updates', async () => {
const { deps, calls } = createDeps({
checkAppUpdate: async () => ({ available: false, version: '0.14.0', canUpdate: false }),
fetchLatestStableRelease: async () => ({
tag_name: 'v0.15.0',
prerelease: false,
draft: false,
assets: [],
}),
showUpdateAvailableDialog: async (version) => {
calls.push(`available-dialog:${version}`);
return 'update';
},
updateLauncher: async (_launcherPath, channel) => {
calls.push(`launcher:${channel}`);
return { status: 'updated' };
},
showRestartDialog: async () => {
calls.push('restart-dialog');
return 'restart';
},
quitAndInstall: () => {
calls.push('quit-install');
},
});
const service = createUpdateService(deps);
const result = await service.checkForUpdates({ source: 'manual' });
assert.equal(result.status, 'update-available');
assert.deepEqual(calls, [
'available-dialog:0.15.0',
'launcher:stable',
'manual-install:0.15.0',
]);
});
test('automatic update check skips inside configured interval', async () => {
+5 -3
View File
@@ -48,6 +48,7 @@ export interface UpdateServiceDeps {
showNoUpdateDialog: (version: string) => Promise<void>;
showUpdateAvailableDialog: (version: string) => Promise<'update' | 'close'>;
showUpdateFailedDialog: (message: string) => Promise<void>;
showManualUpdateRequiredDialog: (version: string) => Promise<void>;
downloadAppUpdate: () => Promise<void>;
showRestartDialog: () => Promise<'restart' | 'later'>;
quitAndInstall: () => void | Promise<void>;
@@ -158,8 +159,9 @@ export function createUpdateService(deps: UpdateServiceDeps) {
return { status: 'update-available', version: latest.version };
}
const canInstallAppUpdate = appUpdate.available && appUpdate.canUpdate !== false;
let appUpdateApplied = false;
if (appUpdate.available && appUpdate.canUpdate !== false) {
if (canInstallAppUpdate) {
await deps.downloadAppUpdate();
appUpdateApplied = true;
}
@@ -168,8 +170,8 @@ export function createUpdateService(deps: UpdateServiceDeps) {
deps.log(`Launcher update requires manual command: ${launcherResult.command}`);
}
const launcherUpdateApplied = launcherResult.status === 'updated';
if (!appUpdateApplied && !launcherUpdateApplied) {
if (!appUpdateApplied) {
await deps.showManualUpdateRequiredDialog(latest.version);
return { status: 'update-available', version: latest.version };
}