diff --git a/localization/locales/zh_CN/translation.json b/localization/locales/zh_CN/translation.json index 2002f61f..3b58a08c 100644 --- a/localization/locales/zh_CN/translation.json +++ b/localization/locales/zh_CN/translation.json @@ -110,6 +110,9 @@ "CreateWiki": "创建WIKI", "ImportWiki": "导入WIKI", "CloneWiki": "导入线上Wiki", + "OpenLocalWikiFromHTML":"导入WIKI.HTML", + "LocalWikiHtml":"tiddlywiki.html文件路径", + "StoreWikiFolderLocation":"转换后的WIKI父文件夹路径", "NotLoggedIn": "未登录", "LogoutToGetStorageServiceToken": "登录在线存储服务以获取最新凭证", "LogoutGithubAccount": "登出Github账号", diff --git a/src/pages/AddWorkspace/ImportHtmlWikiDoneButton.tsx b/src/pages/AddWorkspace/ImportHtmlWikiDoneButton.tsx new file mode 100644 index 00000000..37973b56 --- /dev/null +++ b/src/pages/AddWorkspace/ImportHtmlWikiDoneButton.tsx @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import { useTranslation } from 'react-i18next'; +import Alert from '@material-ui/lab/Alert'; + +import { Typography, LinearProgress, Snackbar } from '@material-ui/core'; +import type { IWikiWorkspaceFormProps } from './useForm'; +import { useValidateHtmlWiki, useImportHtmlWiki } from './useImportHtmlWiki'; +import { useWikiCreationProgress } from './useIndicator'; +import { WikiLocation, CloseButton, ReportErrorFabButton } from './FormComponents'; + +export function ImportHtmlWikiDoneButton({ + form, + isCreateMainWorkspace, + isCreateSyncedWorkspace, + errorInWhichComponentSetter, +}: IWikiWorkspaceFormProps & { isCreateMainWorkspace: boolean; isCreateSyncedWorkspace: boolean }): JSX.Element { + const { t } = useTranslation(); + const [hasError, wikiCreationMessage, wikiCreationMessageSetter, hasErrorSetter] = useValidateHtmlWiki( + isCreateMainWorkspace, + isCreateSyncedWorkspace, + form, + errorInWhichComponentSetter, + ); + const onSubmit = useImportHtmlWiki( + isCreateMainWorkspace, + isCreateSyncedWorkspace, + form, + wikiCreationMessageSetter, + hasErrorSetter, + errorInWhichComponentSetter, + ); + const [logPanelOpened, logPanelSetter, inProgressOrError] = useWikiCreationProgress(wikiCreationMessageSetter, wikiCreationMessage, hasError); + if (hasError) { + return ( + <> + + {wikiCreationMessage} + + {wikiCreationMessage !== undefined && } + + ); + } + return ( + <> + {inProgressOrError && } + {/* 这个好像是log面板 */} + logPanelSetter(false)}> + {wikiCreationMessage} + + + + + {t('AddWorkspace.ImportWiki')} + + {form.wikiHtmlPath} + + + ); +} diff --git a/src/pages/AddWorkspace/ImportHtmlWikiForm.tsx b/src/pages/AddWorkspace/ImportHtmlWikiForm.tsx new file mode 100644 index 00000000..aa5a13f5 --- /dev/null +++ b/src/pages/AddWorkspace/ImportHtmlWikiForm.tsx @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import React, { useCallback } from 'react'; + +import { useTranslation } from 'react-i18next'; +import { Typography } from '@material-ui/core'; +import { Folder as FolderIcon } from '@material-ui/icons'; +import { useValidateHtmlWiki } from './useImportHtmlWiki'; + +import { CreateContainer, LocationPickerContainer, LocationPickerInput, LocationPickerButton } from './FormComponents'; + +import type { IWikiWorkspaceFormProps } from './useForm'; + +export function ImportHtmlWikiForm({ + form, + isCreateMainWorkspace, + isCreateSyncedWorkspace, + errorInWhichComponent, + errorInWhichComponentSetter, +}: IWikiWorkspaceFormProps & { isCreateSyncedWorkspace: boolean }): JSX.Element { + const { t } = useTranslation(); + const { wikiHtmlPathSetter, extractWikiHtmlParentFolderSetter } = form; + + useValidateHtmlWiki(isCreateMainWorkspace, isCreateSyncedWorkspace, form, errorInWhichComponentSetter); + + const onWikiLocationChange = useCallback( + async (newLocation: string) => { + if (newLocation !== undefined) { + wikiHtmlPathSetter(newLocation); + } + }, + [wikiHtmlPathSetter], + ); + const onSaveLocationChange = useCallback( + async (newLocation: string) => { + if (newLocation !== undefined) { + extractWikiHtmlParentFolderSetter(newLocation); + } + }, + [extractWikiHtmlParentFolderSetter], + ); + return ( + + + { + // https://zh-hans.reactjs.org/docs/events.html#clipboard-events + onWikiLocationChange(event.target.value); + }} + label={t('AddWorkspace.LocalWikiHtml')} + value={form.wikiHtmlPath} + /> + { + // first clear the text, so button will refresh + wikiHtmlPathSetter(''); + const filePaths = await window.service.native.pickFile([{ name: 'html文件', extensions: ['html', 'htm'] }]); + if (filePaths?.length > 0) { + wikiHtmlPathSetter(filePaths[0]); + } + }} + endIcon={}> + + {t('AddWorkspace.Choose')} + + + + + { + onSaveLocationChange(event.target.value); + }} + label={t('AddWorkspace.StoreWikiFolderLocation')} + value={form.extractWikiHtmlParentFolder} + /> + { + // first clear the text, so button will refresh + extractWikiHtmlParentFolderSetter(''); + const filePaths = await window.service.native.pickDirectory(form.wikiFolderLocation); + if (filePaths?.length > 0) { + extractWikiHtmlParentFolderSetter(filePaths[0]); + } + }} + endIcon={}> + + {t('AddWorkspace.Choose')} + + + + + ); +} diff --git a/src/pages/AddWorkspace/index.tsx b/src/pages/AddWorkspace/index.tsx index 91200838..bc61f631 100644 --- a/src/pages/AddWorkspace/index.tsx +++ b/src/pages/AddWorkspace/index.tsx @@ -23,11 +23,14 @@ import { TokenForm } from '@/components/TokenForm'; import { GitRepoUrlForm } from './GitRepoUrlForm'; import { LocationPickerContainer, LocationPickerInput } from './FormComponents'; import { usePromiseValue } from '@/helpers/useServiceValue'; +import { ImportHtmlWikiForm } from './ImportHtmlWikiForm'; +import { ImportHtmlWikiDoneButton } from './ImportHtmlWikiDoneButton'; enum CreateWorkspaceTabs { CloneOnlineWiki = 'CloneOnlineWiki', CreateNewWiki = 'CreateNewWiki', OpenLocalWiki = 'OpenLocalWiki', + OpenLocalWikiFromHtml = 'OpenLocalWikiFromHtml', } export const Paper = styled(PaperRaw)` @@ -122,6 +125,7 @@ export function AddWorkspace(): JSX.Element { + @@ -172,6 +176,12 @@ export function AddWorkspace(): JSX.Element { + + + + + + ); } diff --git a/src/pages/AddWorkspace/useForm.ts b/src/pages/AddWorkspace/useForm.ts index 626ae047..0d63f1ed 100644 --- a/src/pages/AddWorkspace/useForm.ts +++ b/src/pages/AddWorkspace/useForm.ts @@ -106,6 +106,23 @@ export function useWikiWorkspaceForm(options?: { fromExisted: boolean }) { })(); }, [gitRepoUrlSetter, wikiFolderLocation, options?.fromExisted]); + /* + * 对于wikiHTML,我们使用两个状态保存文件与wiki解压父文件夹路径,并设置默认的wiki文件夹保存位置. + * wikiHtmlPath、wikiHtmlPathSetter、extractWikiHtmlParentFolder、extractWikiHtmlParentFolderSetter, + */ + const [wikiHtmlPath, wikiHtmlPathSetter] = useState(''); + useEffect(() => { + void (async function getDefaultWikiHtmlPathEffect() {})(); + }, []); + + const [extractWikiHtmlParentFolder, extractWikiHtmlParentFolderSetter] = useState(''); + useEffect(() => { + void (async function getDefaultExtractWikiHtmlFolderPathEffect() { + const desktopPathAsDefaultExtractWikiHtmlParentFolderPath = await window.service.context.get('DEFAULT_WIKI_FOLDER'); + extractWikiHtmlParentFolderSetter(desktopPathAsDefaultExtractWikiHtmlParentFolderPath); + })(); + }, []); + return { storageProvider, storageProviderSetter, @@ -128,6 +145,10 @@ export function useWikiWorkspaceForm(options?: { fromExisted: boolean }) { workspaceList, mainWorkspaceList, mainWikiToLinkIndex, + wikiHtmlPath, + wikiHtmlPathSetter, + extractWikiHtmlParentFolder, + extractWikiHtmlParentFolderSetter, }; } diff --git a/src/pages/AddWorkspace/useImportHtmlWiki.ts b/src/pages/AddWorkspace/useImportHtmlWiki.ts new file mode 100644 index 00000000..750f8c6d --- /dev/null +++ b/src/pages/AddWorkspace/useImportHtmlWiki.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IErrorInWhichComponent, IWikiWorkspaceForm } from './useForm'; +import { updateErrorInWhichComponentSetterByErrorMessage } from './useIndicator'; + +export function useValidateHtmlWiki( + isCreateMainWorkspace: boolean, + isCreateSyncedWorkspace: boolean, + form: IWikiWorkspaceForm, + errorInWhichComponentSetter: (errors: IErrorInWhichComponent) => void, +): [boolean, string | undefined, (m: string) => void, (m: boolean) => void] { + const { t } = useTranslation(); + const [wikiCreationMessage, wikiCreationMessageSetter] = useState(); + const [hasError, hasErrorSetter] = useState(false); + useEffect(() => { + if (!form.parentFolderLocation) { + wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}:${t('AddWorkspace.WorkspaceFolder')}`); + errorInWhichComponentSetter({ parentFolderLocation: true }); + hasErrorSetter(true); + } else if (!form.wikiHtmlPath) { + // 判断wikiHtmlPath是否存在 + wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}:${t('AddWorkspace.LocalWikiHtml')}`); + errorInWhichComponentSetter({ wikiHtmlPath: true }); + hasErrorSetter(true); + } else if (!form.wikiFolderName) { + wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}:${t('AddWorkspace.WorkspaceFolderNameToCreate')}`); + errorInWhichComponentSetter({ wikiFolderName: true }); + hasErrorSetter(true); + } else if (isCreateSyncedWorkspace && !form.gitRepoUrl) { + wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}:${t('AddWorkspace.GitRepoUrl')}`); + errorInWhichComponentSetter({ gitRepoUrl: true }); + hasErrorSetter(true); + } else if (!isCreateMainWorkspace && !form.mainWikiToLink?.wikiFolderLocation) { + wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}:${t('AddWorkspace.MainWorkspace')}`); + errorInWhichComponentSetter({ mainWikiToLink: true }); + hasErrorSetter(true); + } else if (isCreateSyncedWorkspace && (form.gitUserInfo === undefined || !(form.gitUserInfo.accessToken?.length > 0))) { + wikiCreationMessageSetter(t('AddWorkspace.NotLoggedIn')); + errorInWhichComponentSetter({ gitUserInfo: true }); + hasErrorSetter(true); + } else { + wikiCreationMessageSetter(''); + errorInWhichComponentSetter({}); + hasErrorSetter(false); + } + }, [ + t, + isCreateMainWorkspace, + isCreateSyncedWorkspace, + form.parentFolderLocation, + form.wikiFolderName, + form.gitRepoUrl, + form.gitUserInfo, + form.mainWikiToLink?.wikiFolderLocation, + form.tagName, + // 监听wikiHtmlPath,如果不存在就在提示用户。 + form.wikiHtmlPath, + errorInWhichComponentSetter, + ]); + + return [hasError, wikiCreationMessage, wikiCreationMessageSetter, hasErrorSetter]; +} + +export function useImportHtmlWiki( + isCreateMainWorkspace: boolean, + isCreateSyncedWorkspace: boolean, + form: IWikiWorkspaceForm, + wikiCreationMessageSetter: (m: string) => void, + hasErrorSetter: (m: boolean) => void, + errorInWhichComponentSetter: (errors: IErrorInWhichComponent) => void, +): () => Promise { + const { t } = useTranslation(); + + const onSubmit = useCallback(async () => { + wikiCreationMessageSetter(t('AddWorkspace.Processing')); + hasErrorSetter(false); + try { + // 如果是HTML文件,即使转换错误,删掉在执行一次也不会出错。 + // 我希望判断用户输入的是否是HTML文件,如果不是就不予执行。然后在判断如果失败了就删除这个数据并且提示错误信息。如果输入的是html类型的文件是不会出错的,即使是非wiki类型的文件。如果输出的目录非空,那么会导致异常闪退。 + // 我希望,可以创建一个与HTML文件名一样的文件夹,这样的话就可以在父文件夹中解压出一个HTML文件名一样的文件夹而不只是wiki数据。 + // 我希望信息可以显示在log面板。当出现解压错误后就提示用户。 + const extractState = await window.service.wiki.extractWikiHTML(form.wikiHtmlPath, form.extractWikiHtmlParentFolder); + // 待解决: var extractState用于接收执行是否解压是否成功。但是接收不到window.service.wiki.extractWikiHTML函数的返回值,并且在该函数上下打log都未显示。 + // 我希望在解压成功后设置好工作区的信息,执行打开解压后的wiki文件夹的操作。 + } catch (error) { + wikiCreationMessageSetter((error as Error).message); + updateErrorInWhichComponentSetterByErrorMessage(t, (error as Error).message, errorInWhichComponentSetter); + hasErrorSetter(true); + } + }, [ + wikiCreationMessageSetter, + t, + hasErrorSetter, + form, + isCreateMainWorkspace, + isCreateSyncedWorkspace, + form.wikiHtmlPath, + form.extractWikiHtmlParentFolder, + errorInWhichComponentSetter, + ]); + + return onSubmit; +} diff --git a/src/services/wiki/index.ts b/src/services/wiki/index.ts index 2a4e6ef5..f6629c92 100644 --- a/src/services/wiki/index.ts +++ b/src/services/wiki/index.ts @@ -168,6 +168,22 @@ export class Wiki implements IWikiService { }); } + public async extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath: string): Promise { + // hope saveWikiFolderPath = ParentFolderPath + wikifolderPath + // await fs.remove(saveWikiFolderPath); removes the folder function that failed to convert. + // We want the folder where the WIKI is saved to be empty, and we want the input htmlWiki to be an HTML file even if it is a non-wikiHTML file. Otherwise the program will exit abnormally. + // this.wikiWorkers[saveWikiFolderPath] = worker; + // Then, you can use this.getWorker (saveWikiFolderPath) method to call this wikiWorker that belongs to the HTMLWIKI after decompression + const worker = await spawn(new Worker(workerURL as string), { timeout: 1000 * 60 }); + const result = await worker.ExtractWikiHTMLAndGetExtractState(htmlWikiPath, saveWikiFolderPath); + return result; + } + + public async packetHTMLFromWikiFolder(folderWikiPath: string, saveWikiHtmlFolder: string): Promise { + const worker = await spawn(new Worker(workerURL as string), { timeout: 1000 * 60 }); + await worker.packetHTMLFromWikiFolder(folderWikiPath, saveWikiHtmlFolder); + } + public async stopWiki(wikiFolderLocation: string): Promise { const worker = this.getWorker(wikiFolderLocation); if (worker === undefined) { diff --git a/src/services/wiki/interface.ts b/src/services/wiki/interface.ts index 641c8ece..b317cec7 100644 --- a/src/services/wiki/interface.ts +++ b/src/services/wiki/interface.ts @@ -35,6 +35,7 @@ export interface IWikiService { */ createSubWiki(parentFolderLocation: string, folderName: string, mainWikiPath: string, tagName?: string, onlyLink?: boolean): Promise; ensureWikiExist(wikiPath: string, shouldBeMainWiki: boolean): Promise; + extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath: string): Promise; getSubWikiPluginContent(mainWikiPath: string): Promise; getTiddlerText(workspace: IWorkspace, title: string): Promise; getWikiLogs(homePath: string): Promise<{ content: string; filePath: string }>; @@ -50,6 +51,7 @@ export interface IWikiService { * @param title tiddler title to open */ openTiddlerInExternal(homePath: string, title: string): Promise; + packetHTMLFromWikiFolder(folderWikiPath: string, saveWikiHtmlfolder: string): Promise; removeWiki(wikiPath: string, mainWikiToUnLink?: string, onlyRemoveLink?: boolean): Promise; requestOpenTiddlerInWiki(tiddlerName: string): Promise; /** send tiddlywiki action message to current active wiki */ @@ -98,6 +100,9 @@ export const WikiServiceIPCDescriptor = { updateSubWikiPluginContent: ProxyPropertyType.Function, wikiOperation: ProxyPropertyType.Function, wikiStartup: ProxyPropertyType.Function, + // Register here to unpack and package wikiHTML functions + extractWikiHTML: ProxyPropertyType.Function, + packetHTMLFromWikiFolder: ProxyPropertyType.Function, }, }; diff --git a/src/services/wiki/wikiWorker.ts b/src/services/wiki/wikiWorker.ts index 0bf7bdee..91d73af6 100644 --- a/src/services/wiki/wikiWorker.ts +++ b/src/services/wiki/wikiWorker.ts @@ -131,6 +131,52 @@ function executeZxScript(file: IZxFileInput, zxPath: string): Observable wikiInstance?.boot?.files?.[tiddlerTitle], executeZxScript }; +function extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath: string): boolean { + // tiddlywiki --load ./mywiki.html --savewikifolder ./mywikifolder + // --savewikifolder [] + // . /mywikifolder is the path where the tiddlder and plugins folders are stored + let extractState = false; + // eslint-disable-next-line prefer-regex-literals + const reg = new RegExp(/(?:html|htm|Html|HTML|HTM)$/); + const isHtmlWiki = reg.test(htmlWikiPath); + if (!isHtmlWiki) { + console.error('Please enter the path to the tiddlywiki.html file. But the current path is: ' + htmlWikiPath); + return extractState; + } else { + try { + const wikiInstance = TiddlyWiki(); + wikiInstance.boot.argv = ['--load', htmlWikiPath, '--savewikifolder', saveWikiFolderPath]; + wikiInstance.boot.startup({}); + // eslint-disable-next-line security-node/detect-crlf + console.log('Extract Wiki Html Successful: ' + saveWikiFolderPath); + extractState = true; + } catch (error) { + const message = `Tiddlywiki extractWikiHTML with error ${(error as Error).message} ${(error as Error).stack ?? ''}`; + console.error(message); + } + } + return extractState; +} + +function packetHTMLFromWikiFolder(folderWikiPath: string, saveWikiHtmlFolder: string): void { + // tiddlywiki ./mywikifolder --rendertiddler '$:/core/save/all' mywiki.html text/plain + // . /mywikifolder is the path to the wiki folder, which generally contains the tiddlder and plugins directories + try { + const wikiInstance = TiddlyWiki(); + wikiInstance.boot.argv = [folderWikiPath, '--rendertiddler', '$:/core/save/all', saveWikiHtmlFolder, 'text/plain']; + wikiInstance.boot.startup({}); + } catch (error) { + const message = `Tiddlywiki packetHTMLFromWikiFolder with error ${(error as Error).message} ${(error as Error).stack ?? ''}`; + console.error(message); + } +} + +const wikiWorker = { + startNodeJSWiki, + getTiddlerFileMetadata: (tiddlerTitle: string) => wikiInstance?.boot?.files?.[tiddlerTitle], + executeZxScript, + ExtractWikiHTMLAndGetExtractState: (htmlWikiPath: string, saveWikiFolderPath: string) => extractWikiHTML(htmlWikiPath, saveWikiFolderPath), + packetHTMLFromWikiFolder, +}; export type WikiWorker = typeof wikiWorker; expose(wikiWorker);