Add file load UI

This commit is contained in:
2022-04-25 21:32:50 +08:00
parent 8a272a9626
commit 0ab53c1800
11 changed files with 372 additions and 194 deletions
@@ -9,6 +9,7 @@ interface IConfirmPopupProps {
titleI18N?: AllI18nKeys;
titleI18NOption?: Record<string, string>;
infoI18n?: AllI18nKeys;
infoI18nOption?: Record<string, string>;
yesI18n?: AllI18nKeys;
noI18n?: AllI18nKeys;
renderInfo?: () => ReactNode;
@@ -64,8 +65,10 @@ class ConfirmPopup extends Popup<IConfirmPopupProps> {
this.props.renderInfo ?
this.props.renderInfo() :
this.props.infoI18n ?
<Message i18nKey={this.props.infoI18n}/> :
null
<Message
i18nKey={this.props.infoI18n}
options={this.props.infoI18nOption}
/> : null
}
</ConfirmContent>
}
+6 -1
View File
@@ -1,10 +1,16 @@
@import "../Theme/Theme.scss";
div.load-file-app-root {
width: 100%;
height: 100%;
}
div.load-file-layer-root {
position: fixed;
z-index: 1000;
width: 100%;
height: 100%;
pointer-events: none;
box-sizing: border-box;
padding: 20px;
@@ -19,7 +25,6 @@ div.load-file-layer-root {
border-radius: 3px;
div {
pointer-events: none;
user-select: none;
text-align: center;
width: 100%;
+132 -59
View File
@@ -1,69 +1,142 @@
import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup";
import { Localization } from "@Component/Localization/Localization";
import { FontLevel, Theme } from "@Component/Theme/Theme";
import { Status, useStatus, IMixinStatusProps } from "@Context/Status";
import { Icon } from "@fluentui/react";
import { Component, ReactNode } from "react";
import { FunctionComponent } from "react";
import { useDrop } from 'react-dnd'
import { NativeTypes } from "react-dnd-html5-backend"
import "./LoadFile.scss";
interface ILoadFileState {
show: boolean;
}
const DragFileMask: FunctionComponent = () => {
class LoadFile extends Component<{}, ILoadFileState> {
public state: Readonly<ILoadFileState> = {
show: false
};
private renderMask() {
return <Theme
className="load-file-layer-root"
fontLevel={FontLevel.normal}
onDragEnter={this.handleWindowsDragOnFiles}
onDragLeave={this.handleWindowsDragOffFiles}
>
<div className="load-file-layer">
<div className="drag-icon">
<Icon iconName="KnowledgeArticle"/>
</div>
<div className="drag-title">
<Localization i18nKey="Info.Hint.Load.File.Title"/>
</div>
<div className="drag-intro">
<Localization i18nKey="Info.Hint.Load.File.Intro"/>
</div>
return <Theme
className="load-file-layer-root"
fontLevel={FontLevel.normal}
>
<div className="load-file-layer">
<div className="drag-icon">
<Icon iconName="KnowledgeArticle"/>
</div>
</Theme>;
}
private offDragTimer: NodeJS.Timeout | undefined;
private handleWindowsDragOnFiles = () => {
clearTimeout(this.offDragTimer as number | undefined);
this.setState({
show: true
});
}
private handleWindowsDragOffFiles = () => {
clearTimeout(this.offDragTimer as number | undefined);
this.offDragTimer = setTimeout(() => {
this.setState({
show: false
});
});
}
public componentDidMount() {
window.addEventListener("dragenter", this.handleWindowsDragOnFiles);
}
public componentWillUnmount() {
window.removeEventListener("dragenter", this.handleWindowsDragOnFiles);
}
public render(): ReactNode {
return this.state.show ? this.renderMask() : null;
}
<div className="drag-title">
<Localization i18nKey="Info.Hint.Load.File.Title"/>
</div>
<div className="drag-intro">
<Localization i18nKey="Info.Hint.Load.File.Intro"/>
</div>
</div>
</Theme>;
}
async function fileChecker(status: Status, file?: File) {
if (!status) return undefined;
return new Promise((r, j) => {
// 检查文件存在性
if (!file) {
status.popup.showPopup(ConfirmPopup, {
infoI18n: "Popup.Load.Save.Error.Empty",
titleI18N: "Popup.Load.Save.Title",
yesI18n: "Popup.Load.Save.confirm"
});
return j();
}
// 检测拓展名
let extendName = (file.name.match(/\.(\w+)$/) ?? [])[1];
if (extendName !== "ltss") {
status.popup.showPopup(ConfirmPopup, {
infoI18n: "Popup.Load.Save.Error.Type",
infoI18nOption: { ext: extendName },
titleI18N: "Popup.Load.Save.Title",
yesI18n: "Popup.Load.Save.confirm"
});
return j();
}
// 文件读取
let fileReader = new FileReader();
fileReader.readAsText(file);
fileReader.onload = () => {
const loadFunc = () => {
// 进行转换
let errorMessage = status.archive.load(status.model, fileReader.result as string, file.name);
if (errorMessage) {
status.popup.showPopup(ConfirmPopup, {
infoI18n: "Popup.Load.Save.Error.Parse",
infoI18nOption: { why: errorMessage },
titleI18N: "Popup.Load.Save.Title",
yesI18n: "Popup.Load.Save.confirm"
});
j();
}
else {
r(undefined);
}
}
// 如果保存进行提示
if (!status.archive.isSaved) {
status.popup.showPopup(ConfirmPopup, {
infoI18n: "Popup.Load.Save.Overwrite.Info",
titleI18N: "Popup.Load.Save.Title",
yesI18n: "Popup.Load.Save.Overwrite",
noI18n: "Popup.Action.No",
red: "yes",
yes: () => {
loadFunc();
},
no: () => {
j();
}
});
}
else {
loadFunc();
}
}
fileReader.onerror = () => {
status.popup.showPopup(ConfirmPopup, {
infoI18n: "Popup.Load.Save.Error.Parse",
infoI18nOption: { why: "Unknown error" },
titleI18N: "Popup.Load.Save.Title",
yesI18n: "Popup.Load.Save.confirm"
});
j();
}
});
}
const LoadFileView: FunctionComponent<IMixinStatusProps> = (props) => {
const [{ isOver }, drop] = useDrop(() => ({
accept: NativeTypes.FILE,
drop: (item: { files: File[] }) => {
if (props.status) {
fileChecker(props.status, item.files[0]).catch((e) => undefined);
}
},
collect: (monitor) => ({
isOver: monitor.isOver()
})
}));
return <>
{
isOver ? <DragFileMask/> : null
}
<div className="load-file-app-root" ref={drop}>
{props.children}
</div>
</>
}
const LoadFile = useStatus(LoadFileView);
export { LoadFile };
+5
View File
@@ -161,6 +161,11 @@ class Status extends Emitter<IStatusEvent> {
this.emit("labelChange");
this.emit("behaviorChange");
// 清除焦点对象
this.setBehaviorObject();
this.setFocusObject(new Set());
this.setLabelObject();
// 映射
this.emit("fileLoad");
});
+7
View File
@@ -65,6 +65,13 @@ const EN_US = {
"Popup.Delete.Behavior.Confirm": "Are you sure you want to delete this behavior? The behavior is deleted and cannot be recalled.",
"Popup.Restore.Behavior.Confirm": "Are you sure you want to reset all parameters of this behavior? This operation cannot be recalled.",
"Popup.Setting.Title": "Preferences setting",
"Popup.Load.Save.Title": "Load save",
"Popup.Load.Save.confirm": "Got it",
"Popup.Load.Save.Overwrite": "Overwrite and continue",
"Popup.Load.Save.Overwrite.Info": "The current workspace will be overwritten after the archive is loaded, and all unsaved progress will be lost. Are you sure you want to continue?",
"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.Add.Behavior.Title": "Add behavior",
"Popup.Add.Behavior.Action.Add": "Add all select behavior",
"Popup.Add.Behavior.Select.Counter": "Selected {count} behavior",
+7
View File
@@ -65,6 +65,13 @@ const ZH_CN = {
"Popup.Delete.Behavior.Confirm": "你确定要删除这个行为吗?行为被删除将无法撤回。",
"Popup.Restore.Behavior.Confirm": "你确定要重置此行为的全部参数吗?此操作无法撤回。",
"Popup.Setting.Title": "首选项设置",
"Popup.Load.Save.Title": "加载存档",
"Popup.Load.Save.confirm": "我知道了",
"Popup.Load.Save.Overwrite": "覆盖并继续",
"Popup.Load.Save.Overwrite.Info": "存档加载后将覆盖当前工作区,未保存的进度将全部丢失,确定要继续吗?",
"Popup.Load.Save.Error.Empty": "文件信息获取错误,文件已丢失或已被移动",
"Popup.Load.Save.Error.Type": "暂时无法加载拓展名为 \"{ext}\" 的文件",
"Popup.Load.Save.Error.Parse": "存档解析错误,详细原因: \n{why}",
"Popup.Add.Behavior.Title": "添加行为",
"Popup.Add.Behavior.Action.Add": "添加全部选中行为",
"Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为",
+6 -3
View File
@@ -100,7 +100,7 @@ class Archive extends Emitter<IArchiveEvent> {
// 解析为 JSON 对象
const archive: IArchiveObject = JSON.parse(data);
console.log(archive);
// console.log(archive);
// 实例化全部对象
const objectPool: CtrlObject[] = [];
@@ -254,7 +254,7 @@ class Archive extends Emitter<IArchiveEvent> {
* 加载文件为模型
* @return Model
*/
public load(model: Model, data: string): string | undefined {
public load(model: Model, data: string, name: string, url?: string): string | undefined {
try {
this.loadArchiveIntoModel(model, data);
@@ -262,8 +262,11 @@ class Archive extends Emitter<IArchiveEvent> {
return e as string;
}
this.isSaved = true;
this.emit("fileLoad", this);
this.fileName = name;
this.isSaved = true;
this.isNewFile = false;
this.emit("fileSave", this);
};
public constructor() {
+14 -123
View File
@@ -1,4 +1,6 @@
import { Component, ReactNode } from "react";
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { SettingProvider, Setting, Platform } from "@Context/Setting";
import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme";
import { StatusProvider, Status } from "@Context/Status";
@@ -7,12 +9,10 @@ import { initializeIcons } from '@fluentui/font-icons-mdl2';
import { RootContainer } from "@Component/Container/RootContainer";
import { LayoutDirection } from "@Context/Layout";
import { LoadFile } from "@Component/LoadFile/LoadFile";
import { AllBehaviors, getBehaviorById } from "@Behavior/Behavior";
import { CommandBar } from "@Component/CommandBar/CommandBar";
import { HeaderBar } from "@Component/HeaderBar/HeaderBar";
import { Popup } from "@Component/Popup/Popup";
import { Entry } from "../Entry/Entry";
import { Group } from "@Model/Group";
import "./SimulatorWeb.scss";
initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/");
@@ -42,118 +42,6 @@ class SimulatorWeb 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;
})
};
// 测试代码
if (false) {
let group = this.status.newGroup();
let range = this.status.newRange();
range.color = [.1, .5, .9];
group.new(100);
group.color = [.8, .1, .6];
randomPosition(group);
this.status.model.update(0);
this.status.newLabel().name = "New Label";
this.status.newLabel().name = "Test Label 01";
let template = this.status.model.addBehavior(AllBehaviors[0]);
template.name = "Template"; template.color = [150, 20, 220];
let dynamic = this.status.model.addBehavior(AllBehaviors[1]);
dynamic.name = "Dynamic"; dynamic.color = [250, 200, 80];
let brownian = this.status.model.addBehavior(AllBehaviors[2]);
brownian.name = "Brownian"; brownian.color = [200, 80, 250];
let boundary = this.status.model.addBehavior(AllBehaviors[3]);
boundary.name = "Boundary"; boundary.color = [80, 200, 250];
boundary.parameter.range.picker = this.status.model.allRangeLabel;
group.addBehavior(template);
group.addBehavior(dynamic);
group.addBehavior(brownian);
group.addBehavior(boundary);
}
// 鱼群模型测试
if (false) {
let fish1 = this.status.newGroup();
let fish2 = this.status.newGroup();
let shark = this.status.newGroup();
let range = this.status.newRange();
range.displayName = "Experimental site";
range.color = [.8, .1, .6];
fish1.new(100);
fish1.displayName = "Fish A";
fish1.color = [.1, .5, .9];
randomPosition(fish1);
fish2.new(50);
fish2.displayName = "Fish B";
fish2.color = [.3, .2, .9];
randomPosition(fish2);
shark.new(3);
shark.displayName = "Shark";
shark.color = [.8, .2, .3];
shark.renderParameter.size = 100;
shark.renderParameter.shape = "5";
randomPosition(shark);
this.status.model.update(0);
let fishLabel = this.status.newLabel();
fishLabel.name = "Fish";
fish1.addLabel(fishLabel);
fish2.addLabel(fishLabel);
let template = this.status.model.addBehavior(getBehaviorById("Template"));
template.name = "Template"; template.color = [150, 20, 220];
let dynamicFish = this.status.model.addBehavior(getBehaviorById("PhysicsDynamics"));
dynamicFish.name = "Dynamic Fish"; dynamicFish.color = [250, 200, 80];
let dynamicShark = this.status.model.addBehavior(getBehaviorById("PhysicsDynamics"));
dynamicShark.name = "Dynamic Shark"; dynamicShark.color = [250, 200, 80];
dynamicShark.parameter.maxAcceleration = 8.5;
dynamicShark.parameter.maxVelocity = 15.8;
dynamicShark.parameter.resistance = 3.6;
let brownian = this.status.model.addBehavior(getBehaviorById("Brownian"));
brownian.name = "Brownian"; brownian.color = [200, 80, 250];
let boundary = this.status.model.addBehavior(getBehaviorById("BoundaryConstraint"));
boundary.name = "Boundary"; boundary.color = [80, 200, 250];
boundary.parameter.range.picker = this.status.model.allRangeLabel;
let tracking = this.status.model.addBehavior(getBehaviorById("Tracking"));
tracking.name = "Tracking"; tracking.color = [80, 200, 250];
tracking.parameter.target.picker = fishLabel;
let attacking = this.status.model.addBehavior(getBehaviorById("ContactAttacking"));
attacking.name = "Contact Attacking"; attacking.color = [120, 100, 250];
attacking.parameter.target.picker = fishLabel;
fish1.addBehavior(dynamicFish);
fish1.addBehavior(brownian);
fish1.addBehavior(boundary);
fish2.addBehavior(dynamicFish);
fish2.addBehavior(brownian);
fish2.addBehavior(boundary);
shark.addBehavior(dynamicShark);
shark.addBehavior(boundary);
shark.addBehavior(tracking);
shark.addBehavior(attacking);
setTimeout(() => {
this.status.model.updateBehaviorParameter();
}, 200)
}
(window as any).LT = {
status: this.status,
setting: this.setting
@@ -193,7 +81,9 @@ class SimulatorWeb extends Component {
public render(): ReactNode {
return <SettingProvider value={this.setting}>
<StatusProvider value={this.status}>
{this.renderContent()}
<DndProvider backend={HTML5Backend}>
{this.renderContent()}
</DndProvider>
</StatusProvider>
</SettingProvider>
}
@@ -204,15 +94,16 @@ class SimulatorWeb extends Component {
backgroundLevel={BackgroundLevel.Level5}
fontLevel={FontLevel.Level3}
>
<LoadFile/>
<Popup/>
<HeaderBar height={45}/>
<div className="app-root-space" style={{
height: `calc( 100% - ${45}px)`
}}>
<CommandBar/>
<RootContainer/>
</div>
<LoadFile>
<HeaderBar height={45}/>
<div className="app-root-space" style={{
height: `calc( 100% - ${45}px)`
}}>
<CommandBar/>
<RootContainer/>
</div>
</LoadFile>
</Theme>
}
}