added clipcascade
This commit is contained in:
Executable
+309
@@ -0,0 +1,309 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
from core.constants import *
|
||||
|
||||
from core.config import Config
|
||||
from utils.request_manager import RequestManager
|
||||
from utils.cipher_manager import CipherManager
|
||||
from stomp_ws.stomp_manager import STOMPManager
|
||||
from p2p.p2p_manager import P2PManager
|
||||
|
||||
if PLATFORM == WINDOWS:
|
||||
import ctypes
|
||||
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
||||
import fcntl
|
||||
|
||||
|
||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
||||
import pyfiglet
|
||||
from cli.login import LoginForm
|
||||
from cli.info import CustomDialog
|
||||
from cli.tray import TaskbarPanel
|
||||
from cli.message_box import MessageBox
|
||||
from cli.echo import Echo
|
||||
else:
|
||||
from gui.login import LoginForm
|
||||
from gui.info import CustomDialog
|
||||
from gui.tray import TaskbarPanel
|
||||
from gui.message_box import MessageBox
|
||||
|
||||
|
||||
class Application:
|
||||
def __init__(
|
||||
self,
|
||||
log_file_path=LOG_FILE_NAME,
|
||||
data_file_path=DATA_FILE_NAME,
|
||||
mutex_identifier=MUTEX_NAME,
|
||||
):
|
||||
try:
|
||||
self.log_file_path = os.path.join(
|
||||
get_program_files_directory(), log_file_path
|
||||
)
|
||||
self.data_file_path = os.path.join(
|
||||
get_program_files_directory(), data_file_path
|
||||
)
|
||||
self.mutex_identifier = mutex_identifier
|
||||
|
||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
||||
self.lock_file = None # File(lock) object
|
||||
self.mutex_identifier = os.path.join(
|
||||
get_program_files_directory(), self.mutex_identifier
|
||||
)
|
||||
|
||||
self.config = Config(
|
||||
file_name=self.data_file_path
|
||||
) # Maintain a single configuration instance for the entire application lifecycle.
|
||||
|
||||
self.request_manager = RequestManager(self.config)
|
||||
self.stomp_manager = STOMPManager(self.config)
|
||||
self.p2p_manager = P2PManager(self.config)
|
||||
self.cipher_manager = CipherManager(self.config)
|
||||
except Exception as e:
|
||||
CustomDialog(
|
||||
f"An error occurred during application initialization: {e}",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
|
||||
def setup_logging(self):
|
||||
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||
logging.basicConfig(
|
||||
level=LOG_LEVEL,
|
||||
format=LOG_FORMAT,
|
||||
filename=self.log_file_path,
|
||||
filemode="w",
|
||||
)
|
||||
|
||||
def ensure_single_instance(self):
|
||||
if PLATFORM == WINDOWS:
|
||||
ctypes.windll.kernel32.CreateMutexW(None, False, self.mutex_identifier)
|
||||
if ctypes.windll.kernel32.GetLastError() == 183: # ERROR_ALREADY_EXISTS
|
||||
CustomDialog(
|
||||
"Another instance of ClipCascade is already running.",
|
||||
msg_type="warning",
|
||||
).mainloop()
|
||||
sys.exit(0)
|
||||
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
||||
if PLATFORM == MACOS:
|
||||
app_dir = get_program_files_directory()
|
||||
if not os.path.exists(app_dir):
|
||||
try:
|
||||
os.makedirs(app_dir)
|
||||
except Exception as e:
|
||||
CustomDialog(
|
||||
f"An error occurred while creating the directory '{app_dir}'. Error: {e}",
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
sys.exit(1)
|
||||
|
||||
# Create the lock file
|
||||
try:
|
||||
self.create_lock_file()
|
||||
except IOError:
|
||||
run_anyway = MessageBox().askquestion(
|
||||
"ClipCascade",
|
||||
"Another instance of ClipCascade is already running. Do you want to run anyway?",
|
||||
)
|
||||
if run_anyway == "yes":
|
||||
os.remove(self.mutex_identifier)
|
||||
self.create_lock_file()
|
||||
else:
|
||||
self.lock_file = None
|
||||
sys.exit(0)
|
||||
|
||||
def create_lock_file(self, path=None):
|
||||
if path is None:
|
||||
path = self.mutex_identifier
|
||||
|
||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
||||
self.lock_file = open(path, "w")
|
||||
fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
|
||||
def authenticate_and_connect(self):
|
||||
# Attempt to connect with existing cookie
|
||||
if self.config.data.get("cookie"):
|
||||
ws_conn_successful, msg = self._get_ws_manager().connect()
|
||||
if ws_conn_successful:
|
||||
self._get_ws_manager().is_login_phase = False
|
||||
return
|
||||
|
||||
# enable login form
|
||||
used_saved_credentials = False
|
||||
display_login_success_dialog = False
|
||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
||||
Echo("═" * 14 + "\n║ LOGIN FORM ║\n" + "═" * 14)
|
||||
while True:
|
||||
if (
|
||||
self.config.data.get("cookie") is not None
|
||||
and self.config.data["save_password"]
|
||||
and self.config.data["cipher_enabled"] == False
|
||||
and not used_saved_credentials
|
||||
):
|
||||
# Attempt to connect with password when using saved credentials
|
||||
used_saved_credentials = True
|
||||
else:
|
||||
display_login_success_dialog = True
|
||||
self.config.data["password"] = "" # Clear the password
|
||||
login_form = LoginForm(
|
||||
self.config,
|
||||
on_quit_callback=(
|
||||
None
|
||||
if (PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI)
|
||||
else lambda: sys.exit(0)
|
||||
),
|
||||
)
|
||||
login_form.mainloop() # wait until login form is closed
|
||||
raw_password = self.config.data[
|
||||
"password"
|
||||
] # Store the raw password temporarily for hashing
|
||||
self.config.data["password"] = (
|
||||
CipherManager.string_to_sha3_512_lowercase_hex(raw_password)
|
||||
) # Hash the password
|
||||
|
||||
login_successful, msg_login, self.config.data["cookie"] = (
|
||||
self.request_manager.login()
|
||||
)
|
||||
if login_successful:
|
||||
self.config.data["csrf_token"] = self.request_manager.get_csrf_token()
|
||||
self.config.data["server_mode"] = self.request_manager.get_server_mode()
|
||||
if self.config.data["server_mode"] == "P2P":
|
||||
self.config.data["stun_url"] = self.request_manager.get_stun_url()
|
||||
self.config.data["maxsize"] = -1
|
||||
self.config.data["websocket_url"] = Config.convert_to_websocket_url(
|
||||
self.config.data["server_url"], WEBSOCKET_ENDPOINT_P2P
|
||||
)
|
||||
else:
|
||||
self.config.data["stun_url"] = ""
|
||||
self.config.data["maxsize"] = self.request_manager.maxsize()
|
||||
self.config.data["websocket_url"] = Config.convert_to_websocket_url(
|
||||
self.config.data["server_url"], WEBSOCKET_ENDPOINT
|
||||
)
|
||||
ws_conn_successful, msg = self._get_ws_manager().connect()
|
||||
if ws_conn_successful:
|
||||
self._get_ws_manager().is_login_phase = False
|
||||
if self.config.data["cipher_enabled"]:
|
||||
self.config.data["hashed_password"] = (
|
||||
self.cipher_manager.hash_password(raw_password)
|
||||
)
|
||||
if not self.config.data["save_password"]:
|
||||
self.config.data["password"] = ""
|
||||
if display_login_success_dialog:
|
||||
CustomDialog(
|
||||
"Success! ClipCascade will now run in the task bar/menu bar.",
|
||||
msg_type="success",
|
||||
timeout=5000,
|
||||
).mainloop()
|
||||
break
|
||||
else:
|
||||
CustomDialog(
|
||||
"Login successful but websocket connection failed. \nPlease check websocket-url\n"
|
||||
+ msg,
|
||||
msg_type="error",
|
||||
).mainloop()
|
||||
else:
|
||||
CustomDialog("Login Failed\n" + msg_login, msg_type="error").mainloop()
|
||||
|
||||
raw_password = None # Clear the raw password
|
||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
||||
Echo("-" * 53)
|
||||
|
||||
def _get_ws_manager(self):
|
||||
if self.config.data["server_mode"] == "P2P":
|
||||
return self.p2p_manager
|
||||
else:
|
||||
return self.stomp_manager
|
||||
|
||||
def get_version_update_status(self) -> list:
|
||||
"""
|
||||
Checks for a new version of the application by comparing the current version
|
||||
with the one available in a remote JSON file.
|
||||
|
||||
Returns:
|
||||
list: [bool, str, str, str] - [Is new version available, latest version, current version, release URL]
|
||||
"""
|
||||
try:
|
||||
response = RequestManager.get(VERSION_URL)
|
||||
response_data = response.json()
|
||||
if PLATFORM == WINDOWS:
|
||||
key = "windows"
|
||||
elif PLATFORM == MACOS:
|
||||
key = "macos"
|
||||
elif PLATFORM.startswith(LINUX):
|
||||
if not LINUX_USE_CLI_UI:
|
||||
key = "linux_gui"
|
||||
else:
|
||||
key = "linux_non_gui"
|
||||
|
||||
if response_data[key] != APP_VERSION:
|
||||
return [True, response_data[key], APP_VERSION, RELEASE_URL]
|
||||
except Exception as e:
|
||||
logging.error(f"Error checking for new version: {e}")
|
||||
return [False, "", APP_VERSION, RELEASE_URL]
|
||||
|
||||
def get_donation_url(self) -> str:
|
||||
try:
|
||||
metadata = self.request_manager.get_metadata()
|
||||
if metadata is not None:
|
||||
return metadata.get("funding", None)
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching metadata: {e}")
|
||||
return None
|
||||
|
||||
def logoff_and_exit(self):
|
||||
try:
|
||||
self._get_ws_manager().disconnect()
|
||||
self.request_manager.logout()
|
||||
self.config.data["hashed_password"] = None
|
||||
self.config.data["cookie"] = None
|
||||
self.config.data["maxsize"] = None
|
||||
self.config.data["password"] = ""
|
||||
self.config.data["csrf_token"] = ""
|
||||
self.config.save()
|
||||
except Exception as e:
|
||||
raise Exception(f"Error during logging off: {e}")
|
||||
|
||||
def banner(self):
|
||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
||||
Echo(pyfiglet.figlet_format(APP_NAME))
|
||||
Echo("*" * 53)
|
||||
Echo("Real-Time Clipboard Syncing".center(53))
|
||||
Echo(GITHUB_URL.center(53))
|
||||
Echo("*" * 53)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.banner()
|
||||
self.setup_logging()
|
||||
self.ensure_single_instance()
|
||||
self.config.load()
|
||||
self.authenticate_and_connect()
|
||||
self.config.save()
|
||||
update_available = self.get_version_update_status()
|
||||
donation_url = self.get_donation_url()
|
||||
|
||||
sys_tray = TaskbarPanel(
|
||||
on_connect_callback=self._get_ws_manager().manual_reconnect,
|
||||
on_disconnect_callback=self._get_ws_manager().disconnect,
|
||||
on_logoff_callback=self.logoff_and_exit,
|
||||
new_version_available=update_available,
|
||||
github_url=GITHUB_URL,
|
||||
donation_url=donation_url,
|
||||
ws_interface=self._get_ws_manager(),
|
||||
config=self.config,
|
||||
)
|
||||
self._get_ws_manager().set_tray_ref(sys_tray)
|
||||
sys_tray.run()
|
||||
except Exception as e:
|
||||
msg = f"An unexpected error has occurred: {e}"
|
||||
logging.error(msg)
|
||||
CustomDialog(
|
||||
msg + "\nCheck logs in project directory", msg_type="error"
|
||||
).mainloop()
|
||||
finally:
|
||||
self._get_ws_manager().disconnect()
|
||||
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
||||
if self.lock_file is not None:
|
||||
fcntl.flock(self.lock_file, fcntl.LOCK_UN)
|
||||
self.lock_file.close()
|
||||
os.remove(self.mutex_identifier)
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from core.constants import *
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, file_name=DATA_FILE_NAME):
|
||||
self.file_name = file_name
|
||||
self.data = {
|
||||
"cipher_enabled": True,
|
||||
"server_url": "http://localhost:8080",
|
||||
"websocket_url": "",
|
||||
"username": "",
|
||||
"hashed_password": None,
|
||||
"cookie": None,
|
||||
"maxsize": None,
|
||||
"hash_rounds": 664937,
|
||||
"salt": "",
|
||||
"csrf_token": "",
|
||||
"notification": True,
|
||||
"save_password": False,
|
||||
"password": "",
|
||||
"max_clipboard_size_local_limit_bytes": None,
|
||||
"enable_image_sharing": True,
|
||||
"enable_file_sharing": True,
|
||||
"default_file_download_location": "",
|
||||
"server_mode": "P2S",
|
||||
"stun_url": "",
|
||||
"ssl_ca_bundle": "",
|
||||
}
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save data to file
|
||||
"""
|
||||
try:
|
||||
temp = self.data.copy()
|
||||
if self.data.get("cipher_enabled") and self.data.get("hashed_password"):
|
||||
temp["hashed_password"] = base64.b64encode(
|
||||
temp["hashed_password"]
|
||||
).decode("utf-8")
|
||||
with open(self.file_name, "w") as f:
|
||||
json.dump(temp, f, indent=4)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to save data: {e}")
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load data from file
|
||||
"""
|
||||
if os.path.isfile(self.file_name):
|
||||
try:
|
||||
with open(self.file_name, "r") as f:
|
||||
file_data = json.load(f)
|
||||
self.data.update(file_data)
|
||||
# Decode hashed_password if present
|
||||
if self.data.get("hashed_password"):
|
||||
self.data["hashed_password"] = base64.b64decode(
|
||||
self.data["hashed_password"]
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load data: {e}")
|
||||
logging.error(
|
||||
"Try deleting DATA file in the program directory, and re-run the program again"
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def convert_to_websocket_url(input_url: str, endpoint: str = None) -> str:
|
||||
if not input_url or not isinstance(input_url, str):
|
||||
raise ValueError("Invalid URL provided")
|
||||
|
||||
# Trim whitespace, remove trailing slashes, and convert to lowercase
|
||||
input_url = re.sub(r"/+$", "", input_url.strip()).lower()
|
||||
|
||||
# Determine protocol and convert
|
||||
if input_url.startswith("https://"):
|
||||
ws_url = input_url.replace("https://", "wss://", 1)
|
||||
elif input_url.startswith("http://"):
|
||||
ws_url = input_url.replace("http://", "ws://", 1)
|
||||
else:
|
||||
raise ValueError(f"Unsupported protocol in URL: {input_url}")
|
||||
|
||||
if endpoint is not None:
|
||||
# Append the WebSocket endpoint and remove any trailing slash
|
||||
ws_url += endpoint
|
||||
ws_url = re.sub(r"/+$", "", ws_url)
|
||||
|
||||
return ws_url
|
||||
Executable
+262
@@ -0,0 +1,262 @@
|
||||
import logging
|
||||
import platform
|
||||
import os
|
||||
import sys
|
||||
|
||||
# platform constants
|
||||
WINDOWS = "Windows"
|
||||
MACOS = "macOS"
|
||||
LINUX = "Linux"
|
||||
LINUX_X11 = f"{LINUX}_X11"
|
||||
LINUX_WAYLAND = f"{LINUX}_Wayland"
|
||||
LINUX_HEADLESS = f"{LINUX}_Headless"
|
||||
|
||||
|
||||
# OS detection
|
||||
def get_os_and_display_server():
|
||||
system = platform.system().lower().strip()
|
||||
if system == "windows":
|
||||
return WINDOWS
|
||||
elif system == "darwin":
|
||||
return MACOS
|
||||
elif system == "linux":
|
||||
session_type = os.environ.get("XDG_SESSION_TYPE", "").lower().strip()
|
||||
if session_type == "wayland":
|
||||
return LINUX_WAYLAND
|
||||
elif session_type == "x11":
|
||||
return LINUX_X11
|
||||
else:
|
||||
# Could be a headless session or another type of session
|
||||
return LINUX_HEADLESS
|
||||
else:
|
||||
return "Unknown OS"
|
||||
|
||||
|
||||
def detect_linux_display_server():
|
||||
session_type = os.environ.get("XDG_SESSION_TYPE", "").lower().strip()
|
||||
session_desktop = os.environ.get("XDG_SESSION_DESKTOP", "").lower().strip()
|
||||
wayland_display = os.environ.get("WAYLAND_DISPLAY")
|
||||
x_display = os.environ.get("DISPLAY")
|
||||
|
||||
# Priority detection order: X11 > XWayland > Hyprland > Wayland > Unknown
|
||||
|
||||
if session_type == "x11":
|
||||
return "X11"
|
||||
|
||||
if session_type == "wayland" and x_display and wayland_display:
|
||||
return "XWayland"
|
||||
|
||||
if session_type == "wayland" and session_desktop == "hyprland":
|
||||
return "Hyprland"
|
||||
|
||||
if session_type == "wayland":
|
||||
return "Wayland"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def _parse_linux_bool_flag_token(raw):
|
||||
if raw is None or not str(raw).strip():
|
||||
raise ValueError("empty value")
|
||||
s = str(raw).strip().lower()
|
||||
if s == "true":
|
||||
return True
|
||||
if s == "false":
|
||||
return False
|
||||
raise ValueError(raw)
|
||||
|
||||
|
||||
def _parse_linux_positive_float(raw):
|
||||
if raw is None or not str(raw).strip():
|
||||
raise ValueError("empty value")
|
||||
try:
|
||||
value = float(str(raw).strip())
|
||||
except ValueError:
|
||||
raise ValueError(f"not a number: {raw!r}")
|
||||
if value <= 0:
|
||||
raise ValueError("must be a positive number")
|
||||
return value
|
||||
|
||||
|
||||
def _strip_linux_cli_overrides(argv):
|
||||
"""
|
||||
Linux-only: parse --gui / --xmode (true|false), --polling (seconds), remove them from argv.
|
||||
Last occurrence wins if a flag is repeated.
|
||||
"""
|
||||
gui_override = None
|
||||
xmode_override = None
|
||||
polling_override = None
|
||||
i = 1
|
||||
kept = [argv[0]] if argv else []
|
||||
while i < len(argv):
|
||||
item = argv[i]
|
||||
if item.startswith("--gui="):
|
||||
gui_override = _parse_linux_bool_flag_token(item.split("=", 1)[1])
|
||||
i += 1
|
||||
continue
|
||||
if item == "--gui":
|
||||
if i + 1 >= len(argv):
|
||||
raise ValueError("--gui requires true or false")
|
||||
gui_override = _parse_linux_bool_flag_token(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if item.startswith("--xmode="):
|
||||
xmode_override = _parse_linux_bool_flag_token(item.split("=", 1)[1])
|
||||
i += 1
|
||||
continue
|
||||
if item == "--xmode":
|
||||
if i + 1 >= len(argv):
|
||||
raise ValueError("--xmode requires true or false")
|
||||
xmode_override = _parse_linux_bool_flag_token(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if item.startswith("--polling="):
|
||||
polling_override = _parse_linux_positive_float(item.split("=", 1)[1])
|
||||
i += 1
|
||||
continue
|
||||
if item == "--polling":
|
||||
if i + 1 >= len(argv):
|
||||
raise ValueError("--polling requires a positive number (seconds)")
|
||||
polling_override = _parse_linux_positive_float(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
kept.append(item)
|
||||
i += 1
|
||||
argv[:] = kept
|
||||
return gui_override, xmode_override, polling_override
|
||||
|
||||
|
||||
PLATFORM = get_os_and_display_server()
|
||||
|
||||
# Linux: CLI UI vs GTK tray — False on other platforms (unused except behind LINUX checks).
|
||||
LINUX_USE_CLI_UI = False
|
||||
# Linux: optional override for xclip/wl-paste polling sleep (seconds); None = use built-ins (0.3 / 3).
|
||||
LINUX_CLIPBOARD_POLL_INTERVAL_SEC = None
|
||||
|
||||
if PLATFORM.startswith(LINUX):
|
||||
if (
|
||||
detect_linux_display_server() == "X11"
|
||||
or detect_linux_display_server() == "XWayland"
|
||||
or detect_linux_display_server() == "Unknown"
|
||||
):
|
||||
XMODE = True
|
||||
else:
|
||||
XMODE = False
|
||||
|
||||
try:
|
||||
gui_override, xmode_override, polling_override = _strip_linux_cli_overrides(
|
||||
sys.argv
|
||||
)
|
||||
except ValueError as e:
|
||||
print(f"clipcascade (Linux CLI): {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if xmode_override is not None:
|
||||
XMODE = xmode_override
|
||||
|
||||
if gui_override is not None:
|
||||
LINUX_USE_CLI_UI = not gui_override
|
||||
else:
|
||||
LINUX_USE_CLI_UI = not XMODE
|
||||
|
||||
if polling_override is not None:
|
||||
LINUX_CLIPBOARD_POLL_INTERVAL_SEC = polling_override
|
||||
|
||||
|
||||
# App version
|
||||
if PLATFORM == WINDOWS:
|
||||
APP_VERSION = "3.2.0"
|
||||
elif PLATFORM == MACOS:
|
||||
APP_VERSION = "3.2.0"
|
||||
elif PLATFORM.startswith(LINUX):
|
||||
if XMODE:
|
||||
APP_VERSION = "3.2.0" # gui version
|
||||
else:
|
||||
APP_VERSION = "3.2.0" # non-gui(cli) version
|
||||
|
||||
|
||||
# core constants
|
||||
RECONNECT_WS_TIMER = 10 # seconds
|
||||
WEBSOCKET_TIMEOUT = 3000 # milliseconds
|
||||
|
||||
# P2P signaling WebSocket keepalive (RFC 6455 ping/pong).
|
||||
P2P_WS_PING_INTERVAL_SEC = 25
|
||||
P2P_WS_PING_TIMEOUT_SEC = 20
|
||||
# Data-channel keepalive (JSON envelope with _cc_keepalive); helps idle sessions and mobile radios.
|
||||
P2P_DC_HEARTBEAT_INTERVAL_SEC = 20
|
||||
# After sleep, aiortc RTCPeerConnection.close() can block; cap wait so the asyncio
|
||||
# thread does not stall (which would also block processing ASSIGNED_ID / PEER_LIST).
|
||||
P2P_PC_CLOSE_TIMEOUT_SEC = 5.0
|
||||
|
||||
LOG_FILE_NAME = "clipcascade_log.log"
|
||||
LOG_LEVEL = logging.INFO # Use valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
DATA_FILE_NAME = "DATA"
|
||||
MAX_SIZE = 1048576 # 1 MiB
|
||||
FRAGMENT_SIZE = 15360 # 15 KiB
|
||||
SUBSCRIPTION_DESTINATION = "/user/queue/cliptext"
|
||||
SEND_DESTINATION = "/app/cliptext"
|
||||
LOGIN_URL = "/login"
|
||||
LOGOUT_URL = "/logout"
|
||||
MAXSIZE_URL = "/max-size"
|
||||
CSRF_URL = "/csrf-token"
|
||||
SERVER_MODE_URL = "/server-mode"
|
||||
WEBSOCKET_ENDPOINT = "/clipsocket"
|
||||
WEBSOCKET_ENDPOINT_P2P = "/p2psignaling"
|
||||
STUN_URL = "/stun-url"
|
||||
|
||||
VERSION_URL = "https://raw.githubusercontent.com/Sathvik-Rao/ClipCascade/main/version.json"
|
||||
RELEASE_URL = "https://github.com/Sathvik-Rao/ClipCascade/releases/latest"
|
||||
GITHUB_URL = "https://github.com/Sathvik-Rao/ClipCascade"
|
||||
APP_NAME = "ClipCascade"
|
||||
HELP_URL = f"{GITHUB_URL}/blob/main/README.md"
|
||||
METADATA_URL = "https://raw.githubusercontent.com/Sathvik-Rao/ClipCascade/main/metadata.json"
|
||||
|
||||
if PLATFORM == WINDOWS:
|
||||
MUTEX_NAME = "Global\\ClipCascade_Mutex_PSSR"
|
||||
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
||||
MUTEX_NAME = "clipcascade.lock"
|
||||
|
||||
|
||||
# helper functions
|
||||
def get_user_home_directory():
|
||||
"""
|
||||
Get the user's home directory in a cross-platform manner.
|
||||
On Windows, this resolves to something like C:\\Users\\<Username>
|
||||
On macOS and Linux, this typically resolves to /Users/<Username> or /home/<Username>, respectively.
|
||||
"""
|
||||
return os.path.expanduser("~")
|
||||
|
||||
|
||||
def get_program_files_directory():
|
||||
"""
|
||||
Get the directory containing the program files.
|
||||
"""
|
||||
if PLATFORM == MACOS:
|
||||
app_dir = os.path.join(
|
||||
get_user_home_directory(), "Library", "Application Support", "ClipCascade"
|
||||
)
|
||||
if not os.path.exists(app_dir):
|
||||
try:
|
||||
os.makedirs(app_dir)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
return app_dir
|
||||
else:
|
||||
if getattr(sys, "frozen", False): # Running as a PyInstaller executable
|
||||
return os.path.dirname(sys.executable)
|
||||
else: # Running as a regular Python script
|
||||
running_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(running_dir) # Go one folder up
|
||||
return parent_dir
|
||||
|
||||
|
||||
def get_downloads_folder():
|
||||
"""
|
||||
Get the path to the user's Downloads folder in a cross-platform manner.
|
||||
By default, the Downloads directory is commonly located as:
|
||||
- Windows: C:\\Users\\<Username>\\Downloads
|
||||
- macOS: /Users/<Username>/Downloads
|
||||
- Linux: /home/<Username>/Downloads
|
||||
"""
|
||||
return os.path.join(get_user_home_directory(), "Downloads")
|
||||
Reference in New Issue
Block a user