mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-07 13:08:54 -07:00
718 lines
24 KiB
TypeScript
718 lines
24 KiB
TypeScript
import { normalizeJellyfinRecentServers } from './jellyfin-cli-auth';
|
|
|
|
type JellyfinSession = {
|
|
serverUrl: string;
|
|
username: string;
|
|
accessToken: string;
|
|
userId: string;
|
|
};
|
|
|
|
type JellyfinClientInfo = {
|
|
clientName: string;
|
|
clientVersion: string;
|
|
deviceId: string;
|
|
};
|
|
|
|
type FocusableWindowLike = {
|
|
focus: () => void;
|
|
};
|
|
|
|
type JellyfinSetupWebContentsLike = {
|
|
on: (event: 'will-navigate', handler: (event: unknown, url: string) => void) => void;
|
|
executeJavaScript?: (code: string, userGesture?: boolean) => Promise<unknown>;
|
|
};
|
|
|
|
type JellyfinSetupWindowLike = FocusableWindowLike & {
|
|
webContents: JellyfinSetupWebContentsLike;
|
|
loadURL: (url: string) => unknown;
|
|
on: (event: 'closed', handler: () => void) => void;
|
|
isDestroyed: () => boolean;
|
|
close: () => void;
|
|
};
|
|
|
|
export type JellyfinSetupAction = 'login' | 'logout' | 'done';
|
|
|
|
export type JellyfinSetupSubmission = {
|
|
action: JellyfinSetupAction;
|
|
server: string;
|
|
username: string;
|
|
password: string;
|
|
};
|
|
|
|
export type JellyfinSetupViewState = {
|
|
selectedServerUrl: string;
|
|
username: string;
|
|
hasStoredSession: boolean;
|
|
statusMessage: string;
|
|
statusKind: 'idle' | 'success' | 'error' | 'loading';
|
|
};
|
|
|
|
type JellyfinSetupViewOverrides = {
|
|
selectedServerUrl?: string;
|
|
username?: string;
|
|
statusMessage?: string;
|
|
statusKind?: JellyfinSetupViewState['statusKind'];
|
|
};
|
|
|
|
export type JellyfinSetupIpcResult = {
|
|
handled: boolean;
|
|
statusMessage?: string;
|
|
statusKind?: JellyfinSetupViewState['statusKind'];
|
|
};
|
|
|
|
type RegisterJellyfinSetupIpcHandler = (
|
|
handler: (submission: unknown) => Promise<JellyfinSetupIpcResult>,
|
|
) => () => void;
|
|
|
|
function escapeHtmlAttr(value: string): string {
|
|
return value.replace(/"/g, '"');
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null;
|
|
}
|
|
|
|
function normalizeSetupAction(value: unknown): JellyfinSetupAction {
|
|
return value === 'logout' || value === 'done' ? value : 'login';
|
|
}
|
|
|
|
function normalizeString(value: unknown): string {
|
|
return typeof value === 'string' ? value : '';
|
|
}
|
|
|
|
export function createMaybeFocusExistingJellyfinSetupWindowHandler(deps: {
|
|
getSetupWindow: () => FocusableWindowLike | null;
|
|
}) {
|
|
return (): boolean => {
|
|
const window = deps.getSetupWindow();
|
|
if (!window) {
|
|
return false;
|
|
}
|
|
window.focus();
|
|
return true;
|
|
};
|
|
}
|
|
|
|
export function buildJellyfinSetupViewState(input: {
|
|
config: {
|
|
serverUrl?: string | null;
|
|
username?: string | null;
|
|
recentServers?: unknown[];
|
|
};
|
|
defaultServerUrl: string;
|
|
hasStoredSession: boolean;
|
|
statusMessage?: string;
|
|
statusKind?: JellyfinSetupViewState['statusKind'];
|
|
selectedServerUrl?: string;
|
|
username?: string;
|
|
}): JellyfinSetupViewState {
|
|
const configServer = normalizeJellyfinRecentServers([input.config.serverUrl || ''])[0] || '';
|
|
const recentServers = normalizeJellyfinRecentServers(input.config.recentServers || []);
|
|
const defaultServer = normalizeJellyfinRecentServers([input.defaultServerUrl])[0] || '';
|
|
|
|
const selectedServerUrl =
|
|
normalizeJellyfinRecentServers([input.selectedServerUrl || ''])[0] ||
|
|
configServer ||
|
|
recentServers[0] ||
|
|
defaultServer;
|
|
|
|
return {
|
|
selectedServerUrl,
|
|
username: input.username ?? input.config.username ?? '',
|
|
hasStoredSession: input.hasStoredSession,
|
|
statusMessage: input.statusMessage || '',
|
|
statusKind: input.statusKind || 'idle',
|
|
};
|
|
}
|
|
|
|
export function buildJellyfinSetupFormHtml(state: JellyfinSetupViewState): string {
|
|
const statusClass = `status ${state.statusKind}`;
|
|
return `<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Jellyfin Setup</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--ctp-red: #ed8796;
|
|
--ctp-peach: #f5a97f;
|
|
--ctp-yellow: #eed49f;
|
|
--ctp-green: #a6da95;
|
|
--ctp-blue: #8aadf4;
|
|
--ctp-lavender: #b7bdf8;
|
|
--ctp-text: #cad3f5;
|
|
--ctp-subtext1: #b8c0e0;
|
|
--ctp-subtext0: #a5adcb;
|
|
--ctp-overlay2: #939ab7;
|
|
--ctp-overlay1: #8087a2;
|
|
--ctp-overlay0: #6e738d;
|
|
--ctp-surface1: #494d64;
|
|
--ctp-surface0: #363a4f;
|
|
--ctp-base: #24273a;
|
|
--ctp-mantle: #1e2030;
|
|
--ctp-crust: #181926;
|
|
--line: rgba(110, 115, 141, 0.28);
|
|
--line-soft: rgba(110, 115, 141, 0.14);
|
|
--text: var(--ctp-text);
|
|
--muted: var(--ctp-subtext0);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { width: 100%; height: 100%; margin: 0; }
|
|
html { background: var(--ctp-base); }
|
|
body {
|
|
min-height: 100vh;
|
|
background: var(--ctp-base);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Yu Gothic", sans-serif;
|
|
font-size: 13px;
|
|
line-height: 1.45;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
main { padding: 32px 22px; max-width: 520px; margin: 0 auto; }
|
|
h1 { margin: 0 0 6px; font-size: 20px; font-weight: 800; color: var(--ctp-text); letter-spacing: -0.01em; }
|
|
p { margin: 0 0 18px; color: var(--muted); font-size: 13px; line-height: 1.5; }
|
|
label { display: block; margin: 14px 0 6px; font-size: 11px; font-weight: 800; color: var(--ctp-overlay2); text-transform: uppercase; letter-spacing: 0.1em; }
|
|
input {
|
|
width: 100%;
|
|
padding: 9px 11px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
background: rgba(24, 25, 38, 0.85);
|
|
color: var(--text);
|
|
font: inherit;
|
|
outline: none;
|
|
transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
|
|
}
|
|
input::placeholder { color: var(--ctp-overlay0); }
|
|
input:hover { border-color: rgba(138, 173, 244, 0.32); }
|
|
input:focus {
|
|
border-color: rgba(138, 173, 244, 0.65);
|
|
background: rgba(24, 25, 38, 0.95);
|
|
box-shadow: 0 0 0 3px rgba(138, 173, 244, 0.15);
|
|
}
|
|
button {
|
|
height: 36px;
|
|
padding: 0 16px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
font: inherit;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 60ms ease;
|
|
}
|
|
button:active { transform: translateY(1px); }
|
|
button:disabled { cursor: wait; opacity: 0.7; }
|
|
button.primary {
|
|
border-color: transparent;
|
|
background: var(--ctp-blue);
|
|
color: var(--ctp-crust);
|
|
}
|
|
button.primary:hover:not(:disabled) { filter: brightness(1.06); }
|
|
button.primary:disabled {
|
|
background: rgba(54, 58, 79, 0.55);
|
|
color: var(--ctp-overlay0);
|
|
border-color: var(--line);
|
|
}
|
|
button.secondary {
|
|
background: rgba(54, 58, 79, 0.5);
|
|
color: var(--text);
|
|
}
|
|
button.secondary:hover:not(:disabled) {
|
|
border-color: rgba(138, 173, 244, 0.45);
|
|
background: rgba(73, 77, 100, 0.6);
|
|
color: var(--ctp-lavender);
|
|
}
|
|
button.danger {
|
|
background: rgba(237, 135, 150, 0.12);
|
|
color: var(--ctp-red);
|
|
border-color: rgba(237, 135, 150, 0.45);
|
|
}
|
|
button.danger:hover:not(:disabled) {
|
|
background: rgba(237, 135, 150, 0.22);
|
|
}
|
|
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 18px; }
|
|
.actions .primary { grid-column: 1 / -1; }
|
|
.status {
|
|
min-height: 18px;
|
|
margin-top: 14px;
|
|
font-size: 12.5px;
|
|
color: var(--muted);
|
|
}
|
|
.status:empty { display: none; }
|
|
.status.loading,
|
|
.status.success,
|
|
.status.error {
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--line);
|
|
background: var(--ctp-surface0);
|
|
font-weight: 600;
|
|
}
|
|
.status.success {
|
|
border-color: rgba(166, 218, 149, 0.45);
|
|
background: rgba(166, 218, 149, 0.1);
|
|
color: var(--ctp-green);
|
|
}
|
|
.status.error {
|
|
border-color: rgba(237, 135, 150, 0.55);
|
|
background: rgba(237, 135, 150, 0.1);
|
|
color: var(--ctp-red);
|
|
}
|
|
.hint {
|
|
margin-top: 16px;
|
|
font-size: 11.5px;
|
|
color: var(--ctp-overlay2);
|
|
line-height: 1.55;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Jellyfin Setup</h1>
|
|
<p>Enter your Jellyfin server URL, sign in, and SubMiner will save a session token for Jellyfin commands and cast discovery.</p>
|
|
<form id="form">
|
|
<label for="server">Server URL</label>
|
|
<input id="server" name="server" value="${escapeHtmlAttr(state.selectedServerUrl)}" required />
|
|
<label for="username">Username</label>
|
|
<input id="username" name="username" value="${escapeHtmlAttr(state.username)}" required />
|
|
<label for="password">Password</label>
|
|
<input id="password" name="password" type="password" required />
|
|
<div id="status" class="${statusClass}" aria-live="polite">${escapeHtml(state.statusMessage)}</div>
|
|
<div class="actions">
|
|
<button class="primary" type="submit">Login</button>
|
|
${
|
|
state.hasStoredSession
|
|
? '<button id="logout" class="danger" type="button">Logout</button>'
|
|
: '<span></span>'
|
|
}
|
|
<button id="done" class="secondary" type="button">Done</button>
|
|
</div>
|
|
<div class="hint">Equivalent CLI: --jellyfin-login --jellyfin-server ... --jellyfin-username ... --jellyfin-password ...</div>
|
|
</form>
|
|
</main>
|
|
<script>
|
|
const form = document.getElementById("form");
|
|
const server = document.getElementById("server");
|
|
const username = document.getElementById("username");
|
|
const password = document.getElementById("password");
|
|
const status = document.getElementById("status");
|
|
const buttons = Array.from(document.querySelectorAll("button"));
|
|
function setBusy(message) {
|
|
if (status) {
|
|
status.textContent = message;
|
|
status.className = "status loading";
|
|
}
|
|
for (const button of buttons) button.disabled = true;
|
|
}
|
|
function setStatus(message, kind) {
|
|
if (status) {
|
|
status.textContent = message;
|
|
status.className = "status " + kind;
|
|
}
|
|
for (const button of buttons) button.disabled = false;
|
|
}
|
|
async function submitAction(action) {
|
|
const serverValue = String(server?.value || "");
|
|
const usernameValue = String(username?.value || "");
|
|
const passwordValue = String(password?.value || "");
|
|
setBusy(action === "login" ? "Logging in to Jellyfin..." : action === "logout" ? "Logging out..." : "Closing...");
|
|
const bridge = window.subminerJellyfinSetup;
|
|
if (bridge?.submit) {
|
|
try {
|
|
const result = await bridge.submit({
|
|
action,
|
|
server: serverValue,
|
|
username: usernameValue,
|
|
password: passwordValue,
|
|
});
|
|
if (result?.handled === false) {
|
|
setStatus(result.statusMessage || "Jellyfin setup action was not accepted.", result.statusKind || "error");
|
|
}
|
|
} catch (error) {
|
|
const message = error && typeof error === "object" && "message" in error ? String(error.message) : String(error || "Unknown error");
|
|
setStatus("Jellyfin setup action failed: " + message, "error");
|
|
}
|
|
return;
|
|
}
|
|
const params = new URLSearchParams();
|
|
params.set("action", action);
|
|
if (action === "login") {
|
|
params.set("server", serverValue);
|
|
params.set("username", usernameValue);
|
|
window.__subminerJellyfinPassword = passwordValue;
|
|
}
|
|
window.location.href = "subminer://jellyfin-setup?" + params.toString();
|
|
}
|
|
form?.addEventListener("submit", (event) => {
|
|
event.preventDefault();
|
|
submitAction("login");
|
|
});
|
|
document.getElementById("logout")?.addEventListener("click", () => submitAction("logout"));
|
|
document.getElementById("done")?.addEventListener("click", () => submitAction("done"));
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
export function parseJellyfinSetupSubmissionUrl(rawUrl: string): {
|
|
action: JellyfinSetupAction;
|
|
server: string;
|
|
username: string;
|
|
password: string;
|
|
} | null {
|
|
if (!rawUrl.startsWith('subminer://jellyfin-setup')) {
|
|
return null;
|
|
}
|
|
const parsed = new URL(rawUrl);
|
|
const rawAction = parsed.searchParams.get('action') || 'login';
|
|
const action: JellyfinSetupAction =
|
|
rawAction === 'logout' || rawAction === 'done' ? rawAction : 'login';
|
|
return {
|
|
action,
|
|
server: parsed.searchParams.get('server') || '',
|
|
username: parsed.searchParams.get('username') || '',
|
|
password: parsed.searchParams.get('password') || '',
|
|
};
|
|
}
|
|
|
|
export function normalizeJellyfinSetupIpcSubmission(
|
|
value: unknown,
|
|
): JellyfinSetupSubmission | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
return {
|
|
action: normalizeSetupAction(value.action),
|
|
server: normalizeString(value.server),
|
|
username: normalizeString(value.username),
|
|
password: normalizeString(value.password),
|
|
};
|
|
}
|
|
|
|
export function buildJellyfinSetupSubmissionUrl(submission: JellyfinSetupSubmission): string {
|
|
const params = new URLSearchParams();
|
|
params.set('action', submission.action);
|
|
if (submission.action === 'login') {
|
|
params.set('server', submission.server);
|
|
params.set('username', submission.username);
|
|
}
|
|
return `subminer://jellyfin-setup?${params.toString()}`;
|
|
}
|
|
|
|
export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
|
parseSubmissionUrl: (
|
|
rawUrl: string,
|
|
) => { action: JellyfinSetupAction; server: string; username: string; password: string } | null;
|
|
authenticateWithPassword: (
|
|
server: string,
|
|
username: string,
|
|
password: string,
|
|
clientInfo: JellyfinClientInfo,
|
|
) => Promise<JellyfinSession>;
|
|
getJellyfinClientInfo: () => JellyfinClientInfo;
|
|
saveStoredSession: (session: { accessToken: string; userId: string }) => void;
|
|
clearStoredSession: () => void;
|
|
patchJellyfinConfig: (session: JellyfinSession) => void;
|
|
persistAuthenticatedSession?: (session: JellyfinSession, clientInfo: JellyfinClientInfo) => void;
|
|
restartRemoteSession?: () => Promise<void> | void;
|
|
stopRemoteSession?: () => void;
|
|
logInfo: (message: string) => void;
|
|
logError: (message: string, error: unknown) => void;
|
|
showMpvOsd: (message: string) => void;
|
|
closeSetupWindow: () => void;
|
|
reloadSetupWindow: (state?: JellyfinSetupViewOverrides) => void;
|
|
}) {
|
|
let loginInFlight = false;
|
|
|
|
return async (rawUrl: string, passwordOverride?: string): Promise<boolean> => {
|
|
const submission = deps.parseSubmissionUrl(rawUrl);
|
|
if (!submission) {
|
|
return false;
|
|
}
|
|
|
|
if (submission.action === 'done') {
|
|
deps.closeSetupWindow();
|
|
return true;
|
|
}
|
|
|
|
if (submission.action === 'logout') {
|
|
try {
|
|
deps.clearStoredSession();
|
|
deps.stopRemoteSession?.();
|
|
deps.logInfo('Cleared stored Jellyfin auth session.');
|
|
deps.showMpvOsd('Jellyfin logged out');
|
|
deps.reloadSetupWindow({
|
|
statusMessage: 'Jellyfin session cleared.',
|
|
statusKind: 'success',
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
deps.logError('Jellyfin logout failed', error);
|
|
deps.showMpvOsd(`Jellyfin logout failed: ${message}`);
|
|
deps.reloadSetupWindow({
|
|
statusMessage: message,
|
|
statusKind: 'error',
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (loginInFlight) {
|
|
deps.showMpvOsd('Jellyfin login already in progress');
|
|
deps.reloadSetupWindow({
|
|
selectedServerUrl: submission.server,
|
|
username: submission.username,
|
|
statusMessage: 'Jellyfin login already in progress.',
|
|
statusKind: 'loading',
|
|
});
|
|
return true;
|
|
}
|
|
|
|
loginInFlight = true;
|
|
try {
|
|
const clientInfo = deps.getJellyfinClientInfo();
|
|
const session = await deps.authenticateWithPassword(
|
|
submission.server,
|
|
submission.username,
|
|
passwordOverride ?? submission.password,
|
|
clientInfo,
|
|
);
|
|
if (deps.persistAuthenticatedSession) {
|
|
deps.persistAuthenticatedSession(session, clientInfo);
|
|
} else {
|
|
deps.saveStoredSession({ accessToken: session.accessToken, userId: session.userId });
|
|
deps.patchJellyfinConfig(session);
|
|
}
|
|
await deps.restartRemoteSession?.();
|
|
deps.logInfo(`Jellyfin setup saved for ${session.username}.`);
|
|
deps.showMpvOsd('Jellyfin login success');
|
|
deps.reloadSetupWindow({
|
|
selectedServerUrl: session.serverUrl,
|
|
username: session.username,
|
|
statusMessage: `Authenticated as ${session.username}.`,
|
|
statusKind: 'success',
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
deps.logError('Jellyfin setup failed', error);
|
|
deps.showMpvOsd(`Jellyfin login failed: ${message}`);
|
|
deps.reloadSetupWindow({
|
|
selectedServerUrl: submission.server,
|
|
username: submission.username,
|
|
statusMessage: message,
|
|
statusKind: 'error',
|
|
});
|
|
} finally {
|
|
loginInFlight = false;
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
export function createHandleJellyfinSetupNavigationHandler(deps: {
|
|
setupSchemePrefix: string;
|
|
handleSubmission: (rawUrl: string) => Promise<unknown>;
|
|
logError: (message: string, error: unknown) => void;
|
|
}) {
|
|
return (params: { url: string; preventDefault: () => void }): boolean => {
|
|
if (!params.url.startsWith(deps.setupSchemePrefix)) {
|
|
return false;
|
|
}
|
|
params.preventDefault();
|
|
void deps.handleSubmission(params.url).catch((error) => {
|
|
deps.logError('Failed handling Jellyfin setup submission', error);
|
|
});
|
|
return true;
|
|
};
|
|
}
|
|
|
|
async function readJellyfinSetupPasswordFromWindow(
|
|
setupWindow: JellyfinSetupWindowLike,
|
|
): Promise<string | undefined> {
|
|
const executeJavaScript = setupWindow.webContents.executeJavaScript;
|
|
if (!executeJavaScript) {
|
|
return undefined;
|
|
}
|
|
|
|
const value = await executeJavaScript(
|
|
`(() => {
|
|
const input = document.getElementById("password");
|
|
const password = String(window.__subminerJellyfinPassword || input?.value || "");
|
|
window.__subminerJellyfinPassword = "";
|
|
if (input) input.value = "";
|
|
return password;
|
|
})()`,
|
|
true,
|
|
);
|
|
return typeof value === 'string' ? value : '';
|
|
}
|
|
|
|
export function createHandleJellyfinSetupWindowClosedHandler(deps: {
|
|
clearSetupWindow: () => void;
|
|
}) {
|
|
return (): void => {
|
|
deps.clearSetupWindow();
|
|
};
|
|
}
|
|
|
|
export function createHandleJellyfinSetupWindowOpenedHandler(deps: { setSetupWindow: () => void }) {
|
|
return (): void => {
|
|
deps.setSetupWindow();
|
|
};
|
|
}
|
|
|
|
export function createOpenJellyfinSetupWindowHandler<
|
|
TWindow extends JellyfinSetupWindowLike,
|
|
>(deps: {
|
|
maybeFocusExistingSetupWindow: () => boolean;
|
|
createSetupWindow: () => TWindow;
|
|
getResolvedJellyfinConfig: () => {
|
|
serverUrl?: string | null;
|
|
username?: string | null;
|
|
recentServers?: unknown[];
|
|
};
|
|
buildSetupFormHtml: (state: JellyfinSetupViewState) => string;
|
|
parseSubmissionUrl: (
|
|
rawUrl: string,
|
|
) => { action: JellyfinSetupAction; server: string; username: string; password: string } | null;
|
|
authenticateWithPassword: (
|
|
server: string,
|
|
username: string,
|
|
password: string,
|
|
clientInfo: JellyfinClientInfo,
|
|
) => Promise<JellyfinSession>;
|
|
getJellyfinClientInfo: () => JellyfinClientInfo;
|
|
saveStoredSession: (session: { accessToken: string; userId: string }) => void;
|
|
clearStoredSession: () => void;
|
|
patchJellyfinConfig: (session: JellyfinSession) => void;
|
|
persistAuthenticatedSession?: (session: JellyfinSession, clientInfo: JellyfinClientInfo) => void;
|
|
restartRemoteSession?: () => Promise<void> | void;
|
|
stopRemoteSession?: () => void;
|
|
logInfo: (message: string) => void;
|
|
logError: (message: string, error: unknown) => void;
|
|
showMpvOsd: (message: string) => void;
|
|
clearSetupWindow: () => void;
|
|
setSetupWindow: (window: TWindow) => void;
|
|
registerSetupIpcHandler?: RegisterJellyfinSetupIpcHandler;
|
|
encodeURIComponent: (value: string) => string;
|
|
defaultServerUrl: string;
|
|
hasStoredSession: () => boolean;
|
|
}) {
|
|
return (): void => {
|
|
if (deps.maybeFocusExistingSetupWindow()) {
|
|
return;
|
|
}
|
|
|
|
const setupWindow = deps.createSetupWindow();
|
|
const loadSetupForm = (overrides: JellyfinSetupViewOverrides = {}) => {
|
|
const state = buildJellyfinSetupViewState({
|
|
config: deps.getResolvedJellyfinConfig(),
|
|
defaultServerUrl: deps.defaultServerUrl,
|
|
hasStoredSession: deps.hasStoredSession(),
|
|
selectedServerUrl: overrides.selectedServerUrl,
|
|
username: overrides.username,
|
|
statusMessage: overrides.statusMessage,
|
|
statusKind: overrides.statusKind,
|
|
});
|
|
const formHtml = deps.buildSetupFormHtml(state);
|
|
void setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`);
|
|
};
|
|
const handleSubmission = createHandleJellyfinSetupSubmissionHandler({
|
|
parseSubmissionUrl: (rawUrl) => deps.parseSubmissionUrl(rawUrl),
|
|
authenticateWithPassword: (server, username, password, clientInfo) =>
|
|
deps.authenticateWithPassword(server, username, password, clientInfo),
|
|
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
|
|
saveStoredSession: (session) => deps.saveStoredSession(session),
|
|
clearStoredSession: () => deps.clearStoredSession(),
|
|
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
|
|
persistAuthenticatedSession: deps.persistAuthenticatedSession
|
|
? (session, clientInfo) => deps.persistAuthenticatedSession?.(session, clientInfo)
|
|
: undefined,
|
|
restartRemoteSession: deps.restartRemoteSession
|
|
? () => deps.restartRemoteSession?.()
|
|
: undefined,
|
|
stopRemoteSession: deps.stopRemoteSession ? () => deps.stopRemoteSession?.() : undefined,
|
|
logInfo: (message) => deps.logInfo(message),
|
|
logError: (message, error) => deps.logError(message, error),
|
|
showMpvOsd: (message) => deps.showMpvOsd(message),
|
|
closeSetupWindow: () => {
|
|
if (!setupWindow.isDestroyed()) {
|
|
setupWindow.close();
|
|
}
|
|
},
|
|
reloadSetupWindow: (state) => {
|
|
if (!setupWindow.isDestroyed()) {
|
|
loadSetupForm(state);
|
|
}
|
|
},
|
|
});
|
|
const unregisterSetupIpcHandler = deps.registerSetupIpcHandler?.(async (payload) => {
|
|
const submission = normalizeJellyfinSetupIpcSubmission(payload);
|
|
if (!submission) {
|
|
return {
|
|
handled: false,
|
|
statusMessage: 'Invalid Jellyfin setup request.',
|
|
statusKind: 'error',
|
|
};
|
|
}
|
|
const handled = await handleSubmission(
|
|
buildJellyfinSetupSubmissionUrl(submission),
|
|
submission.password,
|
|
);
|
|
return { handled };
|
|
});
|
|
const handleNavigation = createHandleJellyfinSetupNavigationHandler({
|
|
setupSchemePrefix: 'subminer://jellyfin-setup',
|
|
handleSubmission: async (rawUrl) => {
|
|
const submission = deps.parseSubmissionUrl(rawUrl);
|
|
let password: string | undefined;
|
|
if (submission?.action === 'login' && !submission.password) {
|
|
try {
|
|
password = await readJellyfinSetupPasswordFromWindow(setupWindow);
|
|
} catch (error) {
|
|
deps.logError('Failed reading Jellyfin setup password', error);
|
|
password = '';
|
|
}
|
|
}
|
|
return handleSubmission(rawUrl, password);
|
|
},
|
|
logError: (message, error) => deps.logError(message, error),
|
|
});
|
|
const handleWindowClosed = createHandleJellyfinSetupWindowClosedHandler({
|
|
clearSetupWindow: () => deps.clearSetupWindow(),
|
|
});
|
|
const handleWindowOpened = createHandleJellyfinSetupWindowOpenedHandler({
|
|
setSetupWindow: () => deps.setSetupWindow(setupWindow),
|
|
});
|
|
|
|
setupWindow.webContents.on('will-navigate', (event, url) => {
|
|
handleNavigation({
|
|
url,
|
|
preventDefault: () => {
|
|
if (event && typeof event === 'object' && 'preventDefault' in event) {
|
|
const typedEvent = event as { preventDefault?: () => void };
|
|
typedEvent.preventDefault?.();
|
|
}
|
|
},
|
|
});
|
|
});
|
|
loadSetupForm();
|
|
setupWindow.on('closed', () => {
|
|
unregisterSetupIpcHandler?.();
|
|
handleWindowClosed();
|
|
});
|
|
handleWindowOpened();
|
|
};
|
|
}
|