mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 17:01:25 -07:00
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import { ipcRenderer } from 'electron';
|
|
|
|
// Sandboxed preloads can only require `electron`, so these mirror IPC_CHANNELS in
|
|
// src/shared/ipc/contracts.ts (youtubeBrowserOpenVideo / youtubeBrowserToast).
|
|
const OPEN_VIDEO_CHANNEL = 'youtube-browser:open-video';
|
|
const TOAST_CHANNEL = 'youtube-browser:toast';
|
|
|
|
const MOUSE_BUTTON_MIDDLE = 1;
|
|
const MOUSE_BUTTON_BACK = 3;
|
|
const MOUSE_BUTTON_FORWARD = 4;
|
|
|
|
function isYoutubeVideoHref(href: string): boolean {
|
|
let url: URL;
|
|
try {
|
|
url = new URL(href);
|
|
} catch {
|
|
return false;
|
|
}
|
|
const host = url.hostname.toLowerCase();
|
|
if (host !== 'youtube.com' && !host.endsWith('.youtube.com')) return false;
|
|
return (
|
|
(url.pathname === '/watch' && url.searchParams.has('v')) ||
|
|
/^\/shorts\/[^/]+/.test(url.pathname)
|
|
);
|
|
}
|
|
|
|
function findVideoLink(event: MouseEvent): HTMLAnchorElement | null {
|
|
for (const node of event.composedPath()) {
|
|
if (node instanceof HTMLAnchorElement && node.href && isYoutubeVideoHref(node.href)) {
|
|
return node;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Capture phase runs before YouTube's own click handlers, so the page never starts its player.
|
|
// Plain click plays now; middle click or a modifier-click queues behind the current video.
|
|
function interceptVideoClick(event: MouseEvent): void {
|
|
const isPrimary = event.type === 'click' && event.button === 0;
|
|
const isMiddle = event.type === 'auxclick' && event.button === MOUSE_BUTTON_MIDDLE;
|
|
if (!isPrimary && !isMiddle) return;
|
|
const link = findVideoLink(event);
|
|
if (!link) return;
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
const queue = isMiddle || event.shiftKey || event.ctrlKey || event.metaKey;
|
|
ipcRenderer.send(OPEN_VIDEO_CHANNEL, { action: queue ? 'queue' : 'play', url: link.href });
|
|
// Immediate feedback: starting mpv can take a few seconds before the result toast arrives.
|
|
showToast(queue ? 'Queueing in mpv…' : 'Starting mpv…', true);
|
|
}
|
|
|
|
window.addEventListener('click', interceptVideoClick, true);
|
|
window.addEventListener('auxclick', interceptVideoClick, true);
|
|
|
|
// Chromium only maps mouse back/forward buttons to history inside a real browser.
|
|
window.addEventListener('mouseup', (event) => {
|
|
if (event.button === MOUSE_BUTTON_BACK) history.back();
|
|
if (event.button === MOUSE_BUTTON_FORWARD) history.forward();
|
|
});
|
|
|
|
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
function showToast(message: string, ok: boolean): void {
|
|
let toast = document.getElementById('subminer-youtube-toast');
|
|
if (!toast) {
|
|
toast = document.createElement('div');
|
|
toast.id = 'subminer-youtube-toast';
|
|
Object.assign(toast.style, {
|
|
position: 'fixed',
|
|
left: '50%',
|
|
bottom: '32px',
|
|
transform: 'translateX(-50%)',
|
|
zIndex: '2147483647',
|
|
padding: '10px 18px',
|
|
borderRadius: '8px',
|
|
font: '500 14px/1.4 Roboto, Arial, sans-serif',
|
|
color: '#fff',
|
|
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
|
|
pointerEvents: 'none',
|
|
transition: 'opacity 150ms ease',
|
|
});
|
|
document.body.appendChild(toast);
|
|
}
|
|
toast.textContent = message;
|
|
toast.style.background = ok ? '#1f1f1f' : '#8c1d18';
|
|
toast.style.opacity = '1';
|
|
if (toastTimer) clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => {
|
|
if (toast) toast.style.opacity = '0';
|
|
}, 2200);
|
|
}
|
|
|
|
ipcRenderer.on(TOAST_CHANNEL, (_event, payload: unknown) => {
|
|
if (typeof payload !== 'object' || payload === null) return;
|
|
const { message, ok } = payload as { message?: unknown; ok?: unknown };
|
|
if (typeof message === 'string') showToast(message, ok !== false);
|
|
});
|