check page
This commit is contained in:
@@ -250,6 +250,96 @@ fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Resul
|
|||||||
Ok(format!("Successfully renamed {} files.", renamed_count))
|
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(¤t_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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -265,8 +355,10 @@ pub fn run() {
|
|||||||
save_history_item,
|
save_history_item,
|
||||||
remove_history_item,
|
remove_history_item,
|
||||||
check_dir_exists,
|
check_dir_exists,
|
||||||
rename_videos
|
rename_videos,
|
||||||
|
delete_empty_dirs,
|
||||||
|
check_file_naming
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
158
src/App.vue
158
src/App.vue
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<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 { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { ElMessage } from 'element-plus';
|
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';
|
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
@@ -37,7 +37,11 @@ const importedFiles = ref<string[]>([]);
|
|||||||
const videoNameInput = ref("");
|
const videoNameInput = ref("");
|
||||||
const historyList = ref<string[]>([]);
|
const historyList = ref<string[]>([]);
|
||||||
const historyMap = ref<Record<string, 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
|
// Lifecycle
|
||||||
let unlistenDrop: UnlistenFn | null = null;
|
let unlistenDrop: UnlistenFn | null = null;
|
||||||
@@ -140,35 +144,21 @@ const updateHistoryList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFileDrop = (paths: string[]) => {
|
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;
|
importedFiles.value = paths;
|
||||||
|
|
||||||
if (paths.length > 0 && currentDir.value) {
|
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];
|
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 separator = firstFile.includes('\\') ? '\\' : '/';
|
||||||
const lastSeparatorIndex = firstFile.lastIndexOf(separator);
|
const lastSeparatorIndex = firstFile.lastIndexOf(separator);
|
||||||
const parentDir = firstFile.substring(0, lastSeparatorIndex);
|
const parentDir = firstFile.substring(0, lastSeparatorIndex);
|
||||||
|
|
||||||
if (parentDir.startsWith(currentDir.value)) {
|
if (parentDir.startsWith(currentDir.value)) {
|
||||||
let relative = parentDir.substring(currentDir.value.length);
|
let relative = parentDir.substring(currentDir.value.length);
|
||||||
// Remove leading separator if exists
|
|
||||||
if (relative.startsWith(separator)) {
|
if (relative.startsWith(separator)) {
|
||||||
relative = relative.substring(1);
|
relative = relative.substring(1);
|
||||||
}
|
}
|
||||||
currentHistoryKey.value = relative;
|
currentHistoryKey.value = relative;
|
||||||
} else {
|
} 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 = "";
|
currentHistoryKey.value = "";
|
||||||
}
|
}
|
||||||
updateHistoryList();
|
updateHistoryList();
|
||||||
@@ -199,7 +189,6 @@ const handleRename = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to history
|
|
||||||
if (currentHistoryKey.value) {
|
if (currentHistoryKey.value) {
|
||||||
try {
|
try {
|
||||||
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
|
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
|
||||||
@@ -213,7 +202,6 @@ const handleRename = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename Files
|
|
||||||
try {
|
try {
|
||||||
const msg = await invoke<string>('rename_videos', {
|
const msg = await invoke<string>('rename_videos', {
|
||||||
files: importedFiles.value,
|
files: importedFiles.value,
|
||||||
@@ -221,8 +209,6 @@ const handleRename = async () => {
|
|||||||
baseName: videoNameInput.value
|
baseName: videoNameInput.value
|
||||||
});
|
});
|
||||||
ElMessage.success(msg);
|
ElMessage.success(msg);
|
||||||
// Clear imported files after success? Or keep them?
|
|
||||||
// Usually clearing is better feedback that "job done".
|
|
||||||
importedFiles.value = [];
|
importedFiles.value = [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error("重命名失败: " + e);
|
ElMessage.error("重命名失败: " + e);
|
||||||
@@ -233,6 +219,61 @@ const useHistoryItem = (val: string) => {
|
|||||||
videoNameInput.value = val;
|
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
|
// Computed
|
||||||
const isReviewDisabled = computed(() => {
|
const isReviewDisabled = computed(() => {
|
||||||
@@ -306,7 +347,7 @@ const handleCopy = async () => {
|
|||||||
// Reactivity for Current Directory Prefix
|
// Reactivity for Current Directory Prefix
|
||||||
watch(currentDir, (newPath) => {
|
watch(currentDir, (newPath) => {
|
||||||
if (!newPath) return;
|
if (!newPath) return;
|
||||||
const dirName = newPath.split(/[\\/]/).pop();
|
const dirName = newPath.split(/[/\\]/).pop();
|
||||||
if (!dirName) return;
|
if (!dirName) return;
|
||||||
|
|
||||||
const regex = /^(\d{2})(\d{2})视频$/;
|
const regex = /^(\d{2})(\d{2})视频$/;
|
||||||
@@ -454,8 +495,6 @@ watch(currentDir, (newPath) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Removed el-divider here -->
|
|
||||||
|
|
||||||
<!-- Action Area -->
|
<!-- Action Area -->
|
||||||
<div class="action-area">
|
<div class="action-area">
|
||||||
<el-input v-model="videoNameInput" placeholder="输入视频名称 (例如: 文本)" class="name-input">
|
<el-input v-model="videoNameInput" placeholder="输入视频名称 (例如: 文本)" class="name-input">
|
||||||
@@ -483,10 +522,39 @@ watch(currentDir, (newPath) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Check Page -->
|
||||||
<div v-else-if="activeMenu === 'check'">
|
<div v-else-if="activeMenu === 'check'">
|
||||||
<h2>检查</h2>
|
<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>
|
</div>
|
||||||
|
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
@@ -606,4 +674,44 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--el-text-color-secondary);
|
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>
|
</style>
|
||||||
Reference in New Issue
Block a user