From 8231566051b1e515178d960356472da66b59ab7a Mon Sep 17 00:00:00 2001 From: taiyi Date: Sat, 11 Oct 2025 23:35:09 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=9C=AC=E5=9C=B0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudSync/cloudsync/ui/sync_tab.py | 448 ++++++++++++++++++++++------- 1 file changed, 351 insertions(+), 97 deletions(-) diff --git a/CloudSync/cloudsync/ui/sync_tab.py b/CloudSync/cloudsync/ui/sync_tab.py index 1eb9251..0e0a4fd 100644 --- a/CloudSync/cloudsync/ui/sync_tab.py +++ b/CloudSync/cloudsync/ui/sync_tab.py @@ -9,6 +9,7 @@ from typing import List from ..core.base_cloud import BaseCloud from ..core.sync_orchestrator import SyncOrchestrator from ..utils.logger import get_logger +from ..utils.folder_manager import get_folder_manager from .quick_auth import create_quick_auth_panel @@ -18,85 +19,173 @@ class SyncTab: self.parent = parent self.clouds = clouds self.logger = get_logger(__name__) + self.folder_manager = get_folder_manager() self.orchestrator = None self.is_syncing = False self.log_queue = queue.Queue() - + self.frame = ttk.Frame(parent) self._setup_ui() self._start_log_monitor() + + # 加载历史文件夹 + self._load_folder_history() def _setup_ui(self): main_frame = ttk.Frame(self.frame, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) - + self.frame.columnconfigure(0, weight=1) self.frame.rowconfigure(0, weight=1) - main_frame.columnconfigure(1, weight=1) + main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(2, weight=1) - - ttk.Label(main_frame, text="本地目录:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5)) - - dir_frame = ttk.Frame(main_frame) - dir_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=(0, 5)) - dir_frame.columnconfigure(0, weight=1) - - self.local_dir_var = tk.StringVar() - self.local_dir_entry = ttk.Entry(dir_frame, textvariable=self.local_dir_var) - self.local_dir_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5)) - + + # 文件夹管理区域 + folder_mgmt_frame = ttk.LabelFrame(main_frame, text="📁 本地文件夹管理", padding="10") + folder_mgmt_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) + folder_mgmt_frame.columnconfigure(0, weight=1) + + # 当前选中的文件夹显示 + current_folder_frame = ttk.Frame(folder_mgmt_frame) + current_folder_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) + current_folder_frame.columnconfigure(1, weight=1) + + ttk.Label(current_folder_frame, text="当前文件夹:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5)) + + self.current_folder_var = tk.StringVar() + current_folder_entry = ttk.Entry(current_folder_frame, textvariable=self.current_folder_var, state='readonly') + current_folder_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5)) + ttk.Button( - dir_frame, - text="浏览", - command=self._browse_directory - ).grid(row=0, column=1, sticky=tk.E) - - ttk.Label(main_frame, text="远程目录:").grid(row=1, column=0, sticky=tk.W, pady=(0, 5)) - + current_folder_frame, + text="浏览", + command=self._browse_and_add_folder + ).grid(row=0, column=2) + + # 远程目录设置 + remote_dir_frame = ttk.Frame(folder_mgmt_frame) + remote_dir_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) + remote_dir_frame.columnconfigure(1, weight=1) + + ttk.Label(remote_dir_frame, text="远程目录:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5)) + self.remote_dir_var = tk.StringVar(value="/CloudSync") - self.remote_dir_entry = ttk.Entry(main_frame, textvariable=self.remote_dir_var) - self.remote_dir_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=(0, 5)) - - paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL) - paned_window.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) - - local_frame = ttk.LabelFrame(paned_window, text="本地文件", padding="5") - paned_window.add(local_frame, weight=1) - + ttk.Entry(remote_dir_frame, textvariable=self.remote_dir_var).grid(row=0, column=1, sticky=(tk.W, tk.E)) + + # 文件夹列表 + list_frame = ttk.Frame(folder_mgmt_frame) + list_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) + list_frame.columnconfigure(0, weight=1) + list_frame.rowconfigure(0, weight=1) + + # 创建Treeview显示文件夹列表 + columns = ('path', 'remote', 'enabled', 'last_accessed') + self.folder_tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=6) + + # 设置列标题 + self.folder_tree.heading('path', text='本地路径') + self.folder_tree.heading('remote', text='远程路径') + self.folder_tree.heading('enabled', text='状态') + self.folder_tree.heading('last_accessed', text='最后访问') + + # 设置列宽 + self.folder_tree.column('path', width=300) + self.folder_tree.column('remote', width=150) + self.folder_tree.column('enabled', width=60) + self.folder_tree.column('last_accessed', width=150) + + # 滚动条 + folder_scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.folder_tree.yview) + self.folder_tree.configure(yscrollcommand=folder_scrollbar.set) + + self.folder_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + folder_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + # 绑定选择事件 + self.folder_tree.bind('<>', self._on_folder_select) + + # 文件夹操作按钮 + folder_btn_frame = ttk.Frame(folder_mgmt_frame) + folder_btn_frame.grid(row=3, column=0, sticky=tk.W, pady=(0, 5)) + + ttk.Button( + folder_btn_frame, + text="➕ 添加文件夹", + command=self._browse_and_add_folder + ).pack(side=tk.LEFT, padx=(0, 5)) + + ttk.Button( + folder_btn_frame, + text="➖ 移除选中", + command=self._remove_selected_folder + ).pack(side=tk.LEFT, padx=(0, 5)) + + ttk.Button( + folder_btn_frame, + text="🔄 刷新列表", + command=self._refresh_folder_list + ).pack(side=tk.LEFT, padx=(0, 5)) + + ttk.Button( + folder_btn_frame, + text="🧹 清理无效", + command=self._clean_invalid_folders + ).pack(side=tk.LEFT, padx=(0, 5)) + + ttk.Button( + folder_btn_frame, + text="📤 导出配置", + command=self._export_folder_config + ).pack(side=tk.LEFT, padx=(0, 5)) + + ttk.Button( + folder_btn_frame, + text="📥 导入配置", + command=self._import_folder_config + ).pack(side=tk.LEFT) + + # 文件预览区域 + preview_paned = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL) + preview_paned.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) + + local_frame = ttk.LabelFrame(preview_paned, text="本地文件", padding="5") + preview_paned.add(local_frame, weight=1) + self.local_tree = ttk.Treeview(local_frame, show="tree") local_scrollbar = ttk.Scrollbar(local_frame, orient=tk.VERTICAL, command=self.local_tree.yview) self.local_tree.configure(yscrollcommand=local_scrollbar.set) - + self.local_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) local_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) - + local_frame.columnconfigure(0, weight=1) local_frame.rowconfigure(0, weight=1) - - remote_frame = ttk.LabelFrame(paned_window, text="远程文件", padding="5") - paned_window.add(remote_frame, weight=1) - + + remote_frame = ttk.LabelFrame(preview_paned, text="远程文件", padding="5") + preview_paned.add(remote_frame, weight=1) + self.remote_tree = ttk.Treeview(remote_frame, show="tree") remote_scrollbar = ttk.Scrollbar(remote_frame, orient=tk.VERTICAL, command=self.remote_tree.yview) self.remote_tree.configure(yscrollcommand=remote_scrollbar.set) - + self.remote_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) remote_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) - + remote_frame.columnconfigure(0, weight=1) remote_frame.rowconfigure(0, weight=1) - + + # 控制区域 control_frame = ttk.Frame(main_frame) - control_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0)) - control_frame.columnconfigure(1, weight=1) - + control_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(10, 0)) + control_frame.columnconfigure(2, weight=1) + self.sync_button = ttk.Button( - control_frame, - text="开始同步", + control_frame, + text="开始同步", command=self._start_sync ) self.sync_button.grid(row=0, column=0, sticky=tk.W) - + self.watch_var = tk.BooleanVar() self.watch_checkbox = ttk.Checkbutton( control_frame, @@ -105,50 +194,200 @@ class SyncTab: command=self._toggle_watch ) self.watch_checkbox.grid(row=0, column=1, sticky=tk.W, padx=(10, 0)) - + self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar( control_frame, variable=self.progress_var, - maximum=100 + maximum=100, + length=200 ) self.progress_bar.grid(row=0, column=2, sticky=tk.E, padx=(10, 0)) - + + # 日志区域 log_frame = ttk.LabelFrame(main_frame, text="同步日志", padding="5") - log_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) - + log_frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) + self.log_text = tk.Text(log_frame, height=8, state=tk.DISABLED) log_scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview) self.log_text.configure(yscrollcommand=log_scrollbar.set) - + self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) log_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) - + log_frame.columnconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1) main_frame.rowconfigure(4, weight=1) - + # 授权面板(仅在未授权时显示) self.auth_panel = None self._check_and_show_auth_panel() - def _browse_directory(self): - directory = filedialog.askdirectory() + def _load_folder_history(self): + """加载历史文件夹""" + self._refresh_folder_list() + + # 如果有文件夹,选中最近使用的 + recent_folders = self.folder_manager.get_recent_folders(1) + if recent_folders: + self.current_folder_var.set(recent_folders[0]['path']) + self.remote_dir_var.set(recent_folders[0]['remote_path']) + self._refresh_local_tree(recent_folders[0]['path']) + + def _refresh_folder_list(self): + """刷新文件夹列表""" + # 清空现有项目 + for item in self.folder_tree.get_children(): + self.folder_tree.delete(item) + + # 获取所有文件夹 + folders = self.folder_manager.get_all_folders() + + # 添加到树形视图 + for folder in folders: + enabled_text = "✅ 启用" if folder.get('enabled', True) else "❌ 禁用" + last_accessed = folder.get('last_accessed', 'N/A') + if last_accessed != 'N/A': + try: + from datetime import datetime + dt = datetime.fromisoformat(last_accessed) + last_accessed = dt.strftime("%Y-%m-%d %H:%M") + except: + pass + + self.folder_tree.insert('', 'end', values=( + folder['path'], + folder['remote_path'], + enabled_text, + last_accessed + )) + + def _on_folder_select(self, event): + """文件夹选择事件""" + selection = self.folder_tree.selection() + if selection: + item = self.folder_tree.item(selection[0]) + values = item['values'] + if values: + folder_path = values[0] + remote_path = values[1] + + # 更新当前选中的文件夹 + self.current_folder_var.set(folder_path) + self.remote_dir_var.set(remote_path) + + # 刷新文件预览 + self._refresh_local_tree(folder_path) + + # 更新访问记录 + self.folder_manager.update_folder_access(folder_path) + + def _browse_and_add_folder(self): + """浏览并添加文件夹""" + directory = filedialog.askdirectory(title="选择本地同步文件夹") if directory: - self.local_dir_var.set(directory) - self._refresh_local_tree() - - def _refresh_local_tree(self): - local_dir = self.local_dir_var.get() + # 添加到文件夹管理器 + remote_path = self.remote_dir_var.get() + if self.folder_manager.add_folder(directory, remote_path): + self._refresh_folder_list() + self.current_folder_var.set(directory) + self._refresh_local_tree(directory) + self.logger.info(f"Added folder: {directory}") + else: + messagebox.showerror("错误", f"添加文件夹失败: {directory}") + + def _remove_selected_folder(self): + """移除选中的文件夹""" + selection = self.folder_tree.selection() + if not selection: + messagebox.showwarning("提示", "请先选择要移除的文件夹") + return + + item = self.folder_tree.item(selection[0]) + folder_path = item['values'][0] + + # 确认删除 + result = messagebox.askyesno( + "确认移除", + f"确定要从同步列表中移除此文件夹吗?\n\n{folder_path}\n\n注意: 这不会删除本地文件" + ) + + if result: + if self.folder_manager.remove_folder(folder_path): + self._refresh_folder_list() + self.current_folder_var.set("") + self.local_tree.delete(*self.local_tree.get_children()) + self.logger.info(f"Removed folder: {folder_path}") + else: + messagebox.showerror("错误", f"移除文件夹失败: {folder_path}") + + def _clean_invalid_folders(self): + """清理无效的文件夹""" + removed_count = self.folder_manager.clean_invalid_folders() + + if removed_count > 0: + self._refresh_folder_list() + messagebox.showinfo("清理完成", f"已清理 {removed_count} 个无效文件夹") + else: + messagebox.showinfo("清理完成", "没有发现无效文件夹") + + def _export_folder_config(self): + """导出文件夹配置""" + export_path = filedialog.asksaveasfilename( + title="导出文件夹配置", + defaultextension=".json", + filetypes=[("JSON files", "*.json"), ("All files", "*.*")] + ) + + if export_path: + if self.folder_manager.export_config(export_path): + messagebox.showinfo("导出成功", f"配置已导出到:\n{export_path}") + else: + messagebox.showerror("导出失败", "导出配置时出错") + + def _import_folder_config(self): + """导入文件夹配置""" + import_path = filedialog.askopenfilename( + title="导入文件夹配置", + filetypes=[("JSON files", "*.json"), ("All files", "*.*")] + ) + + if import_path: + # 询问是合并还是替换 + result = messagebox.askyesnocancel( + "导入选项", + "是否合并到现有配置?\n\n" + + "点击 '是': 合并配置(保留现有文件夹)\n" + + "点击 '否': 替换配置(清空现有文件夹)\n" + + "点击 '取消': 取消导入" + ) + + if result is not None: + merge = result + if self.folder_manager.import_config(import_path, merge=merge): + self._refresh_folder_list() + messagebox.showinfo("导入成功", "配置已导入") + else: + messagebox.showerror("导入失败", "导入配置时出错") + + def _refresh_local_tree(self, local_dir=None): + """刷新本地文件树""" + if local_dir is None: + local_dir = self.current_folder_var.get() + if not local_dir or not os.path.exists(local_dir): return - + self.local_tree.delete(*self.local_tree.get_children()) - + try: self._populate_tree(self.local_tree, local_dir, "") except Exception as e: self.logger.error(f"Error refreshing local tree: {e}") + + def _browse_directory(self): + """向后兼容的方法""" + self._browse_and_add_folder() def _populate_tree(self, tree, path, parent): try: @@ -166,53 +405,68 @@ class SyncTab: def _start_sync(self): if self.is_syncing: return - - local_dir = self.local_dir_var.get() - remote_dir = self.remote_dir_var.get() - - if not local_dir: - messagebox.showerror("错误", "请选择本地目录") + + # 获取所有启用的文件夹 + enabled_folders = self.folder_manager.get_all_folders(enabled_only=True) + + if not enabled_folders: + messagebox.showerror("错误", "没有可同步的文件夹\n\n请先添加并启用至少一个文件夹") return - - if not os.path.exists(local_dir): - messagebox.showerror("错误", "本地目录不存在") - return - + self.is_syncing = True self.sync_button.config(text="同步中...", state="disabled") - + def sync_worker(): try: - self.orchestrator = SyncOrchestrator( - self.clouds, - local_dir, - remote_dir - ) - - def progress_callback(message, current, total): - progress = (current / total) * 100 if total > 0 else 0 - self.log_queue.put(f"进度: {message} ({current}/{total})") - self.progress_var.set(progress) - - self.log_queue.put("开始全量同步...") - sync_results = self.orchestrator.full_sync(progress_callback) - - self.log_queue.put("同步完成!") - for cloud_name, files in sync_results.items(): - self.log_queue.put(f"{cloud_name}: 同步 {len(files)} 个文件") - - if self.watch_var.get(): + total_folders = len(enabled_folders) + for idx, folder_info in enumerate(enabled_folders, 1): + local_dir = folder_info['path'] + remote_dir = folder_info['remote_path'] + + # 检查文件夹是否存在 + if not os.path.exists(local_dir): + self.log_queue.put(f"⚠️ 跳过不存在的文件夹: {local_dir}") + continue + + self.log_queue.put(f"📁 同步文件夹 ({idx}/{total_folders}): {local_dir}") + + self.orchestrator = SyncOrchestrator( + self.clouds, + local_dir, + remote_dir + ) + + def progress_callback(message, current, total): + folder_progress = ((idx - 1) / total_folders) * 100 + sync_progress = (current / total) * (100 / total_folders) if total > 0 else 0 + total_progress = folder_progress + sync_progress + + self.log_queue.put(f" {message} ({current}/{total})") + self.progress_var.set(total_progress) + + sync_results = self.orchestrator.full_sync(progress_callback) + + for cloud_name, files in sync_results.items(): + self.log_queue.put(f" ✅ {cloud_name}: 同步 {len(files)} 个文件") + + # 更新访问记录 + self.folder_manager.update_folder_access(local_dir) + + self.log_queue.put("✅ 所有文件夹同步完成!") + + # 如果启用了实时监控 + if self.watch_var.get() and self.orchestrator: self.orchestrator.start_watching() - self.log_queue.put("开始实时监控...") - + self.log_queue.put("👁️ 开始实时监控...") + except Exception as e: - self.log_queue.put(f"同步失败: {e}") + self.log_queue.put(f"❌ 同步失败: {e}") self.logger.error(f"Sync failed: {e}") finally: self.is_syncing = False self.sync_button.config(text="开始同步", state="normal") self.progress_var.set(0) - + threading.Thread(target=sync_worker, daemon=True).start() def _toggle_watch(self):