mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26f6953919 | |||
| 5ed15535e7 | |||
| 230fb5d741 | |||
| d7d11480b8 |
@@ -1,6 +1,7 @@
|
|||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod disabled;
|
pub mod disabled;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
pub mod profiles;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod scanner;
|
pub mod scanner;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn profiles_dir() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join(".patheditor")
|
||||||
|
.join("profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn profile_path(name: &str) -> PathBuf {
|
||||||
|
profiles_dir().join(format!("{}.json", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部用的 PathEntry(与前端 PathEntry 字段一致)
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ProfilePathEntry {
|
||||||
|
pub path: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ProfileMeta {
|
||||||
|
pub name: String,
|
||||||
|
pub created: String,
|
||||||
|
pub modified: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ProfileData {
|
||||||
|
pub name: String,
|
||||||
|
pub sys: Vec<ProfilePathEntry>,
|
||||||
|
pub user: Vec<ProfilePathEntry>,
|
||||||
|
pub created: String,
|
||||||
|
pub modified: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出所有配置文件的元数据
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_profiles() -> Result<Vec<ProfileMeta>, String> {
|
||||||
|
let dir = profiles_dir();
|
||||||
|
if !dir.exists() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut profiles: Vec<ProfileMeta> = Vec::new();
|
||||||
|
let entries = fs::read_dir(&dir).map_err(|e| format!("无法读取配置目录: {}", e))?;
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().map_or(true, |e| e != "json") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&path)
|
||||||
|
.map_err(|e| format!("无法读取 {}: {}", path.display(), e))?;
|
||||||
|
if let Ok(data) = serde_json::from_str::<ProfileData>(&content) {
|
||||||
|
profiles.push(ProfileMeta {
|
||||||
|
name: data.name,
|
||||||
|
created: data.created,
|
||||||
|
modified: data.modified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存当前 PATH 为配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn save_profile(
|
||||||
|
name: String,
|
||||||
|
sys: Vec<ProfilePathEntry>,
|
||||||
|
user: Vec<ProfilePathEntry>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let dir = profiles_dir();
|
||||||
|
fs::create_dir_all(&dir).map_err(|e| format!("无法创建配置目录: {}", e))?;
|
||||||
|
|
||||||
|
let path = profile_path(&name);
|
||||||
|
let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||||
|
|
||||||
|
let data = ProfileData {
|
||||||
|
name,
|
||||||
|
sys,
|
||||||
|
user,
|
||||||
|
created: now.clone(),
|
||||||
|
modified: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json =
|
||||||
|
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||||
|
fs::write(&path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
||||||
|
|
||||||
|
log::info!("已保存配置: {}", path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn load_profile(name: String) -> Result<ProfileData, String> {
|
||||||
|
let path = profile_path(&name);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("配置文件不存在: {}", name));
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&path)
|
||||||
|
.map_err(|e| format!("无法读取配置文件: {}", e))?;
|
||||||
|
serde_json::from_str(&content)
|
||||||
|
.map_err(|e| format!("JSON 解析失败: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn delete_profile(name: String) -> Result<(), String> {
|
||||||
|
let path = profile_path(&name);
|
||||||
|
fs::remove_file(&path).map_err(|e| format!("无法删除配置文件: {}", e))?;
|
||||||
|
log::info!("已删除配置: {}", path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重命名配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn rename_profile(old_name: String, new_name: String) -> Result<(), String> {
|
||||||
|
let old_path = profile_path(&old_name);
|
||||||
|
if !old_path.exists() {
|
||||||
|
return Err(format!("配置文件不存在: {}", old_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data: ProfileData =
|
||||||
|
serde_json::from_str(&fs::read_to_string(&old_path).map_err(|e| format!("无法读取配置文件: {}", e))?).map_err(|e| format!("JSON 解析失败: {}", e))?;
|
||||||
|
|
||||||
|
data.name = new_name.clone();
|
||||||
|
data.modified = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||||
|
|
||||||
|
let new_path = profile_path(&new_name);
|
||||||
|
let json =
|
||||||
|
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||||
|
fs::write(&new_path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
||||||
|
|
||||||
|
if old_path != new_path {
|
||||||
|
fs::remove_file(&old_path).map_err(|e| format!("无法删除旧配置文件: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("已重命名配置: {} -> {}", old_name, new_name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ pub fn run() {
|
|||||||
commands::disabled::load_disabled_state,
|
commands::disabled::load_disabled_state,
|
||||||
commands::scanner::scan_conflicts,
|
commands::scanner::scan_conflicts,
|
||||||
commands::scanner::scan_tools,
|
commands::scanner::scan_tools,
|
||||||
|
commands::profiles::list_profiles,
|
||||||
|
commands::profiles::save_profile,
|
||||||
|
commands::profiles::load_profile,
|
||||||
|
commands::profiles::delete_profile,
|
||||||
|
commands::profiles::rename_profile,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Modal } from '@/components/ui/Modal';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import type { PathEntry } from '@/core/path-entry';
|
||||||
|
|
||||||
|
interface ProfileMeta {
|
||||||
|
name: string;
|
||||||
|
created: string;
|
||||||
|
modified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileData {
|
||||||
|
name: string;
|
||||||
|
sys: PathEntry[];
|
||||||
|
user: PathEntry[];
|
||||||
|
created: string;
|
||||||
|
modified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileDialog({ open, onClose }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [profiles, setProfiles] = useState<ProfileMeta[]>([]);
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
|
const [selected, setSelected] = useState<string | null>(null);
|
||||||
|
const [selectedData, setSelectedData] = useState<ProfileData | null>(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState('');
|
||||||
|
|
||||||
|
const refreshProfiles = useCallback(async () => {
|
||||||
|
const list = await invoke<ProfileMeta[]>('list_profiles');
|
||||||
|
setProfiles(list);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) refreshProfiles();
|
||||||
|
}, [open, refreshProfiles]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!newName.trim()) return;
|
||||||
|
setSaving(true);
|
||||||
|
const { sysPaths, userPaths } = useAppStore.getState();
|
||||||
|
await invoke('save_profile', { name: newName.trim(), sys: sysPaths, user: userPaths });
|
||||||
|
setNewName('');
|
||||||
|
setSaving(false);
|
||||||
|
refreshProfiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoad = async (name: string) => {
|
||||||
|
const data = await invoke<ProfileData>('load_profile', { name });
|
||||||
|
setSelected(name);
|
||||||
|
setSelectedData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = async () => {
|
||||||
|
if (!selected || !selectedData) return;
|
||||||
|
if (!window.confirm(t('profile.applyConfirm', { name: selected }))) return;
|
||||||
|
useAppStore.getState().replaceBothPaths(
|
||||||
|
selectedData.sys.map(e => e.path),
|
||||||
|
selectedData.user.map(e => e.path),
|
||||||
|
);
|
||||||
|
// 同步 disabled 状态
|
||||||
|
await invoke('save_disabled_state', {
|
||||||
|
system: selectedData.sys.filter(e => !e.enabled).map(e => e.path),
|
||||||
|
user: selectedData.user.filter(e => !e.enabled).map(e => e.path),
|
||||||
|
});
|
||||||
|
await useAppStore.getState().savePaths();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (name: string) => {
|
||||||
|
if (!window.confirm(`删除配置文件 "${name}"?`)) return;
|
||||||
|
await invoke('delete_profile', { name });
|
||||||
|
if (selected === name) { setSelected(null); setSelectedData(null); }
|
||||||
|
refreshProfiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = async () => {
|
||||||
|
if (!selected || !renameValue.trim()) return;
|
||||||
|
await invoke('rename_profile', { oldName: selected, newName: renameValue.trim() });
|
||||||
|
setRenameOpen(false);
|
||||||
|
setSelected(renameValue.trim());
|
||||||
|
refreshProfiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<div className="flex flex-col" style={{ width: 680, maxHeight: '75vh' }}>
|
||||||
|
<div className="flex items-center justify-between px-5 py-3 border-b" style={{ borderColor: 'var(--app-border)' }}>
|
||||||
|
<h2 className="text-base font-semibold">{t('profile.title')}</h2>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newName}
|
||||||
|
onChange={e => setNewName(e.target.value)}
|
||||||
|
placeholder={t('profile.namePlaceholder')}
|
||||||
|
className="px-2 py-1 text-sm rounded border outline-none w-44"
|
||||||
|
style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 text-sm rounded text-white"
|
||||||
|
style={{ backgroundColor: '#3b82f6' }}
|
||||||
|
disabled={saving || !newName.trim()}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{t('profile.save')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-2 py-1 text-sm rounded hover:opacity-70 transition-opacity"
|
||||||
|
style={{ color: 'var(--app-fg)' }}
|
||||||
|
title="关闭"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
{/* 左侧:列表 */}
|
||||||
|
<div className="w-48 border-r overflow-auto p-2" style={{ borderColor: 'var(--app-border)' }}>
|
||||||
|
{profiles.length === 0 ? (
|
||||||
|
<div className="text-xs text-center py-6" style={{ opacity: 0.5 }}>{t('profile.noProfiles')}</div>
|
||||||
|
) : (
|
||||||
|
profiles.map(p => (
|
||||||
|
<div
|
||||||
|
key={p.name}
|
||||||
|
onClick={() => handleLoad(p.name)}
|
||||||
|
className="px-2 py-1.5 text-sm rounded cursor-pointer mb-0.5"
|
||||||
|
style={{
|
||||||
|
backgroundColor: selected === p.name ? 'rgba(59,130,246,0.15)' : 'transparent',
|
||||||
|
color: selected === p.name ? '#3b82f6' : 'var(--app-fg)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:详情 */}
|
||||||
|
<div className="flex-1 p-3 overflow-auto">
|
||||||
|
{!selectedData ? (
|
||||||
|
<div className="text-center py-10 text-sm" style={{ opacity: 0.4 }}>
|
||||||
|
{profiles.length === 0 ? t('profile.noProfiles') : '选择一个配置文件'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<span className="font-semibold text-sm">{selectedData.name}</span>
|
||||||
|
<span className="text-xs" style={{ opacity: 0.5 }}>{selectedData.modified}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1.5 mb-3">
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 text-xs rounded text-white"
|
||||||
|
style={{ backgroundColor: '#3b82f6' }}
|
||||||
|
onClick={handleApply}
|
||||||
|
>
|
||||||
|
{t('profile.apply')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 text-xs rounded"
|
||||||
|
style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)' }}
|
||||||
|
onClick={() => { setRenameOpen(true); setRenameValue(selectedData.name); }}
|
||||||
|
>
|
||||||
|
{t('profile.rename')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 text-xs rounded text-white"
|
||||||
|
style={{ backgroundColor: '#ef4444' }}
|
||||||
|
onClick={() => handleDelete(selectedData.name)}
|
||||||
|
>
|
||||||
|
{t('profile.delete')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renameOpen && (
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={renameValue}
|
||||||
|
onChange={e => setRenameValue(e.target.value)}
|
||||||
|
className="px-2 py-1 text-xs rounded border outline-none"
|
||||||
|
style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }}
|
||||||
|
/>
|
||||||
|
<button className="px-2 py-1 text-xs rounded text-white" style={{ backgroundColor: '#3b82f6' }} onClick={handleRename}>
|
||||||
|
确认
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PathSection title={`系统 PATH (${selectedData.sys.length})`} paths={selectedData.sys} />
|
||||||
|
<PathSection title={`用户 PATH (${selectedData.user.length})`} paths={selectedData.user} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PathSection({ title, paths }: { title: string; paths: PathEntry[] }) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="text-xs font-medium mb-1" style={{ opacity: 0.7 }}>{title}</div>
|
||||||
|
{paths.length === 0 ? (
|
||||||
|
<div className="text-xs" style={{ opacity: 0.4 }}>(空)</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-0.5 max-h-48 overflow-auto">
|
||||||
|
{paths.map((e, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="text-xs font-mono px-2 py-0.5 rounded flex items-center gap-1.5"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--app-list-bg)',
|
||||||
|
color: e.enabled ? 'var(--app-fg)' : '#ef4444',
|
||||||
|
textDecoration: e.enabled ? 'none' : 'line-through',
|
||||||
|
opacity: e.enabled ? 1 : 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ color: e.enabled ? '#22c55e' : '#ef4444', fontSize: 10 }}>
|
||||||
|
{e.enabled ? '●' : '○'}
|
||||||
|
</span>
|
||||||
|
{e.path}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { PathEditDialog } from '@/components/dialogs/PathEditDialog';
|
|||||||
import { HelpDialog } from '@/components/dialogs/HelpDialog';
|
import { HelpDialog } from '@/components/dialogs/HelpDialog';
|
||||||
import { ImportDialog } from '@/components/dialogs/ImportDialog';
|
import { ImportDialog } from '@/components/dialogs/ImportDialog';
|
||||||
import { AnalyzeDialog } from '@/components/dialogs/AnalyzeDialog';
|
import { AnalyzeDialog } from '@/components/dialogs/AnalyzeDialog';
|
||||||
|
import { ProfileDialog } from '@/components/dialogs/ProfileDialog';
|
||||||
import { useAppActions, type DialogState } from '@/hooks/use-app-actions';
|
import { useAppActions, type DialogState } from '@/hooks/use-app-actions';
|
||||||
|
|
||||||
/** Tauri's File object includes the native filesystem path */
|
/** Tauri's File object includes the native filesystem path */
|
||||||
@@ -35,10 +36,11 @@ export function AppShell() {
|
|||||||
open: false, system: [], user: [],
|
open: false, system: [], user: [],
|
||||||
});
|
});
|
||||||
const [analyzeOpen, setAnalyzeOpen] = useState(false);
|
const [analyzeOpen, setAnalyzeOpen] = useState(false);
|
||||||
|
const [profilesOpen, setProfilesOpen] = useState(false);
|
||||||
|
|
||||||
const actions = useAppActions(activeTab, {
|
const actions = useAppActions(activeTab, {
|
||||||
editDialog, newDialog, helpOpen, importDialog,
|
editDialog, newDialog, helpOpen, importDialog,
|
||||||
setEditDialog, setNewDialog, setHelpOpen, setImportDialog, setAnalyzeOpen,
|
setEditDialog, setNewDialog, setHelpOpen, setImportDialog, setAnalyzeOpen, setProfilesOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabConfig: { id: TabId; label: string }[] = [
|
const tabConfig: { id: TabId; label: string }[] = [
|
||||||
@@ -86,6 +88,7 @@ export function AppShell() {
|
|||||||
const current = localStorage.getItem('i18nextLng') || 'zh-CN';
|
const current = localStorage.getItem('i18nextLng') || 'zh-CN';
|
||||||
i18n.changeLanguage(current === 'zh-CN' ? 'en' : 'zh-CN');
|
i18n.changeLanguage(current === 'zh-CN' ? 'en' : 'zh-CN');
|
||||||
}}
|
}}
|
||||||
|
onProfiles={() => setProfilesOpen(true)}
|
||||||
onAnalyze={() => setAnalyzeOpen(true)}
|
onAnalyze={() => setAnalyzeOpen(true)}
|
||||||
onDarkMode={() => useThemeStore.getState().toggle()}
|
onDarkMode={() => useThemeStore.getState().toggle()}
|
||||||
/>
|
/>
|
||||||
@@ -116,6 +119,7 @@ export function AppShell() {
|
|||||||
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
|
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
|
||||||
<ImportDialog open={importDialog.open} systemCount={importDialog.system.length} userCount={importDialog.user.length} onSelect={actions.handleImportSelect} onCancel={() => setImportDialog({ open: false, system: [], user: [] })} />
|
<ImportDialog open={importDialog.open} systemCount={importDialog.system.length} userCount={importDialog.user.length} onSelect={actions.handleImportSelect} onCancel={() => setImportDialog({ open: false, system: [], user: [] })} />
|
||||||
<AnalyzeDialog open={analyzeOpen} onClose={() => setAnalyzeOpen(false)} />
|
<AnalyzeDialog open={analyzeOpen} onClose={() => setAnalyzeOpen(false)} />
|
||||||
|
<ProfileDialog open={profilesOpen} onClose={() => setProfilesOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export function PathTable({ tabId }: PathTableProps) {
|
|||||||
className="cursor-pointer select-none"
|
className="cursor-pointer select-none"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isSelected
|
backgroundColor: isSelected
|
||||||
? 'rgba(59, 130, 246, 0.3)'
|
? 'var(--app-select-row)'
|
||||||
: rowIdx % 2 === 0
|
: rowIdx % 2 === 0
|
||||||
? 'var(--app-list-bg)'
|
? 'var(--app-list-bg)'
|
||||||
: 'var(--app-list-alt)',
|
: 'var(--app-list-alt)',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface ToolBarProps {
|
|||||||
onLanguage: () => void;
|
onLanguage: () => void;
|
||||||
onDarkMode: () => void;
|
onDarkMode: () => void;
|
||||||
onAnalyze: () => void;
|
onAnalyze: () => void;
|
||||||
|
onProfiles: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolBar(props: ToolBarProps) {
|
export function ToolBar(props: ToolBarProps) {
|
||||||
@@ -70,6 +71,9 @@ export function ToolBar(props: ToolBarProps) {
|
|||||||
<button className={btnClass} style={btnStyle} onClick={props.onAnalyze}>
|
<button className={btnClass} style={btnStyle} onClick={props.onAnalyze}>
|
||||||
{t('button.analyze')}
|
{t('button.analyze')}
|
||||||
</button>
|
</button>
|
||||||
|
<button className={btnClass} style={btnStyle} onClick={props.onProfiles}>
|
||||||
|
{t('button.profiles')}
|
||||||
|
</button>
|
||||||
<button className={btnClass} style={btnStyle} onClick={props.onDarkMode}>
|
<button className={btnClass} style={btnStyle} onClick={props.onDarkMode}>
|
||||||
{t('button.darkMode')}
|
{t('button.darkMode')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface DialogState {
|
|||||||
setHelpOpen: (v: boolean) => void;
|
setHelpOpen: (v: boolean) => void;
|
||||||
setImportDialog: (v: DialogState['importDialog']) => void;
|
setImportDialog: (v: DialogState['importDialog']) => void;
|
||||||
setAnalyzeOpen: (v: boolean) => void;
|
setAnalyzeOpen: (v: boolean) => void;
|
||||||
|
setProfilesOpen: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"analyze": "Analyze",
|
"analyze": "Analyze",
|
||||||
|
"profiles": "Profiles",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"redo": "Redo",
|
"redo": "Redo",
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
@@ -83,6 +84,19 @@
|
|||||||
"searchPlaceholder": "Search executable name...",
|
"searchPlaceholder": "Search executable name...",
|
||||||
"conflictCount": "{{count}} file conflict(s) found"
|
"conflictCount": "{{count}} file conflict(s) found"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "PATH Profiles",
|
||||||
|
"saveCurrent": "Save Current as Profile",
|
||||||
|
"namePlaceholder": "Profile name...",
|
||||||
|
"save": "Save",
|
||||||
|
"load": "Load",
|
||||||
|
"apply": "Apply",
|
||||||
|
"delete": "Delete",
|
||||||
|
"rename": "Rename",
|
||||||
|
"noProfiles": "No saved profiles",
|
||||||
|
"applyConfirm": "This will overwrite current PATH with profile \"{{name}}\" and write to registry. Confirm?",
|
||||||
|
"deleted": "Profile \"{{name}}\" deleted"
|
||||||
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"content": "PathEditor v4.0 — Windows System Environment Variable (PATH) Editor\n\nFeatures:\n• Create/Edit/Delete path entries\n• Move Up/Down to adjust priority\n• One-click cleanup of invalid & duplicate paths\n• Import/Export JSON, CSV, TXT formats\n• Full Undo/Redo support\n\nShortcuts:\n• Ctrl+N New\n• Ctrl+S Save\n• Ctrl+Z Undo\n• Ctrl+Y Redo\n• Ctrl+F Search\n• Delete Delete selected\n• F1 Help\n\nAuthor: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
"content": "PathEditor v4.0 — Windows System Environment Variable (PATH) Editor\n\nFeatures:\n• Create/Edit/Delete path entries\n• Move Up/Down to adjust priority\n• One-click cleanup of invalid & duplicate paths\n• Import/Export JSON, CSV, TXT formats\n• Full Undo/Redo support\n\nShortcuts:\n• Ctrl+N New\n• Ctrl+S Save\n• Ctrl+Z Undo\n• Ctrl+Y Redo\n• Ctrl+F Search\n• Delete Delete selected\n• F1 Help\n\nAuthor: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"analyze": "分析",
|
"analyze": "分析",
|
||||||
|
"profiles": "配置",
|
||||||
"undo": "撤销",
|
"undo": "撤销",
|
||||||
"redo": "重做",
|
"redo": "重做",
|
||||||
"darkMode": "深色模式",
|
"darkMode": "深色模式",
|
||||||
@@ -83,6 +84,19 @@
|
|||||||
"searchPlaceholder": "搜索可执行文件名...",
|
"searchPlaceholder": "搜索可执行文件名...",
|
||||||
"conflictCount": "发现 {{count}} 个文件冲突"
|
"conflictCount": "发现 {{count}} 个文件冲突"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "PATH 配置文件",
|
||||||
|
"saveCurrent": "保存当前 PATH 为配置",
|
||||||
|
"namePlaceholder": "配置名称...",
|
||||||
|
"save": "保存",
|
||||||
|
"load": "加载",
|
||||||
|
"apply": "应用",
|
||||||
|
"delete": "删除",
|
||||||
|
"rename": "重命名",
|
||||||
|
"noProfiles": "暂无配置文件",
|
||||||
|
"applyConfirm": "将用配置 \"{{name}}\" 覆盖当前 PATH 并写入注册表,确定吗?",
|
||||||
|
"deleted": "已删除配置 \"{{name}}\""
|
||||||
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"content": "PathEditor v4.0 — Windows 系统环境变量 (PATH) 编辑器\n\n功能:\n• 新建/编辑/删除路径条目\n• 上移/下移调整优先级\n• 一键清理无效和重复路径\n• 导入/导出 JSON、CSV、TXT 格式\n• 完整撤销/重做支持\n\n快捷键:\n• Ctrl+N 新建\n• Ctrl+S 保存\n• Ctrl+Z 撤销\n• Ctrl+Y 重做\n• Ctrl+F 搜索\n• Delete 删除选中\n• F1 帮助\n\n作者: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
"content": "PathEditor v4.0 — Windows 系统环境变量 (PATH) 编辑器\n\n功能:\n• 新建/编辑/删除路径条目\n• 上移/下移调整优先级\n• 一键清理无效和重复路径\n• 导入/导出 JSON、CSV、TXT 格式\n• 完整撤销/重做支持\n\n快捷键:\n• Ctrl+N 新建\n• Ctrl+S 保存\n• Ctrl+Z 撤销\n• Ctrl+Y 重做\n• Ctrl+F 搜索\n• Delete 删除选中\n• F1 帮助\n\n作者: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ body {
|
|||||||
--app-fg: var(--color-light-fg);
|
--app-fg: var(--color-light-fg);
|
||||||
--app-border: var(--color-light-border);
|
--app-border: var(--color-light-border);
|
||||||
--app-hover: var(--color-light-hover);
|
--app-hover: var(--color-light-hover);
|
||||||
|
--app-select-row: rgba(59, 130, 246, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 深色模式 */
|
/* 深色模式 */
|
||||||
@@ -51,6 +52,7 @@ body {
|
|||||||
--app-fg: var(--color-dark-fg);
|
--app-fg: var(--color-dark-fg);
|
||||||
--app-border: var(--color-dark-border);
|
--app-border: var(--color-dark-border);
|
||||||
--app-hover: var(--color-dark-hover);
|
--app-hover: var(--color-dark-hover);
|
||||||
|
--app-select-row: rgba(96, 165, 250, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条样式 */
|
/* 滚动条样式 */
|
||||||
|
|||||||
Reference in New Issue
Block a user