diff --git a/changes/overlay-notifications.md b/changes/overlay-notifications.md index f4ff50fb..47a7aca0 100644 --- a/changes/overlay-notifications.md +++ b/changes/overlay-notifications.md @@ -10,6 +10,7 @@ breaking: true - Preserved character dictionary checking/building/importing/ready phases in overlay notification history and sent those phases to system notifications when `notificationType` is `both`. - Initialized the tray and visible overlay shell before deferred tokenization warmups finish on visible-overlay startup, while keeping playback paused until SubMiner reports autoplay readiness. - Kept playback feedback such as subtitle visibility, subtitle track, subtitle delay, and AniSkip prompt/skip text on overlay/OSD surfaces only; desktop/system notifications are reserved for real notifications like mined cards, errors, and updates. +- Routed mpv-plugin restart feedback through the configured overlay/OSD feedback surface so `overlay` and `both` notification modes show restart progress and completion in the overlay, while keeping the loading OSD spinner visible until the overlay reports ready. - Reused the active primary/secondary subtitle mode overlay notification while cycling modes so rapid toggles update one card instead of stacking duplicate feedback. - Updated repeated progress notifications such as subsync syncing in place so their spinner stays live instead of flickering on every tick. - Stabilized overlay startup notifications so queued progress updates do not replay the card entrance animation or trigger macOS pass-through hover flicker after the loading OSD hands off to overlay notifications. diff --git a/package.json b/package.json index c161eed9..ee12fd4a 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/integrations.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts", "test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/integrations.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js", "test:config:smoke:dist": "bun test dist/config/path-resolution.test.js", - "test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua", + "test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-restart-feedback.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src", "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/shared/mpv-x11-backend.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/settings-window-z-order.test.ts src/core/services/hyprland-window-placement.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-manager.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/stats-window-lifecycle.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/overlay-runtime.test.ts src/main/runtime/macos-mpv-focus.test.ts src/main/runtime/macos-modal-focus-handoff.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-ready-gate.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/main/runtime/visible-overlay-autoplay-readiness.test.ts src/main/runtime/character-dictionary-manager-gate.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/core/utils/electron-backend.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/linux-overlay-pointer-interaction.test.ts src/main/runtime/linux-overlay-zorder-keepalive.test.ts src/main/runtime/config-settings-window.test.ts src/main/runtime/settings-window-z-order.test.ts src/main/runtime/setup-window-factory.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/log-export.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/overlay-content-measurement.test.ts src/renderer/subtitle-render.test.ts src/renderer/subtitle-render-word-class.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/main/character-dictionary-runtime/term-building.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts src/core/services/overlay-visibility.test.ts src/core/services/overlay-window-config.test.ts src/core/services/overlay-window.test.ts src/main/main-wiring.test.ts src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts src/main/runtime/mpv-main-event-actions.test.ts src/main/runtime/overlay-modal-input-state.test.ts src/main/runtime/overlay-window-factory-main-deps.test.ts src/main/runtime/overlay-window-factory.test.ts src/main/runtime/overlay-window-layout-main-deps.test.ts src/main/runtime/overlay-window-layout.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts src/main/runtime/yomitan-extension-overlay-reload.test.ts src/renderer/modals/subtitle-sidebar.test.ts src/renderer/overlay-mouse-ignore.test.ts src/main/runtime/linux-visible-overlay-window-mode.test.ts src/main/runtime/linux-x11-cursor-point.test.ts src/renderer/renderer-init-order.test.ts", diff --git a/plugin/subminer/process.lua b/plugin/subminer/process.lua index 9a334615..6afb6781 100644 --- a/plugin/subminer/process.lua +++ b/plugin/subminer/process.lua @@ -875,14 +875,22 @@ function M.create(ctx) return end + local function show_restart_feedback(message) + notify_playback_feedback(message, function() + show_osd(message) + end) + end + + start_overlay_loading_osd() subminer_log("info", "process", "Restarting overlay...") - show_osd("Restarting...") + show_restart_feedback("Restarting...") run_control_command_async("stop", nil, function(ok, result) if not ok then local reason = result and result.stderr or "unknown error" subminer_log("warn", "process", "Restart stop command failed: " .. reason) - show_osd("Restart failed") + stop_overlay_loading_osd() + show_restart_feedback("Restart failed") return end @@ -917,14 +925,17 @@ function M.create(ctx) "process", "Overlay start failed: " .. (error or (result and result.stderr) or "unknown error") ) - show_osd("Restart failed") + stop_overlay_loading_osd() + show_restart_feedback("Restart failed") else wait_for_app_ping_state(true, "own the single-instance lock", function() - run_control_command_async("show-visible-overlay") - show_osd("Restarted successfully") + run_control_command_async("show-visible-overlay", nil, function() + show_restart_feedback("Restarted successfully") + end) end, function() - run_control_command_async("show-visible-overlay") - show_osd("Restarted successfully") + run_control_command_async("show-visible-overlay", nil, function() + show_restart_feedback("Restarted successfully") + end) end) end end) @@ -933,7 +944,8 @@ function M.create(ctx) ensure_texthooker_running(function() end) end end, function() - show_osd("Restart failed") + stop_overlay_loading_osd() + show_restart_feedback("Restart failed") end) end) end diff --git a/scripts/test-plugin-restart-feedback.lua b/scripts/test-plugin-restart-feedback.lua new file mode 100644 index 00000000..df6f45a1 --- /dev/null +++ b/scripts/test-plugin-restart-feedback.lua @@ -0,0 +1,170 @@ +package.path = "plugin/subminer/?.lua;" .. package.path + +local process_module = require("process") +local options_helper = require("options") + +local function assert_true(condition, message) + if condition then + return + end + error(message or "assert_true failed") +end + +local function has_arg(args, target) + for _, value in ipairs(args or {}) do + if value == target then + return true + end + end + return false +end + +local function create_restart_runtime(config) + config = config or {} + local recorded = { + async_calls = {}, + feedback = {}, + osd = {}, + periodic_timers = {}, + } + local app_ping_index = 0 + local opts = { + binary_path = "/tmp/SubMiner", + socket_path = "/tmp/subminer-socket", + backend = "x11", + osd_messages = config.osd_messages == true, + texthooker_enabled = false, + log_level = "info", + } + local state = { + binary_path = opts.binary_path, + overlay_running = true, + texthooker_running = false, + } + + local mp = {} + + function mp.command_native_async(command, callback) + recorded.async_calls[#recorded.async_calls + 1] = command + local args = command.args or {} + if has_arg(args, "--playback-feedback") then + recorded.feedback[#recorded.feedback + 1] = args[#args] + callback(true, { status = 0, stdout = "", stderr = "" }, nil) + return + end + if has_arg(args, "--app-ping") then + app_ping_index = app_ping_index + 1 + local statuses = config.app_ping_statuses or { 1, 0 } + local status = statuses[app_ping_index] or statuses[#statuses] + callback(status == 0, { status = status, stdout = "", stderr = "" }, nil) + return + end + callback(true, { status = 0, stdout = "", stderr = "" }, nil) + end + + function mp.add_timeout(_, callback) + return { + killed = false, + kill = function(self) + self.killed = true + end, + callback = callback, + } + end + + function mp.add_periodic_timer() + local timer = { + killed = false, + kill = function(self) + self.killed = true + end, + } + recorded.periodic_timers[#recorded.periodic_timers + 1] = timer + return timer + end + + function mp.get_property(name) + if name == "input-ipc-server" then + return opts.socket_path + end + return "" + end + + function mp.get_time() + return 1 + end + + function mp.set_property_native() end + + local process = process_module.create({ + mp = mp, + utils = {}, + opts = opts, + state = state, + binary = { + ensure_binary_available = function() + return true + end, + }, + environment = { + is_linux = function() + return false + end, + detect_backend = function() + return "x11" + end, + resolve_subminer_config_dir = function() + return "/tmp" + end, + join_path = function(...) + return table.concat({ ... }, "/") + end, + }, + options_helper = options_helper, + log = { + normalize_log_level = function(level) + return level or "info" + end, + subminer_log = function() end, + show_osd = function(message, options) + if opts.osd_messages or (options and options.force == true) then + recorded.osd[#recorded.osd + 1] = message + end + end, + }, + }) + + return { + process = process, + recorded = recorded, + } +end + +do + local runtime = create_restart_runtime({ osd_messages = false }) + + runtime.process.restart_overlay() + + assert_true( + runtime.recorded.osd[1] == "Overlay loading |", + "restart should show the forced overlay loading OSD while the overlay reloads" + ) + assert_true( + #runtime.recorded.periodic_timers == 1, + "restart should refresh the forced overlay loading OSD while the overlay reloads" + ) + assert_true( + runtime.recorded.feedback[1] == "Restarting...", + "restart should route progress through playback feedback" + ) + assert_true( + runtime.recorded.feedback[#runtime.recorded.feedback] == "Restarted successfully", + "restart should route success through playback feedback" + ) + assert_true( + runtime.recorded.periodic_timers[1].killed ~= true, + "restart should keep the loading OSD alive until the overlay reports ready" + ) +end + +print("plugin restart feedback tests: OK")