Skip to content

Commit d49d848

Browse files
author
Hardy--Lee
committed
feat: add incremental save button
1 parent 1b49459 commit d49d848

File tree

9 files changed

+197
-52
lines changed

9 files changed

+197
-52
lines changed

packages/origine2/src/api/Api.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ export interface EditTextFileDto {
5555
textFile: string;
5656
}
5757

58+
export interface CopyFileWithIncrementDto {
59+
/** The source path of the file to be copied */
60+
source: string;
61+
}
62+
5863
export interface TemplateConfigDto {
5964
/** The name of the template */
6065
name: string;
@@ -552,6 +557,26 @@ export class Api<
552557
...params,
553558
}),
554559

560+
/**
561+
* No description
562+
*
563+
* @tags Assets
564+
* @name AssetsControllerCopyFileWithIncrement
565+
* @summary Copy File With Increment
566+
* @request POST:/api/assets/copyFileWithIncrement
567+
*/
568+
assetsControllerCopyFileWithIncrement: (
569+
data: CopyFileWithIncrementDto,
570+
params: RequestParams = {},
571+
) =>
572+
this.request<void, void>({
573+
path: `/api/assets/copyFileWithIncrement`,
574+
method: "POST",
575+
body: data,
576+
type: ContentType.Json,
577+
...params,
578+
}),
579+
555580
/**
556581
* No description
557582
*

packages/origine2/src/locales/en.po

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,10 @@ msgstr "Scenes and branches"
761761
msgid "场景文件"
762762
msgstr "Scene file"
763763

764+
#: src/pages/editor/MainArea/TagsManager.tsx:167
765+
msgid "增量保存"
766+
msgstr "Incremental Save"
767+
764768
#: src/pages/templateEditor/TemplateEditorSidebar/ComponentTree/ComponentTree.tsx:41
765769
msgid "外层文本"
766770
msgstr "Outer Text"
@@ -1491,6 +1495,10 @@ msgstr "Play BGM normally"
14911495
msgid "此指令将结束游戏"
14921496
msgstr "This command will end the game"
14931497

1498+
#: src/pages/editor/MainArea/TagsManager.tsx:142
1499+
msgid "此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。"
1500+
msgstr "This option allows you to save the current file as a backup to prevent data loss if the original file is accidentally damaged."
1501+
14941502
#: src/pages/editor/Topbar/tabs/Settings/SettingsTab.tsx:130
14951503
msgid "永不换行"
14961504
msgstr "Never wrap"

packages/origine2/src/locales/ja.po

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,10 @@ msgstr "シーンとブランチ"
764764
msgid "场景文件"
765765
msgstr "シーンファイル"
766766

767+
#: src/pages/editor/MainArea/TagsManager.tsx:167
768+
msgid "增量保存"
769+
msgstr "インクリメンタルセーブ"
770+
767771
#: src/pages/templateEditor/TemplateEditorSidebar/ComponentTree/ComponentTree.tsx:41
768772
msgid "外层文本"
769773
msgstr "外側テキスト"
@@ -1490,6 +1494,10 @@ msgstr "BGMを再生"
14901494
msgid "此指令将结束游戏"
14911495
msgstr "このコマンドはすべてのゲームが終わるときに使用"
14921496

1497+
#: src/pages/editor/MainArea/TagsManager.tsx:142
1498+
msgid "此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。"
1499+
msgstr "このオプションは現在のファイルをバックアップとして別名保存し、元のファイルが予期せず破損した場合でもすべてのデータが失われるのを防ぎます。"
1500+
14931501
#: src/pages/editor/Topbar/tabs/Settings/SettingsTab.tsx:130
14941502
msgid "永不换行"
14951503
msgstr "折り返しなし"

packages/origine2/src/locales/zhCn.po

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,10 @@ msgstr "场景与分支"
763763
msgid "场景文件"
764764
msgstr "场景文件"
765765

766+
#: src/pages/editor/MainArea/TagsManager.tsx:167
767+
msgid "增量保存"
768+
msgstr "增量保存"
769+
766770
#: src/pages/templateEditor/TemplateEditorSidebar/ComponentTree/ComponentTree.tsx:41
767771
msgid "外层文本"
768772
msgstr "外层文本"
@@ -1489,6 +1493,10 @@ msgstr "正常播放 BGM"
14891493
msgid "此指令将结束游戏"
14901494
msgstr "此指令将结束游戏"
14911495

1496+
#: src/pages/editor/MainArea/TagsManager.tsx:142
1497+
msgid "此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。"
1498+
msgstr "此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。"
1499+
14921500
#: src/pages/editor/Topbar/tabs/Settings/SettingsTab.tsx:130
14931501
msgid "永不换行"
14941502
msgstr "永不换行"

packages/origine2/src/pages/editor/MainArea/TagsManager.tsx

Lines changed: 91 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { cloneDeep } from "lodash";
44
import { CloseSmall } from "@icon-park/react";
55
import IconWrapper from "@/components/iconWrapper/IconWrapper";
66
import { getFileIcon } from "@/utils/getFileIcon";
7-
import React, { useRef } from "react";
7+
import React, { useMemo, useRef } from "react";
88
import { useGameEditorContext } from "@/store/useGameEditorStore";
99
import { ITag } from "@/types/gameEditor";
10-
import { Tooltip } from "@fluentui/react-components";
10+
import { Button, Tooltip } from "@fluentui/react-components";
11+
import { api } from "@/api";
12+
import useEditorStore from "@/store/useEditorStore";
13+
import { useSWRConfig } from "swr";
14+
import { t } from "@lingui/macro";
1115

1216
export default function TagsManager() {
1317
// 获取 Tags 数据
@@ -77,58 +81,94 @@ export default function TagsManager() {
7781

7882
const containerRef = useRef<HTMLDivElement>(null);
7983

84+
const { mutate } = useSWRConfig();
85+
const handleRefresh = (path: string) => mutate(path);
86+
const gameDir = useEditorStore.use.subPage();
87+
const basePath = useMemo(() => ['games', gameDir, 'game'], [gameDir]);
88+
8089
return (
8190
<>
82-
{
83-
(tags.length > 0) &&
84-
<DragDropContext onDragEnd={onDragEnd}>
85-
<Droppable droppableId="droppable" direction="horizontal">
86-
{(provided, snapshot) => (
87-
// 下面开始书写容器
88-
<div className={styles.tagsContainer}
89-
id="tags-container"
90-
onWheel={handleScroll}
91-
// provided.droppableProps应用的相同元素.
92-
{...provided.droppableProps}
93-
// 为了使 droppable 能够正常工作必须 绑定到最高可能的DOM节点中provided.innerRef.
94-
ref={provided.innerRef}
95-
>
96-
{tags.map((item, index) => (
97-
<Draggable key={item.path} draggableId={item.path} index={index}>
98-
{(provided, snapshot) => (
99-
// 下面开始书写可拖拽的元素
100-
<Tooltip content={item.path} relationship='label' positioning='below-start'>
101-
<div
102-
onClick={() => selectTag(item)}
103-
onMouseDown={(event: any) => {
104-
if (event.button === 1) {
105-
closeTag(event, item);
106-
}
107-
}}
108-
className={item.path === currentTag?.path ? `${styles.tag} ${styles.tag_active}` : styles.tag}
109-
ref={provided.innerRef}
110-
{...provided.draggableProps}
111-
{...provided.dragHandleProps}
112-
>
113-
<IconWrapper src={getFileIcon(item.path)} size={24} iconSize={18} />
114-
<div>
115-
{item.name}
116-
</div>
117-
<div className={styles.closeIcon} onClick={(event: any) => closeTag(event, item)}>
118-
<CloseSmall theme="outline" size="15" strokeWidth={3} />
91+
{ (tags.length > 0) && (
92+
<div className={styles.tagsManager}>
93+
<DragDropContext onDragEnd={onDragEnd}>
94+
<Droppable droppableId="droppable" direction="horizontal">
95+
{(provided, snapshot) => (
96+
// 下面开始书写容器
97+
<div className={styles.tagsContainer}
98+
id="tags-container"
99+
onWheel={handleScroll}
100+
// provided.droppableProps应用的相同元素.
101+
{...provided.droppableProps}
102+
// 为了使 droppable 能够正常工作必须 绑定到最高可能的DOM节点中provided.innerRef.
103+
ref={provided.innerRef}
104+
>
105+
{tags.map((item, index) => (
106+
<Draggable key={item.path} draggableId={item.path} index={index}>
107+
{(provided, snapshot) => (
108+
// 下面开始书写可拖拽的元素
109+
<Tooltip content={item.path} relationship='label' positioning='below-start'>
110+
<div
111+
onClick={() => selectTag(item)}
112+
onMouseDown={(event: any) => {
113+
if (event.button === 1) {
114+
closeTag(event, item);
115+
}
116+
}}
117+
className={item.path === currentTag?.path ? `${styles.tag} ${styles.tag_active}` : styles.tag}
118+
ref={provided.innerRef}
119+
{...provided.draggableProps}
120+
{...provided.dragHandleProps}
121+
>
122+
<IconWrapper src={getFileIcon(item.path)} size={24} iconSize={18} />
123+
<div>
124+
{item.name}
125+
</div>
126+
<div className={styles.closeIcon} onClick={(event: any) => closeTag(event, item)}>
127+
<CloseSmall theme="outline" size="15" strokeWidth={3} />
128+
</div>
119129
</div>
120-
</div>
121-
</Tooltip>
122-
)}
123-
</Draggable>
124-
))}
125-
{provided.placeholder}
126-
</div>
127-
)}
128-
</Droppable>
129-
</DragDropContext>
130-
}
130+
</Tooltip>
131+
)}
132+
</Draggable>
133+
))}
134+
{provided.placeholder}
135+
</div>
136+
)}
137+
</Droppable>
138+
</DragDropContext>
139+
<Tooltip
140+
content={
141+
<div className={styles.tooltip}>
142+
{t`此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。`}
143+
</div>}
144+
relationship='description'
145+
showDelay={750}
146+
hideDelay={0}
147+
>
148+
<Button
149+
appearance="transparent"
150+
style={{ display: 'flex', flexShrink: 0 }}
151+
onClick={() => {
152+
if (!currentTag?.path) return;
153+
const targetPath = [
154+
...basePath,
155+
currentTag.path.startsWith(basePath.join('/'))
156+
? currentTag.path.slice(basePath.join('/').length + 1)
157+
: currentTag.path,
158+
].join('/');
159+
api.assetsControllerCopyFileWithIncrement({ source: targetPath }).then(() => {
160+
// 提取目录路径
161+
const dirPath = targetPath.split('/').slice(0, -1).join('/');
162+
// 刷新 Assets 组件
163+
handleRefresh(dirPath);
164+
});
165+
}}
166+
>
167+
{t`增量保存`}
168+
</Button>
169+
</Tooltip>
170+
</div>
171+
)}
131172
</>
132-
133173
);
134174
}

packages/origine2/src/pages/editor/MainArea/tagsManager.module.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
.tagsManager {
2+
display: flex;
3+
width: 100%;
4+
max-width: 100%;
5+
}
6+
17
.tagsContainer {
28
display: flex;
39
overflow: auto;
@@ -83,3 +89,9 @@
8389
.tag_active>.closeIcon {
8490
visibility: visible;
8591
}
92+
93+
.tooltip {
94+
font-size: 120%;
95+
line-height: 150%;
96+
color: var(--primary);
97+
}

packages/terre2/src/Modules/assets/assets.controller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
RenameFileDto,
2222
UploadFilesDto,
2323
EditTextFileDto,
24+
CopyFileWithIncrementDto,
2425
} from './assets.dto';
2526
import { FilesInterceptor } from '@nestjs/platform-express';
2627
import { _open } from '../../util/open';
@@ -159,4 +160,14 @@ export class AssetsController {
159160
const filePath = this.webgalFs.getPathFromRoot(`public/${path}`);
160161
return this.webgalFs.updateTextFile(filePath, editTextFileData.textFile);
161162
}
163+
164+
@Post('copyFileWithIncrement')
165+
@ApiOperation({ summary: 'Copy File With Increment' })
166+
@ApiResponse({ status: 200, description: 'File copied successfully.' })
167+
@ApiResponse({ status: 400, description: 'Failed to copy the file.' })
168+
async copyFileWithIncrement(@Body() copyFileDto: CopyFileWithIncrementDto) {
169+
const { source } = copyFileDto;
170+
const sourcePath = this.webgalFs.getPathFromRoot(`public/${source}`);
171+
return this.webgalFs.copyFileWithIncrement(sourcePath);
172+
}
162173
}

packages/terre2/src/Modules/assets/assets.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export class RenameFileDto {
4242
newName: string;
4343
}
4444

45+
export class CopyFileWithIncrementDto {
46+
@ApiProperty({ description: 'The source path of the file to be copied' })
47+
source: string;
48+
}
49+
4550
export class EditTextFileDto {
4651
@ApiProperty({ description: 'The path of textfile' })
4752
path: string;

packages/terre2/src/Modules/webgal-fs/webgal-fs.service.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ConsoleLogger, Injectable } from '@nestjs/common';
22
import * as fs from 'fs/promises';
3-
import { dirname, extname, join } from 'path';
3+
import { basename, dirname, extname, join } from 'path';
44

55
export interface IFileInfo {
66
name: string;
@@ -335,4 +335,32 @@ export class WebgalFsService {
335335
return false;
336336
}
337337
}
338+
339+
/**
340+
* 复制文件并以“原文件名_编号.扩展名”方式增量保存
341+
*/
342+
async copyFileWithIncrement(filePath: string): Promise<string> {
343+
const dir = dirname(filePath);
344+
const ext = extname(filePath);
345+
const base = basename(filePath, ext);
346+
347+
// 读取目录下所有文件
348+
const files = await fs.readdir(dir);
349+
// 匹配类似 xxx_序号.txt 的文件
350+
const regex = new RegExp(`^${base}_(\\d+)${ext.replace('.', '\\.')}$`);
351+
let maxNum = 0;
352+
for (const file of files) {
353+
const match = file.match(regex);
354+
if (match) {
355+
const num = parseInt(match[1], 10);
356+
if (num > maxNum) maxNum = num;
357+
}
358+
}
359+
const nextNum = (maxNum + 1).toString().padStart(3, '0');
360+
const newName = `${base}_${nextNum}${ext}`;
361+
const newPath = join(dir, newName);
362+
363+
await fs.copyFile(filePath, newPath);
364+
return newPath;
365+
}
338366
}

0 commit comments

Comments
 (0)