mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-30 02:25:55 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41008e9282 | |||
| 732b2aabaa | |||
| 19bdb3078a | |||
| bdbb399ddc |
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cmake --build build)",
|
||||
"Bash(npm --version)",
|
||||
"Bash(npm create *)",
|
||||
"Bash(npm install *)",
|
||||
"Bash(npx tauri *)",
|
||||
"Bash(rm -f src/App.css src/index.css)",
|
||||
"Bash(rm -rf src/assets)",
|
||||
"Bash(npm run *)",
|
||||
"Bash(npx tsc *)",
|
||||
"Bash(npx vite *)",
|
||||
"Bash(cargo check *)",
|
||||
"Bash(rustup show *)",
|
||||
"Bash(rustup toolchain *)",
|
||||
"Bash(npx vitest *)",
|
||||
"Bash(cargo build *)",
|
||||
"Bash(where link.exe)",
|
||||
"Bash(dir \"C:\\\\Program Files\\\\Microsoft Visual Studio\")",
|
||||
"Bash(dir \"C:\\\\Program Files \\(x86\\)\\\\Microsoft Visual Studio\")",
|
||||
"Bash(rustup override *)",
|
||||
"Bash(dir *)",
|
||||
"Bash(cargo clean *)",
|
||||
"Bash(\"D:/settings/Language/Rust/mingw64/bin/gcc.exe\" --version)"
|
||||
]
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -22,4 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.claude/worktrees/
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## 项目概述
|
||||
|
||||
PathEditor v4.0 — Windows 系统环境变量 (PATH) 编辑器,使用 Tauri 2.x + React 19 + TypeScript + Rust 构建。
|
||||
|
||||
## 构建命令
|
||||
|
||||
```bash
|
||||
# 安装前端依赖
|
||||
npm install
|
||||
|
||||
# 开发模式(热更新)
|
||||
npx tauri dev
|
||||
|
||||
# 仅前端
|
||||
npm run dev
|
||||
|
||||
# 前端测试
|
||||
npm test
|
||||
npm run test:watch
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# Rust 后端检查
|
||||
cd src-tauri && cargo check
|
||||
|
||||
# Rust 后端测试
|
||||
cd src-tauri && cargo test
|
||||
|
||||
# 完整构建(安装包)
|
||||
npx tauri build
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
前后端分离,通过 Tauri IPC 通信:
|
||||
|
||||
```
|
||||
src/ # React 前端 (TypeScript)
|
||||
├── core/ # 纯逻辑 — 零 React 依赖
|
||||
│ ├── string-list.ts # StringList 数据结构
|
||||
│ ├── undo-redo.ts # 撤销/重做管理器(8 种操作类型)
|
||||
│ ├── path-manager.ts # 路径增删移清理
|
||||
│ ├── import-export.ts # JSON/CSV/TXT 导入导出
|
||||
│ └── validation.ts # 路径格式验证
|
||||
├── store/ # Zustand 状态管理
|
||||
│ ├── app-store.ts # 主状态(路径、撤销、CRUD、加载/保存)
|
||||
│ └── theme-store.ts # 深色/浅色模式
|
||||
├── components/ # React UI 组件
|
||||
│ ├── layout/ # AppShell、TitleBar、StatusBar
|
||||
│ ├── path-list/ # PathTable、MergePreview
|
||||
│ ├── toolbar/ # ToolBar、ActionButtons、UndoRedoButtons、SearchInput
|
||||
│ └── dialogs/ # PathEditDialog、HelpDialog、ImportDialog
|
||||
├── hooks/ # use-keyboard、use-path-validation
|
||||
├── i18n/ # i18next 中英文翻译
|
||||
└── config/ # default.json UI 参数配置
|
||||
|
||||
src-tauri/ # Tauri Rust 后端
|
||||
├── src/
|
||||
│ ├── commands/
|
||||
│ │ ├── registry.rs # 注册表读写(load/save system & user paths)
|
||||
│ │ ├── system.rs # check_admin、validate_path、expand_env_vars、broadcast
|
||||
│ │ └── backup.rs # backup_registry、get_appdata_dir
|
||||
│ ├── error.rs
|
||||
│ └── lib.rs # 注册所有 IPC commands
|
||||
├── Cargo.toml
|
||||
└── tauri.conf.json
|
||||
|
||||
tests/unit/ # Vitest 前端单元测试
|
||||
```
|
||||
|
||||
## IPC 接口(Rust → Frontend)
|
||||
|
||||
| Command | 功能 |
|
||||
|---------|------|
|
||||
| `load_system_paths` | 从 HKLM 注册表读取系统 PATH |
|
||||
| `load_user_paths` | 从 HKCU 注册表读取用户 PATH |
|
||||
| `save_system_paths` | 保存系统 PATH 到注册表 |
|
||||
| `save_user_paths` | 保存用户 PATH 到注册表 |
|
||||
| `check_admin` | 检测管理员权限 |
|
||||
| `validate_path` | 验证路径目录是否存在 |
|
||||
| `expand_env_vars` | 展开 %VAR% 环境变量 |
|
||||
| `broadcast_env_change` | 广播 WM_SETTINGCHANGE |
|
||||
| `backup_registry` | 备份注册表 PATH 到文件 |
|
||||
| `get_appdata_dir` | 获取备份目录路径 |
|
||||
|
||||
## 关键约束
|
||||
|
||||
- Rust 工具链:`stable-x86_64-pc-windows-gnu`(项目已设 override)
|
||||
- `.cargo/config.toml` 添加了 `-lmcfgthread` 兼容 GCC 15.2.0 MinGW
|
||||
- 移除 `cdylib` crate-type 避免 DLL 导出序数溢出
|
||||
- 运行需要管理员权限才能编辑系统 PATH
|
||||
- `cargo test` 需要 MinGW bin 在 PATH 中(GCC 15.2.0 运行时依赖 `libmcfgthread-2.dll`),开发模式下可用 `npx tauri dev` 替代
|
||||
@@ -1,88 +1,94 @@
|
||||
# PathEditor v4.0
|
||||
<p align="center">
|
||||
<h1>PathEditor</h1>
|
||||
<p>Windows 系统环境变量 (PATH) 编辑器</p>
|
||||
</p>
|
||||
|
||||
Windows 系统环境变量 (PATH) 编辑器,基于 Tauri 2.x + React 19 + TypeScript + Rust 构建。
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-4.0.0-blue" alt="version">
|
||||
<img src="https://img.shields.io/badge/tauri-2.x-ffa03a" alt="tauri">
|
||||
<img src="https://img.shields.io/badge/react-19-61dafb" alt="react">
|
||||
<img src="https://img.shields.io/badge/rust-1.95-000000" alt="rust">
|
||||
<img src="https://img.shields.io/badge/typescript-strict-blue" alt="typescript">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="license">
|
||||
<img src="https://img.shields.io/badge/tests-55%20passed-brightgreen" alt="tests">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 简介
|
||||
|
||||
PathEditor 是 Windows PATH 环境变量的可视化管理工具。支持系统变量和用户变量的增删改查、拖拽排序、一键清理无效路径、导入导出以及完整的撤销/重做。
|
||||
|
||||
v4.0 使用 **Tauri 2.x + React 19 + TypeScript + Rust** 完全重写,替代了原有的 C + IUP GUI。
|
||||
|
||||
## 截图
|
||||
|
||||
_[待补充]_
|
||||
|
||||
## 功能
|
||||
|
||||
- 查看和编辑系统/用户 PATH 环境变量
|
||||
### 路径管理
|
||||
- 查看和编辑 **系统 PATH**(HKLM)和 **用户 PATH**(HKCU)
|
||||
- 新建、编辑、删除、上移、下移路径条目
|
||||
- 一键清理无效和重复路径
|
||||
- 完整撤销/重做支持(最多 50 步)
|
||||
- 导入/导出 JSON、CSV、TXT 三种格式
|
||||
- 深色模式 / 浅色模式切换
|
||||
- 中英文界面切换
|
||||
- 合并预览(同时查看系统 + 用户路径)
|
||||
- 搜索过滤
|
||||
- 多选批量删除
|
||||
- 实时搜索过滤
|
||||
- 合并预览(系统 + 用户路径并列显示)
|
||||
- 文件夹拖拽添加
|
||||
- 注册表备份
|
||||
|
||||
## 运行
|
||||
### 路径验证
|
||||
- **红色**标记:路径在文件系统中不存在
|
||||
- **橙色**标记:路径在列表中重复出现
|
||||
- 环境变量路径(含 `%VAR%`)悬浮展开预览
|
||||
|
||||
需要管理员权限才能编辑系统 PATH(非管理员自动进入只读模式)。
|
||||
### 撤销/重做
|
||||
- 支持 8 种操作类型,最多 50 步历史
|
||||
- 新增、删除、编辑、移动、清理、清空、导入均可撤销
|
||||
|
||||
### 导入/导出
|
||||
- **JSON**:结构化导出,含版本和时间戳
|
||||
- **CSV**:UTF-8 BOM 编码,兼容 Excel
|
||||
- **TXT**:纯文本,每行一个路径
|
||||
|
||||
### 安全
|
||||
- 保存前自动备份注册表到 `%APPDATA%/PathEditor/backups/`
|
||||
- PATH 长度检查(Windows 单变量上限 32767 字符)
|
||||
- 非管理员自动进入**只读模式**
|
||||
- 保存中途失败精确提示哪个注册表 hive 出错
|
||||
|
||||
### 界面
|
||||
- 深色模式 / 浅色模式
|
||||
- 中文 / English 界面切换
|
||||
- 全局键盘快捷键
|
||||
- 修改状态指示(黄点)+ 未保存退出确认
|
||||
|
||||
## 安装
|
||||
|
||||
从 [Releases](https://github.com/LHY0125/PathEditor/releases) 下载最新版 `PathEditor_4.0.0_x64-setup.exe` 安装。
|
||||
|
||||
或从源码构建:
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发模式(热更新)
|
||||
npx tauri dev
|
||||
|
||||
# 构建安装包
|
||||
npx tauri build
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|---|---|
|
||||
| 前端框架 | React 19 + TypeScript |
|
||||
| UI 样式 | Tailwind CSS 4 |
|
||||
| 状态管理 | Zustand |
|
||||
| 国际化 | i18next |
|
||||
| 桌面框架 | Tauri 2.x |
|
||||
| 后端语言 | Rust |
|
||||
| 测试 | Vitest (前端) |
|
||||
| 构建 | Vite |
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
src/ # React 前端
|
||||
├── core/ # 纯逻辑(StringList、撤销/重做、路径管理、导入导出)
|
||||
├── store/ # Zustand 状态管理
|
||||
├── components/ # UI 组件(列表、工具栏、对话框)
|
||||
├── hooks/ # 自定义 Hooks(键盘快捷键、路径验证)
|
||||
├── i18n/ # 中英文翻译
|
||||
└── config/ # UI 参数配置
|
||||
|
||||
src-tauri/ # Rust 后端
|
||||
└── src/commands/
|
||||
├── registry.rs # 注册表读写
|
||||
├── system.rs # 权限检测、路径验证、环境变量展开、系统广播
|
||||
└── backup.rs # 注册表备份
|
||||
```
|
||||
|
||||
## 快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| Ctrl+N | 新建路径 |
|
||||
| Ctrl+S | 保存 |
|
||||
| Ctrl+Z | 撤销 |
|
||||
| Ctrl+Y | 重做 |
|
||||
| Ctrl+F | 搜索 |
|
||||
| Delete | 删除选中 |
|
||||
| F1 | 帮助 |
|
||||
> **要求**:Windows 10+(自带 WebView2),管理员权限才能编辑系统 PATH。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 开发模式(热更新)
|
||||
npx tauri dev
|
||||
|
||||
# 仅前端
|
||||
npm run dev
|
||||
|
||||
# 前端测试
|
||||
npm test
|
||||
|
||||
# 前端测试(监听模式)
|
||||
npm run test:watch
|
||||
|
||||
# Rust 后端检查
|
||||
cd src-tauri && cargo check
|
||||
|
||||
@@ -90,10 +96,78 @@ cd src-tauri && cargo check
|
||||
cd src-tauri && cargo test
|
||||
```
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|---|---|
|
||||
| 前端框架 | React 19 + TypeScript (strict) |
|
||||
| UI 样式 | Tailwind CSS 4 |
|
||||
| 状态管理 | Zustand |
|
||||
| 国际化 | i18next |
|
||||
| 桌面框架 | Tauri 2.x |
|
||||
| 后端 | Rust (winreg + windows-rs FFI) |
|
||||
| 前端测试 | Vitest (45 个测试) |
|
||||
| Rust 测试 | cargo test (10 个测试) |
|
||||
| 构建 | Vite |
|
||||
| 打包 | NSIS |
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
src/ # React 前端
|
||||
├── core/ # 纯逻辑 — 零框架依赖、零平台依赖
|
||||
├── store/ # Zustand 状态管理
|
||||
├── components/
|
||||
│ ├── layout/ # AppShell、TitleBar、StatusBar、ErrorBoundary
|
||||
│ ├── path-list/ # PathTable、MergePreview
|
||||
│ ├── toolbar/ # ToolBar、ActionButtons、UndoRedoButtons、SearchInput
|
||||
│ ├── dialogs/ # PathEditDialog、HelpDialog、ImportDialog
|
||||
│ └── ui/ # Modal、buttons(共享组件)
|
||||
├── hooks/ # useAppActions、useKeyboard
|
||||
├── i18n/ # zh-CN / en
|
||||
└── config/ # default.json
|
||||
|
||||
src-tauri/ # Rust 后端
|
||||
└── src/commands/
|
||||
├── registry.rs # 注册表读写
|
||||
├── system.rs # 权限检测、路径验证、环境变量展开
|
||||
└── backup.rs # 注册表备份
|
||||
|
||||
tests/unit/ # 前端单元测试
|
||||
```
|
||||
|
||||
## 快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `Ctrl+N` | 新建路径 |
|
||||
| `Ctrl+S` | 保存 |
|
||||
| `Ctrl+Z` | 撤销 |
|
||||
| `Ctrl+Y` | 重做 |
|
||||
| `Ctrl+F` | 搜索 |
|
||||
| `Delete` | 删除选中 |
|
||||
| `F1` | 帮助 |
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request。在开始大改动前,建议先开 Issue 讨论。
|
||||
|
||||
### 本地开发环境
|
||||
|
||||
- Node.js 22+
|
||||
- Rust 1.95+ (stable-x86_64-pc-windows-gnu)
|
||||
- MinGW-w64 (GCC 15.x 需配置 `-lmcfgthread` 链接标志)
|
||||
|
||||
### 代码规范
|
||||
|
||||
- TypeScript `strict: true`,零编译错误
|
||||
- 所有 Rust `unsafe` 块必须有 `// SAFETY:` 注释
|
||||
- 前端核心逻辑在 `src/core/`,纯函数,零依赖,可独立测试
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 作者
|
||||
|
||||
刘航宇 — [GitHub](https://github.com/LHY0125/PathEditor)
|
||||
[刘航宇](https://github.com/LHY0125) — 河南理工大学人工智能协会
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde::Serialize;
|
||||
|
||||
/// 传给前端的统一错误类型(保留供未来使用,当前命令返回 Result<T, String>)
|
||||
/// 传给前端的统一错误类型(保留供未来迁移使用,届时所有命令改为返回 Result<T, AppError>)
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AppError {
|
||||
pub message: String,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { btnClass, btnStyle } from '@/components/ui/buttons';
|
||||
|
||||
interface ActionButtonsProps {
|
||||
onNew: () => void;
|
||||
@@ -24,14 +25,6 @@ export function ActionButtons({
|
||||
const isAdmin = useAppStore((s) => s.isAdmin);
|
||||
const disabled = !isAdmin;
|
||||
|
||||
const btnClass =
|
||||
'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
|
||||
const btnStyle = {
|
||||
backgroundColor: 'var(--app-bg)',
|
||||
color: 'var(--app-fg)',
|
||||
borderColor: 'var(--app-border)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<button className={btnClass} style={btnStyle} disabled={disabled} onClick={onNew}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { btnClass, btnStyle } from '@/components/ui/buttons';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { ActionButtons } from './ActionButtons';
|
||||
import { UndoRedoButtons } from './UndoRedoButtons';
|
||||
@@ -26,14 +27,6 @@ export function ToolBar(props: ToolBarProps) {
|
||||
const isAdmin = useAppStore((s) => s.isAdmin);
|
||||
const isModified = useAppStore((s) => s.isModified);
|
||||
|
||||
const sysBtnClass =
|
||||
'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
|
||||
const sysBtnStyle = {
|
||||
backgroundColor: 'var(--app-bg)',
|
||||
color: 'var(--app-fg)',
|
||||
borderColor: 'var(--app-border)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pb-2 border-b" style={{ borderColor: 'var(--app-border)' }}>
|
||||
{/* 第一行: 搜索 + 系统按钮 */}
|
||||
@@ -42,38 +35,38 @@ export function ToolBar(props: ToolBarProps) {
|
||||
<div className="flex-1" />
|
||||
<UndoRedoButtons />
|
||||
<button
|
||||
className={sysBtnClass}
|
||||
style={sysBtnStyle}
|
||||
className={btnClass}
|
||||
style={btnStyle}
|
||||
disabled={!isAdmin}
|
||||
onClick={props.onImport}
|
||||
>
|
||||
{t('button.import')}
|
||||
</button>
|
||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onExport}>
|
||||
<button className={btnClass} style={btnStyle} onClick={props.onExport}>
|
||||
{t('button.export')}
|
||||
</button>
|
||||
<button
|
||||
className={sysBtnClass}
|
||||
className={btnClass}
|
||||
style={{
|
||||
...sysBtnStyle,
|
||||
backgroundColor: isModified ? '#2563eb' : sysBtnStyle.backgroundColor,
|
||||
color: isModified ? '#fff' : sysBtnStyle.color,
|
||||
...btnStyle,
|
||||
backgroundColor: isModified ? '#2563eb' : btnStyle.backgroundColor,
|
||||
color: isModified ? '#fff' : btnStyle.color,
|
||||
}}
|
||||
disabled={!isAdmin}
|
||||
onClick={props.onSave}
|
||||
>
|
||||
{t('button.save')}
|
||||
</button>
|
||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onCancel}>
|
||||
<button className={btnClass} style={btnStyle} onClick={props.onCancel}>
|
||||
{t('button.cancel')}
|
||||
</button>
|
||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onHelp}>
|
||||
<button className={btnClass} style={btnStyle} onClick={props.onHelp}>
|
||||
{t('button.help')}
|
||||
</button>
|
||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onLanguage}>
|
||||
<button className={btnClass} style={btnStyle} onClick={props.onLanguage}>
|
||||
{t('button.language')}
|
||||
</button>
|
||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onDarkMode}>
|
||||
<button className={btnClass} style={btnStyle} onClick={props.onDarkMode}>
|
||||
{t('button.darkMode')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { btnClass, btnStyle } from '@/components/ui/buttons';
|
||||
|
||||
export function UndoRedoButtons() {
|
||||
const { t } = useTranslation();
|
||||
@@ -8,14 +9,6 @@ export function UndoRedoButtons() {
|
||||
const undo = useAppStore((s) => s.undo);
|
||||
const redo = useAppStore((s) => s.redo);
|
||||
|
||||
const btnClass =
|
||||
'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
|
||||
const btnStyle = {
|
||||
backgroundColor: 'var(--app-bg)',
|
||||
color: 'var(--app-fg)',
|
||||
borderColor: 'var(--app-border)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const btnClass = 'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
|
||||
|
||||
export const btnStyle: React.CSSProperties = {
|
||||
backgroundColor: 'var(--app-bg)',
|
||||
color: 'var(--app-fg)',
|
||||
borderColor: 'var(--app-border)',
|
||||
};
|
||||
@@ -193,10 +193,10 @@ export function importFromContent(
|
||||
content: string,
|
||||
filepath: string,
|
||||
): ImportResult {
|
||||
const ext = filepath.toLowerCase();
|
||||
if (ext.endsWith('.csv')) {
|
||||
const lower = filepath.toLowerCase();
|
||||
if (lower.endsWith('.csv')) {
|
||||
return importFromCsv(content);
|
||||
} else if (ext.endsWith('.json')) {
|
||||
} else if (lower.endsWith('.json')) {
|
||||
return importFromJson(content);
|
||||
} else {
|
||||
// TXT 文件:所有路径放入 system(用户后续可选择目标)
|
||||
|
||||
@@ -108,7 +108,7 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
const content = isCsv ? exportToCsv(data) : exportToJson(data);
|
||||
const mime = isCsv ? 'text/csv' : 'application/json';
|
||||
const ext = isCsv ? '.csv' : '.json';
|
||||
const blob = new Blob([isCsv ? '' : '', content], { type: mime });
|
||||
const blob = new Blob([content], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
@@ -39,8 +39,6 @@ interface AppState {
|
||||
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
canUndo: () => boolean;
|
||||
canRedo: () => boolean;
|
||||
|
||||
loadPaths: () => Promise<void>;
|
||||
savePaths: () => Promise<void>;
|
||||
@@ -227,9 +225,6 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) });
|
||||
},
|
||||
|
||||
canUndo: () => get().undoRedo.canUndo(),
|
||||
canRedo: () => get().undoRedo.canRedo(),
|
||||
|
||||
loadPaths: async () => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
|
||||
Reference in New Issue
Block a user