fix(jellyfin): fix overlay toggle sync, redirect reload, and AppImage bi

- Sync visible-overlay state back to plugin via script messages to avoid toggle/hide drift
- Collapse duplicate toggle events within 250ms to prevent hide-then-show on single keypress
- Preserve manual hide across Jellyfin path-changing redirects even when media-title drops
- Rearm managed subtitle defaults on path-changing redirects
- Route toggleVisibleOverlay session binding through plugin toggle instead of app-side IPC
- Show Linux/Hyprland overlay passively (showInactive) to avoid stealing mpv keyboard focus
- Fix AppImage binary resolution to prefer $APPIMAGE env over mounted inner binary
- Add stats window layer management so delete/update dialogs appear above stats window
- Fix Jellyfin remote progress sync during Linux websocket reconnect windows
This commit is contained in:
2026-05-23 01:45:09 -07:00
parent 80d05aef27
commit 81b941fe8c
46 changed files with 1472 additions and 79 deletions
+7
View File
@@ -114,6 +114,13 @@ function M.create(ctx)
end
end
if not environment.is_windows() then
local appimage_path = resolve_binary_candidate(os.getenv("APPIMAGE"))
if appimage_path and appimage_path ~= "" then
return appimage_path
end
end
return nil
end
+40
View File
@@ -33,6 +33,20 @@ function M.create(ctx)
return nil
end
local function resolve_media_title()
local media_title = mp.get_property("media-title")
if type(media_title) == "string" and media_title ~= "" then
return media_title
end
local filename = mp.get_property("filename")
if type(filename) == "string" and filename ~= "" then
return filename
end
return nil
end
local function is_reload_end_file(reason)
return reason == "reload" or reason == "redirect"
end
@@ -125,6 +139,10 @@ function M.create(ctx)
local function on_start_file()
if state.pending_reload_media_identity ~= nil then
local media_identity = resolve_media_identity()
if media_identity ~= nil and media_identity ~= state.pending_reload_media_identity then
rearm_managed_subtitle_load_defaults()
end
return
end
rearm_managed_subtitle_load_defaults()
@@ -132,12 +150,23 @@ function M.create(ctx)
local function on_file_loaded()
local media_identity = resolve_media_identity()
local media_title = resolve_media_title()
local retry_generation = next_auto_start_retry_generation()
local previous_media_identity = state.current_media_identity
local pending_reload_title = state.pending_reload_media_title
local pending_reload_reason = state.pending_reload_reason
local same_media_reload = (
media_identity ~= nil
and state.pending_reload_media_identity ~= nil
and media_identity == state.pending_reload_media_identity
) or (
state.pending_reload_media_identity ~= nil
and media_title ~= nil
and pending_reload_title ~= nil
and media_title == pending_reload_title
) or (
pending_reload_reason == "redirect"
and state.pending_reload_media_identity ~= nil
)
local same_media_loaded = (
media_identity ~= nil
@@ -146,7 +175,10 @@ function M.create(ctx)
)
local new_media_loaded = media_identity ~= nil and not same_media_reload and not same_media_loaded
state.pending_reload_media_identity = nil
state.pending_reload_media_title = nil
state.pending_reload_reason = nil
state.current_media_identity = media_identity
state.current_media_title = media_title
if new_media_loaded then
state.suppress_ready_overlay_restore = false
end
@@ -191,7 +223,10 @@ function M.create(ctx)
hover.clear_hover_overlay()
process.disarm_auto_play_ready_gate()
state.current_media_identity = nil
state.current_media_title = nil
state.pending_reload_media_identity = nil
state.pending_reload_media_title = nil
state.pending_reload_reason = nil
end
local function register_lifecycle_hooks()
@@ -207,11 +242,16 @@ function M.create(ctx)
local reason = type(event) == "table" and event.reason or nil
if is_reload_end_file(reason) then
state.pending_reload_media_identity = state.current_media_identity or resolve_media_identity()
state.pending_reload_media_title = state.current_media_title or resolve_media_title()
state.pending_reload_reason = reason
return
end
next_auto_start_retry_generation()
state.current_media_identity = nil
state.current_media_title = nil
state.pending_reload_media_identity = nil
state.pending_reload_media_title = nil
state.pending_reload_reason = nil
if state.overlay_running and reason ~= "quit" then
process.hide_visible_overlay()
end
+6
View File
@@ -17,6 +17,12 @@ function M.create(ctx)
mp.register_script_message("subminer-toggle", function()
process.toggle_overlay()
end)
mp.register_script_message("subminer-visible-overlay-hidden", function()
process.record_visible_overlay_visibility(false)
end)
mp.register_script_message("subminer-visible-overlay-shown", function()
process.record_visible_overlay_visibility(true)
end)
mp.register_script_message("subminer-menu", function()
ui.show_menu()
end)
+35
View File
@@ -7,6 +7,7 @@ local OVERLAY_RESTART_PING_MAX_ATTEMPTS = 20
local AUTO_PLAY_READY_LOADING_OSD = "Loading subtitle tokenization..."
local AUTO_PLAY_READY_READY_OSD = "Subtitle tokenization ready"
local DEFAULT_AUTO_PLAY_READY_TIMEOUT_SECONDS = 15
local DUPLICATE_VISIBLE_OVERLAY_TOGGLE_SECONDS = 0.25
function M.create(ctx)
local mp = ctx.mp
@@ -91,6 +92,35 @@ function M.create(ctx)
end
end
local function record_visible_overlay_visibility(visible)
if visible then
state.visible_overlay_requested = true
state.suppress_ready_overlay_restore = false
return
end
state.visible_overlay_requested = false
state.suppress_ready_overlay_restore = true
end
local function should_ignore_duplicate_visible_overlay_toggle()
if type(mp.get_time) ~= "function" then
return false
end
local now = mp.get_time()
if type(now) ~= "number" then
return false
end
local previous = state.last_visible_overlay_toggle_time
state.last_visible_overlay_toggle_time = now
if type(previous) ~= "number" then
return false
end
local elapsed = now - previous
return elapsed >= 0 and elapsed < DUPLICATE_VISIBLE_OVERLAY_TOGGLE_SECONDS
end
local function normalize_socket_path(path)
if type(path) ~= "string" then
return nil
@@ -604,6 +634,10 @@ function M.create(ctx)
show_osd("Error: binary not found")
return
end
if should_ignore_duplicate_visible_overlay_toggle() then
subminer_log("debug", "process", "Ignoring duplicate visible overlay toggle")
return
end
if state.visible_overlay_requested == true then
state.suppress_ready_overlay_restore = true
hide_visible_overlay({ resume_playback = false })
@@ -751,6 +785,7 @@ function M.create(ctx)
build_command_args = build_command_args,
has_matching_mpv_ipc_socket = has_matching_mpv_ipc_socket,
run_control_command_async = run_control_command_async,
record_visible_overlay_visibility = record_visible_overlay_visibility,
run_binary_command_async = run_binary_command_async,
parse_start_script_message_overrides = parse_start_script_message_overrides,
ensure_texthooker_running = ensure_texthooker_running,
+5
View File
@@ -312,6 +312,11 @@ function M.create(ctx)
return
end
if binding.actionId == "toggleVisibleOverlay" and type(process.toggle_overlay) == "function" then
process.toggle_overlay()
return
end
if binding.actionId == "copySubtitleMultiple" or binding.actionId == "mineSentenceMultiple" then
start_numeric_selection(binding.actionId, numeric_selection_timeout_ms, binding.key.modifiers)
return
+4
View File
@@ -36,8 +36,12 @@ function M.new()
suppress_ready_overlay_restore = false,
force_ready_overlay_restore = false,
visible_overlay_requested = nil,
last_visible_overlay_toggle_time = nil,
current_media_identity = nil,
current_media_title = nil,
pending_reload_media_identity = nil,
pending_reload_media_title = nil,
pending_reload_reason = nil,
auto_start_retry_generation = 0,
session_binding_generation = 0,
session_binding_names = {},