Auto Copy Folder - Source Code

 import tkinter as tk

from tkinter import ttk, filedialog, messagebox, scrolledtext

import shutil

import threading

import time

import json

import os

from datetime import datetime, timedelta

import pystray

from PIL import Image, ImageDraw

import sys

import winshell

from win32com.client import Dispatch

import keyboard

import subprocess


class FolderCopierTool:

    def __init__(self, root):

        self.root = root

        self.root.title('Folder Copier Tool')

        self.root.geometry('1200x900')

        self.root.resizable(True, True)

        

        # Copy Configuration variables

        self.copy_pairs = []

        self.timer_running = False

        self.interval = 5

        self.show_notifications = False

        self.disable_logging = False

        self.start_as_icon = False

        self.auto_start = True

        self.overwrite_files = False

        self.copy_thread = None

        

        # Auto Saver variables

        self.auto_saver_pairs = []

        self.saver_hours = 0

        self.saver_minutes = 0

        self.saver_seconds = 30

        self.saver_running = False

        self.saver_thread = None

        self.last_save_time = None

        self.auto_start_saver = False

        self.saver_next_save_time = None

        self.countdown_update_job = None

        

        # Manual saver variables

        self.manual_saver_pairs = []  # List of (source, dest, hotkey)

        self.registered_hotkeys = {}  # Dict of {hotkey: index}

        

        # Common variables

        self.tray_icon = None

        self.is_minimized_to_tray = False

        self.config_file = 'copier_config.json'

        

        # Initialize variables before load_config

        self.disable_log_var = tk.BooleanVar()

        self.start_as_icon_var = tk.BooleanVar()

        self.auto_start_var = tk.BooleanVar()

        self.notify_var = tk.BooleanVar()

        self.overwrite_var = tk.BooleanVar(value=self.overwrite_files)

        self.auto_start_saver_var = tk.BooleanVar()

        

        self.load_config()

        self.create_widgets()

        

        if self.start_as_icon:

            self.root.after(100, self.minimize_to_tray)

        

        if self.auto_start and self.copy_pairs:

            self.root.after(500, self.start_timer)

        

        # Auto start saver if enabled

        if self.auto_start_saver and self.auto_saver_pairs:

            self.root.after(500, self.start_auto_saver)

        

        # Register all manual saver hotkeys

        self.register_all_manual_hotkeys()


    def create_widgets(self):

        notebook = ttk.Notebook(self.root)

        notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        

        # Copy Configuration Tab

        main_frame = ttk.Frame(notebook, padding='10')

        notebook.add(main_frame, text='Copy Configuration')

        main_frame.columnconfigure(1, weight=1)

        

        ttk.Label(main_frame, text='Source -> Destination Folder Pairs:', 

                  font=('Arial', 10, 'bold')).grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=(0, 5))

        

        columns = ('Source', 'Destination')

        self.pairs_tree = ttk.Treeview(main_frame, columns=columns, show='headings', height=8)

        self.pairs_tree.heading('Source', text='Source Folder')

        self.pairs_tree.heading('Destination', text='Destination Folder')

        self.pairs_tree.column('Source', width=400)

        self.pairs_tree.column('Destination', width=400)

        self.pairs_tree.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))

        

        # Right-click menu for copy pairs

        self.copy_context_menu = tk.Menu(self.root, tearoff=0)

        self.copy_context_menu.add_command(label="Open Source Folder", command=self.open_copy_source_folder)

        self.copy_context_menu.add_command(label="Open Destination Folder", command=self.open_copy_destination_folder)

        self.pairs_tree.bind("<Button-3>", self.show_copy_context_menu)

        

        tree_scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.pairs_tree.yview)

        tree_scrollbar.grid(row=1, column=3, sticky=(tk.N, tk.S), pady=(0, 10))

        self.pairs_tree.configure(yscrollcommand=tree_scrollbar.set)

        

        for source, dest in self.copy_pairs:

            self.pairs_tree.insert('', 'end', values=(source, dest))

        

        pairs_buttons = ttk.Frame(main_frame)

        pairs_buttons.grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=(0, 20))

        ttk.Button(pairs_buttons, text='Add Pair', command=self.add_copy_pair).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(pairs_buttons, text='Edit Pair', command=self.edit_copy_pair).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(pairs_buttons, text='Remove Pair', command=self.remove_copy_pair).pack(side=tk.LEFT)

        

        # Timer Settings

        timer_frame = ttk.LabelFrame(main_frame, text='Timer Settings', padding='10')

        timer_frame.grid(row=3, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(0, 10))

        timer_frame.columnconfigure(1, weight=1)

        

        ttk.Label(timer_frame, text='Copy Interval:').grid(row=0, column=0, sticky=tk.W, pady=(0, 5))

        self.interval_var = tk.StringVar(value=str(self.interval))

        intervals = [('5 seconds', '5'), ('15 seconds', '15'), ('30 seconds', '30'), 

                    ('1 minute', '60'), ('5 minutes', '300'), ('15 minutes', '900'), 

                    ('30 minutes', '1800'), ('1 hour', '3600'), ('2 hours', '7200'), ('3 hours', '10800')]

        

        interval_combo = ttk.Combobox(timer_frame, textvariable=self.interval_var, 

                                    values=[val[1] for val in intervals], state='readonly')

        interval_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=(0, 5), padx=(5, 0))

        

        self.interval_desc = tk.StringVar()

        for text, value in intervals:

            if value == self.interval_var.get():

                self.interval_desc.set(text)

                break

        

        ttk.Label(timer_frame, textvariable=self.interval_desc).grid(row=0, column=2, sticky=tk.W, padx=(5, 0))

        interval_combo.bind('<<ComboboxSelected>>', self.on_interval_change)

        

        self.notify_var.set(self.show_notifications)

        notify_check = ttk.Checkbutton(timer_frame, text='Show Copy Completion Notifications', 

                                      variable=self.notify_var, command=self.on_notify_change)

        notify_check.grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))

        

        self.start_as_icon_var.set(self.start_as_icon)

        start_icon_check = ttk.Checkbutton(timer_frame, text='Start as Icon (Launch in system tray)', 

                                          variable=self.start_as_icon_var, command=self.on_start_as_icon_change)

        start_icon_check.grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))

        

        self.auto_start_var.set(self.auto_start)

        auto_start_check = ttk.Checkbutton(timer_frame, text='Auto Start (Automatic copy on launch)', 

                                          variable=self.auto_start_var, command=self.on_auto_start_change)

        auto_start_check.grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))

        

        self.overwrite_var.set(self.overwrite_files)

        overwrite_check = ttk.Checkbutton(timer_frame, text='Overwrite Existing Files (NOT checked by default)', 

                                         variable=self.overwrite_var, command=self.on_overwrite_change)

        overwrite_check.grid(row=4, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))

        

        ttk.Button(timer_frame, text='Create Auto Launch at PC Startup', 

                  command=self.create_auto_launch_shortcut).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))

        

        # Control buttons

        control_frame = ttk.Frame(main_frame)

        control_frame.grid(row=4, column=0, columnspan=4, pady=20)

        self.start_button = ttk.Button(control_frame, text='Start Automatic Copy', command=self.start_timer)

        self.start_button.pack(side=tk.LEFT, padx=(0, 10))

        self.stop_button = ttk.Button(control_frame, text='Stop Automatic Copy', command=self.stop_timer, state=tk.DISABLED)

        self.stop_button.pack(side=tk.LEFT, padx=(0, 10))

        ttk.Button(control_frame, text='Copy Now', command=self.copy_now).pack(side=tk.LEFT, padx=(0, 10))

        ttk.Button(control_frame, text='Minimize to Tray', command=self.minimize_to_tray).pack(side=tk.LEFT)

        

        # Status

        status_frame = ttk.Frame(main_frame)

        status_frame.grid(row=5, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(10, 0))

        ttk.Label(status_frame, text='Status:').pack(side=tk.LEFT)

        self.status_var = tk.StringVar(value='Ready')

        ttk.Label(status_frame, textvariable=self.status_var, foreground='blue').pack(side=tk.LEFT, padx=(5, 0))

        

        # Saver Tab

        saver_frame = ttk.Frame(notebook, padding='10')

        notebook.add(saver_frame, text='Saver')

        

        # Create a canvas with scrollbar for saver tab

        saver_canvas = tk.Canvas(saver_frame)

        saver_scrollbar = ttk.Scrollbar(saver_frame, orient="vertical", command=saver_canvas.yview)

        saver_scrollable_frame = ttk.Frame(saver_canvas)

        

        saver_scrollable_frame.bind(

            "<Configure>",

            lambda e: saver_canvas.configure(scrollregion=saver_canvas.bbox("all"))

        )

        

        saver_canvas.create_window((0, 0), window=saver_scrollable_frame, anchor="nw")

        saver_canvas.configure(yscrollcommand=saver_scrollbar.set)

        

        saver_canvas.pack(side="left", fill="both", expand=True)

        saver_scrollbar.pack(side="right", fill="y")

        

        # Automatic Folder Saver Section

        auto_saver_section = ttk.LabelFrame(saver_scrollable_frame, text='Automatic Folder Saver', padding='10')

        auto_saver_section.pack(fill=tk.BOTH, expand=False, pady=(0, 15))

        

        # Auto Start Saver checkbox

        self.auto_start_saver_var.set(self.auto_start_saver)

        auto_start_saver_check = ttk.Checkbutton(auto_saver_section, 

                                                 text='Auto Start Saver when Open', 

                                                 variable=self.auto_start_saver_var, 

                                                 command=self.on_auto_start_saver_change)

        auto_start_saver_check.pack(anchor=tk.W, pady=(0, 10))

        

        # Auto Saver Pairs List

        ttk.Label(auto_saver_section, text='Source -> Destination Pairs:', 

                  font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))

        

        auto_columns = ('Source', 'Destination')

        self.auto_saver_tree = ttk.Treeview(auto_saver_section, columns=auto_columns, show='headings', height=6)

        self.auto_saver_tree.heading('Source', text='Source Folder')

        self.auto_saver_tree.heading('Destination', text='Destination Folder')

        self.auto_saver_tree.column('Source', width=400)

        self.auto_saver_tree.column('Destination', width=400)

        self.auto_saver_tree.pack(fill=tk.BOTH, expand=True, pady=(0, 10))

        

        # Right-click menu for auto saver pairs

        self.auto_saver_context_menu = tk.Menu(self.root, tearoff=0)

        self.auto_saver_context_menu.add_command(label="Open Source Folder", command=self.open_auto_saver_source_folder)

        self.auto_saver_context_menu.add_command(label="Open Destination Folder", command=self.open_auto_saver_destination_folder)

        self.auto_saver_tree.bind("<Button-3>", self.show_auto_saver_context_menu)

        

        for source, dest in self.auto_saver_pairs:

            self.auto_saver_tree.insert('', 'end', values=(source, dest))

        

        auto_pairs_buttons = ttk.Frame(auto_saver_section)

        auto_pairs_buttons.pack(anchor=tk.W, pady=(0, 10))

        ttk.Button(auto_pairs_buttons, text='Add Pair', command=self.add_auto_saver_pair).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(auto_pairs_buttons, text='Edit Pair', command=self.edit_auto_saver_pair).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(auto_pairs_buttons, text='Remove Pair', command=self.remove_auto_saver_pair).pack(side=tk.LEFT)

        

        # Time interval settings

        time_frame = ttk.LabelFrame(auto_saver_section, text='Save Interval', padding='10')

        time_frame.pack(fill=tk.X, pady=(0, 10))

        

        time_controls = ttk.Frame(time_frame)

        time_controls.pack()

        

        ttk.Label(time_controls, text='Hours:').pack(side=tk.LEFT, padx=(0, 5))

        self.saver_hours_var = tk.StringVar(value=str(self.saver_hours))

        hours_spin = ttk.Spinbox(time_controls, from_=0, to=23, textvariable=self.saver_hours_var, width=5)

        hours_spin.pack(side=tk.LEFT, padx=(0, 15))

        

        ttk.Label(time_controls, text='Minutes:').pack(side=tk.LEFT, padx=(0, 5))

        self.saver_minutes_var = tk.StringVar(value=str(self.saver_minutes))

        minutes_spin = ttk.Spinbox(time_controls, from_=0, to=59, textvariable=self.saver_minutes_var, width=5)

        minutes_spin.pack(side=tk.LEFT, padx=(0, 15))

        

        ttk.Label(time_controls, text='Seconds:').pack(side=tk.LEFT, padx=(0, 5))

        self.saver_seconds_var = tk.StringVar(value=str(self.saver_seconds))

        seconds_spin = ttk.Spinbox(time_controls, from_=0, to=59, textvariable=self.saver_seconds_var, width=5)

        seconds_spin.pack(side=tk.LEFT)

        

        # Control buttons for auto saver

        saver_control_frame = ttk.Frame(auto_saver_section)

        saver_control_frame.pack(pady=(10, 0))

        

        self.saver_start_button = ttk.Button(saver_control_frame, text='Start Auto Saver', 

                                            command=self.start_auto_saver)

        self.saver_start_button.pack(side=tk.LEFT, padx=(0, 10))

        

        self.saver_stop_button = ttk.Button(saver_control_frame, text='Stop Auto Saver', 

                                           command=self.stop_auto_saver, state=tk.DISABLED)

        self.saver_stop_button.pack(side=tk.LEFT)

        

        # Status for auto saver

        saver_status_frame = ttk.Frame(auto_saver_section)

        saver_status_frame.pack(pady=(10, 0))

        

        ttk.Label(saver_status_frame, text='Status:').pack(side=tk.LEFT)

        self.saver_status_var = tk.StringVar(value='Stopped')

        ttk.Label(saver_status_frame, textvariable=self.saver_status_var, foreground='blue').pack(side=tk.LEFT, padx=(5, 0))

        

        # Countdown timer

        countdown_frame = ttk.Frame(auto_saver_section)

        countdown_frame.pack(pady=(10, 0))

        

        ttk.Label(countdown_frame, text='Next Save In:').pack(side=tk.LEFT)

        self.saver_countdown_var = tk.StringVar(value='--:--:--')

        ttk.Label(countdown_frame, textvariable=self.saver_countdown_var, 

                 foreground='green', font=('Arial', 12, 'bold')).pack(side=tk.LEFT, padx=(5, 0))

        

        # Manual Save Section

        manual_saver_section = ttk.LabelFrame(saver_scrollable_frame, text='Manual Save', padding='10')

        manual_saver_section.pack(fill=tk.BOTH, expand=False, pady=(0, 15))

        

        # Manual Saver Pairs List

        ttk.Label(manual_saver_section, text='Source -> Destination Pairs with Hotkeys:', 

                  font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))

        

        manual_columns = ('Source', 'Destination', 'Hotkey')

        self.manual_saver_tree = ttk.Treeview(manual_saver_section, columns=manual_columns, show='headings', height=6)

        self.manual_saver_tree.heading('Source', text='Source Folder')

        self.manual_saver_tree.heading('Destination', text='Destination Folder')

        self.manual_saver_tree.heading('Hotkey', text='Hotkey')

        self.manual_saver_tree.column('Source', width=350)

        self.manual_saver_tree.column('Destination', width=350)

        self.manual_saver_tree.column('Hotkey', width=100)

        self.manual_saver_tree.pack(fill=tk.BOTH, expand=True, pady=(0, 10))

        

        # Right-click menu for manual saver pairs

        self.manual_saver_context_menu = tk.Menu(self.root, tearoff=0)

        self.manual_saver_context_menu.add_command(label="Open Source Folder", command=self.open_manual_saver_source_folder)

        self.manual_saver_context_menu.add_command(label="Open Destination Folder", command=self.open_manual_saver_destination_folder)

        self.manual_saver_context_menu.add_separator()

        self.manual_saver_context_menu.add_command(label="Reverse Save (Dest → Source)", command=self.reverse_manual_save)

        self.manual_saver_tree.bind("<Button-3>", self.show_manual_saver_context_menu)

        

        for source, dest, hotkey in self.manual_saver_pairs:

            self.manual_saver_tree.insert('', 'end', values=(source, dest, hotkey))

        

        manual_pairs_buttons = ttk.Frame(manual_saver_section)

        manual_pairs_buttons.pack(anchor=tk.W, pady=(0, 10))

        ttk.Button(manual_pairs_buttons, text='Add Pair', command=self.add_manual_saver_pair).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(manual_pairs_buttons, text='Edit Pair', command=self.edit_manual_saver_pair).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(manual_pairs_buttons, text='Remove Pair', command=self.remove_manual_saver_pair).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(manual_pairs_buttons, text='Save Selected', command=self.manual_save_selected).pack(side=tk.LEFT)

        

        # Hotkey status

        hotkey_status_frame = ttk.Frame(manual_saver_section)

        hotkey_status_frame.pack(pady=(10, 0))

        

        ttk.Label(hotkey_status_frame, text='Registered Hotkeys:').pack(side=tk.LEFT)

        self.hotkey_count_var = tk.StringVar(value='0')

        ttk.Label(hotkey_status_frame, textvariable=self.hotkey_count_var, foreground='green').pack(side=tk.LEFT, padx=(5, 0))

        

        # Log Tab

        log_frame = ttk.Frame(notebook, padding='10')

        notebook.add(log_frame, text='Log')

        

        ttk.Label(log_frame, text='Activity Log:', font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))

        

        self.log_text = scrolledtext.ScrolledText(log_frame, height=30, state=tk.DISABLED, wrap=tk.WORD)

        self.log_text.pack(fill=tk.BOTH, expand=True, pady=(0, 10))

        

        log_buttons = ttk.Frame(log_frame)

        log_buttons.pack(fill=tk.X)

        ttk.Button(log_buttons, text='Clear Log', command=self.clear_log).pack(side=tk.LEFT, padx=(0, 10))

        

        self.disable_log_var.set(self.disable_logging)

        disable_log_check = ttk.Checkbutton(log_buttons, text='Disable Logging', 

                                           variable=self.disable_log_var, command=self.on_disable_log_change)

        disable_log_check.pack(side=tk.LEFT)

        

        self.root.protocol('WM_DELETE_WINDOW', self.on_closing)


    # Context menu methods for Copy Configuration

    def show_copy_context_menu(self, event):

        item = self.pairs_tree.identify_row(event.y)

        if item:

            self.pairs_tree.selection_set(item)

            self.copy_context_menu.post(event.x_root, event.y_root)


    def open_copy_source_folder(self):

        selection = self.pairs_tree.selection()

        if selection:

            index = self.pairs_tree.index(selection[0])

            source, _ = self.copy_pairs[index]

            self.open_folder(source)


    def open_copy_destination_folder(self):

        selection = self.pairs_tree.selection()

        if selection:

            index = self.pairs_tree.index(selection[0])

            _, dest = self.copy_pairs[index]

            self.open_folder(dest)


    # Context menu methods for Auto Saver

    def show_auto_saver_context_menu(self, event):

        item = self.auto_saver_tree.identify_row(event.y)

        if item:

            self.auto_saver_tree.selection_set(item)

            self.auto_saver_context_menu.post(event.x_root, event.y_root)


    def open_auto_saver_source_folder(self):

        selection = self.auto_saver_tree.selection()

        if selection:

            index = self.auto_saver_tree.index(selection[0])

            source, _ = self.auto_saver_pairs[index]

            self.open_folder(source)


    def open_auto_saver_destination_folder(self):

        selection = self.auto_saver_tree.selection()

        if selection:

            index = self.auto_saver_tree.index(selection[0])

            _, dest = self.auto_saver_pairs[index]

            self.open_folder(dest)


    # Context menu methods for Manual Saver

    def show_manual_saver_context_menu(self, event):

        item = self.manual_saver_tree.identify_row(event.y)

        if item:

            self.manual_saver_tree.selection_set(item)

            self.manual_saver_context_menu.post(event.x_root, event.y_root)


    def open_manual_saver_source_folder(self):

        selection = self.manual_saver_tree.selection()

        if selection:

            index = self.manual_saver_tree.index(selection[0])

            source, _, _ = self.manual_saver_pairs[index]

            self.open_folder(source)


    def open_manual_saver_destination_folder(self):

        selection = self.manual_saver_tree.selection()

        if selection:

            index = self.manual_saver_tree.index(selection[0])

            _, dest, _ = self.manual_saver_pairs[index]

            self.open_folder(dest)


    def open_folder(self, path):

        try:

            if os.path.exists(path):

                if sys.platform == 'win32':

                    os.startfile(path)

                elif sys.platform == 'darwin':

                    subprocess.Popen(['open', path])

                else:

                    subprocess.Popen(['xdg-open', path])

            else:

                messagebox.showwarning('Warning', f'Folder does not exist:\n{path}')

        except Exception as e:

            messagebox.showerror('Error', f'Cannot open folder:\n{str(e)}')


    # Auto Saver Pair Management

    def add_auto_saver_pair(self):

        source = filedialog.askdirectory(title='Select Source Folder for Auto Saver')

        if not source:

            return

        

        dest = filedialog.askdirectory(title='Select Destination Folder for Auto Saver')

        if not dest:

            return

        

        try:

            source = os.path.normpath(source)

            dest = os.path.normpath(dest)

        except Exception as e:

            messagebox.showerror('Error', f'Invalid path: {str(e)}')

            return

        

        self.auto_saver_pairs.append((source, dest))

        self.auto_saver_tree.insert('', 'end', values=(source, dest))

        self.save_config()

        self.log_message(f'Added auto saver pair: {os.path.basename(source)} -> {os.path.basename(dest)}')


    def edit_auto_saver_pair(self):

        selection = self.auto_saver_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair to edit')

            return

        

        item = selection[0]

        index = self.auto_saver_tree.index(item)

        

        source = filedialog.askdirectory(title='Select New Source Folder for Auto Saver')

        if not source:

            return

        

        dest = filedialog.askdirectory(title='Select New Destination Folder for Auto Saver')

        if not dest:

            return

        

        try:

            source = os.path.normpath(source)

            dest = os.path.normpath(dest)

        except Exception as e:

            messagebox.showerror('Error', f'Invalid path: {str(e)}')

            return

        

        self.auto_saver_pairs[index] = (source, dest)

        self.auto_saver_tree.item(item, values=(source, dest))

        self.save_config()

        self.log_message(f'Updated auto saver pair: {os.path.basename(source)} -> {os.path.basename(dest)}')


    def remove_auto_saver_pair(self):

        selection = self.auto_saver_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair to remove')

            return

        

        item = selection[0]

        index = self.auto_saver_tree.index(item)

        

        source, dest = self.auto_saver_pairs[index]

        self.auto_saver_pairs.pop(index)

        self.auto_saver_tree.delete(item)

        self.save_config()

        self.log_message(f'Removed auto saver pair: {os.path.basename(source)} -> {os.path.basename(dest)}')


    # Manual Saver Pair Management

    def add_manual_saver_pair(self):

        source = filedialog.askdirectory(title='Select Source Folder for Manual Saver')

        if not source:

            return

        

        dest = filedialog.askdirectory(title='Select Destination Folder for Manual Saver')

        if not dest:

            return

        

        # Ask for hotkey

        hotkey = self.ask_hotkey()

        if not hotkey:

            return

        

        # Check if hotkey already exists

        if hotkey in self.registered_hotkeys:

            messagebox.showerror('Error', f'Hotkey "{hotkey}" is already in use')

            return

        

        try:

            source = os.path.normpath(source)

            dest = os.path.normpath(dest)

        except Exception as e:

            messagebox.showerror('Error', f'Invalid path: {str(e)}')

            return

        

        self.manual_saver_pairs.append((source, dest, hotkey))

        self.manual_saver_tree.insert('', 'end', values=(source, dest, hotkey))

        self.save_config()

        

        # Register the hotkey

        self.register_single_hotkey(len(self.manual_saver_pairs) - 1, hotkey)

        

        self.log_message(f'Added manual saver pair: {os.path.basename(source)} -> {os.path.basename(dest)} [{hotkey}]')


    def edit_manual_saver_pair(self):

        selection = self.manual_saver_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair to edit')

            return

        

        item = selection[0]

        index = self.manual_saver_tree.index(item)

        old_source, old_dest, old_hotkey = self.manual_saver_pairs[index]

        

        source = filedialog.askdirectory(title='Select New Source Folder for Manual Saver')

        if not source:

            return

        

        dest = filedialog.askdirectory(title='Select New Destination Folder for Manual Saver')

        if not dest:

            return

        

        # Ask for new hotkey

        hotkey = self.ask_hotkey(default=old_hotkey)

        if not hotkey:

            return

        

        # Check if hotkey already exists (and it's not the same hotkey)

        if hotkey != old_hotkey and hotkey in self.registered_hotkeys:

            messagebox.showerror('Error', f'Hotkey "{hotkey}" is already in use')

            return

        

        try:

            source = os.path.normpath(source)

            dest = os.path.normpath(dest)

        except Exception as e:

            messagebox.showerror('Error', f'Invalid path: {str(e)}')

            return

        

        # Unregister old hotkey if changed

        if hotkey != old_hotkey:

            self.unregister_single_hotkey(old_hotkey)

        

        self.manual_saver_pairs[index] = (source, dest, hotkey)

        self.manual_saver_tree.item(item, values=(source, dest, hotkey))

        self.save_config()

        

        # Register new hotkey if changed

        if hotkey != old_hotkey:

            self.register_single_hotkey(index, hotkey)

        

        self.log_message(f'Updated manual saver pair: {os.path.basename(source)} -> {os.path.basename(dest)} [{hotkey}]')


    def remove_manual_saver_pair(self):

        selection = self.manual_saver_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair to remove')

            return

        

        item = selection[0]

        index = self.manual_saver_tree.index(item)

        

        source, dest, hotkey = self.manual_saver_pairs[index]

        

        # Unregister hotkey

        self.unregister_single_hotkey(hotkey)

        

        self.manual_saver_pairs.pop(index)

        self.manual_saver_tree.delete(item)

        self.save_config()

        

        # Re-register all hotkeys with updated indices

        self.unregister_all_hotkeys()

        self.register_all_manual_hotkeys()

        

        self.log_message(f'Removed manual saver pair: {os.path.basename(source)} -> {os.path.basename(dest)} [{hotkey}]')


    def ask_hotkey(self, default=''):

        dialog = tk.Toplevel(self.root)

        dialog.title('Enter Hotkey')

        dialog.geometry('300x120')

        dialog.resizable(False, False)

        dialog.transient(self.root)

        dialog.grab_set()

        

        ttk.Label(dialog, text='Enter hotkey (e.g., alt+h, ctrl+shift+s):').pack(pady=(10, 5))

        

        hotkey_var = tk.StringVar(value=default)

        entry = ttk.Entry(dialog, textvariable=hotkey_var, width=30)

        entry.pack(pady=5)

        entry.focus()

        

        result = {'hotkey': None}

        

        def on_ok():

            result['hotkey'] = hotkey_var.get().strip().lower()

            dialog.destroy()

        

        def on_cancel():

            dialog.destroy()

        

        button_frame = ttk.Frame(dialog)

        button_frame.pack(pady=10)

        ttk.Button(button_frame, text='OK', command=on_ok).pack(side=tk.LEFT, padx=5)

        ttk.Button(button_frame, text='Cancel', command=on_cancel).pack(side=tk.LEFT, padx=5)

        

        entry.bind('<Return>', lambda e: on_ok())

        entry.bind('<Escape>', lambda e: on_cancel())

        

        dialog.wait_window()

        return result['hotkey']


    def register_all_manual_hotkeys(self):

        for i, (source, dest, hotkey) in enumerate(self.manual_saver_pairs):

            self.register_single_hotkey(i, hotkey)


    def register_single_hotkey(self, index, hotkey):

        try:

            keyboard.add_hotkey(hotkey, lambda idx=index: self.manual_save_by_index(idx))

            self.registered_hotkeys[hotkey] = index

            self.update_hotkey_count()

            self.log_message(f'Hotkey registered: {hotkey}')

        except Exception as e:

            self.log_message(f'ERROR registering hotkey {hotkey}: {str(e)}')


    def unregister_single_hotkey(self, hotkey):

        if hotkey in self.registered_hotkeys:

            try:

                keyboard.remove_hotkey(hotkey)

                del self.registered_hotkeys[hotkey]

                self.update_hotkey_count()

            except Exception as e:

                self.log_message(f'ERROR unregistering hotkey {hotkey}: {str(e)}')


    def unregister_all_hotkeys(self):

        for hotkey in list(self.registered_hotkeys.keys()):

            self.unregister_single_hotkey(hotkey)


    def update_hotkey_count(self):

        self.hotkey_count_var.set(str(len(self.registered_hotkeys)))


    def manual_save_by_index(self, index):

        if 0 <= index < len(self.manual_saver_pairs):

            self.root.after(0, lambda: self.perform_manual_save(index))


    def manual_save_selected(self):

        selection = self.manual_saver_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair to save')

            return

        

        index = self.manual_saver_tree.index(selection[0])

        self.perform_manual_save(index)


    def reverse_manual_save(self):

        selection = self.manual_saver_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair for reverse save')

            return

        

        index = self.manual_saver_tree.index(selection[0])

        

        result = messagebox.askyesno('Confirm Reverse Save', 

                                     'This will copy files from DESTINATION to SOURCE, overwriting existing files.\n\n'

                                     'Are you sure you want to continue?')

        if result:

            self.perform_reverse_manual_save(index)


    def perform_reverse_manual_save(self, index):

        try:

            source, dest, hotkey = self.manual_saver_pairs[index]

            

            # Validate folders exist

            if not os.path.exists(dest):

                messagebox.showerror('Error', f'Destination folder does not exist:\n{dest}')

                return

            

            if not os.path.exists(source):

                os.makedirs(source, exist_ok=True)

            

            # Get the source folder name to identify the correct subfolder

            source_folder_name = os.path.basename(source)

            dest_subfolder = os.path.join(dest, source_folder_name)

            

            if not os.path.exists(dest_subfolder):

                messagebox.showerror('Error', f'No saves found for this source folder in:\n{dest_subfolder}')

                return

            

            # Get list of timestamped folders in the specific subfolder

            timestamp_folders = [f for f in os.listdir(dest_subfolder) 

                               if os.path.isdir(os.path.join(dest_subfolder, f)) and f.startswith('Manual_')]

            

            if not timestamp_folders:

                messagebox.showwarning('Warning', 'No manual save folders found for this source')

                return

            

            # Sort to get the most recent

            timestamp_folders.sort(reverse=True)

            most_recent = timestamp_folders[0]

            

            # The Manual folder contains the saved content

            reverse_source = os.path.join(dest_subfolder, most_recent)

            

            # Get the first folder inside Manual_ (this is the saved source folder)

            saved_folders = [f for f in os.listdir(reverse_source) 

                           if os.path.isdir(os.path.join(reverse_source, f))]

            

            if not saved_folders:

                messagebox.showerror('Error', f'No folders found in:\n{reverse_source}')

                return

            

            # Use the first folder found (should be the source folder)

            saved_folder_name = saved_folders[0]

            actual_reverse_source = os.path.join(reverse_source, saved_folder_name)

            

            # Copy files from destination back to source (with overwrite)

            files_copied = 0

            for root, dirs, files in os.walk(actual_reverse_source):

                rel_path = os.path.relpath(root, actual_reverse_source)

                dest_dir = os.path.join(source, rel_path)

                

                if not os.path.exists(dest_dir):

                    os.makedirs(dest_dir, exist_ok=True)

                

                for file in files:

                    src_file = os.path.join(root, file)

                    dst_file = os.path.join(dest_dir, file)

                    

                    try:

                        shutil.copy2(src_file, dst_file)

                        files_copied += 1

                    except Exception as e:

                        self.log_message(f'ERROR copying file in reverse save: {str(e)}')

            

            self.log_message(f'Reverse Save completed: {files_copied} files restored from {most_recent} to source')

            messagebox.showinfo('Success', f'Reverse save completed!\n{files_copied} files restored to source folder.')

            

        except Exception as e:

            self.log_message(f'ERROR in reverse save: {str(e)}')

            messagebox.showerror('Error', f'Reverse save failed: {str(e)}')


    def start_auto_saver(self):

        try:

            self.saver_hours = int(self.saver_hours_var.get())

            self.saver_minutes = int(self.saver_minutes_var.get())

            self.saver_seconds = int(self.saver_seconds_var.get())

        except ValueError:

            messagebox.showerror('Error', 'Please enter valid numbers for time interval')

            return

        

        # Validate inputs

        if not self.auto_saver_pairs:

            messagebox.showerror('Error', 'Please add at least one source-destination pair')

            return

        

        total_seconds = self.saver_hours * 3600 + self.saver_minutes * 60 + self.saver_seconds

        if total_seconds <= 0:

            messagebox.showerror('Error', 'Time interval must be greater than 0')

            return

        

        self.save_config()

        

        self.saver_running = True

        self.saver_start_button.config(state=tk.DISABLED)

        self.saver_stop_button.config(state=tk.NORMAL)

        self.saver_status_var.set('Running')

        

        # Initialize next save time

        self.saver_next_save_time = datetime.now() + timedelta(seconds=total_seconds)

        

        self.log_message('Auto Saver started')

        

        # Start countdown update

        self.update_saver_countdown()

        

        # Start the saver thread

        self.saver_thread = threading.Thread(target=self.auto_saver_worker, daemon=True)

        self.saver_thread.start()


    def stop_auto_saver(self):

        self.saver_running = False

        self.saver_start_button.config(state=tk.NORMAL)

        self.saver_stop_button.config(state=tk.DISABLED)

        self.saver_status_var.set('Stopped')

        self.saver_countdown_var.set('--:--:--')

        

        # Cancel countdown update

        if self.countdown_update_job:

            self.root.after_cancel(self.countdown_update_job)

            self.countdown_update_job = None

        

        self.log_message('Auto Saver stopped')


    def auto_saver_worker(self):

        while self.saver_running:

            total_seconds = self.saver_hours * 3600 + self.saver_minutes * 60 + self.saver_seconds

            

            # Wait for the interval

            for _ in range(total_seconds):

                if not self.saver_running:

                    return

                time.sleep(1)

            

            # Perform the save

            self.perform_auto_save()

            

            # Update next save time

            if self.saver_running:

                self.saver_next_save_time = datetime.now() + timedelta(seconds=total_seconds)


    def update_saver_countdown(self):

        if not self.saver_running:

            return

        

        if self.saver_next_save_time:

            now = datetime.now()

            time_remaining = self.saver_next_save_time - now

            

            if time_remaining.total_seconds() > 0:

                hours, remainder = divmod(int(time_remaining.total_seconds()), 3600)

                minutes, seconds = divmod(remainder, 60)

                self.saver_countdown_var.set(f'{hours:02d}:{minutes:02d}:{seconds:02d}')

            else:

                self.saver_countdown_var.set('00:00:00')

        

        # Schedule next update in 1 second

        self.countdown_update_job = self.root.after(1000, self.update_saver_countdown)


    def perform_auto_save(self):

        try:

            # Create timestamped folder name with "Auto" prefix

            timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')

            auto_folder_name = f'Auto_{timestamp}'

            

            total_files = 0

            

            # Save all pairs

            for source, dest in self.auto_saver_pairs:

                if not os.path.exists(source):

                    self.log_message(f'WARNING: Auto saver source folder does not exist: {source}')

                    continue

                

                # Create a unique subfolder based on source folder name

                source_folder_name = os.path.basename(source)

                dest_with_subfolder = os.path.join(dest, source_folder_name)

                

                dest_folder = os.path.join(dest_with_subfolder, auto_folder_name)

                

                # Get the source folder name for the final destination

                final_dest = os.path.join(dest_folder, source_folder_name)

                

                # Create destination directory

                os.makedirs(final_dest, exist_ok=True)

                

                # Copy the entire folder

                files_copied = 0

                for root, dirs, files in os.walk(source):

                    rel_path = os.path.relpath(root, source)

                    dest_dir = os.path.join(final_dest, rel_path)

                    

                    if not os.path.exists(dest_dir):

                        os.makedirs(dest_dir, exist_ok=True)

                    

                    for file in files:

                        src_file = os.path.join(root, file)

                        dst_file = os.path.join(dest_dir, file)

                        

                        try:

                            shutil.copy2(src_file, dst_file)

                            files_copied += 1

                        except Exception as e:

                            self.log_message(f'ERROR copying file in auto save: {str(e)}')

                

                total_files += files_copied

                self.log_message(f'Auto saved {files_copied} files from {os.path.basename(source)}')

            

            self.last_save_time = datetime.now()

            self.log_message(f'Auto Save completed: {total_files} total files saved to {auto_folder_name}')

            

        except Exception as e:

            self.log_message(f'ERROR in auto save: {str(e)}')


    def perform_manual_save(self, index):

        try:

            source, dest, hotkey = self.manual_saver_pairs[index]

            

            # Validate inputs

            if not os.path.exists(source):

                messagebox.showerror('Error', f'Source folder does not exist:\n{source}')

                return

            

            # Create a unique subfolder based on source folder name

            source_folder_name = os.path.basename(source)

            dest_with_subfolder = os.path.join(dest, source_folder_name)

            

            # Create timestamped folder name with "Manual" prefix

            timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')

            manual_folder_name = f'Manual_{timestamp}'

            dest_folder = os.path.join(dest_with_subfolder, manual_folder_name)

            

            # Get the source folder name for the final destination

            final_dest = os.path.join(dest_folder, source_folder_name)

            

            # Create destination directory

            os.makedirs(final_dest, exist_ok=True)

            

            # Copy the entire folder

            files_copied = 0

            for root, dirs, files in os.walk(source):

                rel_path = os.path.relpath(root, source)

                dest_dir = os.path.join(final_dest, rel_path)

                

                if not os.path.exists(dest_dir):

                    os.makedirs(dest_dir, exist_ok=True)

                

                for file in files:

                    src_file = os.path.join(root, file)

                    dst_file = os.path.join(dest_dir, file)

                    

                    try:

                        shutil.copy2(src_file, dst_file)

                        files_copied += 1

                    except Exception as e:

                        self.log_message(f'ERROR copying file in manual save: {str(e)}')

            

            self.log_message(f'Manual Save completed [{hotkey}]: {files_copied} files saved to {dest_folder}')

            

        except Exception as e:

            self.log_message(f'ERROR in manual save: {str(e)}')

            messagebox.showerror('Error', f'Manual save failed: {str(e)}')


    def on_auto_start_saver_change(self):

        self.auto_start_saver = self.auto_start_saver_var.get()

        self.save_config()


    def add_copy_pair(self):

        source = filedialog.askdirectory(title='Select Source Folder')

        if not source:

            return

        

        dest = filedialog.askdirectory(title='Select Destination Folder')

        if not dest:

            return

        

        try:

            source = os.path.normpath(source)

            dest = os.path.normpath(dest)

        except Exception as e:

            messagebox.showerror('Error', f'Invalid path: {str(e)}')

            return

        

        for existing_source, _ in self.copy_pairs:

            if os.path.normpath(existing_source) == source:

                messagebox.showwarning('Warning', 'This source folder is already in the list')

                return

        

        self.copy_pairs.append((source, dest))

        self.pairs_tree.insert('', 'end', values=(source, dest))

        self.save_config()

        self.log_message(f'Added copy pair: {os.path.basename(source)} -> {os.path.basename(dest)}')


    def edit_copy_pair(self):

        selection = self.pairs_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair to edit')

            return

        

        item = selection[0]

        index = self.pairs_tree.index(item)

        

        source = filedialog.askdirectory(title='Select New Source Folder')

        if not source:

            return

        

        dest = filedialog.askdirectory(title='Select New Destination Folder')

        if not dest:

            return

        

        try:

            source = os.path.normpath(source)

            dest = os.path.normpath(dest)

        except Exception as e:

            messagebox.showerror('Error', f'Invalid path: {str(e)}')

            return

        

        self.copy_pairs[index] = (source, dest)

        self.pairs_tree.item(item, values=(source, dest))

        self.save_config()

        self.log_message(f'Updated copy pair: {os.path.basename(source)} -> {os.path.basename(dest)}')


    def remove_copy_pair(self):

        selection = self.pairs_tree.selection()

        if not selection:

            messagebox.showwarning('Warning', 'Please select a pair to remove')

            return

        

        item = selection[0]

        index = self.pairs_tree.index(item)

        

        source, dest = self.copy_pairs[index]

        self.copy_pairs.pop(index)

        self.pairs_tree.delete(item)

        self.save_config()

        self.log_message(f'Removed copy pair: {os.path.basename(source)} -> {os.path.basename(dest)}')


    def on_interval_change(self, event):

        try:

            new_interval = int(self.interval_var.get())

            self.interval = new_interval

            

            intervals = [('5 seconds', '5'), ('15 seconds', '15'), ('30 seconds', '30'), 

                        ('1 minute', '60'), ('5 minutes', '300'), ('15 minutes', '900'), 

                        ('30 minutes', '1800'), ('1 hour', '3600'), ('2 hours', '7200'), ('3 hours', '10800')]

            

            for text, value in intervals:

                if value == str(new_interval):

                    self.interval_desc.set(text)

                    break

            

            self.save_config()

            self.log_message(f'Copy interval changed to {self.interval_desc.get()}')

        except ValueError:

            pass


    def on_notify_change(self):

        self.show_notifications = self.notify_var.get()

        self.save_config()


    def on_disable_log_change(self):

        self.disable_logging = self.disable_log_var.get()

        self.save_config()


    def on_start_as_icon_change(self):

        self.start_as_icon = self.start_as_icon_var.get()

        self.save_config()


    def on_auto_start_change(self):

        self.auto_start = self.auto_start_var.get()

        self.save_config()


    def on_overwrite_change(self):

        self.overwrite_files = self.overwrite_var.get()

        self.save_config()


    def start_timer(self):

        if not self.copy_pairs:

            messagebox.showwarning('Warning', 'Please add at least one source-destination pair')

            return

        

        self.timer_running = True

        self.start_button.config(state=tk.DISABLED)

        self.stop_button.config(state=tk.NORMAL)

        self.update_status('Running')

        self.log_message('Automatic copy started')

        

        self.copy_thread = threading.Thread(target=self.copy_worker, daemon=True)

        self.copy_thread.start()


    def stop_timer(self):

        self.timer_running = False

        self.start_button.config(state=tk.NORMAL)

        self.stop_button.config(state=tk.DISABLED)

        self.update_status('Stopped')

        self.log_message('Automatic copy stopped')


    def copy_now(self):

        if not self.copy_pairs:

            messagebox.showwarning('Warning', 'Please add at least one source-destination pair')

            return

        

        threading.Thread(target=self.perform_copy, daemon=True).start()


    def copy_worker(self):

        while self.timer_running:

            self.perform_copy()

            

            for _ in range(self.interval):

                if not self.timer_running:

                    return

                time.sleep(1)


    def perform_copy(self):

        self.update_status('Copying...')

        start_time = time.time()

        total_files = 0

        all_success = True

        

        for source, dest in self.copy_pairs:

            if not os.path.exists(source):

                self.log_message(f'WARNING: Source folder does not exist: {source}')

                all_success = False

                continue

            

            files_copied, success = self.copy_folder(source, dest, self.overwrite_files)

            total_files += files_copied

            if not success:

                all_success = False

        

        elapsed_time = time.time() - start_time

        

        status_msg = f'Completed - {total_files} files in {elapsed_time:.1f}s'

        self.update_status(status_msg)

        

        if self.show_notifications and all_success:

            self.show_notification('Copy Completed', f'Successfully copied {total_files} files')


    def copy_folder(self, source, dest, overwrite=False):

        files_copied = 0

        operation_success = True

        

        if not os.path.exists(dest):

            os.makedirs(dest, exist_ok=True)

        

        # Copy files one by one

        for root, dirs, files in os.walk(source):

            rel_path = os.path.relpath(root, source)

            dest_dir = os.path.join(dest, rel_path)

            

            if not os.path.exists(dest_dir):

                os.makedirs(dest_dir, exist_ok=True)

            

            for file in files:

                src_file = os.path.join(root, file)

                dst_file = os.path.join(dest_dir, file)

                

                try:

                    if overwrite or not os.path.exists(dst_file):

                        shutil.copy2(src_file, dst_file)

                        files_copied += 1

                except Exception as e:

                    self.log_message(f'ERROR copying \'{src_file}\': {str(e)}')

                    operation_success = False

        

        mode = "with overwrite" if overwrite else "missing files only"

        self.log_message(f'Copied {files_copied} files {mode} from \'{os.path.basename(source)}\'')

        

        return files_copied, operation_success


    def clear_log(self):

        self.log_text.config(state=tk.NORMAL)

        self.log_text.delete('1.0', tk.END)

        self.log_text.config(state=tk.DISABLED)


    def show_notification(self, title, message):

        try:

            if self.tray_icon and self.is_minimized_to_tray:

                self.tray_icon.notify(title, message)

            else:

                messagebox.showinfo(title, message)

        except Exception as e:

            self.log_message(f'Notification error: {str(e)}')


    def minimize_to_tray(self):

        self.root.withdraw()

        self.is_minimized_to_tray = True

        

        if not self.tray_icon:

            image = self.create_tray_icon()

            menu = pystray.Menu(

                pystray.MenuItem('Show', self.show_from_tray),

                pystray.MenuItem('Exit', self.exit_application)

            )

            self.tray_icon = pystray.Icon('Folder Copier', image, 'Folder Copier Tool', menu)

            

            threading.Thread(target=self.tray_icon.run, daemon=True).start()


    def create_tray_icon(self):

        width = 64

        height = 64

        image = Image.new('RGB', (width, height), 'white')

        dc = ImageDraw.Draw(image)

        dc.rectangle([0, 0, width, height], fill='#1a73e8')

        dc.rectangle([width//4, height//4, 3*width//4, 3*height//4], fill='white')

        return image


    def show_from_tray(self):

        self.is_minimized_to_tray = False

        self.root.deiconify()


    def exit_application(self):

        self.timer_running = False

        self.saver_running = False

        

        # Unregister all hotkeys

        self.unregister_all_hotkeys()

        

        if self.tray_icon:

            self.tray_icon.stop()

        

        self.root.quit()

        self.root.destroy()


    def create_auto_launch_shortcut(self):

        try:

            startup_folder = winshell.startup()

            shortcut_path = os.path.join(startup_folder, 'FolderCopierTool.lnk')

            

            target = sys.executable

            wDir = os.path.dirname(os.path.abspath(__file__))

            icon = sys.executable

            

            shell = Dispatch('WScript.Shell')

            shortcut = shell.CreateShortCut(shortcut_path)

            shortcut.Targetpath = target

            shortcut.Arguments = f'"{os.path.abspath(__file__)}"'

            shortcut.WorkingDirectory = wDir

            shortcut.IconLocation = icon

            shortcut.save()

            

            messagebox.showinfo('Success', 'Auto-launch shortcut created successfully!')

            self.log_message('Auto-launch shortcut created in startup folder')

            

        except Exception as e:

            messagebox.showerror('Error', f'Failed to create auto-launch shortcut: {str(e)}')

            self.log_message(f'ERROR creating auto-launch shortcut: {str(e)}')


    def log_message(self, message):

        if self.disable_logging:

            return

        

        def update_log():

            self.log_text.config(state=tk.NORMAL)

            timestamp = datetime.now().strftime('%H:%M:%S')

            self.log_text.insert(tk.END, f'[{timestamp}] {message}\n')

            self.log_text.see(tk.END)

            self.log_text.config(state=tk.DISABLED)

        

        self.root.after(0, update_log)


    def update_status(self, message):

        def update():

            self.status_var.set(message)

        self.root.after(0, update)


    def load_config(self):

        if os.path.exists(self.config_file):

            try:

                with open(self.config_file, 'r') as f:

                    config = json.load(f)

                

                # Load copy configuration

                pairs = config.get('copy_pairs', [])

                self.copy_pairs = []

                

                for source, dest in pairs:

                    try:

                        norm_source = os.path.normpath(source)

                        norm_dest = os.path.normpath(dest)

                        self.copy_pairs.append((norm_source, norm_dest))

                    except Exception:

                        pass

                

                self.interval = config.get('interval', 5)

                self.show_notifications = config.get('show_notifications', False)

                self.disable_logging = config.get('disable_logging', False)

                self.start_as_icon = config.get('start_as_icon', False)

                self.auto_start = config.get('auto_start', True)

                self.overwrite_files = config.get('overwrite_files', False)

                

                # Load auto saver configuration

                auto_pairs = config.get('auto_saver_pairs', [])

                self.auto_saver_pairs = []

                for source, dest in auto_pairs:

                    try:

                        norm_source = os.path.normpath(source)

                        norm_dest = os.path.normpath(dest)

                        self.auto_saver_pairs.append((norm_source, norm_dest))

                    except Exception:

                        pass

                

                self.saver_hours = config.get('saver_hours', 0)

                self.saver_minutes = config.get('saver_minutes', 0)

                self.saver_seconds = config.get('saver_seconds', 30)

                self.auto_start_saver = config.get('auto_start_saver', False)

                

                # Load manual saver configuration

                manual_pairs = config.get('manual_saver_pairs', [])

                self.manual_saver_pairs = []

                for item in manual_pairs:

                    try:

                        if len(item) == 3:

                            source, dest, hotkey = item

                            norm_source = os.path.normpath(source)

                            norm_dest = os.path.normpath(dest)

                            self.manual_saver_pairs.append((norm_source, norm_dest, hotkey))

                    except Exception:

                        pass

                

                # Set variable values

                self.disable_log_var.set(self.disable_logging)

                self.start_as_icon_var.set(self.start_as_icon)

                self.auto_start_var.set(self.auto_start)

                self.notify_var.set(self.show_notifications)

                self.overwrite_var.set(self.overwrite_files)

                self.auto_start_saver_var.set(self.auto_start_saver)

                

            except Exception as e:

                self.log_message(f'Error loading configuration: {str(e)}')

                self.use_default_config()

        else:

            self.use_default_config()


    def use_default_config(self):

        self.copy_pairs = []

        self.interval = 5

        self.show_notifications = False

        self.disable_logging = False

        self.start_as_icon = False

        self.auto_start = True

        self.overwrite_files = False

        self.auto_saver_pairs = []

        self.saver_hours = 0

        self.saver_minutes = 0

        self.saver_seconds = 30

        self.auto_start_saver = False

        self.manual_saver_pairs = []

        

        self.disable_log_var.set(self.disable_logging)

        self.start_as_icon_var.set(self.start_as_icon)

        self.auto_start_var.set(self.auto_start)

        self.notify_var.set(self.show_notifications)

        self.overwrite_var.set(self.overwrite_files)

        self.auto_start_saver_var.set(self.auto_start_saver)


    def save_config(self):

        try:

            config = {

                # Copy configuration

                'copy_pairs': self.copy_pairs,

                'interval': self.interval,

                'show_notifications': self.show_notifications,

                'disable_logging': self.disable_logging,

                'start_as_icon': self.start_as_icon,

                'auto_start': self.auto_start,

                'overwrite_files': self.overwrite_files,

                

                # Auto saver configuration

                'auto_saver_pairs': self.auto_saver_pairs,

                'saver_hours': self.saver_hours,

                'saver_minutes': self.saver_minutes,

                'saver_seconds': self.saver_seconds,

                'auto_start_saver': self.auto_start_saver,

                

                # Manual saver configuration

                'manual_saver_pairs': self.manual_saver_pairs

            }

            

            with open(self.config_file, 'w') as f:

                json.dump(config, f, indent=2)

                

        except Exception as e:

            self.log_message(f'ERROR: Cannot save configuration: {str(e)}')


    def on_closing(self):

        """Handle window closing"""

        if self.is_minimized_to_tray:

            self.exit_application()

        else:

            result = messagebox.askyesnocancel('Exit', 

                'Do you want to minimize to system tray instead of exiting?\n\n'

                'Yes: Minimize to tray\n'

                'No: Exit completely\n'

                'Cancel: Stay in application')

            

            if result is True:

                self.minimize_to_tray()

            elif result is False:

                self.exit_application()

            # If result is None (Cancel), do nothing


if __name__ == '__main__':

    root = tk.Tk()

    app = FolderCopierTool(root)

    root.mainloop()

Commenti

Post popolari in questo blog

No Man's Sky Similar Games

Tower Defense Games