mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-13 15:13:32 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1158be5b39 |
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: overlay
|
|
||||||
|
|
||||||
- Fixed macOS Yomitan popup focus after card mining or popup reload while still allowing click-away to close the popup without a hide/reappear cycle.
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Kept the visible overlay active while mpv advances to the next playlist item, even when the next episode loads after the warm transition delay.
|
||||||
@@ -51,6 +51,15 @@ function M.create(ctx)
|
|||||||
return reason == "reload" or reason == "redirect"
|
return reason == "reload" or reason == "redirect"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function has_next_playlist_item()
|
||||||
|
local playlist_count = mp.get_property_number("playlist-count")
|
||||||
|
local playlist_pos = mp.get_property_number("playlist-pos")
|
||||||
|
if type(playlist_count) ~= "number" or type(playlist_pos) ~= "number" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return playlist_count > 0 and playlist_pos >= 0 and playlist_pos < playlist_count - 1
|
||||||
|
end
|
||||||
|
|
||||||
local function clear_pending_visible_overlay_hide()
|
local function clear_pending_visible_overlay_hide()
|
||||||
local timer = state.pending_visible_overlay_hide_timer
|
local timer = state.pending_visible_overlay_hide_timer
|
||||||
if timer and timer.kill then
|
if timer and timer.kill then
|
||||||
@@ -63,6 +72,9 @@ function M.create(ctx)
|
|||||||
local resolve_auto_start_visible_overlay_enabled
|
local resolve_auto_start_visible_overlay_enabled
|
||||||
|
|
||||||
local function hide_visible_overlay_after_end_file()
|
local function hide_visible_overlay_after_end_file()
|
||||||
|
if has_next_playlist_item() then
|
||||||
|
return
|
||||||
|
end
|
||||||
if state.visible_overlay_requested == true and not resolve_auto_start_visible_overlay_enabled() then
|
if state.visible_overlay_requested == true and not resolve_auto_start_visible_overlay_enabled() then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ local function run_plugin_scenario(config)
|
|||||||
if name == "osd-height" then
|
if name == "osd-height" then
|
||||||
return config.osd_height or 720
|
return config.osd_height or 720
|
||||||
end
|
end
|
||||||
|
if name == "playlist-count" then
|
||||||
|
return config.playlist_count
|
||||||
|
end
|
||||||
|
if name == "playlist-pos" then
|
||||||
|
return config.playlist_pos
|
||||||
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -627,6 +633,46 @@ do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local scenario = {
|
||||||
|
process_list = "",
|
||||||
|
defer_timeouts = true,
|
||||||
|
option_overrides = {
|
||||||
|
binary_path = binary_path,
|
||||||
|
auto_start = "yes",
|
||||||
|
auto_start_visible_overlay = "yes",
|
||||||
|
auto_start_pause_until_ready = "yes",
|
||||||
|
socket_path = "/tmp/subminer-socket",
|
||||||
|
},
|
||||||
|
input_ipc_server = "/tmp/subminer-socket",
|
||||||
|
path = "/media/slow-episode-01.mkv",
|
||||||
|
media_title = "Slow Episode 1",
|
||||||
|
playlist_count = 2,
|
||||||
|
playlist_pos = 0,
|
||||||
|
files = {
|
||||||
|
[binary_path] = true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local recorded, err = run_plugin_scenario(scenario)
|
||||||
|
assert_true(recorded ~= nil, "plugin failed to load for slow warm playlist visibility scenario: " .. tostring(err))
|
||||||
|
fire_event(recorded, "file-loaded")
|
||||||
|
recorded.script_messages["subminer-autoplay-ready"]()
|
||||||
|
fire_event(recorded, "end-file", { reason = "eof" })
|
||||||
|
fire_pending_timeouts(recorded)
|
||||||
|
scenario.path = "/media/slow-episode-02.mkv"
|
||||||
|
scenario.media_title = "Slow Episode 2"
|
||||||
|
scenario.playlist_pos = 1
|
||||||
|
fire_event(recorded, "file-loaded")
|
||||||
|
assert_true(
|
||||||
|
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 0,
|
||||||
|
"slow playlist advance should preserve visible overlay state while the next episode is pending"
|
||||||
|
)
|
||||||
|
assert_true(
|
||||||
|
count_start_calls(recorded.async_calls) == 1,
|
||||||
|
"slow playlist visibility reuse should not issue another --start command"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
local scenario = {
|
local scenario = {
|
||||||
process_list = "",
|
process_list = "",
|
||||||
|
|||||||
@@ -5307,15 +5307,6 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
focusMainWindow: () => {
|
focusMainWindow: () => {
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
focusMacOSOverlayWindow({
|
|
||||||
platform: process.platform,
|
|
||||||
getOverlayWindow: () => mainWindow,
|
|
||||||
stealAppFocus: () => app.focus({ steal: true }),
|
|
||||||
warn: (message, details) => logger.warn(message, details),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!mainWindow.isFocused()) {
|
if (!mainWindow.isFocused()) {
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -842,12 +842,7 @@ test('nested popup close reasserts interactive state and focus when another popu
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupYomitanPopupFocusHarness(
|
test('window blur reclaims overlay focus while a yomitan popup remains visible on Windows', async () => {
|
||||||
options: {
|
|
||||||
isMacOSPlatform?: boolean;
|
|
||||||
visiblePopupHost?: boolean;
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
const ctx = createMouseTestContext();
|
const ctx = createMouseTestContext();
|
||||||
const previousWindow = (globalThis as { window?: unknown }).window;
|
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||||
const previousDocument = (globalThis as { document?: unknown }).document;
|
const previousDocument = (globalThis as { document?: unknown }).document;
|
||||||
@@ -860,7 +855,6 @@ function setupYomitanPopupFocusHarness(
|
|||||||
let overlayFocusCalls = 0;
|
let overlayFocusCalls = 0;
|
||||||
|
|
||||||
ctx.platform.shouldToggleMouseIgnore = true;
|
ctx.platform.shouldToggleMouseIgnore = true;
|
||||||
ctx.platform.isMacOSPlatform = options.isMacOSPlatform === true;
|
|
||||||
(ctx.dom.overlay as { focus?: (options?: { preventScroll?: boolean }) => void }).focus = () => {
|
(ctx.dom.overlay as { focus?: (options?: { preventScroll?: boolean }) => void }).focus = () => {
|
||||||
overlayFocusCalls += 1;
|
overlayFocusCalls += 1;
|
||||||
};
|
};
|
||||||
@@ -908,8 +902,8 @@ function setupYomitanPopupFocusHarness(
|
|||||||
querySelector: () => null,
|
querySelector: () => null,
|
||||||
querySelectorAll: (selector: string) => {
|
querySelectorAll: (selector: string) => {
|
||||||
if (
|
if (
|
||||||
(options.visiblePopupHost === true && selector === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR) ||
|
selector === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR ||
|
||||||
(options.visiblePopupHost === true && selector === YOMITAN_POPUP_HOST_SELECTOR)
|
selector === YOMITAN_POPUP_HOST_SELECTOR
|
||||||
) {
|
) {
|
||||||
return [visiblePopupHost];
|
return [visiblePopupHost];
|
||||||
}
|
}
|
||||||
@@ -933,111 +927,46 @@ function setupYomitanPopupFocusHarness(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlers = createMouseHandlers(ctx as never, {
|
|
||||||
modalStateReader: {
|
|
||||||
isAnySettingsModalOpen: () => false,
|
|
||||||
isAnyModalOpen: () => false,
|
|
||||||
},
|
|
||||||
applyYPercent: () => {},
|
|
||||||
getCurrentYPercent: () => 10,
|
|
||||||
persistSubtitlePositionPatch: () => {},
|
|
||||||
getSubtitleHoverAutoPauseEnabled: () => false,
|
|
||||||
getYomitanPopupAutoPauseEnabled: () => false,
|
|
||||||
getPlaybackPaused: async () => false,
|
|
||||||
sendMpvCommand: () => {},
|
|
||||||
});
|
|
||||||
handlers.setupYomitanObserver();
|
|
||||||
|
|
||||||
return {
|
|
||||||
ctx,
|
|
||||||
windowListeners,
|
|
||||||
ignoreCalls,
|
|
||||||
focusMainWindowCalls: () => focusMainWindowCalls,
|
|
||||||
windowFocusCalls: () => windowFocusCalls,
|
|
||||||
overlayFocusCalls: () => overlayFocusCalls,
|
|
||||||
restore: () => {
|
|
||||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
|
||||||
Object.defineProperty(globalThis, 'document', {
|
|
||||||
configurable: true,
|
|
||||||
value: previousDocument,
|
|
||||||
});
|
|
||||||
Object.defineProperty(globalThis, 'MutationObserver', {
|
|
||||||
configurable: true,
|
|
||||||
value: previousMutationObserver,
|
|
||||||
});
|
|
||||||
Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('window blur reclaims overlay focus while a yomitan popup remains visible on Windows', async () => {
|
|
||||||
const harness = setupYomitanPopupFocusHarness({ visiblePopupHost: true });
|
|
||||||
try {
|
try {
|
||||||
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
|
const handlers = createMouseHandlers(ctx as never, {
|
||||||
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
|
modalStateReader: {
|
||||||
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
|
isAnySettingsModalOpen: () => false,
|
||||||
harness.ignoreCalls.length = 0;
|
isAnyModalOpen: () => false,
|
||||||
|
},
|
||||||
|
applyYPercent: () => {},
|
||||||
|
getCurrentYPercent: () => 10,
|
||||||
|
persistSubtitlePositionPatch: () => {},
|
||||||
|
getSubtitleHoverAutoPauseEnabled: () => false,
|
||||||
|
getYomitanPopupAutoPauseEnabled: () => false,
|
||||||
|
getPlaybackPaused: async () => false,
|
||||||
|
sendMpvCommand: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
for (const listener of harness.windowListeners.get('blur') ?? []) {
|
handlers.setupYomitanObserver();
|
||||||
|
assert.equal(ctx.state.yomitanPopupVisible, true);
|
||||||
|
assert.equal(ctx.dom.overlay.classList.contains('interactive'), true);
|
||||||
|
assert.deepEqual(ignoreCalls, [{ ignore: false, forward: undefined }]);
|
||||||
|
ignoreCalls.length = 0;
|
||||||
|
|
||||||
|
for (const listener of windowListeners.get('blur') ?? []) {
|
||||||
listener();
|
listener();
|
||||||
}
|
}
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
|
assert.equal(ctx.state.yomitanPopupVisible, true);
|
||||||
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
|
assert.equal(ctx.dom.overlay.classList.contains('interactive'), true);
|
||||||
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
|
assert.deepEqual(ignoreCalls, [{ ignore: false, forward: undefined }]);
|
||||||
assert.equal(harness.focusMainWindowCalls(), 1);
|
assert.equal(focusMainWindowCalls, 1);
|
||||||
assert.equal(harness.windowFocusCalls(), 1);
|
assert.equal(windowFocusCalls, 1);
|
||||||
assert.equal(harness.overlayFocusCalls(), 1);
|
assert.equal(overlayFocusCalls, 1);
|
||||||
} finally {
|
} finally {
|
||||||
harness.restore();
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
}
|
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||||
});
|
Object.defineProperty(globalThis, 'MutationObserver', {
|
||||||
|
configurable: true,
|
||||||
test('window blur on macOS keeps yomitan popup interactive without stealing click-away focus', async () => {
|
value: previousMutationObserver,
|
||||||
const harness = setupYomitanPopupFocusHarness({
|
});
|
||||||
isMacOSPlatform: true,
|
Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
|
||||||
visiblePopupHost: true,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
|
|
||||||
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
|
|
||||||
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
|
|
||||||
harness.ignoreCalls.length = 0;
|
|
||||||
|
|
||||||
for (const listener of harness.windowListeners.get('blur') ?? []) {
|
|
||||||
listener();
|
|
||||||
}
|
|
||||||
await Promise.resolve();
|
|
||||||
|
|
||||||
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
|
|
||||||
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
|
|
||||||
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
|
|
||||||
assert.equal(harness.focusMainWindowCalls(), 0);
|
|
||||||
assert.equal(harness.windowFocusCalls(), 0);
|
|
||||||
assert.equal(harness.overlayFocusCalls(), 0);
|
|
||||||
} finally {
|
|
||||||
harness.restore();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('popup shown reclaims overlay focus on macOS', () => {
|
|
||||||
const harness = setupYomitanPopupFocusHarness({ isMacOSPlatform: true });
|
|
||||||
try {
|
|
||||||
harness.ignoreCalls.length = 0;
|
|
||||||
|
|
||||||
for (const listener of harness.windowListeners.get(YOMITAN_POPUP_SHOWN_EVENT) ?? []) {
|
|
||||||
listener();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
|
|
||||||
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
|
|
||||||
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
|
|
||||||
assert.equal(harness.focusMainWindowCalls(), 1);
|
|
||||||
assert.equal(harness.windowFocusCalls(), 1);
|
|
||||||
assert.equal(harness.overlayFocusCalls(), 1);
|
|
||||||
} finally {
|
|
||||||
harness.restore();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function createMouseHandlers(
|
|||||||
if (!ctx.platform.shouldToggleMouseIgnore) {
|
if (!ctx.platform.shouldToggleMouseIgnore) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ctx.platform.isLinuxPlatform) {
|
if (ctx.platform.isMacOSPlatform || ctx.platform.isLinuxPlatform) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,11 +467,7 @@ export function createMouseHandlers(
|
|||||||
reconcilePopupInteraction({ allowPause: true });
|
reconcilePopupInteraction({ allowPause: true });
|
||||||
|
|
||||||
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
|
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
|
||||||
reconcilePopupInteraction({
|
reconcilePopupInteraction({ assumeVisible: true, allowPause: true });
|
||||||
assumeVisible: true,
|
|
||||||
allowPause: true,
|
|
||||||
reclaimFocus: ctx.platform.isMacOSPlatform,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => {
|
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => {
|
||||||
@@ -495,7 +491,7 @@ export function createMouseHandlers(
|
|||||||
if (typeof document === 'undefined' || document.visibilityState !== 'visible') {
|
if (typeof document === 'undefined' || document.visibilityState !== 'visible') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reconcilePopupInteraction({ reclaimFocus: !ctx.platform.isMacOSPlatform });
|
reconcilePopupInteraction({ reclaimFocus: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user