mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:37:22 +08:00
feat: 新增 PATH 智能分析功能 — 冲突检测 + 工具清单
- scan_conflicts: 检测不同目录中的同名可执行文件(遮蔽冲突) - scan_tools: 扫描各目录提供的可执行文件,支持关键词搜索 - Rust scanner.rs 后端,前端 AnalyzeDialog 弹窗 - 工具栏新增「分析」按钮 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
pub mod registry;
|
|
||||||
pub mod system;
|
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod fs;
|
|
||||||
pub mod disabled;
|
pub mod disabled;
|
||||||
|
pub mod fs;
|
||||||
|
pub mod registry;
|
||||||
|
pub mod scanner;
|
||||||
|
pub mod system;
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const EXECUTABLE_EXTENSIONS: &[&str] = &["exe", "bat", "cmd", "com", "ps1"];
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
pub struct ConflictLocation {
|
||||||
|
pub dir: String,
|
||||||
|
pub priority: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
pub struct ConflictEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub locations: Vec<ConflictLocation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct ToolGroup {
|
||||||
|
pub dir: String,
|
||||||
|
pub exists: bool,
|
||||||
|
pub exes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 扫描 PATH 中的可执行文件冲突
|
||||||
|
///
|
||||||
|
/// 遍历每个 PATH 目录,查找 .exe/.bat/.cmd/.com/.ps1 文件,
|
||||||
|
/// 标记出现在多个目录中的同名文件(后面的目录会被前面的「遮蔽」)
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn scan_conflicts(paths: Vec<String>) -> Result<Vec<ConflictEntry>, String> {
|
||||||
|
// exe_name (小写) → [(priority, dir)]
|
||||||
|
let mut map: HashMap<String, Vec<(usize, String)>> = HashMap::new();
|
||||||
|
|
||||||
|
for (priority, dir) in paths.iter().enumerate() {
|
||||||
|
let p = Path::new(dir);
|
||||||
|
if !p.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let fname = entry.file_name();
|
||||||
|
let name = fname.to_string_lossy();
|
||||||
|
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
||||||
|
let ext_lower = ext.to_ascii_lowercase();
|
||||||
|
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
||||||
|
let key = name.to_lowercase();
|
||||||
|
map.entry(key).or_default().push((priority, dir.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results: Vec<ConflictEntry> = map
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, locs)| locs.len() >= 2)
|
||||||
|
.map(|(name, locs)| ConflictEntry {
|
||||||
|
name,
|
||||||
|
locations: locs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(priority, dir)| ConflictLocation { dir, priority })
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
results.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 扫描 PATH 中各目录提供的可执行文件
|
||||||
|
///
|
||||||
|
/// query 非空时只返回文件名包含关键词的结果
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn scan_tools(paths: Vec<String>, query: String) -> Result<Vec<ToolGroup>, String> {
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
let mut groups: Vec<ToolGroup> = Vec::new();
|
||||||
|
|
||||||
|
for dir in &paths {
|
||||||
|
let p = Path::new(dir);
|
||||||
|
if !p.is_dir() {
|
||||||
|
groups.push(ToolGroup {
|
||||||
|
dir: dir.clone(),
|
||||||
|
exists: false,
|
||||||
|
exes: vec![],
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
||||||
|
let mut exes: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let fname = entry.file_name();
|
||||||
|
let name = fname.to_string_lossy();
|
||||||
|
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
||||||
|
let ext_lower = ext.to_ascii_lowercase();
|
||||||
|
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
||||||
|
if query_lower.is_empty() || name.to_lowercase().contains(&query_lower) {
|
||||||
|
exes.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exes.sort();
|
||||||
|
groups.push(ToolGroup {
|
||||||
|
dir: dir.clone(),
|
||||||
|
exists: true,
|
||||||
|
exes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(groups)
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ pub fn run() {
|
|||||||
commands::fs::read_text_file,
|
commands::fs::read_text_file,
|
||||||
commands::disabled::save_disabled_state,
|
commands::disabled::save_disabled_state,
|
||||||
commands::disabled::load_disabled_state,
|
commands::disabled::load_disabled_state,
|
||||||
|
commands::scanner::scan_conflicts,
|
||||||
|
commands::scanner::scan_tools,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { useState, useEffect, useMemo } 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';
|
||||||
|
|
||||||
|
interface ConflictLocation {
|
||||||
|
dir: string;
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConflictEntry {
|
||||||
|
name: string;
|
||||||
|
locations: ConflictLocation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolGroup {
|
||||||
|
dir: string;
|
||||||
|
exists: boolean;
|
||||||
|
exes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabType = 'conflicts' | 'tools';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnalyzeDialog({ open, onClose }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [tab, setTab] = useState<TabType>('conflicts');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [conflicts, setConflicts] = useState<ConflictEntry[]>([]);
|
||||||
|
const [toolGroups, setToolGroups] = useState<ToolGroup[]>([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setLoading(true);
|
||||||
|
const paths = getEnabledPaths();
|
||||||
|
Promise.all([
|
||||||
|
invoke<ConflictEntry[]>('scan_conflicts', { paths }),
|
||||||
|
invoke<ToolGroup[]>('scan_tools', { paths, query: '' }),
|
||||||
|
])
|
||||||
|
.then(([c, t]) => {
|
||||||
|
setConflicts(c);
|
||||||
|
setToolGroups(t);
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// 搜索的工具清单
|
||||||
|
const filteredTools = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return toolGroups;
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
return toolGroups
|
||||||
|
.map((g) => ({ ...g, exes: g.exes.filter((e) => e.toLowerCase().includes(q)) }))
|
||||||
|
.filter((g) => g.exes.length > 0);
|
||||||
|
}, [toolGroups, searchQuery]);
|
||||||
|
|
||||||
|
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('analyze.title')}</h2>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{(['conflicts', 'tools'] as TabType[]).map((tb) => (
|
||||||
|
<button
|
||||||
|
key={tb}
|
||||||
|
onClick={() => setTab(tb)}
|
||||||
|
className="px-3 py-1 text-sm rounded transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: tab === tb ? '#3b82f6' : 'transparent',
|
||||||
|
color: tab === tb ? '#fff' : 'var(--app-fg)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tb === 'conflicts' ? t('analyze.conflicts') : t('analyze.tools')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="flex-1 overflow-auto p-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12 text-sm" style={{ color: 'var(--app-fg)', opacity: 0.6 }}>
|
||||||
|
{t('analyze.scanning')}
|
||||||
|
</div>
|
||||||
|
) : tab === 'conflicts' ? (
|
||||||
|
<ConflictsTab conflicts={conflicts} />
|
||||||
|
) : (
|
||||||
|
<ToolsTab groups={filteredTools} query={searchQuery} onQueryChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConflictsTab({ conflicts }: { conflicts: ConflictEntry[] }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (conflicts.length === 0) {
|
||||||
|
return <EmptyHint text={t('analyze.noConflicts')} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm mb-2" style={{ color: 'var(--app-fg)', opacity: 0.7 }}>
|
||||||
|
{t('analyze.conflictCount', { count: conflicts.length })}
|
||||||
|
</p>
|
||||||
|
<table className="w-full text-sm border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b" style={{ borderColor: 'var(--app-border)' }}>
|
||||||
|
<th className="text-left py-1.5 pr-3 font-medium">EXE</th>
|
||||||
|
<th className="text-left py-1.5 font-medium">{t('analyze.priority')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{conflicts.map((c) => (
|
||||||
|
<tr key={c.name} className="border-b" style={{ borderColor: 'var(--app-border)' }}>
|
||||||
|
<td className="py-1.5 pr-3 font-mono">{c.name}</td>
|
||||||
|
<td className="py-1.5">
|
||||||
|
{c.locations.map((loc, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="text-xs py-0.5"
|
||||||
|
style={{ color: i === 0 ? '#22c55e' : '#ef4444' }}
|
||||||
|
>
|
||||||
|
{i === 0 ? '✓' : '✗'} {loc.dir}
|
||||||
|
{i > 0 && (
|
||||||
|
<span className="ml-1" style={{ opacity: 0.5 }}>
|
||||||
|
({t('analyze.shadowed')})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolsTab({
|
||||||
|
groups,
|
||||||
|
query,
|
||||||
|
onQueryChange,
|
||||||
|
}: {
|
||||||
|
groups: ToolGroup[];
|
||||||
|
query: string;
|
||||||
|
onQueryChange: (q: string) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => onQueryChange(e.target.value)}
|
||||||
|
placeholder={t('analyze.searchPlaceholder')}
|
||||||
|
className="w-full px-3 py-1.5 text-sm rounded mb-3 border outline-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--app-list-bg)',
|
||||||
|
color: 'var(--app-fg)',
|
||||||
|
borderColor: 'var(--app-border)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{groups.length === 0 ? (
|
||||||
|
<EmptyHint text={t('analyze.noTools')} />
|
||||||
|
) : (
|
||||||
|
groups.map((g) => (
|
||||||
|
<div key={g.dir} className="mb-3">
|
||||||
|
<div
|
||||||
|
className="text-xs font-mono py-1 px-2 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: g.exists ? 'transparent' : 'rgba(239,68,68,0.1)',
|
||||||
|
color: g.exists ? 'var(--app-fg)' : '#ef4444',
|
||||||
|
opacity: g.exists ? 1 : 0.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{g.dir} {!g.exists && '(不存在)'}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1 ml-2">
|
||||||
|
{g.exes.map((exe) => (
|
||||||
|
<span
|
||||||
|
key={exe}
|
||||||
|
className="text-xs font-mono px-1.5 py-0.5 rounded"
|
||||||
|
style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)' }}
|
||||||
|
>
|
||||||
|
{exe}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyHint({ text }: { text: string }) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12 text-sm" style={{ color: 'var(--app-fg)', opacity: 0.5 }}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledPaths(): string[] {
|
||||||
|
const { sysPaths, userPaths } = useAppStore.getState();
|
||||||
|
return [...sysPaths.filter((e) => e.enabled), ...userPaths.filter((e) => e.enabled)].map((e) => e.path);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { MergePreview } from '@/components/path-list/MergePreview';
|
|||||||
import { PathEditDialog } from '@/components/dialogs/PathEditDialog';
|
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 { 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 */
|
||||||
@@ -33,10 +34,11 @@ export function AppShell() {
|
|||||||
const [importDialog, setImportDialog] = useState<DialogState['importDialog']>({
|
const [importDialog, setImportDialog] = useState<DialogState['importDialog']>({
|
||||||
open: false, system: [], user: [],
|
open: false, system: [], user: [],
|
||||||
});
|
});
|
||||||
|
const [analyzeOpen, setAnalyzeOpen] = useState(false);
|
||||||
|
|
||||||
const actions = useAppActions(activeTab, {
|
const actions = useAppActions(activeTab, {
|
||||||
editDialog, newDialog, helpOpen, importDialog,
|
editDialog, newDialog, helpOpen, importDialog,
|
||||||
setEditDialog, setNewDialog, setHelpOpen, setImportDialog,
|
setEditDialog, setNewDialog, setHelpOpen, setImportDialog, setAnalyzeOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabConfig: { id: TabId; label: string }[] = [
|
const tabConfig: { id: TabId; label: string }[] = [
|
||||||
@@ -84,6 +86,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');
|
||||||
}}
|
}}
|
||||||
|
onAnalyze={() => setAnalyzeOpen(true)}
|
||||||
onDarkMode={() => useThemeStore.getState().toggle()}
|
onDarkMode={() => useThemeStore.getState().toggle()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,6 +115,7 @@ export function AppShell() {
|
|||||||
<PathEditDialog open={editDialog.open} title={t('dialog.editPath')} initialValue={editDialog.value} onConfirm={actions.handleEditConfirm} onCancel={() => setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })} />
|
<PathEditDialog open={editDialog.open} title={t('dialog.editPath')} initialValue={editDialog.value} onConfirm={actions.handleEditConfirm} onCancel={() => setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })} />
|
||||||
<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)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface ToolBarProps {
|
|||||||
onHelp: () => void;
|
onHelp: () => void;
|
||||||
onLanguage: () => void;
|
onLanguage: () => void;
|
||||||
onDarkMode: () => void;
|
onDarkMode: () => void;
|
||||||
|
onAnalyze: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolBar(props: ToolBarProps) {
|
export function ToolBar(props: ToolBarProps) {
|
||||||
@@ -66,6 +67,9 @@ export function ToolBar(props: ToolBarProps) {
|
|||||||
<button className={btnClass} style={btnStyle} onClick={props.onLanguage}>
|
<button className={btnClass} style={btnStyle} onClick={props.onLanguage}>
|
||||||
{t('button.language')}
|
{t('button.language')}
|
||||||
</button>
|
</button>
|
||||||
|
<button className={btnClass} style={btnStyle} onClick={props.onAnalyze}>
|
||||||
|
{t('button.analyze')}
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface DialogState {
|
|||||||
setNewDialog: (v: boolean) => void;
|
setNewDialog: (v: boolean) => void;
|
||||||
setHelpOpen: (v: boolean) => void;
|
setHelpOpen: (v: boolean) => void;
|
||||||
setImportDialog: (v: DialogState['importDialog']) => void;
|
setImportDialog: (v: DialogState['importDialog']) => void;
|
||||||
|
setAnalyzeOpen: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"save": "OK",
|
"save": "OK",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
|
"analyze": "Analyze",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"redo": "Redo",
|
"redo": "Redo",
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
@@ -70,6 +71,18 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"search": "Search paths..."
|
"search": "Search paths..."
|
||||||
},
|
},
|
||||||
|
"analyze": {
|
||||||
|
"title": "PATH Analysis",
|
||||||
|
"conflicts": "Conflicts",
|
||||||
|
"tools": "Tools",
|
||||||
|
"scanning": "Scanning...",
|
||||||
|
"noConflicts": "No executable conflicts found",
|
||||||
|
"noTools": "No matching executables found",
|
||||||
|
"priority": "Prioritized",
|
||||||
|
"shadowed": "Shadowed",
|
||||||
|
"searchPlaceholder": "Search executable name...",
|
||||||
|
"conflictCount": "{{count}} file conflict(s) found"
|
||||||
|
},
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"save": "确定",
|
"save": "确定",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
|
"analyze": "分析",
|
||||||
"undo": "撤销",
|
"undo": "撤销",
|
||||||
"redo": "重做",
|
"redo": "重做",
|
||||||
"darkMode": "深色模式",
|
"darkMode": "深色模式",
|
||||||
@@ -70,6 +71,18 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"search": "搜索路径..."
|
"search": "搜索路径..."
|
||||||
},
|
},
|
||||||
|
"analyze": {
|
||||||
|
"title": "PATH 分析",
|
||||||
|
"conflicts": "冲突检测",
|
||||||
|
"tools": "工具清单",
|
||||||
|
"scanning": "正在扫描...",
|
||||||
|
"noConflicts": "未发现可执行文件冲突",
|
||||||
|
"noTools": "未找到匹配的可执行文件",
|
||||||
|
"priority": "优先执行",
|
||||||
|
"shadowed": "被遮蔽",
|
||||||
|
"searchPlaceholder": "搜索可执行文件名...",
|
||||||
|
"conflictCount": "发现 {{count}} 个文件冲突"
|
||||||
|
},
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user