diff --git a/localization/locales/en/translation.json b/localization/locales/en/translation.json index a018b192..4b8a3572 100644 --- a/localization/locales/en/translation.json +++ b/localization/locales/en/translation.json @@ -40,7 +40,9 @@ "SearchWithGoogle": "Search With Google", "Cut": "Cut", "Copy": "Copy", - "Paste": "Paste" + "Paste": "Paste", + "RestartService": "Restart Service", + "RestartServiceComplete": "Restart Service Complete" }, "AddWorkspace": { "MainPageTipWithoutSidebar": "<0>Click Workspaces > Add Workspace<2> on the menu to start using TiddlyWiki!", diff --git a/localization/locales/zh_CN/translation.json b/localization/locales/zh_CN/translation.json index 0365f73a..79f10455 100644 --- a/localization/locales/zh_CN/translation.json +++ b/localization/locales/zh_CN/translation.json @@ -41,7 +41,9 @@ "CopyLink": "复制链接", "OpenLinkInBrowser": "在浏览器中打开链接", "Paste": "粘贴", - "SearchWithGoogle": "用 Google 搜索" + "SearchWithGoogle": "用 Google 搜索", + "RestartService": "重启服务", + "RestartServiceComplete": "重启服务成功" }, "Menu": { "TiddlyGit": "太记", diff --git a/src/constants/channels.ts b/src/constants/channels.ts index 0a69d277..5e64fa05 100644 --- a/src/constants/channels.ts +++ b/src/constants/channels.ts @@ -47,6 +47,7 @@ export enum WikiChannel { getTiddlerTextDone = 'wiki-get-tiddler-text-done', /** show message inside tiddlywiki to show git sync progress */ syncProgress = 'wiki-sync-progress', + generalNotification = 'wiki-notification-tiddly-git', /** used to show wiki creation messages in the TiddlyGit UI for user to read */ createProgress = 'wiki-create-progress', openTiddler = 'wiki-open-tiddler', diff --git a/src/pages/Main/SortableWorkspaceSelector.tsx b/src/pages/Main/SortableWorkspaceSelector.tsx index e9d03da8..a13a411d 100644 --- a/src/pages/Main/SortableWorkspaceSelector.tsx +++ b/src/pages/Main/SortableWorkspaceSelector.tsx @@ -7,6 +7,7 @@ import WorkspaceSelector from './WorkspaceSelector'; import { IWorkspace } from '@services/workspaces/interface'; import defaultIcon from '@/images/default-icon.png'; +import { WikiChannel } from '@/constants/channels'; export interface ISortableItemProps { index: number; @@ -64,6 +65,17 @@ export function SortableWorkspaceSelector({ index, workspace, showSidebarShortcu label: t('ContextMenu.Reload'), click: async () => await window.service.view.reloadViewsWebContents(id), }, + { + label: t('ContextMenu.RestartService'), + click: async () => { + const workspaceToRestart = await window.service.workspace.get(id); + if (workspaceToRestart !== undefined) { + await window.service.wiki.restartWiki(workspaceToRestart); + await window.service.view.reloadViewsWebContents(id); + await window.service.wiki.wikiOperation(WikiChannel.generalNotification, [t('ContextMenu.RestartServiceComplete')]); + } + }, + }, ]; if (!active && !isSubWiki) { diff --git a/src/preload/wikiOperation.ts b/src/preload/wikiOperation.ts index 33ad233d..a4b20d45 100644 --- a/src/preload/wikiOperation.ts +++ b/src/preload/wikiOperation.ts @@ -31,6 +31,12 @@ ipcRenderer.on(WikiChannel.syncProgress, async (event, message: string) => { $tw.notifier.display('$:/state/notification/${WikiChannel.syncProgress}'); `); }); +ipcRenderer.on(WikiChannel.generalNotification, async (event, message: string) => { + await webFrame.executeJavaScript(` + $tw.wiki.addTiddler({ title: '$:/state/notification/${WikiChannel.generalNotification}', text: '${message}' }); + $tw.notifier.display('$:/state/notification/${WikiChannel.generalNotification}'); + `); +}); // open a tiddler ipcRenderer.on(WikiChannel.openTiddler, async (event, tiddlerName: string) => { await webFrame.executeJavaScript(` diff --git a/src/services/libs/log/rendererTransport.ts b/src/services/libs/log/rendererTransport.ts index 5f8cf1e8..2f3a4e7d 100644 --- a/src/services/libs/log/rendererTransport.ts +++ b/src/services/libs/log/rendererTransport.ts @@ -1,31 +1,11 @@ /* eslint-disable global-require */ import Transport from 'winston-transport'; -import { container } from '@services/container'; -import type { IViewService } from '@services/view/interface'; -import type { IWindowService } from '@services/windows/interface'; -import serviceIdentifier from '@services/serviceIdentifier'; -import { WindowNames } from '@services/windows/WindowProperties'; -import { WikiChannel } from '@/constants/channels'; - -const handlers = { - [WikiChannel.createProgress]: (message: string) => { - const windowService = container.get(serviceIdentifier.Window); - const createWorkspaceWindow = windowService.get(WindowNames.addWorkspace); - createWorkspaceWindow?.webContents?.send(WikiChannel.createProgress, message); - }, - [WikiChannel.syncProgress]: async (message: string) => { - const viewService = container.get(serviceIdentifier.View); - const browserView = await viewService.getActiveBrowserView(); - browserView?.webContents?.send(WikiChannel.syncProgress, message); - }, -}; - -export type IHandlers = typeof handlers; +import { IWikiOperations, wikiOperations } from '@services/wiki/wikiOperations'; export interface IInfo { /** which method or handler function we are logging for */ - handler: keyof IHandlers; + handler: keyof IWikiOperations; /** the detailed massage for debugging */ message: string; } @@ -40,8 +20,8 @@ export default class RendererTransport extends Transport { }); // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (info.handler && info.handler in handlers) { - void handlers[info.handler](info.message); + if (info.handler && info.handler in wikiOperations) { + void wikiOperations[info.handler](info.message); } callback(); diff --git a/src/services/wiki/index.ts b/src/services/wiki/index.ts index f5833517..b89964ca 100644 --- a/src/services/wiki/index.ts +++ b/src/services/wiki/index.ts @@ -33,6 +33,7 @@ import type { WikiWorker } from './wikiWorker'; // @ts-expect-error it don't want .ts // eslint-disable-next-line import/no-webpack-loader-syntax import workerURL from 'threads-plugin/dist/loader?name=wikiWorker!./wikiWorker.ts'; +import { IWikiOperations, wikiOperations } from './wikiOperations'; @injectable() export class Wiki implements IWikiService { @@ -379,8 +380,20 @@ export class Wiki implements IWikiService { return this.justStartedWiki[wikiFolderLocation] ?? false; } + /** + * Watch wiki change so we can trigger git sync + * Simply do some check before calling `this.watchWikiForDebounceCommitAndSync` + */ + private async tryWatchForSync(workspace: IWorkspace, watchPath?: string): Promise { + const { wikiFolderLocation, gitUrl: githubRepoUrl, storageService } = workspace; + const userInfo = await this.authService.getStorageServiceUserInfo(storageService); + if (storageService !== SupportedStorageServices.local && typeof githubRepoUrl === 'string' && userInfo !== undefined) { + await this.watchWikiForDebounceCommitAndSync(wikiFolderLocation, githubRepoUrl, userInfo, watchPath); + } + } + public async wikiStartup(workspace: IWorkspace): Promise { - const { wikiFolderLocation, gitUrl: githubRepoUrl, port, isSubWiki, mainWikiToLink, storageService } = workspace; + const { wikiFolderLocation, port, isSubWiki, mainWikiToLink } = workspace; // remove $:/StoryList, otherwise it sometimes cause $__StoryList_1.tid to be generated try { @@ -389,21 +402,15 @@ export class Wiki implements IWikiService { // do nothing } - const userInfo = await this.authService.getStorageServiceUserInfo(storageService); // use workspace specific userName first, and fall back to preferences' userName, pass empty editor username if undefined // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions const userName = (workspace.userName || (await this.authService.get('userName'))) ?? ''; - /** watch wiki change so we can trigger git sync */ - const tryWatchForSync = async (watchPath?: string): Promise => { - if (storageService !== SupportedStorageServices.local && typeof githubRepoUrl === 'string' && userInfo !== undefined) { - await this.watchWikiForDebounceCommitAndSync(wikiFolderLocation, githubRepoUrl, userInfo, watchPath); - } - }; + // if is main wiki if (!isSubWiki) { await this.startWiki(wikiFolderLocation, port, userName); // sync to cloud, do this in a non-blocking way - void tryWatchForSync(path.join(wikiFolderLocation, TIDDLERS_PATH)); + void this.tryWatchForSync(workspace, path.join(wikiFolderLocation, TIDDLERS_PATH)); } else { // if is private repo wiki // if we are creating a sub-wiki just now, restart the main wiki to load content from private wiki @@ -412,17 +419,33 @@ export class Wiki implements IWikiService { if (mainWorkspace === undefined) { throw new Error(`mainWorkspace is undefined in wikiStartup() for mainWikiPath ${mainWikiToLink}`); } - await this.stopWatchWiki(mainWikiToLink); - await this.stopWiki(mainWikiToLink); - await this.startWiki(mainWikiToLink, mainWorkspace.port, userName); - // sync main wiki to cloud, do this in a non-blocking way - void tryWatchForSync(path.join(mainWikiToLink, TIDDLERS_PATH)); + await this.restartWiki(mainWorkspace); // sync self to cloud, subwiki's content is all in root folder path, do this in a non-blocking way - void tryWatchForSync(); + void this.tryWatchForSync(workspace); } } } + public async restartWiki(workspace: IWorkspace): Promise { + const { wikiFolderLocation, port, userName: workspaceUserName, isSubWiki } = workspace; + // use workspace specific userName first, and fall back to preferences' userName, pass empty editor username if undefined + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + const userName = (workspaceUserName || (await this.authService.get('userName'))) ?? ''; + + await this.stopWatchWiki(wikiFolderLocation); + if (!isSubWiki) { + await this.stopWiki(wikiFolderLocation); + await this.startWiki(wikiFolderLocation, port, userName); + } + if (isSubWiki) { + // sync sub wiki to cloud, do this in a non-blocking way + void this.tryWatchForSync(workspace, wikiFolderLocation); + } else { + // sync main wiki to cloud, do this in a non-blocking way + void this.tryWatchForSync(workspace, path.join(wikiFolderLocation, TIDDLERS_PATH)); + } + } + // watch-wiki.ts private readonly frequentlyChangedFileThatShouldBeIgnoredFromWatch = ['output', /\$__StoryList/]; private readonly topLevelFoldersToIgnored = ['node_modules', '.git']; @@ -512,4 +535,19 @@ export class Wiki implements IWikiService { public async updateSubWikiPluginContent(mainWikiPath: string, newConfig?: IWorkspace, oldConfig?: IWorkspace): Promise { return updateSubWikiPluginContent(mainWikiPath, newConfig, oldConfig); } + + public wikiOperation( + operationType: OP, + arguments_: Parameters, + ): undefined | ReturnType { + if (typeof wikiOperations[operationType] !== 'function') { + throw new TypeError(`${operationType} gets no useful handler`); + } + if (!Array.isArray(arguments_)) { + // TODO: better type handling here + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/restrict-template-expressions + throw new TypeError(`${(arguments_ as any) ?? ''} (${typeof arguments_}) is not a good argument array for ${operationType}`); + } + return wikiOperations[operationType].apply(undefined, arguments_) as unknown as ReturnType; + } } diff --git a/src/services/wiki/interface.ts b/src/services/wiki/interface.ts index 05312410..443270ea 100644 --- a/src/services/wiki/interface.ts +++ b/src/services/wiki/interface.ts @@ -3,6 +3,7 @@ import { WikiChannel } from '@/constants/channels'; import { IWorkspace } from '@services/workspaces/interface'; import { IGitUserInfos } from '@services/git/interface'; import type { ISubWikiPluginContent } from './plugin/subWikiPlugin'; +import { IWikiOperations } from './wikiOperations'; export type IWikiMessage = IWikiLogMessage | IWikiControlMessage; export interface IWikiLogMessage { @@ -30,6 +31,7 @@ export interface IWikiService { /** call wiki worker to actually start nodejs wiki */ startWiki(homePath: string, tiddlyWikiPort: number, userName: string): Promise; stopWiki(homePath: string): Promise; + restartWiki(workspace: IWorkspace): Promise; stopAllWiki(): Promise; copyWikiTemplate(newFolderPath: string, folderName: string): Promise; getSubWikiPluginContent(mainWikiPath: string): Promise; @@ -66,6 +68,7 @@ export interface IWikiService { setWikiStartLockOn(wikiFolderLocation: string): void; setAllWikiStartLockOff(): void; checkWikiStartLock(wikiFolderLocation: string): boolean; + wikiOperation(operationType: OP, arguments_: Parameters): undefined | ReturnType; } export const WikiServiceIPCDescriptor = { channel: WikiChannel.name, @@ -73,6 +76,7 @@ export const WikiServiceIPCDescriptor = { updateSubWikiPluginContent: ProxyPropertyType.Function, startWiki: ProxyPropertyType.Function, stopWiki: ProxyPropertyType.Function, + restartWiki: ProxyPropertyType.Function, stopAllWiki: ProxyPropertyType.Function, copyWikiTemplate: ProxyPropertyType.Function, getSubWikiPluginContent: ProxyPropertyType.Function, @@ -89,5 +93,6 @@ export const WikiServiceIPCDescriptor = { watchWikiForDebounceCommitAndSync: ProxyPropertyType.Function, stopWatchWiki: ProxyPropertyType.Function, stopWatchAllWiki: ProxyPropertyType.Function, + wikiOperation: ProxyPropertyType.Function, }, }; diff --git a/src/services/wiki/wikiOperations.ts b/src/services/wiki/wikiOperations.ts new file mode 100644 index 00000000..a3d5449b --- /dev/null +++ b/src/services/wiki/wikiOperations.ts @@ -0,0 +1,29 @@ +import { WikiChannel } from '@/constants/channels'; +import { container } from '@services/container'; +import serviceIdentifier from '@services/serviceIdentifier'; +import { IViewService } from '@services/view/interface'; +import { IWindowService } from '@services/windows/interface'; +import { WindowNames } from '@services/windows/WindowProperties'; + +/** + * Handle sending message to trigger operations defined in `src/preload/wikiOperation.ts` + */ +export const wikiOperations = { + [WikiChannel.createProgress]: (message: string): void => { + const windowService = container.get(serviceIdentifier.Window); + const createWorkspaceWindow = windowService.get(WindowNames.addWorkspace); + createWorkspaceWindow?.webContents?.send(WikiChannel.createProgress, message); + }, + [WikiChannel.syncProgress]: async (message: string): Promise => { + const viewService = container.get(serviceIdentifier.View); + const browserView = await viewService.getActiveBrowserView(); + browserView?.webContents?.send(WikiChannel.syncProgress, message); + }, + [WikiChannel.generalNotification]: async (message: string): Promise => { + const viewService = container.get(serviceIdentifier.View); + const browserView = await viewService.getActiveBrowserView(); + browserView?.webContents?.send(WikiChannel.generalNotification, message); + }, + // TODO: add more operations here from `src/preload/wikiOperation.ts` +}; +export type IWikiOperations = typeof wikiOperations;