check page

This commit is contained in:
Julian Freeman
2025-12-07 20:27:00 -04:00
parent ffd6916313
commit f42ee4ec9e
2 changed files with 227 additions and 27 deletions

View File

@@ -250,6 +250,96 @@ fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Resul
Ok(format!("Successfully renamed {} files.", renamed_count))
}
// --- Check Logic ---
fn remove_empty_dirs_recursive(path: &Path, deleted_list: &mut Vec<String>) -> std::io::Result<()> {
if path.is_dir() {
let read_dir = fs::read_dir(path)?;
for entry in read_dir {
let entry = entry?;
let child_path = entry.path();
if child_path.is_dir() {
remove_empty_dirs_recursive(&child_path, deleted_list)?;
}
}
// Re-check if empty after processing children
// Use read_dir again to check if it's empty now
let mut is_empty = true;
let read_dir_check = fs::read_dir(path)?;
for _ in read_dir_check {
is_empty = false;
break;
}
if is_empty {
fs::remove_dir(path)?;
deleted_list.push(path.to_string_lossy().to_string());
}
}
Ok(())
}
#[tauri::command]
fn delete_empty_dirs(path: String) -> Result<Vec<String>, String> {
let root_path = Path::new(&path);
if !root_path.exists() || !root_path.is_dir() {
return Err("Path is not a valid directory".to_string());
}
let mut deleted = Vec::new();
// We cannot delete the root path itself even if empty, per logic usually expected in tools.
// So we iterate children and call recursive function.
let read_dir = fs::read_dir(root_path).map_err(|e| e.to_string())?;
for entry in read_dir {
let entry = entry.map_err(|e| e.to_string())?;
let child_path = entry.path();
if child_path.is_dir() {
remove_empty_dirs_recursive(&child_path, &mut deleted).map_err(|e| e.to_string())?;
}
}
Ok(deleted)
}
#[tauri::command]
fn check_file_naming(path: String, prefix: String) -> Result<Vec<String>, String> {
let root_path = Path::new(&path);
if !root_path.exists() || !root_path.is_dir() {
return Err("Path is not a valid directory".to_string());
}
let mut mismatches = Vec::new();
let mut stack = vec![root_path.to_path_buf()];
while let Some(current_dir) = stack.pop() {
let read_dir = fs::read_dir(&current_dir).map_err(|e| e.to_string())?;
for entry in read_dir {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
// Check against prefix
// NOTE: Should we check hidden files? Assuming ignoring hidden files for now if starting with dot?
// Or just check everything. Let's check everything visible.
if !name.starts_with(".") {
if !name.starts_with(&prefix) {
mismatches.push(path.to_string_lossy().to_string());
}
}
}
}
}
}
Ok(mismatches)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -265,8 +355,10 @@ pub fn run() {
save_history_item,
remove_history_item,
check_dir_exists,
rename_videos
rename_videos,
delete_empty_dirs,
check_file_naming
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
}

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { ref, watch, onMounted, computed, onUnmounted } from "vue";
import { ref, watch, onMounted, computed, onUnmounted, nextTick } from "vue";
import { open } from "@tauri-apps/plugin-dialog";
import { invoke } from "@tauri-apps/api/core";
import { ElMessage } from 'element-plus';
import { Document, VideoPlay, CircleCheck, Moon, Sunny, Delete, UploadFilled } from '@element-plus/icons-vue';
import { Document, VideoPlay, CircleCheck, Moon, Sunny, Delete, UploadFilled, Refresh } from '@element-plus/icons-vue';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
// Navigation
@@ -37,7 +37,11 @@ const importedFiles = ref<string[]>([]);
const videoNameInput = ref("");
const historyList = ref<string[]>([]);
const historyMap = ref<Record<string, string[]>>({});
const currentHistoryKey = ref(""); // Derived from dropped files relative to currentDir
const currentHistoryKey = ref("");
// Check Data
const checkLogs = ref<string[]>([]);
const logContainerRef = ref<HTMLElement | null>(null);
// Lifecycle
let unlistenDrop: UnlistenFn | null = null;
@@ -140,35 +144,21 @@ const updateHistoryList = () => {
}
const handleFileDrop = (paths: string[]) => {
// Filter for common video extensions if needed, or just accept all
// For now, accept all but assume they are videos
importedFiles.value = paths;
if (paths.length > 0 && currentDir.value) {
// Calculate Key: Path relative to Current Dir
// Logic: Take first file's parent dir. Remove currentDir prefix.
// Example: Current=C:\A, File=C:\A\B\vid.mp4. Parent=C:\A\B. Key=B.
// Normalize paths for comparison (remove trailing slashes, handle win/unix separators)
// Simple string manipulation for now.
const firstFile = paths[0];
// We need the parent directory of the file
// Since we don't have node 'path' module easily, we do simple string parsing
const separator = firstFile.includes('\\') ? '\\' : '/';
const lastSeparatorIndex = firstFile.lastIndexOf(separator);
const parentDir = firstFile.substring(0, lastSeparatorIndex);
if (parentDir.startsWith(currentDir.value)) {
let relative = parentDir.substring(currentDir.value.length);
// Remove leading separator if exists
if (relative.startsWith(separator)) {
relative = relative.substring(1);
}
currentHistoryKey.value = relative;
} else {
// File is outside current dir?
// Requirement says: "video file path minus current directory prefix"
// If outside, maybe use full path or just handle gracefully
currentHistoryKey.value = "";
}
updateHistoryList();
@@ -199,7 +189,6 @@ const handleRename = async () => {
return;
}
// Save to history
if (currentHistoryKey.value) {
try {
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
@@ -213,7 +202,6 @@ const handleRename = async () => {
}
}
// Rename Files
try {
const msg = await invoke<string>('rename_videos', {
files: importedFiles.value,
@@ -221,8 +209,6 @@ const handleRename = async () => {
baseName: videoNameInput.value
});
ElMessage.success(msg);
// Clear imported files after success? Or keep them?
// Usually clearing is better feedback that "job done".
importedFiles.value = [];
} catch (e) {
ElMessage.error("重命名失败: " + e);
@@ -233,6 +219,61 @@ const useHistoryItem = (val: string) => {
videoNameInput.value = val;
}
// Check Logic
const appendLog = (msg: string) => {
checkLogs.value.push(msg);
nextTick(() => {
if (logContainerRef.value) {
logContainerRef.value.scrollTop = logContainerRef.value.scrollHeight;
}
});
}
const handleDeleteEmptyDirs = async () => {
checkLogs.value = [];
appendLog("正在扫描并删除空目录...");
try {
const deleted = await invoke<string[]>('delete_empty_dirs', { path: currentDir.value });
if (deleted.length === 0) {
appendLog("未发现空目录。");
} else {
deleted.forEach(path => appendLog(`已删除: ${path}`));
appendLog(`删除完毕,共删除 ${deleted.length} 个空目录。`);
}
} catch (e) {
appendLog(`错误: ${e}`);
}
}
const handleCheckNaming = async () => {
checkLogs.value = [];
appendLog(`正在检查文件命名,前缀要求: ${videoNamePrefix.value} ...`);
try {
const mismatches = await invoke<string[]>('check_file_naming', {
path: currentDir.value,
prefix: videoNamePrefix.value
});
if (mismatches.length === 0) {
appendLog("检查完毕,所有文件均符合命名规范。");
} else {
appendLog(`发现 ${mismatches.length} 个文件不符合规范:`);
// Simple tree view simulation
// Sort to group by dir
mismatches.sort();
mismatches.forEach(path => {
// Determine relative path to show cleaner tree-like structure
let displayPath = path;
if (path.startsWith(currentDir.value)) {
displayPath = "." + path.substring(currentDir.value.length);
}
appendLog(displayPath);
});
}
} catch (e) {
appendLog(`错误: ${e}`);
}
}
// Computed
const isReviewDisabled = computed(() => {
@@ -306,7 +347,7 @@ const handleCopy = async () => {
// Reactivity for Current Directory Prefix
watch(currentDir, (newPath) => {
if (!newPath) return;
const dirName = newPath.split(/[\\/]/).pop();
const dirName = newPath.split(/[/\\]/).pop();
if (!dirName) return;
const regex = /^(\d{2})(\d{2})视频$/;
@@ -454,8 +495,6 @@ watch(currentDir, (newPath) => {
</div>
</div>
<!-- Removed el-divider here -->
<!-- Action Area -->
<div class="action-area">
<el-input v-model="videoNameInput" placeholder="输入视频名称 (例如: 文本)" class="name-input">
@@ -483,10 +522,39 @@ watch(currentDir, (newPath) => {
</div>
</div>
<!-- Check Page -->
<div v-else-if="activeMenu === 'check'">
<h2>检查</h2>
<el-empty description="功能待定" />
<el-alert
v-if="isReviewDisabled"
:title="reviewWarningMessage"
type="warning"
show-icon
:closable="false"
style="margin-bottom: 20px"
/>
<div :class="{ 'disabled-area': isReviewDisabled }">
<div class="check-actions">
<el-button type="danger" :icon="Delete" @click="handleDeleteEmptyDirs">删除空目录</el-button>
<el-button type="primary" :icon="Refresh" @click="handleCheckNaming">检查命名</el-button>
</div>
<div class="log-container">
<div class="log-header">日志输出</div>
<div class="log-content" ref="logContainerRef">
<div v-for="(log, index) in checkLogs" :key="index" class="log-item">
{{ log }}
</div>
<div v-if="checkLogs.length === 0" class="log-empty">
暂无日志
</div>
</div>
</div>
</div>
</div>
</el-main>
</el-container>
</template>
@@ -606,4 +674,44 @@ body {
text-align: center;
color: var(--el-text-color-secondary);
}
/* Check Page Styles */
.check-actions {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.log-container {
border: 1px solid var(--el-border-color);
border-radius: 4px;
background-color: var(--el-bg-color);
display: flex;
flex-direction: column;
height: 400px;
}
.log-header {
padding: 10px 15px;
background-color: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color);
font-weight: bold;
font-size: 14px;
}
.log-content {
flex: 1;
overflow-y: auto;
padding: 10px;
font-family: monospace;
font-size: 13px;
white-space: pre-wrap;
}
.log-item {
margin-bottom: 4px;
line-height: 1.4;
}
.log-empty {
color: var(--el-text-color-secondary);
font-style: italic;
text-align: center;
margin-top: 20px;
}
</style>