From 3795487ec3b9344e3566573f0971bec75833b191 Mon Sep 17 00:00:00 2001 From: ElmGates <396832647@qq.com> Date: Tue, 23 Dec 2025 16:18:16 +0800 Subject: [PATCH] update1.1.5 --- config.json | 2 +- main.py | 380 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 342 insertions(+), 40 deletions(-) diff --git a/config.json b/config.json index f04d50e..589ce4e 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,7 @@ { "app": { "name": "CardCopyer-拷贝乐", - "version": "1.1.3", + "version": "1.1.5", "author": "SuperJia", "description": "现代化的DIT拷卡软件" }, diff --git a/main.py b/main.py index bb22106..7b240fe 100644 --- a/main.py +++ b/main.py @@ -205,6 +205,7 @@ class CopyManager: def __init__(self): self.copying = False self.verifying = False + self.backup_copying = False self.total_files = 0 self.copied_files = 0 self.verified_files = 0 @@ -240,6 +241,16 @@ class CopyManager: # 性能优化:缓存文件大小计算结果 self._size_cache = {} self._size_cache_timeout = 5 # 缓存超时时间(秒) + + # 备用拷贝进度相关 + self.backup_total_files = 0 + self.backup_copied_files = 0 + self.backup_total_size = 0 + self.backup_copied_size = 0 + self.total_backup_destinations = 0 + self.current_backup_index = 0 + self.backup_start_time = 0 + self.backup_results = [] def get_folder_size(self, folder_path: str) -> int: """计算文件夹总大小(字节)- 使用缓存优化""" @@ -470,6 +481,9 @@ class DITCopyTool: size=(1400, 900), resizable=(True, True) ) + self.original_theme = "darkly" + self.christmas_mode = False + self.christmas_bg_canvas = None # 立即隐藏窗口,防止显示默认图标 self.window.withdraw() @@ -656,17 +670,17 @@ class DITCopyTool: def setup_ui(self): """设置UI界面""" # 主框架 - main_frame = tb.Frame(self.window, padding=20) - main_frame.pack(fill="both", expand=True) + self.main_frame = tb.Frame(self.window, padding=20) + self.main_frame.pack(fill="both", expand=True) + main_frame = self.main_frame - # 标题 - title_label = tb.Label( - main_frame, - text="CardCopyer-拷贝乐", - font=("Arial", 24, "bold"), - bootstyle="primary" - ) - title_label.pack(pady=(0, 20)) + header_frame = tb.Frame(main_frame) + header_frame.pack(fill="x") + title_label = tb.Label(header_frame, text="CardCopyer-拷贝乐", font=("Arial", 24, "bold"), bootstyle="primary") + title_label.pack(side="left", pady=(0, 20)) + self.christmas_btn = tb.Button(header_frame, text="🎄", bootstyle="success", width=3, command=self.toggle_christmas_theme) + if self.is_christmas_period(): + self.christmas_btn.pack(side="right", pady=(0, 20)) # 主要内容区域 content_frame = tb.Frame(main_frame) @@ -864,6 +878,59 @@ class DITCopyTool: # 绑定项目名称变化事件,实时更新预览 self.project_name_var.trace_add("write", lambda *args: self.update_folder_preview()) self.update_folder_preview() + + self.backup_dest_paths = [] + self.multi_dest_frame = ttk.LabelFrame( + dest_frame, + text="备用目的地", + bootstyle="secondary", + padding=10 + ) + + backup_list_row = tb.Frame(self.multi_dest_frame) + backup_list_row.pack(fill="x") + + self.backup_dest_listbox = tk.Listbox( + backup_list_row, + height=5, + font=("Arial", 10), + bg="#1e1e1e", + fg="white", + selectbackground="#005a9e" + ) + self.backup_dest_listbox.pack(side="left", fill="both", expand=True) + + backup_scroll = tb.Scrollbar(backup_list_row) + backup_scroll.pack(side="right", fill="y") + self.backup_dest_listbox.config(yscrollcommand=backup_scroll.set) + backup_scroll.config(command=self.backup_dest_listbox.yview) + + backup_btn_row = tb.Frame(self.multi_dest_frame) + backup_btn_row.pack(fill="x", pady=(10, 0)) + + add_backup_btn = tb.Button( + backup_btn_row, + text="添加备用目的地", + bootstyle="info", + command=self.add_backup_destination + ) + add_backup_btn.pack(side="left") + + remove_backup_btn = tb.Button( + backup_btn_row, + text="移除选中", + bootstyle="danger-outline", + command=self.remove_selected_backup_destination + ) + remove_backup_btn.pack(side="left", padx=(10, 0)) + + set_primary_btn = tb.Button( + backup_btn_row, + text="设为主目的地", + bootstyle="warning-outline", + command=self.set_primary_destination + ) + set_primary_btn.pack(side="right") def setup_settings_frame(self, parent): settings_frame = ttk.LabelFrame( @@ -909,14 +976,45 @@ class DITCopyTool: ) self.edit_media_btn.pack(side="left") self.on_only_media_toggle() + + self.multi_dest_var = tk.BooleanVar(value=False) + multi_dest_cb = tb.Checkbutton( + settings_frame, + text="多目的地备份", + variable=self.multi_dest_var, + bootstyle="info-round-toggle", + command=self.on_multi_dest_toggle + ) + multi_dest_cb.pack(pady=(10, 0), anchor="w") def on_only_media_toggle(self): if self.only_media_var.get(): self.edit_media_btn.config(state="normal") - messagebox.showinfo("危险!!", "此操作很危险!!开启仅拷贝媒体文件后,非媒体文件将被忽略。请核对文件类型是否正确,如果空间足够不建议打开。") + messagebox.showinfo("提示", "已开启仅拷贝媒体文件,请核对文件类型是否正确。") else: self.edit_media_btn.config(state="disabled") + def on_multi_dest_toggle(self): + if hasattr(self, "multi_dest_frame"): + if self.multi_dest_var.get(): + self.multi_dest_frame.pack(fill="x", pady=(10, 0)) + if hasattr(self, "backup_progress_title"): + self.backup_progress_title.pack(anchor="w", pady=(20, 5)) + if hasattr(self, "backup_progress"): + self.backup_progress.pack(fill="x", pady=(5, 15)) + if hasattr(self, "backup_status_label"): + self.backup_status_label.pack(anchor="w") + else: + self.multi_dest_frame.forget() + if hasattr(self, "backup_progress_title"): + self.backup_progress_title.pack_forget() + if hasattr(self, "backup_progress"): + self.backup_progress.pack_forget() + self.backup_progress.config(value=0) + if hasattr(self, "backup_status_label"): + self.backup_status_label.pack_forget() + self.backup_status_label.config(text="等待备用拷贝...") + def open_media_types_editor(self): editor = tk.Toplevel(self.window) editor.title("编辑媒体文件类型") @@ -1013,6 +1111,23 @@ class DITCopyTool: ) self.verify_speed_label.pack(anchor="w", pady=(2, 0)) + self.backup_progress_title = tb.Label(progress_frame, text="备用拷贝进度:", font=("Arial", 12, "bold")) + + self.backup_progress = tb.Progressbar( + progress_frame, + bootstyle="info-striped", + length=300, + mode='determinate' + ) + + + self.backup_status_label = tb.Label( + progress_frame, + text="等待备用拷贝...", + font=("Arial", 10) + ) + + # 统计信息 stats_frame = tb.Frame(progress_frame) stats_frame.pack(fill="x", pady=(20, 0)) @@ -1095,7 +1210,7 @@ class DITCopyTool: # 版权信息标签(可点击) copyright_label = tb.Label( bottom_frame, - text="Copyright ©️ 2025-Now SuperJia 保留所有权利,CardCopyer-拷贝乐 v1.1.3(beta) 点击前往官网", + text="Copyright ©️ 2025-Now SuperJia 保留所有权利,CardCopyer-拷贝乐 v1.1.5(beta) 点击前往官网", font=("Arial", 9), bootstyle="secondary", cursor="hand2" # 鼠标悬停时显示手型光标 @@ -1115,6 +1230,30 @@ class DITCopyTool: ) exit_btn.pack(side="right") + def is_christmas_period(self): + from datetime import datetime + now = datetime.now() + return now.month == 12 and 20 <= now.day <= 30 + + def toggle_christmas_theme(self): + if not self.is_christmas_period(): + return + if not self.christmas_mode: + try: + self.window.style.theme_use("minty") + except Exception: + try: + self.window.style.theme_use("flatly") + except Exception: + self.window.style.theme_use(self.original_theme) + self.christmas_mode = True + else: + try: + self.window.style.theme_use(self.original_theme) + except Exception: + pass + self.christmas_mode = False + def format_time(self, seconds): """格式化时间显示""" if seconds <= 0: @@ -1311,6 +1450,7 @@ class DITCopyTool: if folder: self.destination_path = folder self.dest_path_label.config(text=folder) + self.update_folder_preview() # 更新目的地信息 try: @@ -1322,8 +1462,53 @@ class DITCopyTool: self.log_message(f"选择目的地: {folder}") - # 更新文件夹预览 - self.update_folder_preview() + # 同步UI + if self.multi_dest_var.get(): + pass + + def add_backup_destination(self): + if len(self.backup_dest_paths) >= 10: + messagebox.showwarning("提示", "最多只能添加10个备用目的地") + return + folder = filedialog.askdirectory(title="选择备用目的地文件夹") + if folder: + if folder == self.destination_path or folder in self.backup_dest_paths: + messagebox.showinfo("提示", "该目的地已存在或与主目的地重复") + return + self.backup_dest_paths.append(folder) + self.backup_dest_listbox.insert(tk.END, folder) + self.log_message(f"添加备用目的地: {folder}") + + def remove_selected_backup_destination(self): + selection = self.backup_dest_listbox.curselection() + if not selection: + messagebox.showinfo("提示", "请先选择要移除的备用目的地") + return + for idx in reversed(selection): + removed = self.backup_dest_paths.pop(idx) + self.backup_dest_listbox.delete(idx) + self.log_message(f"移除备用目的地: {removed}") + + def set_primary_destination(self): + selection = self.backup_dest_listbox.curselection() + if not selection: + messagebox.showinfo("提示", "请从备用目的地列表中选择一个作为主目的地") + return + if len(selection) > 1: + messagebox.showinfo("提示", "一次只能设置一个主目的地") + return + idx = selection[0] + new_primary = self.backup_dest_paths.pop(idx) + old_primary = self.destination_path + self.destination_path = new_primary + self.dest_path_label.config(text=new_primary) + self.update_folder_preview() + if old_primary: + self.backup_dest_paths.append(old_primary) + self.backup_dest_listbox.insert(tk.END, old_primary) + self.backup_dest_listbox.delete(idx) + self.log_message(f"设置主目的地为: {new_primary}") + def update_folder_preview(self): """更新文件夹名称预览""" @@ -1483,6 +1668,39 @@ class DITCopyTool: # 开始验证 if self.copy_manager.copying and self.copy_manager.copied_files > 0: self.verify_files() + + if self.copy_manager.copying and self.multi_dest_var.get() and self.backup_dest_paths: + self.log_message("开始拷贝到备用目的地") + self.copy_manager.backup_copying = True + if self.auto_folder_var.get(): + backup_date_folder = self.copy_manager.date_folder + self.copy_manager.total_backup_destinations = len(self.backup_dest_paths) + for backup_dest in self.backup_dest_paths: + if not self.copy_manager.copying: + break + self.copy_manager.current_backup_index += 1 + self.copy_manager.backup_start_time = time.time() + self.copy_manager.backup_total_files = self.copy_manager.total_files + self.copy_manager.backup_total_size = self.copy_manager.total_size + self.copy_manager.backup_copied_files = 0 + self.copy_manager.backup_copied_size = 0 + self.backup_progress.config(value=0) + if self.auto_folder_var.get(): + backup_final_dest = os.path.join(backup_dest, backup_date_folder) + else: + backup_final_dest = backup_dest + os.makedirs(backup_final_dest, exist_ok=True) + self.log_message(f"创建备用目标文件夹: {backup_final_dest}") + self.backup_status_label.config(text=f"正在拷贝到备用目的地 {self.copy_manager.current_backup_index}/{self.copy_manager.total_backup_destinations}: {backup_dest}") + for source_item in self.source_items: + if not self.copy_manager.copying: + break + folder_name = source_item.get('custom_name', source_item['name']) + self.copy_folder(source_item['path'], backup_final_dest, folder_name) + self.backup_progress.config(value=100) + self.copy_manager.backup_copying = False + self.log_message("备用目的地拷贝完成") + self.verify_backup_destinations() # 完成 if self.copy_manager.copying: @@ -1549,19 +1767,17 @@ class DITCopyTool: last_progress_log = current_time # 批量更新进度(减少UI更新频率) - if self.copy_manager.total_size > 0: + if not self.copy_manager.backup_copying and self.copy_manager.total_size > 0: file_progress_ratio = copied_size / file_size if file_size > 0 else 0 temp_copied_size = self.copy_manager.copied_size + (file_size * file_progress_ratio) temp_copied_size = min(temp_copied_size, self.copy_manager.total_size) - - # 只在有显著变化时更新UI - if abs(temp_copied_size - self.copy_manager.copied_size) > (self.copy_manager.total_size * 0.01): # 变化超过1% + if abs(temp_copied_size - self.copy_manager.copied_size) > (self.copy_manager.total_size * 0.01): original_copied_size = self.copy_manager.copied_size self.copy_manager.copied_size = temp_copied_size self.update_progress() self.copy_manager.copied_size = original_copied_size - self.window.update() # 刷新界面 + self.window.update() last_update_time = current_time except Exception as e: @@ -1657,17 +1873,17 @@ class DITCopyTool: # 文件拷贝完成,更新进度 copy_time = time.time() - copy_start - self.copy_manager.copied_files += 1 - - # 确保总大小正确(在文件拷贝完成后更新总大小) - self.copy_manager.copied_size += file_size - - # 计算速度 - if copy_time > 0: - file_speed = file_size / copy_time # 字节/秒 - self.copy_manager.copy_speed = file_speed - - self.update_progress() + if self.copy_manager.backup_copying: + self.copy_manager.backup_copied_files += 1 + self.copy_manager.backup_copied_size += file_size + self.update_backup_progress() + else: + self.copy_manager.copied_files += 1 + self.copy_manager.copied_size += file_size + if copy_time > 0: + file_speed = file_size / copy_time + self.copy_manager.copy_speed = file_speed + self.update_progress() self.log_message(f"已拷贝: {file} ({self.copy_manager.format_size(file_size)})") except Exception as e: self.log_message(f"拷贝失败 {file}: {str(e)}") @@ -1706,7 +1922,7 @@ class DITCopyTool: for source_item in self.source_items: if not self.copy_manager.copying: break - + source_path = source_item['path'] # 使用与拷贝时相同的路径构建逻辑,包括自定义名称 folder_name = source_item.get('custom_name', source_item['name']) @@ -1720,7 +1936,7 @@ class DITCopyTool: self.log_message(f" 验证目标路径: {dest_path}") if self.auto_folder_var.get() and self.copy_manager.date_folder: self.log_message(f" 使用日期文件夹: {self.copy_manager.date_folder}") - + self.verify_folder(source_path, dest_path) # 验证完成,显示总结 @@ -1735,11 +1951,51 @@ class DITCopyTool: self.log_message(f" 总文件数: {self.copy_manager.total_md5_files}") self.log_message(f" 验证成功: {self.copy_manager.verified_files}") self.log_message(f" 验证失败: {self.copy_manager.total_md5_files - self.copy_manager.verified_files}") - self.log_message(f" 总数据量: {self.copy_manager.format_size(self.copy_manager.md5_calc_size)}") - self.log_message(f" 用时: {self.copy_manager.format_time(int(elapsed_time))}") - self.log_message(f" 平均速度: {self.copy_manager.format_size(int(self.copy_manager.md5_calc_speed))}/s") - self.log_message("="*60) - + + def verify_backup_destinations(self): + if not (self.multi_dest_var.get() and self.backup_dest_paths): + return + results = [] + if self.auto_folder_var.get(): + backup_date_folder = self.copy_manager.date_folder + for backup_dest in self.backup_dest_paths: + if not self.copy_manager.copying: + break + if self.auto_folder_var.get(): + backup_final_dest = os.path.join(backup_dest, backup_date_folder) + else: + backup_final_dest = backup_dest + total_files = 0 + verified_files = 0 + for source_item in self.source_items: + source_path = source_item['path'] + folder_name = source_item.get('custom_name', source_item['name']) + base_dest = os.path.join(backup_final_dest, folder_name) + for root, dirs, files in os.walk(source_path): + rel_path = os.path.relpath(root, source_path) + if rel_path == '.': + current_dest = base_dest + else: + current_dest = os.path.join(base_dest, rel_path) + for file in files: + source_file = os.path.join(root, file) + if self.only_media_var.get() and not self.is_media_file(source_file): + continue + total_files += 1 + dest_file = os.path.join(current_dest, file) + if os.path.exists(dest_file): + try: + if os.path.getsize(source_file) == os.path.getsize(dest_file): + verified_files += 1 + except: + verified_files += 1 + results.append({ + "destination": backup_dest, + "total": total_files, + "verified": verified_files + }) + self.copy_manager.backup_results = results + self.log_message("备用目的地验证完成") self.copy_manager.verifying = False def verify_folder(self, source_path, dest_path): @@ -1935,6 +2191,28 @@ class DITCopyTool: ) self.update_stats() + + def update_backup_progress(self): + import time + if self.copy_manager.backup_total_files > 0: + file_progress = (self.copy_manager.backup_copied_files / self.copy_manager.backup_total_files) * 100 + self.backup_progress.config(value=file_progress) + elapsed_time = 0 + if self.copy_manager.backup_start_time > 0: + elapsed_time = time.time() - self.copy_manager.backup_start_time + if elapsed_time < 0: + elapsed_time = 0 + speed_files_s = 0 + if elapsed_time > 0: + speed_files_s = self.copy_manager.backup_copied_files / elapsed_time + remaining_files = max(0, self.copy_manager.backup_total_files - self.copy_manager.backup_copied_files) + eta_seconds = 0 + if speed_files_s > 0: + eta_seconds = remaining_files / speed_files_s + elapsed_str = self.format_time(elapsed_time) + eta_str = self.format_time(eta_seconds) + self.backup_status_label.config(text=f"备用拷贝 {self.copy_manager.current_backup_index}/{self.copy_manager.total_backup_destinations} | 速度: {speed_files_s:.1f} 文件/秒 | 已用: {elapsed_str} | 剩余: {eta_str}") + self.window.update() def update_stats(self): """更新统计信息""" @@ -1949,6 +2227,9 @@ class DITCopyTool: self.verify_progress.config(value=100) self.copy_status_label.config(text="拷贝完成!") self.verify_status_label.config(text="验证完成!") + if self.multi_dest_var.get() and self.backup_dest_paths: + self.backup_progress.config(value=100) + self.backup_status_label.config(text="备用拷贝完成!") # 仅媒体拷贝提示 try: @@ -1983,7 +2264,8 @@ class DITCopyTool: total_size=self.copy_manager.copied_size, avg_speed=avg_speed, total_time=total_time, - verify_status=verify_status + verify_status=verify_status, + backup_results=self.copy_manager.backup_results if (self.multi_dest_var.get() and self.backup_dest_paths) else [] ) def copy_stopped(self): @@ -2037,7 +2319,7 @@ class DITCopyTool: except Exception as e: messagebox.showerror("错误", f"无法打开官网: {str(e)}") - def celebrate_completion_with_stats(self, total_files, total_size, avg_speed, total_time, verify_status): + def celebrate_completion_with_stats(self, total_files, total_size, avg_speed, total_time, verify_status, backup_results=None): """增强版庆祝完成动画,包含统计信息""" # 创建庆祝窗口 celebrate_window = tk.Toplevel(self.window) @@ -2118,6 +2400,26 @@ class DITCopyTool: ) verify_label.pack(anchor="w", pady=5) + if backup_results: + br_title = ttk.Label( + stats_frame, + text="📦 备用目的地结果:", + font=("Arial", 12, "bold"), + bootstyle="info" + ) + br_title.pack(anchor="w", pady=(15, 5)) + for item in backup_results: + dest = item.get("destination", "") + total = item.get("total", 0) + verified = item.get("verified", 0) + line = ttk.Label( + stats_frame, + text=f"• {dest}:拷贝 {total},验证 {verified}/{total}", + font=("Arial", 11), + bootstyle="secondary" + ) + line.pack(anchor="w", pady=2) + # 按钮框架 button_frame = ttk.Frame(main_frame) button_frame.pack(fill="x", pady=(20, 0))