added clipcascade
This commit is contained in:
Executable
+3
@@ -0,0 +1,3 @@
|
||||
class Echo:
|
||||
def __init__(self, *args, **kwargs):
|
||||
print(*args, **kwargs)
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
from cli.echo import Echo
|
||||
|
||||
|
||||
class CustomDialog:
|
||||
def __init__(self, message, msg_type="", timeout=None):
|
||||
self.message = message
|
||||
self.msg_type = msg_type.lower()
|
||||
self.timeout = timeout
|
||||
|
||||
def mainloop(self):
|
||||
if self.msg_type == "success":
|
||||
Echo("✅", self.message)
|
||||
elif self.msg_type == "error":
|
||||
Echo("❌", self.message)
|
||||
elif self.msg_type == "warning":
|
||||
Echo("⚠️", self.message)
|
||||
elif self.msg_type == "info":
|
||||
Echo("ℹ", self.message)
|
||||
else:
|
||||
Echo(self.message)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
Executable
+214
@@ -0,0 +1,214 @@
|
||||
import getpass
|
||||
import os
|
||||
import re
|
||||
|
||||
from core.config import Config
|
||||
from cli.info import CustomDialog
|
||||
from core.constants import *
|
||||
|
||||
|
||||
class LoginForm:
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
on_login_callback=None,
|
||||
on_quit_callback=None,
|
||||
):
|
||||
self.config = config
|
||||
self.on_login_callback = on_login_callback
|
||||
self.on_quit_callback = on_quit_callback
|
||||
|
||||
@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
|
||||
|
||||
@staticmethod
|
||||
def bool_to_str(value):
|
||||
return "y" if value else "n"
|
||||
|
||||
@staticmethod
|
||||
def str_to_bool(value):
|
||||
return value.lower().strip() == "y"
|
||||
|
||||
@staticmethod
|
||||
def is_positive_integer(value):
|
||||
if value.isdigit() and int(value) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def mainloop(self):
|
||||
# save data to config
|
||||
self.config.data["username"] = (
|
||||
input(f"username [{self.config.data['username']}]: ")
|
||||
or self.config.data["username"]
|
||||
)
|
||||
|
||||
self.config.data["password"] = getpass.getpass("password: ")
|
||||
|
||||
server_url = (
|
||||
input(f"server url [{self.config.data['server_url']}]: ")
|
||||
or self.config.data["server_url"]
|
||||
)
|
||||
self.config.data["server_url"] = LoginForm.remove_trailing_slash(
|
||||
server_url.strip()
|
||||
)
|
||||
|
||||
self.config.data["cipher_enabled"] = LoginForm.str_to_bool(
|
||||
input(
|
||||
f"enabled encryption(recommended) [{LoginForm.bool_to_str(self.config.data['cipher_enabled'])}]: "
|
||||
)
|
||||
or LoginForm.bool_to_str(self.config.data["cipher_enabled"])
|
||||
)
|
||||
|
||||
self.config.data["notification"] = LoginForm.str_to_bool(
|
||||
input(
|
||||
f"enabled notification [{LoginForm.bool_to_str(self.config.data['notification'])}]: "
|
||||
)
|
||||
or LoginForm.bool_to_str(self.config.data["notification"])
|
||||
)
|
||||
|
||||
# extra fields
|
||||
show_extra_fields = input(f"show extra fields [n]: ") or "n"
|
||||
if not LoginForm.str_to_bool(show_extra_fields):
|
||||
CustomDialog("Logging in...").mainloop()
|
||||
return
|
||||
|
||||
# Validate hash_rounds
|
||||
while True:
|
||||
hash_rounds = input(
|
||||
f"hash rounds [{self.config.data['hash_rounds']}]: "
|
||||
) or str(
|
||||
""
|
||||
if self.config.data["hash_rounds"] is None
|
||||
else str(self.config.data["hash_rounds"])
|
||||
)
|
||||
hash_rounds = hash_rounds.strip()
|
||||
|
||||
if not LoginForm.is_positive_integer(hash_rounds):
|
||||
CustomDialog(
|
||||
"Invalid Input\nHash Rounds must be a positive integer.",
|
||||
msg_type="error",
|
||||
)
|
||||
continue
|
||||
|
||||
self.config.data["hash_rounds"] = int(hash_rounds)
|
||||
break
|
||||
|
||||
self.config.data["salt"] = (
|
||||
input(f"salt [{self.config.data['salt']}]: ") or self.config.data["salt"]
|
||||
)
|
||||
|
||||
self.config.data["save_password"] = LoginForm.str_to_bool(
|
||||
input(
|
||||
f"store password locally(not recommended; only works if encryption is disabled) [{LoginForm.bool_to_str(self.config.data['save_password'])}]: "
|
||||
)
|
||||
or LoginForm.bool_to_str(self.config.data["save_password"])
|
||||
)
|
||||
|
||||
# Validate max_clipboard_size_local_limit_bytes
|
||||
while True:
|
||||
saved_mcsll = str(
|
||||
""
|
||||
if self.config.data["max_clipboard_size_local_limit_bytes"] is None
|
||||
else str(self.config.data["max_clipboard_size_local_limit_bytes"])
|
||||
)
|
||||
|
||||
max_clipboard_size_local_limit_bytes = (
|
||||
input(
|
||||
f"maximum clipboard size local limit (in bytes) [{saved_mcsll}]: "
|
||||
)
|
||||
or saved_mcsll
|
||||
)
|
||||
max_clipboard_size_local_limit_bytes = (
|
||||
max_clipboard_size_local_limit_bytes.strip()
|
||||
)
|
||||
if max_clipboard_size_local_limit_bytes == "":
|
||||
self.config.data["max_clipboard_size_local_limit_bytes"] = None
|
||||
break
|
||||
|
||||
if not LoginForm.is_positive_integer(max_clipboard_size_local_limit_bytes):
|
||||
CustomDialog(
|
||||
"Invalid Input\nMax Clipboard Size Local Limit must be a positive integer.",
|
||||
msg_type="error",
|
||||
)
|
||||
continue
|
||||
|
||||
self.config.data["max_clipboard_size_local_limit_bytes"] = int(
|
||||
max_clipboard_size_local_limit_bytes
|
||||
)
|
||||
break
|
||||
|
||||
self.config.data["enable_image_sharing"] = LoginForm.str_to_bool(
|
||||
input(
|
||||
f"enable image sharing [{LoginForm.bool_to_str(self.config.data['enable_image_sharing'])}]: "
|
||||
)
|
||||
or LoginForm.bool_to_str(self.config.data["enable_image_sharing"])
|
||||
)
|
||||
|
||||
self.config.data["enable_file_sharing"] = LoginForm.str_to_bool(
|
||||
input(
|
||||
f"enable file sharing [{LoginForm.bool_to_str(self.config.data['enable_file_sharing'])}]: "
|
||||
)
|
||||
or LoginForm.bool_to_str(self.config.data["enable_file_sharing"])
|
||||
)
|
||||
|
||||
# Validate default file download location
|
||||
while True:
|
||||
default_file_download_location = (
|
||||
input(
|
||||
f"default file download location [{self.config.data['default_file_download_location']}]: "
|
||||
)
|
||||
or self.config.data["default_file_download_location"]
|
||||
)
|
||||
|
||||
default_file_download_location = default_file_download_location.strip()
|
||||
if default_file_download_location == "":
|
||||
self.config.data["default_file_download_location"] = ""
|
||||
break
|
||||
|
||||
if not os.path.isdir(default_file_download_location):
|
||||
CustomDialog(
|
||||
"Invalid directory path.",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
continue
|
||||
|
||||
self.config.data["default_file_download_location"] = (
|
||||
default_file_download_location
|
||||
)
|
||||
break
|
||||
|
||||
while True:
|
||||
ssl_ca_bundle = (
|
||||
input(
|
||||
f"ssl ca bundle path (optional) [{self.config.data.get('ssl_ca_bundle') or ''}]: "
|
||||
)
|
||||
or (self.config.data.get("ssl_ca_bundle") or "")
|
||||
).strip()
|
||||
if ssl_ca_bundle == "":
|
||||
self.config.data["ssl_ca_bundle"] = ""
|
||||
break
|
||||
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()
|
||||
continue
|
||||
self.config.data["ssl_ca_bundle"] = ssl_ca_bundle
|
||||
break
|
||||
|
||||
CustomDialog("Logging in...").mainloop()
|
||||
|
||||
def on_quit(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
class MessageBox:
|
||||
def askquestion(self, title, message):
|
||||
response = input(f"{title}\n{message} (y/n): ").strip().lower()
|
||||
return "yes" if (response.lower().strip() == "y") else "no"
|
||||
Executable
+367
@@ -0,0 +1,367 @@
|
||||
import math
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
|
||||
|
||||
from cli.echo import Echo
|
||||
from cli.info import CustomDialog
|
||||
from core.config import Config
|
||||
from core.constants import *
|
||||
from itertools import count
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Initial state: Connected
|
||||
self.is_connected = True
|
||||
|
||||
self.menu_items = None
|
||||
self.numbered_menu = None
|
||||
self.loop_terminate = False
|
||||
|
||||
self.update_stats() # Start the stats update thread
|
||||
|
||||
def run(self):
|
||||
Echo("-" * 53)
|
||||
self.menu_items, self.numbered_menu = self.create_menu()
|
||||
|
||||
while not self.loop_terminate:
|
||||
Echo("\n")
|
||||
|
||||
self.display_menu()
|
||||
|
||||
# input
|
||||
choice = input("Choice: \n")
|
||||
Echo("Please wait...")
|
||||
if choice == "": # refresh
|
||||
continue
|
||||
|
||||
if choice.isdigit():
|
||||
choice = int(choice)
|
||||
else:
|
||||
Echo("Invalid choice. Please enter a number.")
|
||||
|
||||
if choice in self.numbered_menu:
|
||||
fun = self.numbered_menu[choice][1]
|
||||
if fun is not None:
|
||||
fun()
|
||||
else:
|
||||
Echo("Invalid choice.")
|
||||
|
||||
def get_max_width(self):
|
||||
"""Determine the maximum width of menu item texts."""
|
||||
menu_texts = [
|
||||
item[0] if isinstance(item, tuple) else "" for item in self.menu_items
|
||||
]
|
||||
return max(len(text) for text in menu_texts) + 1
|
||||
|
||||
def display_menu(self):
|
||||
counter = count(1)
|
||||
max_width = self.get_max_width()
|
||||
|
||||
total_width = max_width + 8
|
||||
|
||||
# Box Top Border
|
||||
print("\n╔" + "═" * (max_width + 6) + "╗")
|
||||
print(f"║{' MAIN MENU ':^{max_width + 6}}║")
|
||||
print("╠" + "═" * (max_width + 6) + "╣")
|
||||
|
||||
# Menu Items
|
||||
for item in self.menu_items:
|
||||
if isinstance(item, tuple):
|
||||
width = max_width
|
||||
if "disconnect" in item[0].lower() or "logs" in item[0].lower():
|
||||
if "disconnecting" in item[0].lower():
|
||||
width -= 1
|
||||
else:
|
||||
width += 1
|
||||
else:
|
||||
width -= 1
|
||||
print(f"║ {next(counter):<2}. {item[0]:<{width}} ║")
|
||||
else:
|
||||
print(f"║ {'─' * (max_width + 4)} ║")
|
||||
|
||||
# Box Bottom Border
|
||||
print("╚" + "═" * (max_width + 6) + "╝")
|
||||
|
||||
def create_menu(self, item_: tuple = None):
|
||||
# Menu items
|
||||
menu_items = [
|
||||
"", # Menu Separator
|
||||
("🗒️ Open Logs", self._open_logs),
|
||||
("📂 Program Files", self._open_program_location),
|
||||
"", # Menu Separator
|
||||
("🏠 Homepage", self._open_homepage),
|
||||
("❓ Help", self._open_help),
|
||||
("💟 Donate", self._open_donate),
|
||||
("🌐 GitHub", self._open_github),
|
||||
"", # Menu Separator
|
||||
("🔒 Logoff and Quit", self._on_logoff),
|
||||
("❌ Quit", self._on_quit),
|
||||
]
|
||||
|
||||
# Add connect/disconnect option (top of the menu - 0 index)
|
||||
if not self.is_connected:
|
||||
menu_items.insert(0, ("🔗 Connect", self._on_connect))
|
||||
else:
|
||||
if self.is_disconnecting and self.disconnecting_items is not None:
|
||||
menu_items.insert(
|
||||
self.disconnecting_items[1],
|
||||
(self.disconnecting_items[0], self.disconnecting_items[2]),
|
||||
)
|
||||
else:
|
||||
menu_items.insert(0, ("⛓️💥 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,
|
||||
(
|
||||
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],
|
||||
(self.file_download_items[0], self.file_download_items[2]),
|
||||
)
|
||||
|
||||
# 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],
|
||||
(self.previous_stats_items[0], self.previous_stats_items[2]),
|
||||
)
|
||||
|
||||
# Create numbered menu
|
||||
counter = count(1)
|
||||
numbered_menu = {
|
||||
next(counter): item for item in menu_items if isinstance(item, tuple)
|
||||
}
|
||||
|
||||
return (menu_items, numbered_menu)
|
||||
|
||||
def update_menu(self, item_: tuple = None, no_dialog=False):
|
||||
self.menu_items, self.numbered_menu = self.create_menu(item_=item_)
|
||||
if not no_dialog:
|
||||
CustomDialog(
|
||||
"Menu Updated! Press Enter to Refresh. ⏎", msg_type="info"
|
||||
).mainloop()
|
||||
|
||||
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(no_dialog=True)
|
||||
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):
|
||||
TaskbarPanel.open_webbrowser(self.new_version_available[3])
|
||||
|
||||
def _on_connect(self):
|
||||
if self.on_connect_callback:
|
||||
try:
|
||||
self.on_connect_callback()
|
||||
except Exception as e:
|
||||
pass
|
||||
self.is_connected = True
|
||||
self.update_menu(no_dialog=True)
|
||||
|
||||
def _on_disconnect(self):
|
||||
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, daemon=True).start()
|
||||
else:
|
||||
self.is_connected = False
|
||||
self.update_menu(no_dialog=True)
|
||||
|
||||
def _wait_to_disconnect(self):
|
||||
"""
|
||||
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(no_dialog=True)
|
||||
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):
|
||||
TaskbarPanel.open_webbrowser(self.config.data["server_url"])
|
||||
|
||||
def _open_github(self):
|
||||
TaskbarPanel.open_webbrowser(self.github_url)
|
||||
|
||||
def _open_help(self):
|
||||
TaskbarPanel.open_webbrowser(HELP_URL)
|
||||
|
||||
def _open_donate(self):
|
||||
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):
|
||||
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):
|
||||
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.file_download_items = (
|
||||
"📥 Download File(s)",
|
||||
0,
|
||||
lambda: self._on_download(files),
|
||||
)
|
||||
self.update_menu()
|
||||
|
||||
def disable_files_download(self):
|
||||
self.is_file_download_enabled = False
|
||||
self.file_download_items = None
|
||||
self.update_menu()
|
||||
|
||||
def _on_download(self, files):
|
||||
try:
|
||||
if self.config.data["default_file_download_location"] != "":
|
||||
target_directory = self.config.data["default_file_download_location"]
|
||||
else:
|
||||
target_directory = input("Enter the location to save file(s): ").strip()
|
||||
if not target_directory:
|
||||
CustomDialog(
|
||||
"No input provided. Please enter a valid directory path.",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
return
|
||||
|
||||
if not os.path.isdir(target_directory):
|
||||
CustomDialog("Invalid directory path.", msg_type="error").mainloop()
|
||||
return
|
||||
|
||||
if not os.access(target_directory, os.W_OK):
|
||||
CustomDialog(
|
||||
"Insufficient permissions to write to the directory.",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
return
|
||||
|
||||
CustomDialog(
|
||||
f"Saving files to: {target_directory}", msg_type="info"
|
||||
).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}")
|
||||
CustomDialog("Done.", msg_type="success").mainloop()
|
||||
|
||||
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):
|
||||
try:
|
||||
if self.on_logoff_callback:
|
||||
self.on_logoff_callback()
|
||||
self._on_quit()
|
||||
except Exception as e:
|
||||
CustomDialog(
|
||||
f"An error occurred while logging off: {e}", msg_type="error"
|
||||
).mainloop()
|
||||
|
||||
def _on_quit(self):
|
||||
self.loop_terminate = True
|
||||
Reference in New Issue
Block a user