Add desktop save function

This commit is contained in:
MrKBear 2022-04-26 15:17:24 +08:00
parent 0ab53c1800
commit ec3aaa5e3a
8 changed files with 147 additions and 51 deletions

View File

@ -1,8 +1,9 @@
import { FunctionComponent, useEffect } from "react"; import { FunctionComponent, useEffect } from "react";
import * as download from "downloadjs";
import { useSetting, IMixinSettingProps, Platform } from "@Context/Setting"; import { useSetting, IMixinSettingProps, Platform } from "@Context/Setting";
import { useStatus, IMixinStatusProps } from "@Context/Status"; import { useStatus, IMixinStatusProps } from "@Context/Status";
import { useElectron, IMixinElectronProps } from "@Context/Electron";
import { I18N } from "@Component/Localization/Localization"; import { I18N } from "@Component/Localization/Localization";
import * as download from "downloadjs";
interface IFileInfo { interface IFileInfo {
fileName: string; fileName: string;
@ -21,14 +22,14 @@ interface ICallBackProps {
then: () => any; then: () => any;
} }
const ArchiveSaveDownloadView: FunctionComponent<IFileInfo & ICallBackProps> = function ArchiveSave(props) { const ArchiveSaveDownloadView: FunctionComponent<IFileInfo & ICallBackProps> = function ArchiveSaveDownloadView(props) {
const runner = async () => { const runner = async () => {
const file = await props.fileData(); const file = await props.fileData();
setTimeout(() => { setTimeout(() => {
download(file, props.fileName, "text/json"); download(file, props.fileName, "text/json");
props.then(); props.then();
}, 100); }, 10);
} }
useEffect(() => { runner() }, []); useEffect(() => { runner() }, []);
@ -38,6 +39,45 @@ const ArchiveSaveDownloadView: FunctionComponent<IFileInfo & ICallBackProps> = f
const ArchiveSaveDownload = ArchiveSaveDownloadView; const ArchiveSaveDownload = ArchiveSaveDownloadView;
const ArchiveSaveFsView: FunctionComponent<IFileInfo & ICallBackProps & IMixinElectronProps & IMixinSettingProps & IMixinStatusProps> =
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<IMixinSettingProps & IMixinStatusProps
{ {
props.setting?.platform === Platform.web ? props.setting?.platform === Platform.web ?
<ArchiveSaveDownload {...fileData} then={callBack}/> : <ArchiveSaveDownload {...fileData} then={callBack}/> :
<></> <ArchiveSaveFs {...fileData} then={callBack}/>
} }
</> </>
} }

View File

@ -1,4 +1,5 @@
import { createContext } from "react"; import { createContext } from "react";
import { Emitter } from "@Model/Emitter";
import { superConnect, superConnectWithEvent } from "@Context/Context"; import { superConnect, superConnectWithEvent } from "@Context/Context";
import { ISimulatorAPI, IApiEmitterEvent } from "@Electron/SimulatorAPI"; import { ISimulatorAPI, IApiEmitterEvent } from "@Electron/SimulatorAPI";
@ -6,6 +7,25 @@ interface IMixinElectronProps {
electron?: ISimulatorAPI; electron?: ISimulatorAPI;
} }
const getElectronAPI: () => ISimulatorAPI = () => {
const API = (window as any).API;
const mapperEmitter = new Emitter();
const ClassElectron: new () => ISimulatorAPI = function (this: Record<string, any>) {
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<ISimulatorAPI>((window as any).API ?? {} as ISimulatorAPI); const ElectronContext = createContext<ISimulatorAPI>((window as any).API ?? {} as ISimulatorAPI);
ElectronContext.displayName = "Electron"; ElectronContext.displayName = "Electron";
@ -19,4 +39,4 @@ const useElectron = superConnect<ISimulatorAPI>(ElectronConsumer, "electron");
const useElectronWithEvent = superConnectWithEvent<ISimulatorAPI, IApiEmitterEvent>(ElectronConsumer, "electron"); const useElectronWithEvent = superConnectWithEvent<ISimulatorAPI, IApiEmitterEvent>(ElectronConsumer, "electron");
export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent }; export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent, getElectronAPI };

View File

@ -1,6 +1,7 @@
import { app, BrowserWindow, ipcMain } from "electron"; import { app, BrowserWindow, ipcMain, dialog } from "electron";
import { Service } from "@Service/Service"; import { Service } from "@Service/Service";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { writeFile } from "fs";
const ENV = process.env ?? {}; const ENV = process.env ?? {};
class ElectronApp { class ElectronApp {
@ -55,6 +56,7 @@ class ElectronApp {
this.simulatorWindow.loadURL(this.serviceUrl + (ENV.LIVING_TOGETHER_WEB_PATH ?? "/resources/app.asar/")); this.simulatorWindow.loadURL(this.serviceUrl + (ENV.LIVING_TOGETHER_WEB_PATH ?? "/resources/app.asar/"));
this.handelSimulatorWindowBehavior(); this.handelSimulatorWindowBehavior();
this.handelFileChange();
app.on('window-all-closed', function () { app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit() if (process.platform !== 'darwin') app.quit()
@ -90,6 +92,56 @@ class ElectronApp {
this.simulatorWindow?.on("maximize", sendWindowsChangeMessage); this.simulatorWindow?.on("maximize", sendWindowsChangeMessage);
this.simulatorWindow?.on("unmaximize", 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(); new ElectronApp().runMainThread();

View File

@ -2,6 +2,7 @@ import { Emitter } from "@Model/Emitter";
type IApiEmitterEvent = { type IApiEmitterEvent = {
windowsSizeStateChange: void; windowsSizeStateChange: void;
fileSave: {success: boolean, name: string, url: string};
} }
interface ISimulatorAPI extends Emitter<IApiEmitterEvent> { interface ISimulatorAPI extends Emitter<IApiEmitterEvent> {
@ -30,6 +31,11 @@ interface ISimulatorAPI extends Emitter<IApiEmitterEvent> {
* *
*/ */
minimize: () => void; minimize: () => void;
/**
*
*/
fileSave: (text: string, name: string, title: string, button: string, url?: string) => void;
} }
export { ISimulatorAPI, IApiEmitterEvent } export { ISimulatorAPI, IApiEmitterEvent }

View File

@ -1,23 +1,11 @@
import { contextBridge, ipcRenderer } from "electron"; import { contextBridge, ipcRenderer } from "electron";
import { ISimulatorAPI } from "@Electron/SimulatorAPI" import { ISimulatorAPI } from "@Electron/SimulatorAPI";
const emitterMap: Array<[key: string, value: Function[]]> = []; const emitterMap: { fn?: Function } = { fn: undefined };
const queryEmitter = (key: string) => {
let res: (typeof emitterMap)[0] | undefined;
emitterMap.forEach((item) => {
if (item[0] === key) res = item;
});
if (res) { const emit = (type: string, evt?: any) => {
if (Array.isArray(res[1])) return res[1]; if (emitterMap.fn) {
res[1] = []; emitterMap.fn(type, evt);
return res[1];
}
else {
res = [key, []];
emitterMap.push(res);
return res[1];
} }
} }
@ -43,22 +31,19 @@ const API: ISimulatorAPI = {
ipcRenderer.send("windows.minimize"); ipcRenderer.send("windows.minimize");
}, },
all: new Map() as any, fileSave(text: string, name: string, title: string, button: string, url?: string) {
ipcRenderer.send("windows.fileSave", text, name, title, button, url);
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);
}, },
emit: ((type: string, evt: any) => {
queryEmitter(type).slice().map((handler: any) => { handler(evt) }); mapEmit: (fn: Function) => { emitterMap.fn = fn },
}) as any, } as any;
}
ipcRenderer.on("windows.windowsSizeStateChange", () => { 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); contextBridge.exposeInMainWorld("API", API);

View File

@ -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.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.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.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.Title": "Add behavior",
"Popup.Add.Behavior.Action.Add": "Add all select behavior", "Popup.Add.Behavior.Action.Add": "Add all select behavior",
"Popup.Add.Behavior.Select.Counter": "Selected {count} behavior", "Popup.Add.Behavior.Select.Counter": "Selected {count} behavior",

View File

@ -72,6 +72,9 @@ const ZH_CN = {
"Popup.Load.Save.Error.Empty": "文件信息获取错误,文件已丢失或已被移动", "Popup.Load.Save.Error.Empty": "文件信息获取错误,文件已丢失或已被移动",
"Popup.Load.Save.Error.Type": "暂时无法加载拓展名为 \"{ext}\" 的文件", "Popup.Load.Save.Error.Type": "暂时无法加载拓展名为 \"{ext}\" 的文件",
"Popup.Load.Save.Error.Parse": "存档解析错误,详细原因: \n{why}", "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.Title": "添加行为",
"Popup.Add.Behavior.Action.Add": "添加全部选中行为", "Popup.Add.Behavior.Action.Add": "添加全部选中行为",
"Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为", "Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为",

View File

@ -3,7 +3,7 @@ import { SettingProvider, Setting, Platform } from "@Context/Setting";
import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme";
import { ISimulatorAPI } from "@Electron/SimulatorAPI"; import { ISimulatorAPI } from "@Electron/SimulatorAPI";
import { StatusProvider, Status } from "@Context/Status"; import { StatusProvider, Status } from "@Context/Status";
import { ElectronProvider } from "@Context/Electron"; import { ElectronProvider, getElectronAPI } from "@Context/Electron";
import { ClassicRenderer } from "@GLRender/ClassicRenderer"; import { ClassicRenderer } from "@GLRender/ClassicRenderer";
import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { initializeIcons } from '@fluentui/font-icons-mdl2';
import { RootContainer } from "@Component/Container/RootContainer"; import { RootContainer } from "@Component/Container/RootContainer";
@ -47,25 +47,12 @@ class SimulatorDesktop extends Component {
this.status.bindRenderer(classicRender); this.status.bindRenderer(classicRender);
this.status.setting = this.setting; 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 = { (window as any).LT = {
status: this.status, status: this.status,
setting: this.setting setting: this.setting
}; };
this.electron = {} as ISimulatorAPI; this.electron = getElectronAPI();
if ((window as any).API) {
this.electron = (window as any).API;
} else {
console.error("SimulatorDesktop: Can't find electron API");
}
} }
public componentDidMount() { public componentDidMount() {