TidGi-Desktop/src/services/view/setupViewFileProtocol.ts
lin onetwo b76fc17794
Chore/upgrade (#646)
* docs: deps

* Update dependencies and type usage for AI features

Upgraded multiple dependencies in package.json and pnpm-lock.yaml, including @ai-sdk, @mui, react, and others for improved compatibility and performance. Changed type usage from CoreMessage to ModelMessage in mockOpenAI.test.ts to align with updated ai package. No functional changes to application logic.

* feat: i18n

* feat: test oauth login and use PKCE

* fix: use ollama-ai-provider-v2

* test: github and mock oauth2 login

* test: gitea login

* Refactor context menu cleanup and error message

Moved context menu cleanup for OAuth window to a single closed event handler in Authentication service. Simplified error message formatting in ContextService for missing keys.

* lint: AI fix

* Add tsx as a dev dependency and update scripts

Replaced usage of 'pnpm dlx tsx' with direct 'tsx' command in development and test scripts for improved reliability. Added 'tsx' to devDependencies in package.json.
2025-10-23 23:42:06 +08:00

100 lines
4.6 KiB
TypeScript

import { WikiChannel } from '@/constants/channels';
import { container } from '@services/container';
import { i18n } from '@services/libs/i18n';
import { logger } from '@services/libs/log';
import type { INativeService } from '@services/native/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IWikiService } from '@services/wiki/interface';
import { shell, WebContentsView } from 'electron';
import fs from 'fs-extra';
import type { INewWindowContext } from './handleNewWindow';
import type { INewWindowAction } from './interface';
/**
* Handles in-wiki file link opening.
* This does not handle web request with file:// protocol.
*
* `file://` may resulted in `nextDomain` being `about:blank#blocked`, so we use `open://` instead. But in MacOS it seem to works fine in most cases. Just leave open:// in case as a fallback for users.
*
* For file:/// in-app assets loading., see handleFileProtocol() in `src/services/native/index.ts`.
*/
export function handleOpenFileExternalLink(nextUrl: string, newWindowContext: INewWindowContext): INewWindowAction | undefined {
if (!nextUrl.startsWith('file://') && !nextUrl.startsWith('open://')) return;
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
const absoluteFilePath = nativeService.formatFileUrlToAbsolutePath(nextUrl);
try {
const fileStat = fs.statSync(absoluteFilePath);
if (fileStat.isDirectory()) {
logger.info(`Opening directory ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
void shell.openPath(absoluteFilePath).catch((error_: unknown) => {
const error = error_ as Error;
const message = i18n.t('Log.FailedToOpenDirectory', { path: absoluteFilePath, message: error.message });
logger.warn(message, { function: 'handleOpenFileExternalLink', error });
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
});
} else if (fileStat.isFile()) {
logger.info(`Opening file ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
void shell.openPath(absoluteFilePath).catch((error_: unknown) => {
const error = error_ as Error;
const message = i18n.t('Log.FailedToOpenFile', { path: absoluteFilePath, message: error.message });
logger.warn(message, { function: 'handleOpenFileExternalLink', error });
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
});
}
} catch (error_: unknown) {
const error = error_ as Error;
const message = `${i18n.t('AddWorkspace.PathNotExist', { path: absoluteFilePath })} ${error.message}`;
logger.warn(message, { function: 'handleOpenFileExternalLink', error });
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
}
return {
action: 'deny',
};
}
/**
* Handle file protocol in webview to request file content and show in the view.
*
* Similar to src/services/view/setupIpcServerRoutesHandlers.ts where it is redirect and handled by tiddlywiki server.
*/
export function handleViewFileContentLoading(view: WebContentsView) {
view.webContents.session.webRequest.onBeforeRequest((details, callback) => {
if (details.url.startsWith('file://')) {
handleFileLink(details, callback);
} else {
callback({
cancel: false,
});
}
});
}
function handleFileLink(details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) {
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
const absolutePath: string | undefined = nativeService.formatFileUrlToAbsolutePath(details.url);
// When details.url is an absolute route, we just load it, don't need any redirect
if (
`file://${absolutePath}` === decodeURI(details.url) ||
absolutePath === decodeURI(details.url) ||
// also allow malformed `file:///` on `details.url` on windows, prevent infinite redirect when this check failed.
(process.platform === 'win32' && `file:///${absolutePath}` === decodeURI(details.url))
) {
logger.debug('open file protocol', {
function: 'handleFileLink',
absolutePath: absolutePath ?? '',
});
callback({
cancel: false,
});
} else {
logger.info('redirecting file protocol', {
function: 'handleFileLink',
absolutePath: absolutePath ?? '',
});
callback({
cancel: false,
redirectURL: `file://${absolutePath}`,
});
}
}