Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/origine2/src/api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export interface EditTextFileDto {
textFile: string;
}

export interface CopyFileWithIncrementDto {
/** The source path of the file to be copied */
source: string;
}

export interface TemplateConfigDto {
/** The name of the template */
name: string;
Expand Down Expand Up @@ -552,6 +557,26 @@ export class Api<
...params,
}),

/**
* No description
*
* @tags Assets
* @name AssetsControllerCopyFileWithIncrement
* @summary Copy File With Increment
* @request POST:/api/assets/copyFileWithIncrement
*/
assetsControllerCopyFileWithIncrement: (
data: CopyFileWithIncrementDto,
params: RequestParams = {},
) =>
this.request<void, void>({
path: `/api/assets/copyFileWithIncrement`,
method: "POST",
body: data,
type: ContentType.Json,
...params,
}),

/**
* No description
*
Expand Down
8 changes: 8 additions & 0 deletions packages/origine2/src/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,10 @@ msgstr "Scenes and branches"
msgid "场景文件"
msgstr "Scene file"

#: src/pages/editor/MainArea/TagsManager.tsx:167
msgid "增量保存"
msgstr "Incremental Save"

#: src/pages/templateEditor/TemplateEditorSidebar/ComponentTree/ComponentTree.tsx:41
msgid "外层文本"
msgstr "Outer Text"
Expand Down Expand Up @@ -1491,6 +1495,10 @@ msgstr "Play BGM normally"
msgid "此指令将结束游戏"
msgstr "This command will end the game"

#: src/pages/editor/MainArea/TagsManager.tsx:142
msgid "此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。"
msgstr "This option allows you to save the current file as a backup to prevent data loss if the original file is accidentally damaged."

#: src/pages/editor/Topbar/tabs/Settings/SettingsTab.tsx:130
msgid "永不换行"
msgstr "Never wrap"
Expand Down
8 changes: 8 additions & 0 deletions packages/origine2/src/locales/ja.po
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,10 @@ msgstr "シーンとブランチ"
msgid "场景文件"
msgstr "シーンファイル"

#: src/pages/editor/MainArea/TagsManager.tsx:167
msgid "增量保存"
msgstr "インクリメンタルセーブ"

#: src/pages/templateEditor/TemplateEditorSidebar/ComponentTree/ComponentTree.tsx:41
msgid "外层文本"
msgstr "外側テキスト"
Expand Down Expand Up @@ -1490,6 +1494,10 @@ msgstr "BGMを再生"
msgid "此指令将结束游戏"
msgstr "このコマンドはすべてのゲームが終わるときに使用"

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

#: src/pages/editor/Topbar/tabs/Settings/SettingsTab.tsx:130
msgid "永不换行"
msgstr "折り返しなし"
Expand Down
8 changes: 8 additions & 0 deletions packages/origine2/src/locales/zhCn.po
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,10 @@ msgstr "场景与分支"
msgid "场景文件"
msgstr "场景文件"

#: src/pages/editor/MainArea/TagsManager.tsx:167
msgid "增量保存"
msgstr "增量保存"

#: src/pages/templateEditor/TemplateEditorSidebar/ComponentTree/ComponentTree.tsx:41
msgid "外层文本"
msgstr "外层文本"
Expand Down Expand Up @@ -1489,6 +1493,10 @@ msgstr "正常播放 BGM"
msgid "此指令将结束游戏"
msgstr "此指令将结束游戏"

#: src/pages/editor/MainArea/TagsManager.tsx:142
msgid "此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。"
msgstr "此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。"

#: src/pages/editor/Topbar/tabs/Settings/SettingsTab.tsx:130
msgid "永不换行"
msgstr "永不换行"
Expand Down
142 changes: 91 additions & 51 deletions packages/origine2/src/pages/editor/MainArea/TagsManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { cloneDeep } from "lodash";
import { CloseSmall } from "@icon-park/react";
import IconWrapper from "@/components/iconWrapper/IconWrapper";
import { getFileIcon } from "@/utils/getFileIcon";
import React, { useRef } from "react";
import React, { useMemo, useRef } from "react";
import { useGameEditorContext } from "@/store/useGameEditorStore";
import { ITag } from "@/types/gameEditor";
import { Tooltip } from "@fluentui/react-components";
import { Button, Tooltip } from "@fluentui/react-components";
import { api } from "@/api";
import useEditorStore from "@/store/useEditorStore";
import { useSWRConfig } from "swr";
import { t } from "@lingui/macro";

export default function TagsManager() {
// 获取 Tags 数据
Expand Down Expand Up @@ -77,58 +81,94 @@ export default function TagsManager() {

const containerRef = useRef<HTMLDivElement>(null);

const { mutate } = useSWRConfig();
const handleRefresh = (path: string) => mutate(path);
const gameDir = useEditorStore.use.subPage();
const basePath = useMemo(() => ['games', gameDir, 'game'], [gameDir]);

return (
<>
{
(tags.length > 0) &&
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable" direction="horizontal">
{(provided, snapshot) => (
// 下面开始书写容器
<div className={styles.tagsContainer}
id="tags-container"
onWheel={handleScroll}
// provided.droppableProps应用的相同元素.
{...provided.droppableProps}
// 为了使 droppable 能够正常工作必须 绑定到最高可能的DOM节点中provided.innerRef.
ref={provided.innerRef}
>
{tags.map((item, index) => (
<Draggable key={item.path} draggableId={item.path} index={index}>
{(provided, snapshot) => (
// 下面开始书写可拖拽的元素
<Tooltip content={item.path} relationship='label' positioning='below-start'>
<div
onClick={() => selectTag(item)}
onMouseDown={(event: any) => {
if (event.button === 1) {
closeTag(event, item);
}
}}
className={item.path === currentTag?.path ? `${styles.tag} ${styles.tag_active}` : styles.tag}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<IconWrapper src={getFileIcon(item.path)} size={24} iconSize={18} />
<div>
{item.name}
</div>
<div className={styles.closeIcon} onClick={(event: any) => closeTag(event, item)}>
<CloseSmall theme="outline" size="15" strokeWidth={3} />
{ (tags.length > 0) && (
<div className={styles.tagsManager}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable" direction="horizontal">
{(provided, snapshot) => (
// 下面开始书写容器
<div className={styles.tagsContainer}
id="tags-container"
onWheel={handleScroll}
// provided.droppableProps应用的相同元素.
{...provided.droppableProps}
// 为了使 droppable 能够正常工作必须 绑定到最高可能的DOM节点中provided.innerRef.
ref={provided.innerRef}
>
{tags.map((item, index) => (
<Draggable key={item.path} draggableId={item.path} index={index}>
{(provided, snapshot) => (
// 下面开始书写可拖拽的元素
<Tooltip content={item.path} relationship='label' positioning='below-start'>
<div
onClick={() => selectTag(item)}
onMouseDown={(event: any) => {
if (event.button === 1) {
closeTag(event, item);
}
}}
Comment on lines +112 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

event 参数的类型是 any。为了更好的类型安全和代码可读性,建议使用更具体的 React.MouseEvent 类型。另外,closeTag 函数期望一个原生的 MouseEvent,所以应该传递 event.nativeEvent

Suggested change
onMouseDown={(event: any) => {
if (event.button === 1) {
closeTag(event, item);
}
}}
onMouseDown={(event: React.MouseEvent) => {
if (event.button === 1) {
closeTag(event.nativeEvent, item);
}
}}

className={item.path === currentTag?.path ? `${styles.tag} ${styles.tag_active}` : styles.tag}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<IconWrapper src={getFileIcon(item.path)} size={24} iconSize={18} />
<div>
{item.name}
</div>
<div className={styles.closeIcon} onClick={(event: any) => closeTag(event, item)}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

event 参数的类型是 any。为了更好的类型安全和代码可读性,建议使用更具体的 React.MouseEvent 类型。另外,closeTag 函数期望一个原生的 MouseEvent,所以应该传递 event.nativeEvent

Suggested change
<div className={styles.closeIcon} onClick={(event: any) => closeTag(event, item)}>
<div className={styles.closeIcon} onClick={(event: React.MouseEvent) => closeTag(event.nativeEvent, item)}>

<CloseSmall theme="outline" size="15" strokeWidth={3} />
</div>
</div>
</div>
</Tooltip>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
}
</Tooltip>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
<Tooltip
content={
<div className={styles.tooltip}>
{t`此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。`}
</div>}
relationship='description'
showDelay={750}
hideDelay={0}
>
<Button
appearance="transparent"
style={{ display: 'flex', flexShrink: 0 }}
onClick={() => {
if (!currentTag?.path) return;
const targetPath = [
...basePath,
currentTag.path.startsWith(basePath.join('/'))
? currentTag.path.slice(basePath.join('/').length + 1)
: currentTag.path,
].join('/');
api.assetsControllerCopyFileWithIncrement({ source: targetPath }).then(() => {
// 提取目录路径
const dirPath = targetPath.split('/').slice(0, -1).join('/');
// 刷新 Assets 组件
handleRefresh(dirPath);
});
}}
>
{t`增量保存`}
</Button>
</Tooltip>
</div>
)}
</>

);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.tagsManager {
display: flex;
width: 100%;
max-width: 100%;
}

.tagsContainer {
display: flex;
overflow: auto;
Expand Down Expand Up @@ -83,3 +89,9 @@
.tag_active>.closeIcon {
visibility: visible;
}

.tooltip {
font-size: 120%;
line-height: 150%;
color: var(--primary);
}
11 changes: 11 additions & 0 deletions packages/terre2/src/Modules/assets/assets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
RenameFileDto,
UploadFilesDto,
EditTextFileDto,
CopyFileWithIncrementDto,
} from './assets.dto';
import { FilesInterceptor } from '@nestjs/platform-express';
import { _open } from '../../util/open';
Expand Down Expand Up @@ -159,4 +160,14 @@ export class AssetsController {
const filePath = this.webgalFs.getPathFromRoot(`public/${path}`);
return this.webgalFs.updateTextFile(filePath, editTextFileData.textFile);
}

@Post('copyFileWithIncrement')
@ApiOperation({ summary: 'Copy File With Increment' })
@ApiResponse({ status: 200, description: 'File copied successfully.' })
@ApiResponse({ status: 400, description: 'Failed to copy the file.' })
async copyFileWithIncrement(@Body() copyFileDto: CopyFileWithIncrementDto) {
const { source } = copyFileDto;
const sourcePath = this.webgalFs.getPathFromRoot(`public/${source}`);
return this.webgalFs.copyFileWithIncrement(sourcePath);
}
}
5 changes: 5 additions & 0 deletions packages/terre2/src/Modules/assets/assets.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export class RenameFileDto {
newName: string;
}

export class CopyFileWithIncrementDto {
@ApiProperty({ description: 'The source path of the file to be copied' })
source: string;
}

export class EditTextFileDto {
@ApiProperty({ description: 'The path of textfile' })
path: string;
Expand Down
30 changes: 29 additions & 1 deletion packages/terre2/src/Modules/webgal-fs/webgal-fs.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ConsoleLogger, Injectable } from '@nestjs/common';
import * as fs from 'fs/promises';
import { dirname, extname, join } from 'path';
import { basename, dirname, extname, join } from 'path';

export interface IFileInfo {
name: string;
Expand Down Expand Up @@ -335,4 +335,32 @@ export class WebgalFsService {
return false;
}
}

/**
* 复制文件并以“原文件名_编号.扩展名”方式增量保存
*/
async copyFileWithIncrement(filePath: string): Promise<string> {
const dir = dirname(filePath);
const ext = extname(filePath);
const base = basename(filePath, ext);

// 读取目录下所有文件
const files = await fs.readdir(dir);
// 匹配类似 xxx_序号.txt 的文件
const regex = new RegExp(`^${base}_(\\d+)${ext.replace('.', '\\.')}$`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

在构建正则表达式时,base 变量(即不带扩展名的文件名)没有被转义。如果文件名包含正则表达式的特殊字符(例如 (, ), . 等),这可能会导致 new RegExp 抛出错误或产生意外的匹配行为。为了增强健壮性,建议对 base 变量进行转义。

Suggested change
const regex = new RegExp(`^${base}_(\\d+)${ext.replace('.', '\\.')}$`);
const escapedBase = base.replace(/[.*+?^${}()|[\\\]]/g, '\\$&');
const regex = new RegExp(`^${escapedBase}_(\\d+)${ext.replace('.', '\\.')}$`);

let maxNum = 0;
for (const file of files) {
const match = file.match(regex);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNum) maxNum = num;
}
}
const nextNum = (maxNum + 1).toString().padStart(3, '0');
const newName = `${base}_${nextNum}${ext}`;
const newPath = join(dir, newName);

await fs.copyFile(filePath, newPath);
return newPath;
}
}
Loading