feat(notifications): add overlay notifications with position config (#110)

This commit is contained in:
2026-06-10 22:46:52 -07:00
committed by GitHub
parent c09d009a3e
commit 7be1843c41
177 changed files with 7524 additions and 440 deletions
+230
View File
@@ -0,0 +1,230 @@
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 count_feedback(recorded, target)
local count = 0
for _, message in ipairs(recorded.feedback) do
if message == target then
count = count + 1
end
end
return count
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
if has_arg(args, "--show-visible-overlay") and not has_arg(args, "--start") then
local status = config.show_visible_overlay_status or 0
callback(status == 0, { status = status, stdout = "", stderr = "" }, nil)
return
end
callback(true, { status = 0, stdout = "", stderr = "" }, nil)
end
function mp.add_timeout(_, callback)
if config.run_timeouts_immediately and callback then
callback()
end
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
do
local runtime = create_restart_runtime({
osd_messages = false,
show_visible_overlay_status = 1,
})
runtime.process.restart_overlay()
assert_true(
count_feedback(runtime.recorded, "Restarted successfully") == 0,
"restart should not show success feedback when show-visible-overlay fails after ready ping"
)
assert_true(
runtime.recorded.feedback[#runtime.recorded.feedback] == "Restart failed",
"restart should show failure feedback when show-visible-overlay fails after ready ping"
)
end
do
local statuses = { 1 }
for _ = 1, 20 do
statuses[#statuses + 1] = 1
end
local runtime = create_restart_runtime({
app_ping_statuses = statuses,
osd_messages = false,
run_timeouts_immediately = true,
show_visible_overlay_status = 1,
})
runtime.process.restart_overlay()
assert_true(
count_feedback(runtime.recorded, "Restarted successfully") == 0,
"restart should not show success feedback when fallback show-visible-overlay fails after ping timeout"
)
assert_true(
runtime.recorded.feedback[#runtime.recorded.feedback] == "Restart failed",
"restart should show failure feedback when fallback show-visible-overlay fails after ping timeout"
)
end
print("plugin restart feedback tests: OK")
+136
View File
@@ -900,6 +900,31 @@ do
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "no",
auto_start_visible_overlay = "yes",
overlay_loading_osd = "yes",
osd_messages = false,
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for explicit early overlay loading OSD scenario: " .. tostring(err))
fire_event(recorded, "start-file")
assert_true(
has_osd_message(recorded.osd, "SubMiner: Overlay loading |"),
"explicit overlay loading OSD option should show spinner even when plugin auto-start is disabled"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
@@ -1539,6 +1564,91 @@ do
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
osd_messages = false,
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for early overlay loading OSD scenario: " .. tostring(err))
fire_event(recorded, "start-file")
assert_true(
has_osd_message(recorded.osd, "SubMiner: Overlay loading |"),
"auto-start visible overlay should force overlay loading OSD spinner on start-file"
)
assert_true(
#recorded.periodic_timers == 1,
"auto-start visible overlay should refresh the early overlay loading OSD"
)
local overlay_loading_timer = recorded.periodic_timers[1]
recorded.periodic_timers[1].callback()
assert_true(
has_osd_message(recorded.osd, "SubMiner: Overlay loading /"),
"auto-start visible overlay should advance the early overlay loading OSD spinner"
)
fire_event(recorded, "file-loaded")
assert_true(
overlay_loading_timer.killed ~= true,
"autoplay gate should keep forced overlay loading OSD alive while normal plugin OSD messages are disabled"
)
assert_true(
#recorded.periodic_timers == 1,
"autoplay gate should not replace forced overlay loading OSD with a suppressed tokenization OSD timer"
)
recorded.script_messages["subminer-autoplay-ready"]()
assert_true(
overlay_loading_timer.killed ~= true,
"autoplay readiness should not stop forced overlay loading OSD before overlay content is ready"
)
overlay_loading_timer.callback()
assert_true(
has_osd_message(recorded.osd, "SubMiner: Overlay loading -"),
"forced overlay loading OSD should keep spinning during the overlay startup gap"
)
assert_true(
recorded.script_messages["subminer-overlay-loading-ready"] ~= nil,
"overlay loading ready script message should be registered"
)
recorded.script_messages["subminer-overlay-loading-ready"]()
assert_true(
recorded.periodic_timers[1].killed == true,
"overlay loading ready should stop the early overlay loading OSD refresher"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "no",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for hidden overlay loading OSD scenario: " .. tostring(err))
fire_event(recorded, "start-file")
assert_true(
not has_osd_message(recorded.osd, "SubMiner: Overlay loading |"),
"auto-start hidden visible overlay should not show early overlay loading OSD"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
@@ -1759,6 +1869,32 @@ do
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
auto_start_pause_until_ready = "yes",
auto_start_pause_until_ready_owns_initial_pause = "yes",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
paused = true,
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for default pause timeout scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
assert_true(
has_timeout(recorded.timeouts, 30),
"pause-until-ready default timeout should give cold app startup 30 seconds"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",