Add desktop save function
This commit is contained in:
parent
0ab53c1800
commit
ec3aaa5e3a
@ -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}/>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
@ -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();
|
@ -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 }
|
@ -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);
|
@ -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",
|
||||||
|
@ -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} 个行为",
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user