mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 18:12:05 -07:00
feat(core): add Electron runtime, services, and app composition
This commit is contained in:
323
src/main/runtime/anilist-setup-window.ts
Normal file
323
src/main/runtime/anilist-setup-window.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
type SetupWindowLike = {
|
||||
isDestroyed: () => boolean;
|
||||
};
|
||||
|
||||
type OpenHandlerDecision = { action: 'deny' };
|
||||
|
||||
type FocusableWindowLike = {
|
||||
focus: () => void;
|
||||
};
|
||||
|
||||
type AnilistSetupWebContentsLike = {
|
||||
setWindowOpenHandler: (...args: any[]) => unknown;
|
||||
on: (...args: any[]) => unknown;
|
||||
getURL: () => string;
|
||||
};
|
||||
|
||||
type AnilistSetupWindowLike = FocusableWindowLike & {
|
||||
webContents: AnilistSetupWebContentsLike;
|
||||
on: (...args: any[]) => unknown;
|
||||
isDestroyed: () => boolean;
|
||||
};
|
||||
|
||||
export function createHandleManualAnilistSetupSubmissionHandler(deps: {
|
||||
consumeCallbackUrl: (rawUrl: string) => boolean;
|
||||
redirectUri: string;
|
||||
logWarn: (message: string) => void;
|
||||
}) {
|
||||
return (rawUrl: string): boolean => {
|
||||
if (!rawUrl.startsWith('subminer://anilist-setup')) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(rawUrl);
|
||||
const accessToken = parsed.searchParams.get('access_token')?.trim() ?? '';
|
||||
if (accessToken.length > 0) {
|
||||
return deps.consumeCallbackUrl(
|
||||
`${deps.redirectUri}#access_token=${encodeURIComponent(accessToken)}`,
|
||||
);
|
||||
}
|
||||
deps.logWarn('AniList setup submission missing access token');
|
||||
return true;
|
||||
} catch {
|
||||
deps.logWarn('AniList setup submission had invalid callback input');
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createMaybeFocusExistingAnilistSetupWindowHandler(deps: {
|
||||
getSetupWindow: () => FocusableWindowLike | null;
|
||||
}) {
|
||||
return (): boolean => {
|
||||
const window = deps.getSetupWindow();
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
window.focus();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export function createAnilistSetupWindowOpenHandler(deps: {
|
||||
isAllowedExternalUrl: (url: string) => boolean;
|
||||
openExternal: (url: string) => void;
|
||||
logWarn: (message: string, details?: unknown) => void;
|
||||
}) {
|
||||
return ({ url }: { url: string }): OpenHandlerDecision => {
|
||||
if (!deps.isAllowedExternalUrl(url)) {
|
||||
deps.logWarn('Blocked unsafe AniList setup external URL', { url });
|
||||
return { action: 'deny' };
|
||||
}
|
||||
deps.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
};
|
||||
}
|
||||
|
||||
export function createAnilistSetupWillNavigateHandler(deps: {
|
||||
handleManualSubmission: (url: string) => boolean;
|
||||
consumeCallbackUrl: (url: string) => boolean;
|
||||
redirectUri: string;
|
||||
isAllowedNavigationUrl: (url: string) => boolean;
|
||||
logWarn: (message: string, details?: unknown) => void;
|
||||
}) {
|
||||
return (params: { url: string; preventDefault: () => void }): void => {
|
||||
const { url, preventDefault } = params;
|
||||
if (deps.handleManualSubmission(url)) {
|
||||
preventDefault();
|
||||
return;
|
||||
}
|
||||
if (deps.consumeCallbackUrl(url)) {
|
||||
preventDefault();
|
||||
return;
|
||||
}
|
||||
if (url.startsWith(deps.redirectUri)) {
|
||||
preventDefault();
|
||||
return;
|
||||
}
|
||||
if (url.startsWith(`${deps.redirectUri}#`)) {
|
||||
preventDefault();
|
||||
return;
|
||||
}
|
||||
if (deps.isAllowedNavigationUrl(url)) {
|
||||
return;
|
||||
}
|
||||
preventDefault();
|
||||
deps.logWarn('Blocked unsafe AniList setup navigation URL', { url });
|
||||
};
|
||||
}
|
||||
|
||||
export function createAnilistSetupWillRedirectHandler(deps: {
|
||||
consumeCallbackUrl: (url: string) => boolean;
|
||||
}) {
|
||||
return (params: { url: string; preventDefault: () => void }): void => {
|
||||
if (deps.consumeCallbackUrl(params.url)) {
|
||||
params.preventDefault();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createAnilistSetupDidNavigateHandler(deps: {
|
||||
consumeCallbackUrl: (url: string) => boolean;
|
||||
}) {
|
||||
return (url: string): void => {
|
||||
deps.consumeCallbackUrl(url);
|
||||
};
|
||||
}
|
||||
|
||||
export function createAnilistSetupDidFailLoadHandler(deps: {
|
||||
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => void;
|
||||
}) {
|
||||
return (details: { errorCode: number; errorDescription: string; validatedURL: string }): void => {
|
||||
deps.onLoadFailure(details);
|
||||
};
|
||||
}
|
||||
|
||||
export function createAnilistSetupDidFinishLoadHandler(deps: {
|
||||
getLoadedUrl: () => string;
|
||||
onBlankPageLoaded: () => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
const loadedUrl = deps.getLoadedUrl();
|
||||
if (!loadedUrl || loadedUrl === 'about:blank') {
|
||||
deps.onBlankPageLoaded();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createHandleAnilistSetupWindowClosedHandler(deps: {
|
||||
clearSetupWindow: () => void;
|
||||
setSetupPageOpened: (opened: boolean) => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
deps.clearSetupWindow();
|
||||
deps.setSetupPageOpened(false);
|
||||
};
|
||||
}
|
||||
|
||||
export function createHandleAnilistSetupWindowOpenedHandler(deps: {
|
||||
setSetupWindow: () => void;
|
||||
setSetupPageOpened: (opened: boolean) => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
deps.setSetupWindow();
|
||||
deps.setSetupPageOpened(true);
|
||||
};
|
||||
}
|
||||
|
||||
export function createAnilistSetupFallbackHandler(deps: {
|
||||
authorizeUrl: string;
|
||||
developerSettingsUrl: string;
|
||||
setupWindow: SetupWindowLike;
|
||||
openSetupInBrowser: () => void;
|
||||
loadManualTokenEntry: () => void;
|
||||
logError: (message: string, details: unknown) => void;
|
||||
logWarn: (message: string) => void;
|
||||
}) {
|
||||
return {
|
||||
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => {
|
||||
deps.logError('AniList setup window failed to load', details);
|
||||
deps.openSetupInBrowser();
|
||||
if (!deps.setupWindow.isDestroyed()) {
|
||||
deps.loadManualTokenEntry();
|
||||
}
|
||||
},
|
||||
onBlankPageLoaded: () => {
|
||||
deps.logWarn('AniList setup loaded a blank page; using fallback');
|
||||
deps.openSetupInBrowser();
|
||||
if (!deps.setupWindow.isDestroyed()) {
|
||||
deps.loadManualTokenEntry();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createOpenAnilistSetupWindowHandler<TWindow extends AnilistSetupWindowLike>(deps: {
|
||||
maybeFocusExistingSetupWindow: () => boolean;
|
||||
createSetupWindow: () => TWindow;
|
||||
buildAuthorizeUrl: () => string;
|
||||
consumeCallbackUrl: (rawUrl: string) => boolean;
|
||||
openSetupInBrowser: (authorizeUrl: string) => void;
|
||||
loadManualTokenEntry: (setupWindow: TWindow, authorizeUrl: string) => void;
|
||||
redirectUri: string;
|
||||
developerSettingsUrl: string;
|
||||
isAllowedExternalUrl: (url: string) => boolean;
|
||||
isAllowedNavigationUrl: (url: string) => boolean;
|
||||
logWarn: (message: string, details?: unknown) => void;
|
||||
logError: (message: string, details: unknown) => void;
|
||||
clearSetupWindow: () => void;
|
||||
setSetupPageOpened: (opened: boolean) => void;
|
||||
setSetupWindow: (window: TWindow) => void;
|
||||
openExternal: (url: string) => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
if (deps.maybeFocusExistingSetupWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setupWindow = deps.createSetupWindow();
|
||||
const authorizeUrl = deps.buildAuthorizeUrl();
|
||||
const consumeCallbackUrl = (rawUrl: string): boolean => deps.consumeCallbackUrl(rawUrl);
|
||||
const openSetupInBrowser = () => deps.openSetupInBrowser(authorizeUrl);
|
||||
const loadManualTokenEntry = () => deps.loadManualTokenEntry(setupWindow, authorizeUrl);
|
||||
const handleManualSubmission = createHandleManualAnilistSetupSubmissionHandler({
|
||||
consumeCallbackUrl: (rawUrl) => consumeCallbackUrl(rawUrl),
|
||||
redirectUri: deps.redirectUri,
|
||||
logWarn: (message) => deps.logWarn(message),
|
||||
});
|
||||
const fallback = createAnilistSetupFallbackHandler({
|
||||
authorizeUrl,
|
||||
developerSettingsUrl: deps.developerSettingsUrl,
|
||||
setupWindow,
|
||||
openSetupInBrowser,
|
||||
loadManualTokenEntry,
|
||||
logError: (message, details) => deps.logError(message, details),
|
||||
logWarn: (message) => deps.logWarn(message),
|
||||
});
|
||||
const handleWindowOpen = createAnilistSetupWindowOpenHandler({
|
||||
isAllowedExternalUrl: (url) => deps.isAllowedExternalUrl(url),
|
||||
openExternal: (url) => deps.openExternal(url),
|
||||
logWarn: (message, details) => deps.logWarn(message, details),
|
||||
});
|
||||
const handleWillNavigate = createAnilistSetupWillNavigateHandler({
|
||||
handleManualSubmission: (url) => handleManualSubmission(url),
|
||||
consumeCallbackUrl: (url) => consumeCallbackUrl(url),
|
||||
redirectUri: deps.redirectUri,
|
||||
isAllowedNavigationUrl: (url) => deps.isAllowedNavigationUrl(url),
|
||||
logWarn: (message, details) => deps.logWarn(message, details),
|
||||
});
|
||||
const handleWillRedirect = createAnilistSetupWillRedirectHandler({
|
||||
consumeCallbackUrl: (url) => consumeCallbackUrl(url),
|
||||
});
|
||||
const handleDidNavigate = createAnilistSetupDidNavigateHandler({
|
||||
consumeCallbackUrl: (url) => consumeCallbackUrl(url),
|
||||
});
|
||||
const handleDidFailLoad = createAnilistSetupDidFailLoadHandler({
|
||||
onLoadFailure: (details) => fallback.onLoadFailure(details),
|
||||
});
|
||||
const handleDidFinishLoad = createAnilistSetupDidFinishLoadHandler({
|
||||
getLoadedUrl: () => setupWindow.webContents.getURL(),
|
||||
onBlankPageLoaded: () => fallback.onBlankPageLoaded(),
|
||||
});
|
||||
const handleWindowClosed = createHandleAnilistSetupWindowClosedHandler({
|
||||
clearSetupWindow: () => deps.clearSetupWindow(),
|
||||
setSetupPageOpened: (opened) => deps.setSetupPageOpened(opened),
|
||||
});
|
||||
const handleWindowOpened = createHandleAnilistSetupWindowOpenedHandler({
|
||||
setSetupWindow: () => deps.setSetupWindow(setupWindow),
|
||||
setSetupPageOpened: (opened) => deps.setSetupPageOpened(opened),
|
||||
});
|
||||
|
||||
setupWindow.webContents.setWindowOpenHandler(({ url }: { url: string }) =>
|
||||
handleWindowOpen({ url }),
|
||||
);
|
||||
setupWindow.webContents.on('will-navigate', (event: unknown, url: string) => {
|
||||
handleWillNavigate({
|
||||
url,
|
||||
preventDefault: () => {
|
||||
if (event && typeof event === 'object' && 'preventDefault' in event) {
|
||||
const typedEvent = event as { preventDefault?: () => void };
|
||||
typedEvent.preventDefault?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
setupWindow.webContents.on('will-redirect', (event: unknown, url: string) => {
|
||||
handleWillRedirect({
|
||||
url,
|
||||
preventDefault: () => {
|
||||
if (event && typeof event === 'object' && 'preventDefault' in event) {
|
||||
const typedEvent = event as { preventDefault?: () => void };
|
||||
typedEvent.preventDefault?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
setupWindow.webContents.on('did-navigate', (_event: unknown, url: string) => {
|
||||
handleDidNavigate(url);
|
||||
});
|
||||
setupWindow.webContents.on(
|
||||
'did-fail-load',
|
||||
(
|
||||
_event: unknown,
|
||||
errorCode: number,
|
||||
errorDescription: string,
|
||||
validatedURL: string,
|
||||
) => {
|
||||
handleDidFailLoad({
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
});
|
||||
},
|
||||
);
|
||||
setupWindow.webContents.on('did-finish-load', () => {
|
||||
handleDidFinishLoad();
|
||||
});
|
||||
loadManualTokenEntry();
|
||||
setupWindow.on('closed', () => {
|
||||
handleWindowClosed();
|
||||
});
|
||||
handleWindowOpened();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user