From ec3aaa5e3ae1cfb7f75f21962484499e0ce4bda9 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Tue, 26 Apr 2022 15:17:24 +0800 Subject: [PATCH] Add desktop save function --- source/Context/Archive.tsx | 48 +++++++++++++++-- source/Context/Electron.ts | 22 +++++++- source/Electron/Electron.ts | 54 ++++++++++++++++++- source/Electron/SimulatorAPI.ts | 6 +++ source/Electron/SimulatorWindow.ts | 45 ++++++---------- source/Localization/EN-US.ts | 3 ++ source/Localization/ZH-CN.ts | 3 ++ .../SimulatorDesktop/SimulatorDesktop.tsx | 17 +----- 8 files changed, 147 insertions(+), 51 deletions(-) diff --git a/source/Context/Archive.tsx b/source/Context/Archive.tsx index cef0fe8..8e27be2 100644 --- a/source/Context/Archive.tsx +++ b/source/Context/Archive.tsx @@ -1,8 +1,9 @@ import { FunctionComponent, useEffect } from "react"; -import * as download from "downloadjs"; import { useSetting, IMixinSettingProps, Platform } from "@Context/Setting"; import { useStatus, IMixinStatusProps } from "@Context/Status"; +import { useElectron, IMixinElectronProps } from "@Context/Electron"; import { I18N } from "@Component/Localization/Localization"; +import * as download from "downloadjs"; interface IFileInfo { fileName: string; @@ -21,14 +22,14 @@ interface ICallBackProps { then: () => any; } -const ArchiveSaveDownloadView: FunctionComponent = function ArchiveSave(props) { +const ArchiveSaveDownloadView: FunctionComponent = function ArchiveSaveDownloadView(props) { const runner = async () => { const file = await props.fileData(); setTimeout(() => { download(file, props.fileName, "text/json"); props.then(); - }, 100); + }, 10); } useEffect(() => { runner() }, []); @@ -38,6 +39,45 @@ const ArchiveSaveDownloadView: FunctionComponent = f const ArchiveSaveDownload = ArchiveSaveDownloadView; +const ArchiveSaveFsView: FunctionComponent = +function ArchiveSaveFsView(props) { + + const runner = async () => { + const file = await props.fileData(); + if (props.electron) { + props.electron.fileSave( + file, + I18N(props, "Popup.Load.Save.Select.File.Name"), + I18N(props, "Popup.Load.Save.Select.Path.Title"), + I18N(props, "Popup.Load.Save.Select.Path.Button"), + props.fileUrl + ); + } + } + + const saveEvent = ({name, url, success} : {name: string, url: string, success: boolean}) => { + if (success && props.status) { + props.status.archive.fileUrl = url; + props.status.archive.fileName = name; + props.status.archive.isNewFile = false; + props.status.archive.emit("fileSave", props.status.archive); + } + props.then(); + } + + useEffect(() => { + runner(); + props.electron?.on("fileSave", saveEvent); + return () => { + props.electron?.off("fileSave", saveEvent); + }; + }, []); + + return <>; +} + +const ArchiveSaveFs = useSetting(useElectron(useStatus(ArchiveSaveFsView))); + /** * 保存存档文件 */ @@ -81,7 +121,7 @@ const ArchiveSaveView: FunctionComponent : - <> + } } diff --git a/source/Context/Electron.ts b/source/Context/Electron.ts index ad7787a..76147fa 100644 --- a/source/Context/Electron.ts +++ b/source/Context/Electron.ts @@ -1,4 +1,5 @@ import { createContext } from "react"; +import { Emitter } from "@Model/Emitter"; import { superConnect, superConnectWithEvent } from "@Context/Context"; import { ISimulatorAPI, IApiEmitterEvent } from "@Electron/SimulatorAPI"; @@ -6,6 +7,25 @@ interface IMixinElectronProps { electron?: ISimulatorAPI; } +const getElectronAPI: () => ISimulatorAPI = () => { + const API = (window as any).API; + const mapperEmitter = new Emitter(); + const ClassElectron: new () => ISimulatorAPI = function (this: Record) { + this.resetAll = () => mapperEmitter.resetAll(); + this.reset = (type: string) => mapperEmitter.reset(type); + this.on = (type: string, handel: any) => mapperEmitter.on(type, handel); + this.off = (type: string, handel: any) => mapperEmitter.off(type, handel); + this.emit = (type: string, data: any) => mapperEmitter.emit(type, data); + } as any; + ClassElectron.prototype = API; + + // Emitter Mapper + API.mapEmit((...p: any) => { + mapperEmitter.emit(...p); + }); + return new ClassElectron(); +} + const ElectronContext = createContext((window as any).API ?? {} as ISimulatorAPI); ElectronContext.displayName = "Electron"; @@ -19,4 +39,4 @@ const useElectron = superConnect(ElectronConsumer, "electron"); const useElectronWithEvent = superConnectWithEvent(ElectronConsumer, "electron"); -export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent }; \ No newline at end of file +export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent, getElectronAPI }; \ No newline at end of file diff --git a/source/Electron/Electron.ts b/source/Electron/Electron.ts index e62db7a..3132ab1 100644 --- a/source/Electron/Electron.ts +++ b/source/Electron/Electron.ts @@ -1,6 +1,7 @@ -import { app, BrowserWindow, ipcMain } from "electron"; +import { app, BrowserWindow, ipcMain, dialog } from "electron"; import { Service } from "@Service/Service"; import { join as pathJoin } from "path"; +import { writeFile } from "fs"; const ENV = process.env ?? {}; class ElectronApp { @@ -55,6 +56,7 @@ class ElectronApp { this.simulatorWindow.loadURL(this.serviceUrl + (ENV.LIVING_TOGETHER_WEB_PATH ?? "/resources/app.asar/")); this.handelSimulatorWindowBehavior(); + this.handelFileChange(); app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() @@ -90,6 +92,56 @@ class ElectronApp { this.simulatorWindow?.on("maximize", sendWindowsChangeMessage); this.simulatorWindow?.on("unmaximize", sendWindowsChangeMessage); } + + private handelFileChange() { + + // 文件保存 + const saveFile = async (path: string, text: string) => { + return new Promise((r) => { + writeFile(path ?? "", text, {}, (e) => { + this.simulatorWindow?.webContents.send( + "windows.EndFileSave", + (path.match(/.+(\/|\\)(.+)$/) ?? [])[2], + path, !e + ); + r(undefined); + }); + }) + }; + + // 处理文件保存事件 + ipcMain.on("windows.fileSave", + (_, text: string, name: string, title: string, button: string, url?: string) => { + + // 如果没有路径,询问新的路径 + if (url) { + saveFile(url, text); + } + + // 询问保存位置 + else { + dialog.showSaveDialog(this.simulatorWindow!, { + title: title, + buttonLabel: button, + filters: [ + { name: name, extensions: ["ltss"] } + ] + }).then(res => { + + // 用户选择后继续保存 + if (!res.canceled && res.filePath) { + saveFile(res.filePath, text); + } else { + this.simulatorWindow?.webContents.send( + "windows.EndFileSave", + undefined, undefined, false + ); + } + }); + } + } + ); + } } new ElectronApp().runMainThread(); \ No newline at end of file diff --git a/source/Electron/SimulatorAPI.ts b/source/Electron/SimulatorAPI.ts index 6581821..cfeeea9 100644 --- a/source/Electron/SimulatorAPI.ts +++ b/source/Electron/SimulatorAPI.ts @@ -2,6 +2,7 @@ import { Emitter } from "@Model/Emitter"; type IApiEmitterEvent = { windowsSizeStateChange: void; + fileSave: {success: boolean, name: string, url: string}; } interface ISimulatorAPI extends Emitter { @@ -30,6 +31,11 @@ interface ISimulatorAPI extends Emitter { * 是否处于最大化状态 */ minimize: () => void; + + /** + * 存档 + */ + fileSave: (text: string, name: string, title: string, button: string, url?: string) => void; } export { ISimulatorAPI, IApiEmitterEvent } \ No newline at end of file diff --git a/source/Electron/SimulatorWindow.ts b/source/Electron/SimulatorWindow.ts index 4012366..66a5430 100644 --- a/source/Electron/SimulatorWindow.ts +++ b/source/Electron/SimulatorWindow.ts @@ -1,23 +1,11 @@ import { contextBridge, ipcRenderer } from "electron"; -import { ISimulatorAPI } from "@Electron/SimulatorAPI" +import { ISimulatorAPI } from "@Electron/SimulatorAPI"; -const emitterMap: Array<[key: string, value: Function[]]> = []; -const queryEmitter = (key: string) => { - let res: (typeof emitterMap)[0] | undefined; - emitterMap.forEach((item) => { - if (item[0] === key) res = item; - }); +const emitterMap: { fn?: Function } = { fn: undefined }; - if (res) { - if (Array.isArray(res[1])) return res[1]; - res[1] = []; - return res[1]; - } - - else { - res = [key, []]; - emitterMap.push(res); - return res[1]; +const emit = (type: string, evt?: any) => { + if (emitterMap.fn) { + emitterMap.fn(type, evt); } } @@ -43,22 +31,19 @@ const API: ISimulatorAPI = { ipcRenderer.send("windows.minimize"); }, - all: new Map() as any, - - resetAll: () => emitterMap.splice(0), - reset: (type) => queryEmitter(type).splice(0), - on: (type, handler) => queryEmitter(type).push(handler), - off: (type, handler) => { - const handlers = queryEmitter(type); - handlers.splice(handlers.indexOf(handler!) >>> 0, 1); + fileSave(text: string, name: string, title: string, button: string, url?: string) { + ipcRenderer.send("windows.fileSave", text, name, title, button, url); }, - emit: ((type: string, evt: any) => { - queryEmitter(type).slice().map((handler: any) => { handler(evt) }); - }) as any, -} + + mapEmit: (fn: Function) => { emitterMap.fn = fn }, +} as any; ipcRenderer.on("windows.windowsSizeStateChange", () => { - API.emit("windowsSizeStateChange"); + emit("windowsSizeStateChange"); +}); + +ipcRenderer.on("windows.EndFileSave", (_, name: string, url: string, success: boolean) => { + emit("fileSave", {name, url, success}); }); contextBridge.exposeInMainWorld("API", API); \ No newline at end of file diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index bde9d05..2138ff9 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -72,6 +72,9 @@ const EN_US = { "Popup.Load.Save.Error.Empty": "File information acquisition error. The file has been lost or moved.", "Popup.Load.Save.Error.Type": "The file with extension name \"{ext}\" cannot be loaded temporarily", "Popup.Load.Save.Error.Parse": "Archive parsing error, detailed reason: \n{why}", + "Popup.Load.Save.Select.Path.Title": "Please select an archive location", + "Popup.Load.Save.Select.Path.Button": "Save", + "Popup.Load.Save.Select.File.Name": "Living Together Simulator Save", "Popup.Add.Behavior.Title": "Add behavior", "Popup.Add.Behavior.Action.Add": "Add all select behavior", "Popup.Add.Behavior.Select.Counter": "Selected {count} behavior", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index fb9de81..1ee7e39 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -72,6 +72,9 @@ const ZH_CN = { "Popup.Load.Save.Error.Empty": "文件信息获取错误,文件已丢失或已被移动", "Popup.Load.Save.Error.Type": "暂时无法加载拓展名为 \"{ext}\" 的文件", "Popup.Load.Save.Error.Parse": "存档解析错误,详细原因: \n{why}", + "Popup.Load.Save.Select.Path.Title": "请选择存档保存位置", + "Popup.Load.Save.Select.Path.Button": "保存", + "Popup.Load.Save.Select.File.Name": "群生共进存档", "Popup.Add.Behavior.Title": "添加行为", "Popup.Add.Behavior.Action.Add": "添加全部选中行为", "Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为", diff --git a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx index b1f74a7..62ba58c 100644 --- a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx +++ b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx @@ -3,7 +3,7 @@ import { SettingProvider, Setting, Platform } from "@Context/Setting"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; import { ISimulatorAPI } from "@Electron/SimulatorAPI"; import { StatusProvider, Status } from "@Context/Status"; -import { ElectronProvider } from "@Context/Electron"; +import { ElectronProvider, getElectronAPI } from "@Context/Electron"; import { ClassicRenderer } from "@GLRender/ClassicRenderer"; import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; @@ -47,25 +47,12 @@ class SimulatorDesktop extends Component { this.status.bindRenderer(classicRender); this.status.setting = this.setting; - const randomPosition = (group: Group) => { - group.individuals.forEach((individual) => { - individual.position[0] = (Math.random() - .5) * 2; - individual.position[1] = (Math.random() - .5) * 2; - individual.position[2] = (Math.random() - .5) * 2; - }) - }; - (window as any).LT = { status: this.status, setting: this.setting }; - this.electron = {} as ISimulatorAPI; - if ((window as any).API) { - this.electron = (window as any).API; - } else { - console.error("SimulatorDesktop: Can't find electron API"); - } + this.electron = getElectronAPI(); } public componentDidMount() {