added clipcascade
This commit is contained in:
Executable
+120
@@ -0,0 +1,120 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import scrolledtext
|
||||
import gc
|
||||
import time
|
||||
|
||||
from utils.window_manager import center_window
|
||||
from core.constants import *
|
||||
|
||||
|
||||
class CustomDialog(tk.Tk):
|
||||
def __init__(self, message, msg_type="info", timeout=None):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
self.msg_type = msg_type.lower()
|
||||
self.timeout = timeout
|
||||
self.after_id = None
|
||||
|
||||
self.style = ttk.Style(self)
|
||||
self.style.configure("TFrame", background="#f2f2f2")
|
||||
self.style.configure(
|
||||
"TLabel", background="#f2f2f2", foreground="#333333", font=("Arial", 11)
|
||||
)
|
||||
self.style.configure("Header.TLabel", font=("Arial", 14, "bold"))
|
||||
self.style.configure("TButton", font=("Arial", 11))
|
||||
|
||||
self._configure_window()
|
||||
self._show_dialog()
|
||||
|
||||
if self.timeout:
|
||||
self.after_id = self.after(self.timeout, self.close)
|
||||
|
||||
# Allow closing with keyboard shortcuts
|
||||
self.bind("<Return>", lambda e: self.close())
|
||||
self.bind("<Escape>", lambda e: self.close())
|
||||
|
||||
def _configure_window(self):
|
||||
"""Configure the dialog window properties."""
|
||||
self.title("ClipCascade")
|
||||
self.geometry(
|
||||
"600x300+{}+{}".format(
|
||||
(self.winfo_screenwidth() - 600) // 2,
|
||||
(self.winfo_screenheight() - 300) // 2,
|
||||
)
|
||||
)
|
||||
self.update_idletasks()
|
||||
center_window(self)
|
||||
self.attributes("-topmost", True)
|
||||
self.resizable(False, False)
|
||||
self.focus_force()
|
||||
|
||||
if PLATFORM == MACOS:
|
||||
self.after(100, self._macos_restore_focus)
|
||||
|
||||
def _show_dialog(self):
|
||||
"""Display the appropriate dialog based on the message type."""
|
||||
type_config = {
|
||||
"success": ("Success", "green", "✔"),
|
||||
"error": ("Error", "red", "✖"),
|
||||
"warning": ("Warning", "orange", "⚠"),
|
||||
"info": ("Information", "blue", "ℹ"),
|
||||
}
|
||||
title, color, symbol = type_config.get(self.msg_type, ("Message", "black", "•"))
|
||||
|
||||
# Create a main frame
|
||||
main_frame = ttk.Frame(self, padding="20 20 20 20")
|
||||
main_frame.pack(fill="both", expand=True)
|
||||
|
||||
# Title frame with symbol and title
|
||||
title_frame = ttk.Frame(main_frame)
|
||||
title_frame.pack(pady=(0, 10), fill="x")
|
||||
|
||||
symbol_label = ttk.Label(
|
||||
title_frame, text=symbol, font=("Arial", 18, "bold"), foreground=color
|
||||
)
|
||||
symbol_label.pack(side="left", padx=(0, 10))
|
||||
|
||||
title_label = ttk.Label(title_frame, text=title, style="Header.TLabel", foreground=color)
|
||||
title_label.pack(side="left")
|
||||
|
||||
# Message frame for scrolled text
|
||||
message_frame = ttk.Frame(main_frame)
|
||||
message_frame.pack(fill="both", expand=True, pady=10)
|
||||
|
||||
# ScrolledText widget for the message
|
||||
self.text_widget = scrolledtext.ScrolledText(
|
||||
message_frame, wrap="word", font=("Arial", 11), width=70, height=8
|
||||
)
|
||||
self.text_widget.pack(fill="both", expand=True)
|
||||
|
||||
# Insert the message and disable editing
|
||||
self.text_widget.insert("1.0", self.message)
|
||||
self.text_widget.config(state="disabled")
|
||||
|
||||
# Button frame
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(pady=(10, 0))
|
||||
|
||||
ok_button = ttk.Button(button_frame, text="OK", command=self.close, width=10)
|
||||
ok_button.pack()
|
||||
ok_button.focus_set()
|
||||
|
||||
def _macos_restore_focus(self):
|
||||
"""Force macOS to properly activate and focus the dialog."""
|
||||
try:
|
||||
from AppKit import NSApplication
|
||||
app = NSApplication.sharedApplication()
|
||||
app.activateIgnoringOtherApps_(True)
|
||||
except ImportError:
|
||||
pass
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
|
||||
def close(self):
|
||||
if self.after_id:
|
||||
self.after_cancel(self.after_id) # Cancel the after event if it exists
|
||||
self.after_id = None
|
||||
self.destroy()
|
||||
gc.collect()
|
||||
time.sleep(0.5)
|
||||
Executable
+648
@@ -0,0 +1,648 @@
|
||||
import re
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, font
|
||||
import gc
|
||||
import time
|
||||
import os
|
||||
|
||||
from utils.window_manager import center_window
|
||||
from core.config import Config
|
||||
from gui.info import CustomDialog
|
||||
from core.constants import *
|
||||
|
||||
|
||||
class HoverTooltip:
|
||||
"""Simple hover tooltip for Tk widgets."""
|
||||
|
||||
def __init__(self, widget, text, wraplength=520):
|
||||
self.widget = widget
|
||||
self.text = text
|
||||
self.wraplength = wraplength
|
||||
self.tooltip_window = None
|
||||
self.widget.bind("<Enter>", self._show, add="+")
|
||||
self.widget.bind("<Leave>", self._hide, add="+")
|
||||
self.widget.bind("<ButtonPress>", self._hide, add="+")
|
||||
|
||||
def _show(self, _event=None):
|
||||
if self.tooltip_window or not self.text.strip():
|
||||
return
|
||||
|
||||
# Position tooltip near the widget with an offset.
|
||||
x = self.widget.winfo_rootx() + 18
|
||||
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 8
|
||||
|
||||
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
||||
tw.wm_overrideredirect(True)
|
||||
tw.wm_geometry(f"+{x}+{y}")
|
||||
|
||||
label = tk.Label(
|
||||
tw,
|
||||
text=self.text,
|
||||
justify=tk.LEFT,
|
||||
relief=tk.SOLID,
|
||||
borderwidth=1,
|
||||
background="#ffffe0",
|
||||
foreground="#202020",
|
||||
font=("Helvetica", 10),
|
||||
wraplength=self.wraplength,
|
||||
padx=8,
|
||||
pady=6,
|
||||
)
|
||||
label.pack()
|
||||
|
||||
def _hide(self, _event=None):
|
||||
if self.tooltip_window:
|
||||
self.tooltip_window.destroy()
|
||||
self.tooltip_window = None
|
||||
|
||||
|
||||
class LoginForm(tk.Tk):
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
on_login_callback=None,
|
||||
on_quit_callback=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.on_login_callback = on_login_callback
|
||||
self.on_quit_callback = on_quit_callback
|
||||
|
||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
||||
# Set an initial size to avoid 0 size issue
|
||||
# self.geometry("800x450")
|
||||
self.update_idletasks()
|
||||
|
||||
self.withdraw() # Hide the root window
|
||||
self.title("ClipCascade")
|
||||
self._tooltips = []
|
||||
|
||||
# Make the window appear on top and grab focus
|
||||
self.attributes("-topmost", True)
|
||||
self.focus_force()
|
||||
|
||||
# Configure styles
|
||||
style = ttk.Style(self)
|
||||
# Use default theme (may vary by OS); consider 'clam', 'alt', or 'default'
|
||||
style.configure("TLabel", font=("Helvetica", 13), padding=5)
|
||||
style.configure("TEntry", font=("Helvetica", 13), padding=5)
|
||||
style.configure("TButton", font=("Helvetica", 13), padding=5)
|
||||
style.configure("TCheckbutton", font=("Helvetica", 13), padding=5)
|
||||
|
||||
# Main frame with padding
|
||||
main_frame = ttk.Frame(self, padding=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title Label
|
||||
title_label = ttk.Label(
|
||||
main_frame,
|
||||
text="Please Log In",
|
||||
font=("Helvetica", 16, "bold"),
|
||||
takefocus=False,
|
||||
)
|
||||
title_label.pack(pady=(0, 10))
|
||||
|
||||
# Create a frame for the fields
|
||||
self.field_frame = ttk.Frame(main_frame)
|
||||
self.field_frame.pack(padx=10, pady=10, fill=tk.X)
|
||||
|
||||
# Username
|
||||
user_label = ttk.Label(self.field_frame, text="Username:")
|
||||
user_label.grid(row=0, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.username_entry = ttk.Entry(self.field_frame, width=50, font=("Helvetica", 13))
|
||||
self.username_entry.insert(0, self.config.data["username"])
|
||||
self.username_entry.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
||||
self._add_tooltip(
|
||||
[user_label, self.username_entry],
|
||||
"Username used to log in to your ClipCascade account.\n\n"
|
||||
"Use the same account name on all devices that should share clipboard data.",
|
||||
)
|
||||
|
||||
# Password
|
||||
pass_label = ttk.Label(self.field_frame, text="Password:")
|
||||
pass_label.grid(row=1, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.password_frame = ttk.Frame(self.field_frame)
|
||||
self.password_frame.grid(row=1, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
||||
self.password_entry = ttk.Entry(
|
||||
self.password_frame, show="*", width=47, font=("Helvetica", 13)
|
||||
)
|
||||
self.password_entry.insert(0, self.config.data["password"])
|
||||
self.password_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
|
||||
self.eye_icon = ttk.Label(
|
||||
self.password_frame, text="🙈", cursor="hand2", takefocus=False
|
||||
)
|
||||
self.eye_icon.pack(side=tk.RIGHT, padx=(5, 0))
|
||||
self.eye_icon.bind("<Button-1>", self._toggle_password)
|
||||
self._add_tooltip(
|
||||
[pass_label, self.password_entry, self.eye_icon],
|
||||
"Account password used for server authentication.\n\n"
|
||||
"If encryption is enabled, this password is also used to derive your encryption key.\n"
|
||||
"Use the same password on all devices for successful decrypt/sync.",
|
||||
)
|
||||
|
||||
# Server URL
|
||||
server_label = ttk.Label(self.field_frame, text="Server URL:")
|
||||
server_label.grid(row=2, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.server_url_entry = ttk.Entry(self.field_frame, width=50, font=("Helvetica", 13))
|
||||
self.server_url_entry.insert(0, self.config.data["server_url"])
|
||||
self.server_url_entry.grid(row=2, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
||||
self._add_tooltip(
|
||||
[server_label, self.server_url_entry],
|
||||
"Address of your ClipCascade server.\n\n"
|
||||
"Examples:\n"
|
||||
"- Local server: http://localhost:8080\n"
|
||||
"- LAN server: http://192.168.1.50:8080\n"
|
||||
"- Reverse proxy/domain: https://clipcascade.example.com\n\n"
|
||||
"Include protocol (http/https). Do not add /login or /clipsocket.",
|
||||
)
|
||||
|
||||
# Configure grid weights to ensure entries expand
|
||||
self.field_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Checkboxes frame
|
||||
checks_frame = ttk.Frame(main_frame)
|
||||
checks_frame.pack(pady=(5, 10), fill=tk.X)
|
||||
|
||||
# Encryption Checkbox
|
||||
self.cipher_var = tk.BooleanVar(value=self.config.data["cipher_enabled"])
|
||||
self.cipher_checkbox = ttk.Checkbutton(
|
||||
checks_frame,
|
||||
text="Enable Encryption (recommended)",
|
||||
variable=self.cipher_var,
|
||||
takefocus=False,
|
||||
)
|
||||
self.cipher_checkbox.pack(pady=3, anchor=tk.W)
|
||||
self._add_tooltip(
|
||||
[self.cipher_checkbox],
|
||||
"Enables end-to-end encryption for clipboard content (recommended).\n\n"
|
||||
"When enabled, clipboard data is encrypted on-device before sending.\n"
|
||||
"All devices in the same account must use the same encryption settings "
|
||||
"(password, salt, and hash rounds) to decrypt data correctly.",
|
||||
)
|
||||
|
||||
# Notification Checkbox
|
||||
self.notification_var = tk.BooleanVar(value=self.config.data["notification"])
|
||||
self.notification_checkbox = ttk.Checkbutton(
|
||||
checks_frame,
|
||||
text="Enable Notification",
|
||||
variable=self.notification_var,
|
||||
takefocus=False,
|
||||
)
|
||||
self.notification_checkbox.pack(pady=3, anchor=tk.W)
|
||||
self._add_tooltip(
|
||||
[self.notification_checkbox],
|
||||
"Shows local notifications for connection status events.\n\n"
|
||||
"Useful to know when WebSocket disconnects/reconnects happen.\n"
|
||||
"Turn this off if you prefer a quieter experience with fewer popups.",
|
||||
)
|
||||
|
||||
# Button frame
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(pady=(10, 0))
|
||||
|
||||
# Login Button
|
||||
self.login_button = ttk.Button(button_frame, text="Login", command=self.on_login)
|
||||
self.login_button.pack()
|
||||
|
||||
# Extra Configurations Toggle Label
|
||||
# macOS: show advanced fields by default
|
||||
self.show_extra = PLATFORM == MACOS
|
||||
self.toggle_normal_color = "#007acc" # neutral blue with strong contrast
|
||||
self.toggle_hover_color = "#005a9e" # slightly darker for hover feedback
|
||||
self.toggle_label = tk.Label(
|
||||
main_frame,
|
||||
text="Hide Extra Config" if PLATFORM == MACOS else "Enable Extra Config",
|
||||
fg=self.toggle_normal_color,
|
||||
cursor="hand2",
|
||||
font=font.Font(family="Helvetica", size=13, underline=True),
|
||||
takefocus=False,
|
||||
)
|
||||
self.toggle_label.pack(pady=(5, 10))
|
||||
self.toggle_label.bind("<Button-1>", self._toggle_extra_config)
|
||||
self._add_tooltip(
|
||||
[self.toggle_label],
|
||||
"Shows advanced settings for encryption and clipboard behavior.\n\n"
|
||||
"Most users can keep defaults. Open this section only if you need custom tuning.",
|
||||
)
|
||||
|
||||
# hover effects for the toggle label
|
||||
self.toggle_label.bind("<Enter>", self._on_hover)
|
||||
self.toggle_label.bind("<Leave>", self._on_leave)
|
||||
|
||||
# Extra Configuration Frame with Scrollbar
|
||||
self.extra_frame_container = ttk.Frame(main_frame)
|
||||
if PLATFORM == MACOS:
|
||||
self.extra_frame_container.pack(fill=tk.BOTH, expand=True)
|
||||
else:
|
||||
self.extra_frame_container.pack_forget()
|
||||
|
||||
# Create a canvas inside the extra_frame_container
|
||||
self.extra_canvas = tk.Canvas(
|
||||
self.extra_frame_container,
|
||||
borderwidth=0,
|
||||
background="#f0f0f0",
|
||||
takefocus=False,
|
||||
)
|
||||
self.extra_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Add a vertical scrollbar to the canvas
|
||||
self.scrollbar = ttk.Scrollbar(
|
||||
self.extra_frame_container,
|
||||
orient="vertical",
|
||||
command=self.extra_canvas.yview,
|
||||
takefocus=False,
|
||||
)
|
||||
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Configure the canvas to use the scrollbar
|
||||
self.extra_canvas.configure(yscrollcommand=self.scrollbar.set)
|
||||
|
||||
# Create a frame inside the canvas to hold the extra configuration widgets
|
||||
self.extra_frame = ttk.Frame(self.extra_canvas, padding=10)
|
||||
self.extra_frame_window = self.extra_canvas.create_window(
|
||||
(0, 0), window=self.extra_frame, anchor="nw"
|
||||
)
|
||||
|
||||
# Bind the configure event to update the scrollregion
|
||||
self.extra_frame.bind(
|
||||
"<Configure>",
|
||||
lambda event: self.extra_canvas.configure(scrollregion=self.extra_canvas.bbox("all")),
|
||||
)
|
||||
|
||||
# Bind a configure event to the container to adjust the width
|
||||
self.extra_frame_container.bind("<Configure>", self._on_extra_container_configure)
|
||||
|
||||
# Hash Rounds
|
||||
hash_rounds_label = ttk.Label(self.extra_frame, text="Hash Rounds:")
|
||||
hash_rounds_label.grid(row=0, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.hash_rounds_entry = ttk.Entry(self.extra_frame, width=50, font=("Helvetica", 13))
|
||||
self.hash_rounds_entry.insert(0, str(self.config.data["hash_rounds"]))
|
||||
self.hash_rounds_entry.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
||||
self._add_tooltip(
|
||||
[hash_rounds_label, self.hash_rounds_entry],
|
||||
"Number of hash iterations used for encryption key derivation.\n\n"
|
||||
"Higher value = stronger brute-force resistance, but slower processing.\n"
|
||||
"Default is tuned for balance. Keep default unless you know why to change.\n"
|
||||
"Must be a positive integer and must match on every device.",
|
||||
)
|
||||
|
||||
# Salt
|
||||
salt_label = ttk.Label(self.extra_frame, text="Salt:")
|
||||
salt_label.grid(row=1, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.salt_entry = ttk.Entry(self.extra_frame, width=50, font=("Helvetica", 13))
|
||||
self.salt_entry.insert(0, self.config.data["salt"])
|
||||
self.salt_entry.grid(row=1, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
||||
self._add_tooltip(
|
||||
[salt_label, self.salt_entry],
|
||||
"Optional extra text mixed into encryption key generation.\n\n"
|
||||
"It can be any combination of letters and numbers (you can also include symbols/spaces if desired).\n"
|
||||
"Use the same exact salt on all devices if encryption is enabled.\n"
|
||||
"If left blank, encryption still works using password + hash rounds.\n"
|
||||
"Changing salt later will prevent old devices from decrypting new clipboard data.",
|
||||
)
|
||||
|
||||
# Save Password Locally Checkbox
|
||||
save_password_label = ttk.Label(
|
||||
self.extra_frame,
|
||||
text="Store Password Locally\n(not recommended; \nonly works if encryption is disabled):",
|
||||
)
|
||||
save_password_label.grid(row=2, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.save_password_var = tk.BooleanVar(value=self.config.data["save_password"])
|
||||
self.save_password_checkbox = ttk.Checkbutton(
|
||||
self.extra_frame,
|
||||
variable=self.save_password_var,
|
||||
takefocus=False,
|
||||
)
|
||||
self.save_password_checkbox.grid(row=2, column=1, padx=10, pady=5, sticky=tk.W)
|
||||
self._add_tooltip(
|
||||
[save_password_label, self.save_password_checkbox],
|
||||
"Stores your plain password locally for automatic login convenience.\n\n"
|
||||
"Security trade-off: avoid this on shared or untrusted machines.\n"
|
||||
"This only works when encryption is disabled.",
|
||||
)
|
||||
|
||||
# Maximum Clipboard Size - Local Limit
|
||||
local_clipboard_size_label = ttk.Label(
|
||||
self.extra_frame, text="Maximum Clipboard Size\nLocal Limit (in bytes):"
|
||||
)
|
||||
local_clipboard_size_label.grid(row=3, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.local_clipboard_size_entry = ttk.Entry(
|
||||
self.extra_frame, width=50, font=("Helvetica", 13)
|
||||
)
|
||||
self.local_clipboard_size_entry.insert(
|
||||
0, str(self.config.data["max_clipboard_size_local_limit_bytes"] or "")
|
||||
)
|
||||
self.local_clipboard_size_entry.grid(row=3, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
||||
self._add_tooltip(
|
||||
[local_clipboard_size_label, self.local_clipboard_size_entry],
|
||||
"Optional local safety limit for clipboard payload size (incoming and outgoing) in bytes.\n\n"
|
||||
"Example: 1 MiB = 1048576 bytes.\n"
|
||||
"Leave empty for no local limit.\n"
|
||||
"Use a lower value if your device struggles with very large clipboard data "
|
||||
"(large text/images/files). Must be a positive integer.",
|
||||
)
|
||||
|
||||
# Enable Image Sharing Checkbox
|
||||
enable_image_sharing_label = ttk.Label(self.extra_frame, text="Enable Image Sharing:")
|
||||
enable_image_sharing_label.grid(row=4, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.enable_image_sharing_var = tk.BooleanVar(
|
||||
value=self.config.data["enable_image_sharing"]
|
||||
)
|
||||
self.enable_image_sharing_checkbox = ttk.Checkbutton(
|
||||
self.extra_frame,
|
||||
variable=self.enable_image_sharing_var,
|
||||
takefocus=False,
|
||||
)
|
||||
self.enable_image_sharing_checkbox.grid(row=4, column=1, padx=10, pady=5, sticky=tk.W)
|
||||
self._add_tooltip(
|
||||
[enable_image_sharing_label, self.enable_image_sharing_checkbox],
|
||||
"Controls whether this device sends copied images to other devices.\n\n"
|
||||
"If disabled, this device can still receive images sent from other devices.",
|
||||
)
|
||||
|
||||
# Enable File Sharing Checkbox
|
||||
enable_file_sharing_label = ttk.Label(self.extra_frame, text="Enable File Sharing:")
|
||||
enable_file_sharing_label.grid(row=5, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.enable_file_sharing_var = tk.BooleanVar(value=self.config.data["enable_file_sharing"])
|
||||
self.enable_file_sharing_checkbox = ttk.Checkbutton(
|
||||
self.extra_frame,
|
||||
variable=self.enable_file_sharing_var,
|
||||
takefocus=False,
|
||||
)
|
||||
self.enable_file_sharing_checkbox.grid(row=5, column=1, padx=10, pady=5, sticky=tk.W)
|
||||
self._add_tooltip(
|
||||
[enable_file_sharing_label, self.enable_file_sharing_checkbox],
|
||||
"Controls whether this device sends copied/shared files to other devices.\n\n"
|
||||
"If disabled, this device can still receive files sent from other devices.",
|
||||
)
|
||||
|
||||
# Default File Download Location
|
||||
default_file_download_location_label = ttk.Label(
|
||||
self.extra_frame, text="Default File Download Location:"
|
||||
)
|
||||
default_file_download_location_label.grid(
|
||||
row=6, column=0, padx=(0, 10), pady=5, sticky=tk.W
|
||||
)
|
||||
self.default_file_download_location_entry = ttk.Entry(
|
||||
self.extra_frame, width=50, font=("Helvetica", 13)
|
||||
)
|
||||
self.default_file_download_location_entry.insert(
|
||||
0, self.config.data["default_file_download_location"]
|
||||
)
|
||||
self.default_file_download_location_entry.grid(
|
||||
row=6, column=1, padx=10, pady=5, sticky=tk.W + tk.E
|
||||
)
|
||||
self._add_tooltip(
|
||||
[default_file_download_location_label, self.default_file_download_location_entry],
|
||||
"Optional default folder to save incoming files automatically.\n\n"
|
||||
"Examples:\n"
|
||||
"- Windows: C:\\Users\\YourName\\Downloads\n"
|
||||
"- macOS/Linux: /Users/yourname/Downloads\n\n"
|
||||
"Leave blank to choose a location manually when downloading files.\n"
|
||||
"Path must already exist on this device.",
|
||||
)
|
||||
|
||||
# SSL CA bundle (private / corporate PKI)
|
||||
ssl_ca_label = ttk.Label(self.extra_frame, text="SSL CA bundle (optional):")
|
||||
ssl_ca_label.grid(row=7, column=0, padx=(0, 10), pady=5, sticky=tk.W)
|
||||
self.ssl_ca_bundle_entry = ttk.Entry(
|
||||
self.extra_frame, width=50, font=("Helvetica", 13)
|
||||
)
|
||||
self.ssl_ca_bundle_entry.insert(
|
||||
0, self.config.data.get("ssl_ca_bundle") or ""
|
||||
)
|
||||
self.ssl_ca_bundle_entry.grid(row=7, column=1, padx=10, pady=5, sticky=tk.W + tk.E)
|
||||
self._add_tooltip(
|
||||
[ssl_ca_label, self.ssl_ca_bundle_entry],
|
||||
"Path to a PEM file that contains your root CA (or full chain) for HTTPS/WSS.\n\n"
|
||||
"Leave empty for default verification (public CAs / OS defaults).\n"
|
||||
"Use this when your server uses a certificate from a private or internal CA.\n\n"
|
||||
"Example: /etc/ssl/corp-root.pem or C:\\\\certs\\\\corp-root.pem",
|
||||
)
|
||||
|
||||
# Configure grid weights for extra_frame
|
||||
self.extra_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Bind mouse wheel events for scrolling
|
||||
self._bind_mousewheel()
|
||||
|
||||
# Handle window close
|
||||
self.protocol("WM_DELETE_WINDOW", self.on_quit)
|
||||
|
||||
# Update idle tasks to calculate widget sizes
|
||||
self.update_idletasks()
|
||||
|
||||
# Set a maximum height for the window
|
||||
self.max_window_height = 600
|
||||
|
||||
# on press enter key
|
||||
self.bind("<Return>", lambda event: self.on_login())
|
||||
|
||||
self._sync_login_tab_order()
|
||||
|
||||
self.deiconify() # Show window
|
||||
self.update_idletasks()
|
||||
center_window(self, self.max_window_height)
|
||||
|
||||
# Drop always-on-top after showing
|
||||
self.after(300, lambda: self.attributes("-topmost", False))
|
||||
|
||||
if PLATFORM == MACOS:
|
||||
self.after(400, self._macos_restore_focus)
|
||||
|
||||
def _sync_login_tab_order(self):
|
||||
"""Tab cycles username → password → server → Login → [extra entries if shown] → repeat.
|
||||
Checkboxes and chrome are excluded from keyboard focus."""
|
||||
extra_entries = (
|
||||
self.hash_rounds_entry,
|
||||
self.salt_entry,
|
||||
self.local_clipboard_size_entry,
|
||||
self.default_file_download_location_entry,
|
||||
self.ssl_ca_bundle_entry,
|
||||
)
|
||||
for w in extra_entries:
|
||||
w.configure(takefocus=bool(self.show_extra))
|
||||
|
||||
def _add_tooltip(self, widgets, text):
|
||||
for widget in widgets:
|
||||
self._tooltips.append(HoverTooltip(widget, text))
|
||||
|
||||
def _macos_restore_focus(self):
|
||||
"""Force macOS to properly activate and focus the window.
|
||||
Needed after sequential tk.Tk() creation/destruction cycles."""
|
||||
try:
|
||||
from AppKit import NSApplication
|
||||
app = NSApplication.sharedApplication()
|
||||
app.activateIgnoringOtherApps_(True)
|
||||
except ImportError:
|
||||
pass
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
self.username_entry.focus_set()
|
||||
|
||||
def _toggle_password(self, event=None):
|
||||
if self.password_entry.cget("show") == "*":
|
||||
self.password_entry.config(show="")
|
||||
self.eye_icon.config(text="🐵")
|
||||
else:
|
||||
self.password_entry.config(show="*")
|
||||
self.eye_icon.config(text="🙈")
|
||||
|
||||
def _toggle_extra_config(self, event=None):
|
||||
"""Toggle the visibility of the extra configuration fields."""
|
||||
extra_entries = (
|
||||
self.hash_rounds_entry,
|
||||
self.salt_entry,
|
||||
self.local_clipboard_size_entry,
|
||||
self.default_file_download_location_entry,
|
||||
self.ssl_ca_bundle_entry,
|
||||
)
|
||||
if self.show_extra:
|
||||
focused = self.focus_get()
|
||||
if focused in extra_entries:
|
||||
self.login_button.focus_set()
|
||||
self.extra_frame_container.pack_forget()
|
||||
self.toggle_label.config(text="Enable Extra Config")
|
||||
else:
|
||||
self.extra_frame_container.pack(fill=tk.BOTH, expand=True)
|
||||
self.toggle_label.config(text="Hide Extra Config")
|
||||
self.show_extra = not self.show_extra
|
||||
self._sync_login_tab_order()
|
||||
|
||||
self.update_idletasks()
|
||||
self.geometry("")
|
||||
self.update_idletasks()
|
||||
center_window(self)
|
||||
|
||||
def _on_hover(self, event=None):
|
||||
"""Change text color on hover."""
|
||||
self.toggle_label.config(fg=self.toggle_hover_color)
|
||||
|
||||
def _on_leave(self, event=None):
|
||||
"""Revert text color when not hovered."""
|
||||
self.toggle_label.config(fg=self.toggle_normal_color)
|
||||
|
||||
def _bind_mousewheel(self):
|
||||
"""Bind mouse wheel events to the canvas for scrolling."""
|
||||
if PLATFORM == WINDOWS:
|
||||
self.extra_canvas.bind_all("<MouseWheel>", self._on_mousewheel_windows)
|
||||
elif PLATFORM == MACOS:
|
||||
self.extra_canvas.bind_all("<MouseWheel>", self._on_mousewheel_mac)
|
||||
elif PLATFORM.startswith(LINUX):
|
||||
# Linux uses Button-4 and Button-5 for scroll up/down
|
||||
self.extra_canvas.bind_all("<Button-4>", self._on_mousewheel_linux)
|
||||
self.extra_canvas.bind_all("<Button-5>", self._on_mousewheel_linux)
|
||||
|
||||
def _on_mousewheel_windows(self, event):
|
||||
"""Handle mouse wheel scrolling on Windows."""
|
||||
self.extra_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||
|
||||
def _on_mousewheel_mac(self, event):
|
||||
"""Handle mouse wheel scrolling on macOS."""
|
||||
self.extra_canvas.yview_scroll(int(-1 * event.delta), "units")
|
||||
|
||||
def _on_mousewheel_linux(self, event):
|
||||
"""Handle mouse wheel scrolling on Linux."""
|
||||
if event.num == 4:
|
||||
self.extra_canvas.yview_scroll(-1, "units")
|
||||
elif event.num == 5:
|
||||
self.extra_canvas.yview_scroll(1, "units")
|
||||
|
||||
def _on_extra_container_configure(self, event):
|
||||
"""Adjust the width of the extra_frame as the container is resized."""
|
||||
canvas_width = event.width - self.scrollbar.winfo_width()
|
||||
self.extra_canvas.itemconfigure(self.extra_frame_window, width=canvas_width)
|
||||
|
||||
@staticmethod
|
||||
def remove_trailing_slash(entry):
|
||||
return re.sub(r"/+$", "", entry)
|
||||
|
||||
@staticmethod
|
||||
def is_positive_integer(value):
|
||||
if value.isdigit() and int(value) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def on_login(self):
|
||||
# save data to config
|
||||
self.config.data["username"] = self.username_entry.get()
|
||||
self.config.data["password"] = self.password_entry.get()
|
||||
self.config.data["server_url"] = LoginForm.remove_trailing_slash(
|
||||
self.server_url_entry.get().strip()
|
||||
)
|
||||
self.config.data["cipher_enabled"] = self.cipher_var.get()
|
||||
self.config.data["notification"] = self.notification_var.get()
|
||||
|
||||
# extra fields
|
||||
|
||||
# Validate hash_rounds
|
||||
hash_rounds_input = self.hash_rounds_entry.get().strip()
|
||||
if not LoginForm.is_positive_integer(hash_rounds_input):
|
||||
CustomDialog(
|
||||
"Invalid Input\nHash Rounds must be a positive integer.",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
return # retry login
|
||||
self.config.data["hash_rounds"] = int(hash_rounds_input)
|
||||
|
||||
self.config.data["salt"] = self.salt_entry.get()
|
||||
self.config.data["save_password"] = self.save_password_var.get()
|
||||
|
||||
# Validate max_clipboard_size_local_limit_bytes
|
||||
max_clipboard_size_local_limit_bytes_input = self.local_clipboard_size_entry.get().strip()
|
||||
if max_clipboard_size_local_limit_bytes_input == "":
|
||||
self.config.data["max_clipboard_size_local_limit_bytes"] = None
|
||||
else:
|
||||
if not LoginForm.is_positive_integer(max_clipboard_size_local_limit_bytes_input):
|
||||
CustomDialog(
|
||||
"Invalid Input\nMax Clipboard Size Local Limit must be a positive integer.",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
return # retry login
|
||||
self.config.data["max_clipboard_size_local_limit_bytes"] = int(
|
||||
max_clipboard_size_local_limit_bytes_input
|
||||
)
|
||||
|
||||
self.config.data["enable_image_sharing"] = self.enable_image_sharing_var.get()
|
||||
self.config.data["enable_file_sharing"] = self.enable_file_sharing_var.get()
|
||||
|
||||
# Validate file download location
|
||||
default_file_download_location = self.default_file_download_location_entry.get().strip()
|
||||
if default_file_download_location == "":
|
||||
self.config.data["default_file_download_location"] = ""
|
||||
else:
|
||||
if not os.path.exists(default_file_download_location):
|
||||
CustomDialog(
|
||||
"Invalid Input\nDefault File Download Location does not exist.",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
return # retry login
|
||||
self.config.data["default_file_download_location"] = default_file_download_location
|
||||
|
||||
ssl_ca_bundle = self.ssl_ca_bundle_entry.get().strip()
|
||||
if ssl_ca_bundle:
|
||||
if not os.path.isfile(ssl_ca_bundle):
|
||||
CustomDialog(
|
||||
"Invalid Input\nSSL CA bundle path is not a file or does not exist.",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
return
|
||||
self.config.data["ssl_ca_bundle"] = ssl_ca_bundle
|
||||
|
||||
# call login callback
|
||||
if self.on_login_callback:
|
||||
self.on_login_callback()
|
||||
|
||||
self.close()
|
||||
|
||||
def on_quit(self):
|
||||
if self.on_quit_callback:
|
||||
self.on_quit_callback()
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
self.destroy()
|
||||
gc.collect()
|
||||
time.sleep(0.5)
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
from tkinter import messagebox
|
||||
|
||||
|
||||
class MessageBox:
|
||||
def askquestion(self, title, message):
|
||||
return messagebox.askquestion(title, message)
|
||||
Executable
+397
@@ -0,0 +1,397 @@
|
||||
import math
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
import webbrowser
|
||||
from pystray import Icon, MenuItem as item, Menu
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from core.config import Config
|
||||
from gui.info import CustomDialog
|
||||
from core.constants import *
|
||||
|
||||
if PLATFORM != WINDOWS:
|
||||
import subprocess
|
||||
|
||||
|
||||
class TaskbarPanel:
|
||||
def __init__(
|
||||
self,
|
||||
on_connect_callback: callable = None,
|
||||
on_disconnect_callback: callable = None,
|
||||
on_logoff_callback: callable = None,
|
||||
new_version_available: list = None,
|
||||
github_url: str = GITHUB_URL,
|
||||
donation_url: str = None,
|
||||
ws_interface=None, # type= interfaces.ws_interface.WSInterface
|
||||
config: Config = None,
|
||||
):
|
||||
self.on_connect_callback = on_connect_callback
|
||||
self.on_disconnect_callback = on_disconnect_callback
|
||||
self.on_logoff_callback = on_logoff_callback
|
||||
self.new_version_available = new_version_available
|
||||
self.github_url = github_url
|
||||
self.donation_url = donation_url
|
||||
self.ws_interface = ws_interface
|
||||
self.config = config
|
||||
|
||||
self.is_disconnecting = False
|
||||
self.disconnecting_items = None
|
||||
self.is_file_download_enabled = False
|
||||
self.file_download_items = None
|
||||
self.previous_stats: str = ""
|
||||
self.previous_stats_items = None
|
||||
|
||||
self.root = tk.Tk()
|
||||
self.root.withdraw() # Hide the root window
|
||||
|
||||
# Hide dock icon on macOS after creating tkinter window
|
||||
if PLATFORM == MACOS:
|
||||
try:
|
||||
from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
|
||||
NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Initial state: Connected
|
||||
self.is_connected = True
|
||||
|
||||
# Create the tray icon
|
||||
self.icon = Icon(
|
||||
"ClipCascade", self.create_clipboard_icon(), menu=self.create_menu()
|
||||
)
|
||||
|
||||
self.icon.title = "ClipCascade"
|
||||
|
||||
self.update_stats() # Start the stats update thread
|
||||
|
||||
def run(self):
|
||||
self.icon.run()
|
||||
|
||||
def _create_clipboard_base_image(self):
|
||||
"""Shared clipboard artwork for normal tray icon and file-download badge variant."""
|
||||
width, height = 64, 64
|
||||
image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
fill_color = (220, 220, 220)
|
||||
outline_color = (255, 255, 255)
|
||||
|
||||
board_coords = (12, 12, 52, 57)
|
||||
try:
|
||||
draw.rounded_rectangle(
|
||||
board_coords, radius=5, fill=None, outline=outline_color, width=3
|
||||
)
|
||||
except (AttributeError, TypeError):
|
||||
draw.rectangle(board_coords, fill=None, outline=outline_color)
|
||||
|
||||
clip_coords = (22, 7, 42, 17)
|
||||
try:
|
||||
draw.rounded_rectangle(
|
||||
clip_coords, radius=3, fill=fill_color, outline=outline_color, width=3
|
||||
)
|
||||
except (AttributeError, TypeError):
|
||||
draw.rectangle(clip_coords, fill=fill_color, outline=outline_color)
|
||||
|
||||
return image
|
||||
|
||||
def create_clipboard_icon(self):
|
||||
return self._create_clipboard_base_image()
|
||||
|
||||
def create_clipboard_icon_with_dot(self):
|
||||
"""Same clipboard as normal state, plus a blue notification dot for pending file download."""
|
||||
image = self._create_clipboard_base_image().copy()
|
||||
draw = ImageDraw.Draw(image)
|
||||
width, height = 64, 64
|
||||
|
||||
dot_radius = 8
|
||||
dot_center_x = width - dot_radius - 5
|
||||
dot_center_y = dot_radius + 5
|
||||
dot_bbox = [
|
||||
dot_center_x - dot_radius,
|
||||
dot_center_y - dot_radius,
|
||||
dot_center_x + dot_radius,
|
||||
dot_center_y + dot_radius,
|
||||
]
|
||||
draw.ellipse(dot_bbox, fill=(0, 128, 255, 255))
|
||||
|
||||
highlight_radius = dot_radius // 2
|
||||
highlight_center_x = dot_center_x - highlight_radius // 2
|
||||
highlight_center_y = dot_center_y - highlight_radius // 2
|
||||
highlight_bbox = [
|
||||
highlight_center_x - highlight_radius,
|
||||
highlight_center_y - highlight_radius,
|
||||
highlight_center_x + highlight_radius,
|
||||
highlight_center_y + highlight_radius,
|
||||
]
|
||||
draw.ellipse(highlight_bbox, fill=(255, 255, 255, 180))
|
||||
|
||||
return image
|
||||
|
||||
def create_menu(self, item_: tuple = None):
|
||||
"""Create the menu for the tray icon.
|
||||
|
||||
Args:
|
||||
item_ (tuple, optional): The item that triggered the menu. Defaults to None.
|
||||
item_ = (text, location, callback)
|
||||
Returns:
|
||||
Menu: The menu for the tray icon.
|
||||
"""
|
||||
# Menu items
|
||||
menu_items = [
|
||||
Menu.SEPARATOR,
|
||||
item("🗒️ Open Logs", self._open_logs),
|
||||
item("📂 Program Files", self._open_program_location),
|
||||
Menu.SEPARATOR,
|
||||
item("🏠 Homepage", self._open_homepage),
|
||||
item("❓ Help", self._open_help),
|
||||
item("💟 Donate", self._open_donate),
|
||||
item("🌐 GitHub", self._open_github),
|
||||
Menu.SEPARATOR,
|
||||
item("🔒 Logoff and Quit", self._on_logoff),
|
||||
item("❌ Quit", self._on_quit),
|
||||
]
|
||||
|
||||
# Add connect/disconnect option (top of the menu - 0 index)
|
||||
if not self.is_connected:
|
||||
menu_items.insert(0, item("🔗 Connect", self._on_connect, default=True))
|
||||
else:
|
||||
if self.is_disconnecting and self.disconnecting_items is not None:
|
||||
menu_items.insert(
|
||||
self.disconnecting_items[1],
|
||||
item(self.disconnecting_items[0], self.disconnecting_items[2]),
|
||||
)
|
||||
else:
|
||||
menu_items.insert(0, item("⛓️💥 Disconnect", self._on_disconnect))
|
||||
|
||||
# Add update option (before the last 3 items)
|
||||
if self.new_version_available is not None and self.new_version_available[0]:
|
||||
menu_items.insert(
|
||||
len(menu_items) - 3,
|
||||
item(
|
||||
f"🔄 Update ({self.new_version_available[2]} ➞ {self.new_version_available[1]})",
|
||||
self._on_update,
|
||||
),
|
||||
)
|
||||
|
||||
# Add files download option (top of the menu - 0 index)
|
||||
if self.is_file_download_enabled and self.file_download_items is not None:
|
||||
menu_items.insert(
|
||||
self.file_download_items[1],
|
||||
item(
|
||||
self.file_download_items[0],
|
||||
self.file_download_items[2],
|
||||
default=True,
|
||||
),
|
||||
)
|
||||
|
||||
# Add stats option (top of the menu - 0 index)
|
||||
if self.previous_stats_items is not None:
|
||||
menu_items.insert(
|
||||
self.previous_stats_items[1],
|
||||
item(
|
||||
self.previous_stats_items[0],
|
||||
self.previous_stats_items[2],
|
||||
enabled=self.previous_stats_items[2] is not None,
|
||||
),
|
||||
)
|
||||
|
||||
return Menu(*menu_items)
|
||||
|
||||
def update_menu(self, item_: tuple = None):
|
||||
self.icon.menu = self.create_menu(item_=item_)
|
||||
|
||||
def update_stats(self):
|
||||
threading.Thread(target=self._update_stats_thread, daemon=True).start()
|
||||
|
||||
def _update_stats_thread(self):
|
||||
while True:
|
||||
current_stats = self.ws_interface.get_stats()
|
||||
if current_stats is not None and self.previous_stats != current_stats:
|
||||
self.previous_stats = current_stats
|
||||
self.previous_stats_items = (current_stats, 0, None)
|
||||
self.update_menu()
|
||||
time.sleep(1) # Sleep for 1 second
|
||||
|
||||
@staticmethod
|
||||
def open_webbrowser(url):
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception as e:
|
||||
CustomDialog(
|
||||
f"Failed to open the browser. Here is the URL: {url}\nError: {e}",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
|
||||
def _on_update(self, icon, item):
|
||||
TaskbarPanel.open_webbrowser(self.new_version_available[3])
|
||||
|
||||
def _on_connect(self, icon, item):
|
||||
if self.on_connect_callback:
|
||||
try:
|
||||
self.on_connect_callback()
|
||||
except Exception as e:
|
||||
pass
|
||||
self.is_connected = True
|
||||
self.update_menu()
|
||||
|
||||
def _on_disconnect(self, icon, item):
|
||||
if self.on_disconnect_callback:
|
||||
self.on_disconnect_callback()
|
||||
if self.ws_interface is not None and self.ws_interface.is_auto_reconnecting:
|
||||
threading.Thread(
|
||||
target=self._wait_to_disconnect, args=(icon, item), daemon=True
|
||||
).start()
|
||||
else:
|
||||
self.is_connected = False
|
||||
self.update_menu()
|
||||
|
||||
def _wait_to_disconnect(self, icon, item):
|
||||
"""
|
||||
Wait for the auto-reconnect timer to expire before disconnecting.
|
||||
"""
|
||||
if self.ws_interface is None:
|
||||
return
|
||||
|
||||
timeout = math.ceil(self.ws_interface.get_total_timeout() / 1000)
|
||||
while timeout > 0:
|
||||
self.is_disconnecting = True
|
||||
self.disconnecting_items = (
|
||||
f"⏳ Disconnecting... ({timeout} sec)",
|
||||
0,
|
||||
None,
|
||||
) # text, location, callback
|
||||
self.update_menu()
|
||||
time.sleep(1) # seconds
|
||||
timeout -= 1
|
||||
self.is_disconnecting = False
|
||||
self.disconnecting_items = None
|
||||
self.ws_interface.is_auto_reconnecting = False
|
||||
self.is_connected = False
|
||||
self.update_menu()
|
||||
|
||||
def _open_homepage(self, icon, item):
|
||||
TaskbarPanel.open_webbrowser(self.config.data["server_url"])
|
||||
|
||||
def _open_github(self, icon, item):
|
||||
TaskbarPanel.open_webbrowser(self.github_url)
|
||||
|
||||
def _open_help(self, icon, item):
|
||||
TaskbarPanel.open_webbrowser(HELP_URL)
|
||||
|
||||
def _open_donate(self, icon, item):
|
||||
if self.donation_url is not None:
|
||||
TaskbarPanel.open_webbrowser(self.donation_url)
|
||||
|
||||
def open_location(self, path):
|
||||
if PLATFORM == WINDOWS:
|
||||
os.startfile(path)
|
||||
elif PLATFORM == MACOS:
|
||||
subprocess.run(["open", path])
|
||||
elif PLATFORM.startswith(LINUX):
|
||||
subprocess.run(["xdg-open", path])
|
||||
|
||||
def _open_logs(self, icon, item):
|
||||
log_file_path = os.path.join(get_program_files_directory(), LOG_FILE_NAME)
|
||||
if os.path.exists(log_file_path):
|
||||
try:
|
||||
self.open_location(log_file_path)
|
||||
except Exception as e:
|
||||
CustomDialog(
|
||||
f"Failed to open the log file '{log_file_path}'.\nError: {e}",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
else:
|
||||
CustomDialog(
|
||||
f"Log file not found at '{log_file_path}'.", msg_type="error"
|
||||
).mainloop()
|
||||
|
||||
def _open_program_location(self, icon, item):
|
||||
try:
|
||||
program_location = get_program_files_directory()
|
||||
self.open_location(program_location)
|
||||
except Exception as e:
|
||||
CustomDialog(
|
||||
f"Failed to open the program location '{program_location}'.\nError: {e}",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
|
||||
def enable_files_download(self, files):
|
||||
self.is_file_download_enabled = True
|
||||
self.icon.icon = self.create_clipboard_icon_with_dot()
|
||||
self.file_download_items = (
|
||||
"📥 Download File(s)",
|
||||
0,
|
||||
lambda icon, item: self._on_download(icon, item, files),
|
||||
)
|
||||
self.update_menu()
|
||||
|
||||
def disable_files_download(self):
|
||||
self.is_file_download_enabled = False
|
||||
self.file_download_items = None
|
||||
self.icon.icon = self.create_clipboard_icon()
|
||||
self.update_menu()
|
||||
|
||||
def _on_download(self, icon, item, files):
|
||||
"""Download the files to the user's Downloads folder."""
|
||||
try:
|
||||
try:
|
||||
if self.config.data["default_file_download_location"] != "":
|
||||
target_directory = self.config.data[
|
||||
"default_file_download_location"
|
||||
]
|
||||
else:
|
||||
target_directory = filedialog.askdirectory(
|
||||
title="Select location to Save File(s)",
|
||||
initialdir=get_downloads_folder(),
|
||||
)
|
||||
if not target_directory:
|
||||
logging.debug("No directory selected. Exiting.")
|
||||
return
|
||||
except RuntimeError as re:
|
||||
target_directory = os.path.join(
|
||||
get_program_files_directory(), "downloads"
|
||||
)
|
||||
if not os.path.exists(target_directory):
|
||||
os.makedirs(target_directory)
|
||||
logging.error(
|
||||
f"A runtime error occurred while starting filedialog to select a directory. Error: {re}.\n"
|
||||
+ f"Setting the default location to the program directory '{target_directory}'."
|
||||
)
|
||||
CustomDialog(
|
||||
f"ClipCascade 📥: Saving files to the program directory '{target_directory}'.",
|
||||
msg_type="info",
|
||||
timeout=5000,
|
||||
).mainloop()
|
||||
|
||||
# Save each file to the chosen directory
|
||||
for filename, file_obj in files.items():
|
||||
file_path = os.path.join(target_directory, filename)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file_obj.getvalue())
|
||||
logging.debug(f"Saved: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
msg = f"An error occurred while downloading files. Error: {e}"
|
||||
logging.error(msg)
|
||||
CustomDialog(
|
||||
msg,
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
|
||||
def _on_logoff(self, icon, item):
|
||||
try:
|
||||
if self.on_logoff_callback:
|
||||
self.on_logoff_callback()
|
||||
self.icon.stop()
|
||||
self.root.quit()
|
||||
except Exception as e:
|
||||
CustomDialog(
|
||||
f"An error occurred while logging off: {e}", msg_type="error"
|
||||
).mainloop()
|
||||
|
||||
def _on_quit(self, icon, item):
|
||||
self.icon.stop()
|
||||
self.root.quit()
|
||||
Reference in New Issue
Block a user