From 3b6b40fa88fd503e8b22bec525ad5b26043bc9c0 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 14:22:12 +0200 Subject: [PATCH 01/13] 1 --- SUPERCOPY.py | 1 + core/dukto.py | 453 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 142 ++++++++++++++++ 3 files changed, 596 insertions(+) create mode 100644 core/dukto.py diff --git a/SUPERCOPY.py b/SUPERCOPY.py index 1ddc516..a8f29b1 100644 --- a/SUPERCOPY.py +++ b/SUPERCOPY.py @@ -1,6 +1,7 @@ files = [ "core/app_launcher.py", "core/discord_presence.py", + "core/dukto.py", "core/file_search.py", "core/headers.py", "core/updater.py", diff --git a/core/dukto.py b/core/dukto.py new file mode 100644 index 0000000..37d24eb --- /dev/null +++ b/core/dukto.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 + +import socket +import struct +import threading +import os +import platform +import getpass +from pathlib import Path +from typing import List, Optional, Callable, Dict + +DEFAULT_UDP_PORT = 4644 +DEFAULT_TCP_PORT = 4644 + + +class Peer: + def __init__(self, address: str, signature: str, port: int = DEFAULT_UDP_PORT): + self.address = address + self.signature = signature + self.port = port + + def __repr__(self): + return f"Peer({self.address}, {self.signature}, port={self.port})" + + +class DuktoProtocol: + def __init__(self): + self.local_udp_port = DEFAULT_UDP_PORT + self.local_tcp_port = DEFAULT_TCP_PORT + + self.udp_socket: Optional[socket.socket] = None + self.tcp_server: Optional[socket.socket] = None + + self.peers: Dict[str, Peer] = {} + self.is_sending = False + self.is_receiving = False + + self.running = False + + # Callbacks + self.on_peer_added: Optional[Callable[[Peer], None]] = None + self.on_peer_removed: Optional[Callable[[Peer], None]] = None + self.on_receive_start: Optional[Callable[[str], None]] = None + self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None + self.on_receive_text: Optional[Callable[[str, int], None]] = None + self.on_send_complete: Optional[Callable[[List[str]], None]] = None + self.on_transfer_progress: Optional[Callable[[int, int], None]] = None + self.on_error: Optional[Callable[[str], None]] = None + + def set_ports(self, udp_port: int, tcp_port: int): + self.local_udp_port = udp_port + self.local_tcp_port = tcp_port + + def get_system_signature(self) -> str: + username = getpass.getuser() + hostname = socket.gethostname() + system = platform.system() + return f"{username} at {hostname} ({system})" + + def initialize(self): + # UDP Socket for peer discovery + self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.udp_socket.bind(('', self.local_udp_port)) + + # TCP Server for receiving files + self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.tcp_server.bind(('', self.local_tcp_port)) + self.tcp_server.listen(5) + + self.running = True + + # Start listener threads + threading.Thread(target=self._udp_listener, daemon=True).start() + threading.Thread(target=self._tcp_listener, daemon=True).start() + + def say_hello(self, dest: str = '', port: int = None): + if port is None: + port = self.local_udp_port + + # Prepare packet + if port == DEFAULT_UDP_PORT and self.local_udp_port == DEFAULT_UDP_PORT: + if dest == '': + msg_type = b'\x01' # HELLO MESSAGE (broadcast) + else: + msg_type = b'\x02' # HELLO MESSAGE (unicast) + packet = msg_type + self.get_system_signature().encode('utf-8') + else: + if dest == '': + msg_type = b'\x04' # HELLO MESSAGE (broadcast) with PORT + else: + msg_type = b'\x05' # HELLO MESSAGE (unicast) with PORT + packet = msg_type + struct.pack('': + self._send_to_all_broadcast(packet, port) + if port != DEFAULT_UDP_PORT: + self._send_to_all_broadcast(packet, DEFAULT_UDP_PORT) + else: + self.udp_socket.sendto(packet, (dest, port)) + + def say_goodbye(self): + packet = b'\x03' + b'Bye Bye' + + # Collect all discovered ports + ports = {self.local_udp_port} + if self.local_udp_port != DEFAULT_UDP_PORT: + ports.add(DEFAULT_UDP_PORT) + for peer in self.peers.values(): + ports.add(peer.port) + + # Send to all ports + for port in ports: + self._send_to_all_broadcast(packet, port) + + def send_file(self, ip_dest: str, files: List[str], port: int = 0): + if port == 0: + port = DEFAULT_TCP_PORT + + if self.is_receiving or self.is_sending: + if self.on_error: + self.on_error("Already busy with another transfer") + return + + self.is_sending = True + threading.Thread(target=self._send_file_thread, + args=(ip_dest, port, files), daemon=True).start() + + def send_text(self, ip_dest: str, text: str, port: int = 0): + if port == 0: + port = DEFAULT_TCP_PORT + + if self.is_receiving or self.is_sending: + if self.on_error: + self.on_error("Already busy with another transfer") + return + + self.is_sending = True + threading.Thread(target=self._send_text_thread, + args=(ip_dest, port, text), daemon=True).start() + + def _udp_listener(self): + while self.running: + try: + data, addr = self.udp_socket.recvfrom(4096) + self._handle_message(data, addr[0]) + except Exception as e: + if self.running: + print(f"UDP listener error: {e}") + + def _handle_message(self, data: bytes, sender: str): + if len(data) == 0: + return + + msg_type = data[0] + + if msg_type in (0x01, 0x02): # HELLO (broadcast/unicast) + signature = data[1:].decode('utf-8', errors='ignore') + if signature != self.get_system_signature(): + self.peers[sender] = Peer(sender, signature, DEFAULT_UDP_PORT) + if msg_type == 0x01: # Reply to broadcast + self.say_hello(sender, DEFAULT_UDP_PORT) + if self.on_peer_added: + self.on_peer_added(self.peers[sender]) + + elif msg_type == 0x03: # GOODBYE + if sender in self.peers: + peer = self.peers[sender] + if self.on_peer_removed: + self.on_peer_removed(peer) + del self.peers[sender] + + elif msg_type in (0x04, 0x05): # HELLO with PORT + port = struct.unpack(' List[str]: + expanded = [] + + for filepath in files: + path = Path(filepath) + if path.is_dir(): + self._add_recursive(expanded, path) + else: + expanded.append(str(path)) + + return expanded + + def _add_recursive(self, expanded: List[str], path: Path): + expanded.append(str(path)) + + if path.is_dir(): + for entry in path.iterdir(): + self._add_recursive(expanded, entry) + + def _compute_total_size(self, files: List[str]) -> int: + total = 0 + for filepath in files: + path = Path(filepath) + if path.is_file(): + total += path.stat().st_size + return total + + def _send_to_all_broadcast(self, packet: bytes, port: int): + # Try common broadcast + try: + self.udp_socket.sendto(packet, ('255.255.255.255', port)) + except: + pass + + def shutdown(self): + self.running = False + self.say_goodbye() + + if self.udp_socket: + self.udp_socket.close() + if self.tcp_server: + self.tcp_server.close() \ No newline at end of file diff --git a/main.py b/main.py index 6234f30..feeec6c 100644 --- a/main.py +++ b/main.py @@ -9,9 +9,144 @@ from core.web_search import MullvadLetaWrapper from core.discord_presence import presence from core.app_launcher import list_apps, launch from core.updater import update_repository, is_update_available +from core.dukto import DuktoProtocol, Peer ASSET = Path(__file__).parent / "assets" / "2ktan.png" +class DuktoDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("LAN Transfer (Dukto)") + self.setMinimumSize(600, 400) + + # Dukto Protocol Backend + self.protocol = DuktoProtocol() + self.setup_callbacks() + + # UI Elements + self.peer_list_widget = QtWidgets.QListWidget() + self.send_file_button = QtWidgets.QPushButton("Send File(s)") + self.send_text_button = QtWidgets.QPushButton("Send Text") + self.refresh_button = QtWidgets.QPushButton("Refresh") + self.progress_bar = QtWidgets.QProgressBar() + + # Layout + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(QtWidgets.QLabel("Discovered Peers:")) + layout.addWidget(self.peer_list_widget) + + button_layout = QtWidgets.QHBoxLayout() + button_layout.addWidget(self.refresh_button) + button_layout.addStretch() + button_layout.addWidget(self.send_text_button) + button_layout.addWidget(self.send_file_button) + layout.addLayout(button_layout) + layout.addWidget(self.progress_bar) + + self.progress_bar.hide() + + # Connect signals + self.refresh_button.clicked.connect(self.refresh_peers) + self.send_file_button.clicked.connect(self.send_files) + self.send_text_button.clicked.connect(self.send_text) + + # Initialize Dukto + self.protocol.initialize() + self.refresh_peers() + + def setup_callbacks(self): + self.protocol.on_peer_added = self.add_peer + self.protocol.on_peer_removed = self.remove_peer + self.protocol.on_receive_start = lambda ip: self.show_progress() + self.protocol.on_transfer_progress = self.update_progress + self.protocol.on_receive_text = self.handle_received_text + self.protocol.on_receive_complete = self.handle_receive_complete + self.protocol.on_send_complete = lambda files: self.progress_bar.hide() + self.protocol.on_error = self.handle_error + + @QtCore.Slot(Peer) + def add_peer(self, peer: Peer): + # Check if peer already exists + for i in range(self.peer_list_widget.count()): + item = self.peer_list_widget.item(i) + if item.data(QtCore.Qt.UserRole).address == peer.address: # type: ignore + return + + item = QtWidgets.QListWidgetItem(f"{peer.signature} ({peer.address})") + item.setData(QtCore.Qt.UserRole, peer) # type: ignore + self.peer_list_widget.addItem(item) + + @QtCore.Slot(Peer) + def remove_peer(self, peer: Peer): + for i in range(self.peer_list_widget.count()): + item = self.peer_list_widget.item(i) + if item and item.data(QtCore.Qt.UserRole).address == peer.address: # type: ignore + self.peer_list_widget.takeItem(i) + break + + def refresh_peers(self): + self.peer_list_widget.clear() + self.protocol.peers.clear() + self.protocol.say_hello() + + def get_selected_peer(self) -> Peer | None: + selected_items = self.peer_list_widget.selectedItems() + if not selected_items: + QtWidgets.QMessageBox.warning(self, "No Peer Selected", "Please select a peer from the list.") + return None + return selected_items[0].data(QtCore.Qt.UserRole) # type: ignore + + def send_files(self): + peer = self.get_selected_peer() + if not peer: + return + + files, _ = QtWidgets.QFileDialog.getOpenFileNames(self, "Select Files to Send") + if files: + self.show_progress() + self.protocol.send_file(peer.address, files, peer.port) + + def send_text(self): + peer = self.get_selected_peer() + if not peer: + return + + text, ok = QtWidgets.QInputDialog.getMultiLineText(self, "Send Text", "Enter text to send:") + if ok and text: + self.show_progress() + self.protocol.send_text(peer.address, text, peer.port) + + def show_progress(self): + self.progress_bar.setValue(0) + self.progress_bar.show() + + @QtCore.Slot(int, int) + def update_progress(self, total_size, transferred): + if total_size > 0: + percentage = int((transferred / total_size) * 100) + self.progress_bar.setValue(percentage) + + @QtCore.Slot(str, int) + def handle_received_text(self, text, size): + self.progress_bar.hide() + QtWidgets.QMessageBox.information(self, "Text Received", text) + + @QtCore.Slot(list, int) + def handle_receive_complete(self, files, size): + self.progress_bar.hide() + msg = f"Received {len(files)} file(s) successfully.\nThey are located in the application's directory." + QtWidgets.QMessageBox.information(self, "Transfer Complete", msg) + + @QtCore.Slot(str) + def handle_error(self, error_message): + self.progress_bar.hide() + QtWidgets.QMessageBox.critical(self, "Transfer Error", error_message) + + def closeEvent(self, event): + self.protocol.shutdown() + super().closeEvent(event) + + class AppLauncherDialog(QtWidgets.QDialog): def __init__(self, parent=None): super().__init__(parent) @@ -353,6 +488,7 @@ class MainWindow(QtWidgets.QMainWindow): right_menu.addAction("Launch App", self.start_app_launcher) right_menu.addAction("Search Files", self.start_file_search) right_menu.addAction("Search Web", self.start_web_search) + right_menu.addAction("LAN Transfer (Dukto)", self.start_dukto) right_menu.addSeparator() right_menu.addAction("Check for updates", self.update_git) if restart: @@ -370,6 +506,7 @@ class MainWindow(QtWidgets.QMainWindow): self.left_menu.addAction("Launch App", self.start_app_launcher) self.left_menu.addAction("Search Files", self.start_file_search) self.left_menu.addAction("Search Web", self.start_web_search) + self.left_menu.addAction("LAN Transfer (Dukto)", self.start_dukto) # always on top timer self.stay_on_top_timer = QtCore.QTimer(self) @@ -423,6 +560,11 @@ class MainWindow(QtWidgets.QMainWindow): self.app_launcher_dialog.move(QtGui.QCursor.pos()) self.app_launcher_dialog.show() + def start_dukto(self): + self.dukto_dialog = DuktoDialog(self) + self.dukto_dialog.move(QtGui.QCursor.pos()) + self.dukto_dialog.show() + def start_file_search(self): dialog = QtWidgets.QInputDialog(self) dialog.setWindowTitle("File Search") From b9e2da327acabd89eca2d2d35a0b95b79160bc1a Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 14:29:04 +0200 Subject: [PATCH 02/13] 2 --- core/dukto.py | 15 ++- main.py | 290 +++++++++++++++++++++++++------------------------- 2 files changed, 157 insertions(+), 148 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 37d24eb..20fe3ad 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -46,6 +46,7 @@ class DuktoProtocol: self.on_send_complete: Optional[Callable[[List[str]], None]] = None self.on_transfer_progress: Optional[Callable[[int, int], None]] = None self.on_error: Optional[Callable[[str], None]] = None + self.on_incoming_connection: Optional[Callable[[str, socket.socket], None]] = None def set_ports(self, udp_port: int, tcp_port: int): self.local_udp_port = udp_port @@ -192,11 +193,21 @@ class DuktoProtocol: conn.close() continue - threading.Thread(target=self._receive_files, - args=(conn, addr[0]), daemon=True).start() + if self.on_incoming_connection: + # Pass the connection to the handler to decide + self.on_incoming_connection(addr[0], conn) + else: + # Auto-reject if no handler is set + conn.close() + except Exception as e: if self.running: print(f"TCP listener error: {e}") + + def start_receiving(self, conn: socket.socket, sender_ip: str): + """Starts the receiving process on an already accepted connection.""" + threading.Thread(target=self._receive_files, + args=(conn, sender_ip), daemon=True).start() def _receive_files(self, conn: socket.socket, sender_ip: str): self.is_receiving = True diff --git a/main.py b/main.py index feeec6c..a2082dd 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -import sys, os, subprocess +import sys, os, subprocess, socket from pathlib import Path from PySide6 import QtCore, QtGui, QtWidgets from pynput import keyboard @@ -13,140 +13,6 @@ from core.dukto import DuktoProtocol, Peer ASSET = Path(__file__).parent / "assets" / "2ktan.png" -class DuktoDialog(QtWidgets.QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("LAN Transfer (Dukto)") - self.setMinimumSize(600, 400) - - # Dukto Protocol Backend - self.protocol = DuktoProtocol() - self.setup_callbacks() - - # UI Elements - self.peer_list_widget = QtWidgets.QListWidget() - self.send_file_button = QtWidgets.QPushButton("Send File(s)") - self.send_text_button = QtWidgets.QPushButton("Send Text") - self.refresh_button = QtWidgets.QPushButton("Refresh") - self.progress_bar = QtWidgets.QProgressBar() - - # Layout - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("Discovered Peers:")) - layout.addWidget(self.peer_list_widget) - - button_layout = QtWidgets.QHBoxLayout() - button_layout.addWidget(self.refresh_button) - button_layout.addStretch() - button_layout.addWidget(self.send_text_button) - button_layout.addWidget(self.send_file_button) - layout.addLayout(button_layout) - layout.addWidget(self.progress_bar) - - self.progress_bar.hide() - - # Connect signals - self.refresh_button.clicked.connect(self.refresh_peers) - self.send_file_button.clicked.connect(self.send_files) - self.send_text_button.clicked.connect(self.send_text) - - # Initialize Dukto - self.protocol.initialize() - self.refresh_peers() - - def setup_callbacks(self): - self.protocol.on_peer_added = self.add_peer - self.protocol.on_peer_removed = self.remove_peer - self.protocol.on_receive_start = lambda ip: self.show_progress() - self.protocol.on_transfer_progress = self.update_progress - self.protocol.on_receive_text = self.handle_received_text - self.protocol.on_receive_complete = self.handle_receive_complete - self.protocol.on_send_complete = lambda files: self.progress_bar.hide() - self.protocol.on_error = self.handle_error - - @QtCore.Slot(Peer) - def add_peer(self, peer: Peer): - # Check if peer already exists - for i in range(self.peer_list_widget.count()): - item = self.peer_list_widget.item(i) - if item.data(QtCore.Qt.UserRole).address == peer.address: # type: ignore - return - - item = QtWidgets.QListWidgetItem(f"{peer.signature} ({peer.address})") - item.setData(QtCore.Qt.UserRole, peer) # type: ignore - self.peer_list_widget.addItem(item) - - @QtCore.Slot(Peer) - def remove_peer(self, peer: Peer): - for i in range(self.peer_list_widget.count()): - item = self.peer_list_widget.item(i) - if item and item.data(QtCore.Qt.UserRole).address == peer.address: # type: ignore - self.peer_list_widget.takeItem(i) - break - - def refresh_peers(self): - self.peer_list_widget.clear() - self.protocol.peers.clear() - self.protocol.say_hello() - - def get_selected_peer(self) -> Peer | None: - selected_items = self.peer_list_widget.selectedItems() - if not selected_items: - QtWidgets.QMessageBox.warning(self, "No Peer Selected", "Please select a peer from the list.") - return None - return selected_items[0].data(QtCore.Qt.UserRole) # type: ignore - - def send_files(self): - peer = self.get_selected_peer() - if not peer: - return - - files, _ = QtWidgets.QFileDialog.getOpenFileNames(self, "Select Files to Send") - if files: - self.show_progress() - self.protocol.send_file(peer.address, files, peer.port) - - def send_text(self): - peer = self.get_selected_peer() - if not peer: - return - - text, ok = QtWidgets.QInputDialog.getMultiLineText(self, "Send Text", "Enter text to send:") - if ok and text: - self.show_progress() - self.protocol.send_text(peer.address, text, peer.port) - - def show_progress(self): - self.progress_bar.setValue(0) - self.progress_bar.show() - - @QtCore.Slot(int, int) - def update_progress(self, total_size, transferred): - if total_size > 0: - percentage = int((transferred / total_size) * 100) - self.progress_bar.setValue(percentage) - - @QtCore.Slot(str, int) - def handle_received_text(self, text, size): - self.progress_bar.hide() - QtWidgets.QMessageBox.information(self, "Text Received", text) - - @QtCore.Slot(list, int) - def handle_receive_complete(self, files, size): - self.progress_bar.hide() - msg = f"Received {len(files)} file(s) successfully.\nThey are located in the application's directory." - QtWidgets.QMessageBox.information(self, "Transfer Complete", msg) - - @QtCore.Slot(str) - def handle_error(self, error_message): - self.progress_bar.hide() - QtWidgets.QMessageBox.critical(self, "Transfer Error", error_message) - - def closeEvent(self, event): - self.protocol.shutdown() - super().closeEvent(event) - - class AppLauncherDialog(QtWidgets.QDialog): def __init__(self, parent=None): super().__init__(parent) @@ -453,6 +319,14 @@ class WebSearchResults(QtWidgets.QDialog): class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() + # Dukto signals for thread-safety + peer_added_signal = QtCore.Signal(object) + peer_removed_signal = QtCore.Signal(object) + incoming_connection_signal = QtCore.Signal(str, object) + dukto_error_signal = QtCore.Signal(str) + transfer_progress_signal = QtCore.Signal(int, int) + transfer_complete_signal = QtCore.Signal(str) + text_received_signal = QtCore.Signal(str, str) def __init__(self, restart=False, no_quit=False, super_menu=True): super().__init__() @@ -479,6 +353,7 @@ class MainWindow(QtWidgets.QMainWindow): self.setMask(mask) self.super_menu = super_menu + self.progress_dialog = None self.tray = QtWidgets.QSystemTrayIcon(self) self.tray.setIcon(QtGui.QIcon(str(ASSET))) @@ -488,7 +363,8 @@ class MainWindow(QtWidgets.QMainWindow): right_menu.addAction("Launch App", self.start_app_launcher) right_menu.addAction("Search Files", self.start_file_search) right_menu.addAction("Search Web", self.start_web_search) - right_menu.addAction("LAN Transfer (Dukto)", self.start_dukto) + self.dukto_peers_menu_right = right_menu.addMenu("Dukto") + self.dukto_peers_menu_right.setEnabled(False) right_menu.addSeparator() right_menu.addAction("Check for updates", self.update_git) if restart: @@ -496,7 +372,7 @@ class MainWindow(QtWidgets.QMainWindow): right_menu.addAction("Hide/Show", self.toggle_visible) right_menu.addSeparator() if not no_quit: - right_menu.addAction("Quit", QtWidgets.QApplication.quit) + right_menu.addAction("Quit", self.quit_application) self.tray.setContextMenu(right_menu) self.tray.activated.connect(self.handle_tray_activated) self.tray.show() @@ -506,7 +382,11 @@ class MainWindow(QtWidgets.QMainWindow): self.left_menu.addAction("Launch App", self.start_app_launcher) self.left_menu.addAction("Search Files", self.start_file_search) self.left_menu.addAction("Search Web", self.start_web_search) - self.left_menu.addAction("LAN Transfer (Dukto)", self.start_dukto) + self.dukto_peers_menu_left = self.left_menu.addMenu("Dukto") + self.dukto_peers_menu_left.setEnabled(False) + + # Dukto peer menu storage + self.dukto_peer_menus = {} # always on top timer self.stay_on_top_timer = QtCore.QTimer(self) @@ -517,6 +397,43 @@ class MainWindow(QtWidgets.QMainWindow): self.show_menu_signal.connect(self.show_menu) self.start_hotkey_listener() + # Init Dukto Protocol + self.init_dukto() + + def init_dukto(self): + self.dukto = DuktoProtocol() + + # Connect signals to slots + self.peer_added_signal.connect(self.add_dukto_peer_slot) + self.peer_removed_signal.connect(self.remove_dukto_peer_slot) + self.incoming_connection_signal.connect(self.handle_incoming_transfer_slot) + self.dukto_error_signal.connect(self.handle_dukto_error_slot) + self.transfer_progress_signal.connect(self.update_transfer_progress_slot) + self.transfer_complete_signal.connect(self.finish_transfer_slot) + self.text_received_signal.connect(self.show_received_text_slot) + + # Assign callbacks that emit signals + self.dukto.on_peer_added = lambda peer: self.peer_added_signal.emit(peer) + self.dukto.on_peer_removed = lambda peer: self.peer_removed_signal.emit(peer) + self.dukto.on_incoming_connection = lambda ip, conn: self.incoming_connection_signal.emit(ip, conn) + self.dukto.on_error = lambda msg: self.dukto_error_signal.emit(msg) + + def on_receive_start(sender_ip): + peer_name = self.dukto.peers.get(sender_ip, Peer(sender_ip, sender_ip)).signature + # Use QTimer to ensure this runs on the main thread + QtCore.QTimer.singleShot(0, lambda: self.start_transfer_progress_slot(f"Receiving from {peer_name}...")) + self.dukto.on_receive_start = on_receive_start + + self.dukto.on_transfer_progress = lambda total, current: self.transfer_progress_signal.emit(current, total) + self.dukto.on_receive_complete = lambda files, size: self.transfer_complete_signal.emit("Files received successfully!") + self.dukto.on_send_complete = lambda files: self.transfer_complete_signal.emit("Files sent successfully!") + self.dukto.on_receive_text = lambda text, size: self.text_received_signal.emit( + self.dukto.peers.get("unknown", Peer("unknown", "An unknown user")).signature, text + ) + + self.dukto.initialize() + self.dukto.say_hello() + def show_menu(self): self.left_menu.popup(QtGui.QCursor.pos()) @@ -560,11 +477,6 @@ class MainWindow(QtWidgets.QMainWindow): self.app_launcher_dialog.move(QtGui.QCursor.pos()) self.app_launcher_dialog.show() - def start_dukto(self): - self.dukto_dialog = DuktoDialog(self) - self.dukto_dialog.move(QtGui.QCursor.pos()) - self.dukto_dialog.show() - def start_file_search(self): dialog = QtWidgets.QInputDialog(self) dialog.setWindowTitle("File Search") @@ -660,13 +572,98 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QMessageBox.critical(self, "Update Failed", message) def restart_application(self): + self.dukto.shutdown() presence.end() - args = [sys.executable] + sys.argv - subprocess.Popen(args) - QtWidgets.QApplication.quit() + + def quit_application(self): + self.dukto.shutdown() + QtWidgets.QApplication.quit() + + # --- Dukto Slots --- + def add_dukto_peer_slot(self, peer: Peer): + if not self.dukto_peers_menu_left.isEnabled(): + self.dukto_peers_menu_left.setEnabled(True) + self.dukto_peers_menu_right.setEnabled(True) + + peer_menu_left = QtWidgets.QMenu(peer.signature, self.dukto_peers_menu_left) + peer_menu_right = QtWidgets.QMenu(peer.signature, self.dukto_peers_menu_right) + + for menu in [peer_menu_left, peer_menu_right]: + send_files_action = menu.addAction("Send Files...") + send_files_action.triggered.connect(lambda: self.send_files_to_peer(peer)) + send_text_action = menu.addAction("Send Text...") + send_text_action.triggered.connect(lambda: self.send_text_to_peer(peer)) + + self.dukto_peers_menu_left.addMenu(peer_menu_left) + self.dukto_peers_menu_right.addMenu(peer_menu_right) + self.dukto_peer_menus[peer.address] = (peer_menu_left, peer_menu_right) + + def remove_dukto_peer_slot(self, peer: Peer): + if peer.address in self.dukto_peer_menus: + menus = self.dukto_peer_menus.pop(peer.address) + self.dukto_peers_menu_left.removeAction(menus[0].menuAction()) + self.dukto_peers_menu_right.removeAction(menus[1].menuAction()) + + if not self.dukto_peer_menus: + self.dukto_peers_menu_left.setEnabled(False) + self.dukto_peers_menu_right.setEnabled(False) + + def handle_incoming_transfer_slot(self, sender_ip: str, conn: socket.socket): + peer = self.dukto.peers.get(sender_ip) + peer_name = peer.signature if peer else sender_ip + + reply = QtWidgets.QMessageBox.question(self, "Incoming Transfer", + f"Accept incoming files from {peer_name}?", + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.Yes) + + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + self.dukto.start_receiving(conn, sender_ip) + else: + conn.close() + + def send_files_to_peer(self, peer: Peer): + file_dialog = QtWidgets.QFileDialog(self) + file_dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles) + if file_dialog.exec(): + files = file_dialog.selectedFiles() + if files: + self.start_transfer_progress_slot(f"Sending to {peer.signature}...") + self.dukto.send_file(peer.address, files, peer.port) + + def send_text_to_peer(self, peer: Peer): + text, ok = QtWidgets.QInputDialog.getMultiLineText(self, "Send Text", f"Enter text to send to {peer.signature}:") + if ok and text: + self.dukto.send_text(peer.address, text, peer.port) + + def handle_dukto_error_slot(self, message: str): + QtWidgets.QMessageBox.critical(self, "Dukto Error", message) + if self.progress_dialog: + self.progress_dialog.close() + self.progress_dialog = None + + def start_transfer_progress_slot(self, title: str): + self.progress_dialog = QtWidgets.QProgressDialog(title, "Cancel", 0, 100, self) + self.progress_dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal) + self.progress_dialog.setValue(0) + self.progress_dialog.show() + + def update_transfer_progress_slot(self, current: int, total: int): + if self.progress_dialog: + self.progress_dialog.setMaximum(total) + self.progress_dialog.setValue(current) + + def finish_transfer_slot(self, message: str): + if self.progress_dialog: + self.progress_dialog.close() + self.progress_dialog = None + QtWidgets.QMessageBox.information(self, "Transfer Complete", message) + + def show_received_text_slot(self, sender_name: str, text: str): + QtWidgets.QMessageBox.information(self, f"Text from {sender_name}", text) def main(): @@ -689,6 +686,7 @@ def main(): pet.show() app.aboutToQuit.connect(presence.end) + app.aboutToQuit.connect(pet.dukto.shutdown) sys.exit(app.exec()) From a0417d726786e3e98997e53c3212e57722af7da4 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:03:04 +0200 Subject: [PATCH 03/13] . --- core/dukto.py | 15 +---- main.py | 150 ++------------------------------------------------ 2 files changed, 7 insertions(+), 158 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 20fe3ad..37d24eb 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -46,7 +46,6 @@ class DuktoProtocol: self.on_send_complete: Optional[Callable[[List[str]], None]] = None self.on_transfer_progress: Optional[Callable[[int, int], None]] = None self.on_error: Optional[Callable[[str], None]] = None - self.on_incoming_connection: Optional[Callable[[str, socket.socket], None]] = None def set_ports(self, udp_port: int, tcp_port: int): self.local_udp_port = udp_port @@ -193,21 +192,11 @@ class DuktoProtocol: conn.close() continue - if self.on_incoming_connection: - # Pass the connection to the handler to decide - self.on_incoming_connection(addr[0], conn) - else: - # Auto-reject if no handler is set - conn.close() - + threading.Thread(target=self._receive_files, + args=(conn, addr[0]), daemon=True).start() except Exception as e: if self.running: print(f"TCP listener error: {e}") - - def start_receiving(self, conn: socket.socket, sender_ip: str): - """Starts the receiving process on an already accepted connection.""" - threading.Thread(target=self._receive_files, - args=(conn, sender_ip), daemon=True).start() def _receive_files(self, conn: socket.socket, sender_ip: str): self.is_receiving = True diff --git a/main.py b/main.py index a2082dd..6234f30 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -import sys, os, subprocess, socket +import sys, os, subprocess from pathlib import Path from PySide6 import QtCore, QtGui, QtWidgets from pynput import keyboard @@ -9,7 +9,6 @@ from core.web_search import MullvadLetaWrapper from core.discord_presence import presence from core.app_launcher import list_apps, launch from core.updater import update_repository, is_update_available -from core.dukto import DuktoProtocol, Peer ASSET = Path(__file__).parent / "assets" / "2ktan.png" @@ -319,14 +318,6 @@ class WebSearchResults(QtWidgets.QDialog): class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() - # Dukto signals for thread-safety - peer_added_signal = QtCore.Signal(object) - peer_removed_signal = QtCore.Signal(object) - incoming_connection_signal = QtCore.Signal(str, object) - dukto_error_signal = QtCore.Signal(str) - transfer_progress_signal = QtCore.Signal(int, int) - transfer_complete_signal = QtCore.Signal(str) - text_received_signal = QtCore.Signal(str, str) def __init__(self, restart=False, no_quit=False, super_menu=True): super().__init__() @@ -353,7 +344,6 @@ class MainWindow(QtWidgets.QMainWindow): self.setMask(mask) self.super_menu = super_menu - self.progress_dialog = None self.tray = QtWidgets.QSystemTrayIcon(self) self.tray.setIcon(QtGui.QIcon(str(ASSET))) @@ -363,8 +353,6 @@ class MainWindow(QtWidgets.QMainWindow): right_menu.addAction("Launch App", self.start_app_launcher) right_menu.addAction("Search Files", self.start_file_search) right_menu.addAction("Search Web", self.start_web_search) - self.dukto_peers_menu_right = right_menu.addMenu("Dukto") - self.dukto_peers_menu_right.setEnabled(False) right_menu.addSeparator() right_menu.addAction("Check for updates", self.update_git) if restart: @@ -372,7 +360,7 @@ class MainWindow(QtWidgets.QMainWindow): right_menu.addAction("Hide/Show", self.toggle_visible) right_menu.addSeparator() if not no_quit: - right_menu.addAction("Quit", self.quit_application) + right_menu.addAction("Quit", QtWidgets.QApplication.quit) self.tray.setContextMenu(right_menu) self.tray.activated.connect(self.handle_tray_activated) self.tray.show() @@ -382,11 +370,6 @@ class MainWindow(QtWidgets.QMainWindow): self.left_menu.addAction("Launch App", self.start_app_launcher) self.left_menu.addAction("Search Files", self.start_file_search) self.left_menu.addAction("Search Web", self.start_web_search) - self.dukto_peers_menu_left = self.left_menu.addMenu("Dukto") - self.dukto_peers_menu_left.setEnabled(False) - - # Dukto peer menu storage - self.dukto_peer_menus = {} # always on top timer self.stay_on_top_timer = QtCore.QTimer(self) @@ -397,43 +380,6 @@ class MainWindow(QtWidgets.QMainWindow): self.show_menu_signal.connect(self.show_menu) self.start_hotkey_listener() - # Init Dukto Protocol - self.init_dukto() - - def init_dukto(self): - self.dukto = DuktoProtocol() - - # Connect signals to slots - self.peer_added_signal.connect(self.add_dukto_peer_slot) - self.peer_removed_signal.connect(self.remove_dukto_peer_slot) - self.incoming_connection_signal.connect(self.handle_incoming_transfer_slot) - self.dukto_error_signal.connect(self.handle_dukto_error_slot) - self.transfer_progress_signal.connect(self.update_transfer_progress_slot) - self.transfer_complete_signal.connect(self.finish_transfer_slot) - self.text_received_signal.connect(self.show_received_text_slot) - - # Assign callbacks that emit signals - self.dukto.on_peer_added = lambda peer: self.peer_added_signal.emit(peer) - self.dukto.on_peer_removed = lambda peer: self.peer_removed_signal.emit(peer) - self.dukto.on_incoming_connection = lambda ip, conn: self.incoming_connection_signal.emit(ip, conn) - self.dukto.on_error = lambda msg: self.dukto_error_signal.emit(msg) - - def on_receive_start(sender_ip): - peer_name = self.dukto.peers.get(sender_ip, Peer(sender_ip, sender_ip)).signature - # Use QTimer to ensure this runs on the main thread - QtCore.QTimer.singleShot(0, lambda: self.start_transfer_progress_slot(f"Receiving from {peer_name}...")) - self.dukto.on_receive_start = on_receive_start - - self.dukto.on_transfer_progress = lambda total, current: self.transfer_progress_signal.emit(current, total) - self.dukto.on_receive_complete = lambda files, size: self.transfer_complete_signal.emit("Files received successfully!") - self.dukto.on_send_complete = lambda files: self.transfer_complete_signal.emit("Files sent successfully!") - self.dukto.on_receive_text = lambda text, size: self.text_received_signal.emit( - self.dukto.peers.get("unknown", Peer("unknown", "An unknown user")).signature, text - ) - - self.dukto.initialize() - self.dukto.say_hello() - def show_menu(self): self.left_menu.popup(QtGui.QCursor.pos()) @@ -572,98 +518,13 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QMessageBox.critical(self, "Update Failed", message) def restart_application(self): - self.dukto.shutdown() presence.end() + args = [sys.executable] + sys.argv + subprocess.Popen(args) - QtWidgets.QApplication.quit() - - def quit_application(self): - self.dukto.shutdown() - QtWidgets.QApplication.quit() - - # --- Dukto Slots --- - def add_dukto_peer_slot(self, peer: Peer): - if not self.dukto_peers_menu_left.isEnabled(): - self.dukto_peers_menu_left.setEnabled(True) - self.dukto_peers_menu_right.setEnabled(True) - - peer_menu_left = QtWidgets.QMenu(peer.signature, self.dukto_peers_menu_left) - peer_menu_right = QtWidgets.QMenu(peer.signature, self.dukto_peers_menu_right) - - for menu in [peer_menu_left, peer_menu_right]: - send_files_action = menu.addAction("Send Files...") - send_files_action.triggered.connect(lambda: self.send_files_to_peer(peer)) - send_text_action = menu.addAction("Send Text...") - send_text_action.triggered.connect(lambda: self.send_text_to_peer(peer)) - self.dukto_peers_menu_left.addMenu(peer_menu_left) - self.dukto_peers_menu_right.addMenu(peer_menu_right) - self.dukto_peer_menus[peer.address] = (peer_menu_left, peer_menu_right) - - def remove_dukto_peer_slot(self, peer: Peer): - if peer.address in self.dukto_peer_menus: - menus = self.dukto_peer_menus.pop(peer.address) - self.dukto_peers_menu_left.removeAction(menus[0].menuAction()) - self.dukto_peers_menu_right.removeAction(menus[1].menuAction()) - - if not self.dukto_peer_menus: - self.dukto_peers_menu_left.setEnabled(False) - self.dukto_peers_menu_right.setEnabled(False) - - def handle_incoming_transfer_slot(self, sender_ip: str, conn: socket.socket): - peer = self.dukto.peers.get(sender_ip) - peer_name = peer.signature if peer else sender_ip - - reply = QtWidgets.QMessageBox.question(self, "Incoming Transfer", - f"Accept incoming files from {peer_name}?", - QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, - QtWidgets.QMessageBox.StandardButton.Yes) - - if reply == QtWidgets.QMessageBox.StandardButton.Yes: - self.dukto.start_receiving(conn, sender_ip) - else: - conn.close() - - def send_files_to_peer(self, peer: Peer): - file_dialog = QtWidgets.QFileDialog(self) - file_dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles) - if file_dialog.exec(): - files = file_dialog.selectedFiles() - if files: - self.start_transfer_progress_slot(f"Sending to {peer.signature}...") - self.dukto.send_file(peer.address, files, peer.port) - - def send_text_to_peer(self, peer: Peer): - text, ok = QtWidgets.QInputDialog.getMultiLineText(self, "Send Text", f"Enter text to send to {peer.signature}:") - if ok and text: - self.dukto.send_text(peer.address, text, peer.port) - - def handle_dukto_error_slot(self, message: str): - QtWidgets.QMessageBox.critical(self, "Dukto Error", message) - if self.progress_dialog: - self.progress_dialog.close() - self.progress_dialog = None - - def start_transfer_progress_slot(self, title: str): - self.progress_dialog = QtWidgets.QProgressDialog(title, "Cancel", 0, 100, self) - self.progress_dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal) - self.progress_dialog.setValue(0) - self.progress_dialog.show() - - def update_transfer_progress_slot(self, current: int, total: int): - if self.progress_dialog: - self.progress_dialog.setMaximum(total) - self.progress_dialog.setValue(current) - - def finish_transfer_slot(self, message: str): - if self.progress_dialog: - self.progress_dialog.close() - self.progress_dialog = None - QtWidgets.QMessageBox.information(self, "Transfer Complete", message) - - def show_received_text_slot(self, sender_name: str, text: str): - QtWidgets.QMessageBox.information(self, f"Text from {sender_name}", text) + QtWidgets.QApplication.quit() def main(): @@ -686,7 +547,6 @@ def main(): pet.show() app.aboutToQuit.connect(presence.end) - app.aboutToQuit.connect(pet.dukto.shutdown) sys.exit(app.exec()) From 9b20fa745e0f74484eeaac633549121c27bb3ad0 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:14:13 +0200 Subject: [PATCH 04/13] receive confirmation --- core/dukto.py | 43 +++++++++++++++++++++---------- main.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 37d24eb..0d1e19d 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -40,6 +40,7 @@ class DuktoProtocol: # Callbacks self.on_peer_added: Optional[Callable[[Peer], None]] = None self.on_peer_removed: Optional[Callable[[Peer], None]] = None + self.on_receive_request: Optional[Callable[[str, int, int], bool]] = None self.on_receive_start: Optional[Callable[[str], None]] = None self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None self.on_receive_text: Optional[Callable[[str, int], None]] = None @@ -192,29 +193,45 @@ class DuktoProtocol: conn.close() continue - threading.Thread(target=self._receive_files, + threading.Thread(target=self._handle_connection_request, args=(conn, addr[0]), daemon=True).start() except Exception as e: if self.running: print(f"TCP listener error: {e}") - - def _receive_files(self, conn: socket.socket, sender_ip: str): + + def _handle_connection_request(self, conn: socket.socket, sender_ip: str): + try: + conn.settimeout(10) + + header = conn.recv(16) + if len(header) < 16: + conn.close() + return + + elements_count = struct.unpack(' bool: + self.receive_confirmation_event.clear() + + QtCore.QMetaObject.invokeMethod( + self, "show_receive_dialog", QtCore.Qt.QueuedConnection, #type: ignore + QtCore.Q_ARG(str, sender_ip), + QtCore.Q_ARG(int, file_count), + QtCore.Q_ARG(int, total_size), + ) + + self.receive_confirmation_event.wait() + return self.receive_confirmation_result[0] + def restart_application(self): presence.end() + self.dukto_handler.shutdown() args = [sys.executable] + sys.argv @@ -534,9 +591,16 @@ def main(): restart = "--restart" in sys.argv no_quit = "--no-quit" in sys.argv super_menu = not "--no-super" in sys.argv - pet = MainWindow(restart=restart, no_quit=no_quit, super_menu=super_menu) + + dukto_handler = DuktoProtocol() + + pet = MainWindow(dukto_handler=dukto_handler, restart=restart, no_quit=no_quit, super_menu=super_menu) + presence.start() + dukto_handler.initialize() + dukto_handler.say_hello() + # bottom right corner screen_geometry = app.primaryScreen().availableGeometry() pet_geometry = pet.frameGeometry() @@ -547,6 +611,7 @@ def main(): pet.show() app.aboutToQuit.connect(presence.end) + app.aboutToQuit.connect(dukto_handler.shutdown) sys.exit(app.exec()) From 25eaad149172c1dfc9c8bcd39b303663f3ebbc23 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:16:16 +0200 Subject: [PATCH 05/13] text --- core/dukto.py | 4 ++-- main.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 0d1e19d..72e88f8 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -43,7 +43,7 @@ class DuktoProtocol: self.on_receive_request: Optional[Callable[[str, int, int], bool]] = None self.on_receive_start: Optional[Callable[[str], None]] = None self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None - self.on_receive_text: Optional[Callable[[str, int], None]] = None + self.on_receive_text: Optional[Callable[[str, str, int], None]] = None self.on_send_complete: Optional[Callable[[List[str]], None]] = None self.on_transfer_progress: Optional[Callable[[int, int], None]] = None self.on_error: Optional[Callable[[str], None]] = None @@ -322,7 +322,7 @@ class DuktoProtocol: if receiving_text: text = text_data.decode('utf-8') if self.on_receive_text: - self.on_receive_text(text, total_size) + self.on_receive_text(sender_ip, text, total_size) else: if self.on_receive_complete: self.on_receive_complete(received_files, total_size) diff --git a/main.py b/main.py index 2af1fb1..f433ed0 100644 --- a/main.py +++ b/main.py @@ -346,6 +346,50 @@ class ReceiveConfirmationDialog(QtWidgets.QDialog): self.setLayout(layout) +class ShowTextDialog(QtWidgets.QDialog): + def __init__(self, text, sender_ip, parent=None): + super().__init__(parent) + self.setWindowTitle("Received Text") + self.setMinimumSize(400, 300) + + layout = QtWidgets.QVBoxLayout() + + # Info label + info_label = QtWidgets.QLabel(f"Received text from {sender_ip}:") + layout.addWidget(info_label) + + # Text display + self.text_edit = QtWidgets.QTextEdit() + self.text_edit.setPlainText(text) + self.text_edit.setReadOnly(True) + layout.addWidget(self.text_edit) + + # Buttons + button_layout = QtWidgets.QHBoxLayout() + self.copy_button = QtWidgets.QPushButton("Copy to Clipboard") + self.copy_button.clicked.connect(self.copy_to_clipboard) + + close_button = QtWidgets.QPushButton("Close") + close_button.clicked.connect(self.accept) + + button_layout.addStretch() + button_layout.addWidget(self.copy_button) + button_layout.addWidget(close_button) + + layout.addLayout(button_layout) + self.setLayout(layout) + + def copy_to_clipboard(self): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self.text_edit.toPlainText()) + self.copy_button.setText("Copied!") + + timer = QtCore.QTimer(self) + timer.setSingleShot(True) + timer.timeout.connect(lambda: self.copy_button.setText("Copy to Clipboard")) + timer.start(2000) + + class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() @@ -376,6 +420,7 @@ class MainWindow(QtWidgets.QMainWindow): self.super_menu = super_menu self.dukto_handler = dukto_handler self.dukto_handler.on_receive_request = self.handle_receive_request + self.dukto_handler.on_receive_text = self.handle_receive_text self.receive_confirmation_event = threading.Event() self.receive_confirmation_result = [False] @@ -573,6 +618,19 @@ class MainWindow(QtWidgets.QMainWindow): self.receive_confirmation_event.wait() return self.receive_confirmation_result[0] + @QtCore.Slot(str, str, int) + def show_text_dialog(self, sender_ip, text, total_size): + dialog = ShowTextDialog(text, sender_ip, self) + dialog.exec() + + def handle_receive_text(self, sender_ip, text, total_size): + QtCore.QMetaObject.invokeMethod( + self, "show_text_dialog", QtCore.Qt.QueuedConnection, #type: ignore + QtCore.Q_ARG(str, sender_ip), + QtCore.Q_ARG(str, text), + QtCore.Q_ARG(int, total_size), + ) + def restart_application(self): presence.end() self.dukto_handler.shutdown() From a8739b12b606ad079f4031e055139c039d0339fd Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:26:20 +0200 Subject: [PATCH 06/13] Revert "text" This reverts commit 25eaad149172c1dfc9c8bcd39b303663f3ebbc23. --- core/dukto.py | 4 ++-- main.py | 58 --------------------------------------------------- 2 files changed, 2 insertions(+), 60 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 72e88f8..0d1e19d 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -43,7 +43,7 @@ class DuktoProtocol: self.on_receive_request: Optional[Callable[[str, int, int], bool]] = None self.on_receive_start: Optional[Callable[[str], None]] = None self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None - self.on_receive_text: Optional[Callable[[str, str, int], None]] = None + self.on_receive_text: Optional[Callable[[str, int], None]] = None self.on_send_complete: Optional[Callable[[List[str]], None]] = None self.on_transfer_progress: Optional[Callable[[int, int], None]] = None self.on_error: Optional[Callable[[str], None]] = None @@ -322,7 +322,7 @@ class DuktoProtocol: if receiving_text: text = text_data.decode('utf-8') if self.on_receive_text: - self.on_receive_text(sender_ip, text, total_size) + self.on_receive_text(text, total_size) else: if self.on_receive_complete: self.on_receive_complete(received_files, total_size) diff --git a/main.py b/main.py index f433ed0..2af1fb1 100644 --- a/main.py +++ b/main.py @@ -346,50 +346,6 @@ class ReceiveConfirmationDialog(QtWidgets.QDialog): self.setLayout(layout) -class ShowTextDialog(QtWidgets.QDialog): - def __init__(self, text, sender_ip, parent=None): - super().__init__(parent) - self.setWindowTitle("Received Text") - self.setMinimumSize(400, 300) - - layout = QtWidgets.QVBoxLayout() - - # Info label - info_label = QtWidgets.QLabel(f"Received text from {sender_ip}:") - layout.addWidget(info_label) - - # Text display - self.text_edit = QtWidgets.QTextEdit() - self.text_edit.setPlainText(text) - self.text_edit.setReadOnly(True) - layout.addWidget(self.text_edit) - - # Buttons - button_layout = QtWidgets.QHBoxLayout() - self.copy_button = QtWidgets.QPushButton("Copy to Clipboard") - self.copy_button.clicked.connect(self.copy_to_clipboard) - - close_button = QtWidgets.QPushButton("Close") - close_button.clicked.connect(self.accept) - - button_layout.addStretch() - button_layout.addWidget(self.copy_button) - button_layout.addWidget(close_button) - - layout.addLayout(button_layout) - self.setLayout(layout) - - def copy_to_clipboard(self): - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(self.text_edit.toPlainText()) - self.copy_button.setText("Copied!") - - timer = QtCore.QTimer(self) - timer.setSingleShot(True) - timer.timeout.connect(lambda: self.copy_button.setText("Copy to Clipboard")) - timer.start(2000) - - class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() @@ -420,7 +376,6 @@ class MainWindow(QtWidgets.QMainWindow): self.super_menu = super_menu self.dukto_handler = dukto_handler self.dukto_handler.on_receive_request = self.handle_receive_request - self.dukto_handler.on_receive_text = self.handle_receive_text self.receive_confirmation_event = threading.Event() self.receive_confirmation_result = [False] @@ -618,19 +573,6 @@ class MainWindow(QtWidgets.QMainWindow): self.receive_confirmation_event.wait() return self.receive_confirmation_result[0] - @QtCore.Slot(str, str, int) - def show_text_dialog(self, sender_ip, text, total_size): - dialog = ShowTextDialog(text, sender_ip, self) - dialog.exec() - - def handle_receive_text(self, sender_ip, text, total_size): - QtCore.QMetaObject.invokeMethod( - self, "show_text_dialog", QtCore.Qt.QueuedConnection, #type: ignore - QtCore.Q_ARG(str, sender_ip), - QtCore.Q_ARG(str, text), - QtCore.Q_ARG(int, total_size), - ) - def restart_application(self): presence.end() self.dukto_handler.shutdown() From 696b7a2264bfdb314300460f3393b9634043ee75 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:26:42 +0200 Subject: [PATCH 07/13] Revert "receive confirmation" This reverts commit 9b20fa745e0f74484eeaac633549121c27bb3ad0. --- core/dukto.py | 43 ++++++++++--------------------- main.py | 71 +++------------------------------------------------ 2 files changed, 16 insertions(+), 98 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 0d1e19d..37d24eb 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -40,7 +40,6 @@ class DuktoProtocol: # Callbacks self.on_peer_added: Optional[Callable[[Peer], None]] = None self.on_peer_removed: Optional[Callable[[Peer], None]] = None - self.on_receive_request: Optional[Callable[[str, int, int], bool]] = None self.on_receive_start: Optional[Callable[[str], None]] = None self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None self.on_receive_text: Optional[Callable[[str, int], None]] = None @@ -193,45 +192,29 @@ class DuktoProtocol: conn.close() continue - threading.Thread(target=self._handle_connection_request, + threading.Thread(target=self._receive_files, args=(conn, addr[0]), daemon=True).start() except Exception as e: if self.running: print(f"TCP listener error: {e}") - - def _handle_connection_request(self, conn: socket.socket, sender_ip: str): - try: - conn.settimeout(10) - - header = conn.recv(16) - if len(header) < 16: - conn.close() - return - - elements_count = struct.unpack(' bool: - self.receive_confirmation_event.clear() - - QtCore.QMetaObject.invokeMethod( - self, "show_receive_dialog", QtCore.Qt.QueuedConnection, #type: ignore - QtCore.Q_ARG(str, sender_ip), - QtCore.Q_ARG(int, file_count), - QtCore.Q_ARG(int, total_size), - ) - - self.receive_confirmation_event.wait() - return self.receive_confirmation_result[0] - def restart_application(self): presence.end() - self.dukto_handler.shutdown() args = [sys.executable] + sys.argv @@ -591,16 +534,9 @@ def main(): restart = "--restart" in sys.argv no_quit = "--no-quit" in sys.argv super_menu = not "--no-super" in sys.argv - - dukto_handler = DuktoProtocol() - - pet = MainWindow(dukto_handler=dukto_handler, restart=restart, no_quit=no_quit, super_menu=super_menu) - + pet = MainWindow(restart=restart, no_quit=no_quit, super_menu=super_menu) presence.start() - dukto_handler.initialize() - dukto_handler.say_hello() - # bottom right corner screen_geometry = app.primaryScreen().availableGeometry() pet_geometry = pet.frameGeometry() @@ -611,7 +547,6 @@ def main(): pet.show() app.aboutToQuit.connect(presence.end) - app.aboutToQuit.connect(dukto_handler.shutdown) sys.exit(app.exec()) From 440eec467716756f552d7e8b930a7979ee58a74b Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:29:16 +0200 Subject: [PATCH 08/13] LET'S GO YOU CAN REJECT --- core/dukto.py | 39 ++++++++++++++++++++++++++++++++++++--- main.py | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 37d24eb..1f11d65 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -34,13 +34,19 @@ class DuktoProtocol: self.peers: Dict[str, Peer] = {} self.is_sending = False self.is_receiving = False + self.is_awaiting_approval = False self.running = False + # Confirmation for receiving + self._transfer_decision = threading.Event() + self._transfer_approved = False + # Callbacks self.on_peer_added: Optional[Callable[[Peer], None]] = None self.on_peer_removed: Optional[Callable[[Peer], None]] = None self.on_receive_start: Optional[Callable[[str], None]] = None + self.on_receive_request: Optional[Callable[[str], None]] = None self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None self.on_receive_text: Optional[Callable[[str, int], None]] = None self.on_send_complete: Optional[Callable[[List[str]], None]] = None @@ -143,6 +149,14 @@ class DuktoProtocol: threading.Thread(target=self._send_text_thread, args=(ip_dest, port, text), daemon=True).start() + def approve_transfer(self): + self._transfer_approved = True + self._transfer_decision.set() + + def reject_transfer(self): + self._transfer_approved = False + self._transfer_decision.set() + def _udp_listener(self): while self.running: try: @@ -188,16 +202,35 @@ class DuktoProtocol: while self.running: try: conn, addr = self.tcp_server.accept() - if self.is_receiving or self.is_sending: + if self.is_receiving or self.is_sending or self.is_awaiting_approval: conn.close() continue - threading.Thread(target=self._receive_files, + threading.Thread(target=self._handle_transfer_request, args=(conn, addr[0]), daemon=True).start() except Exception as e: if self.running: print(f"TCP listener error: {e}") - + + def _handle_transfer_request(self, conn: socket.socket, sender_ip: str): + try: + self.is_awaiting_approval = True + self._transfer_decision.clear() + + if self.on_receive_request: + self.on_receive_request(sender_ip) + else: + self.reject_transfer() + + self._transfer_decision.wait() + + if self._transfer_approved: + self._receive_files(conn, sender_ip) + else: + conn.close() + finally: + self.is_awaiting_approval = False + def _receive_files(self, conn: socket.socket, sender_ip: str): self.is_receiving = True diff --git a/main.py b/main.py index 6234f30..ae31d44 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ from core.web_search import MullvadLetaWrapper from core.discord_presence import presence from core.app_launcher import list_apps, launch from core.updater import update_repository, is_update_available +from core.dukto import DuktoProtocol ASSET = Path(__file__).parent / "assets" / "2ktan.png" @@ -318,8 +319,9 @@ class WebSearchResults(QtWidgets.QDialog): class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() + receive_request_signal = QtCore.Signal(str) - def __init__(self, restart=False, no_quit=False, super_menu=True): + def __init__(self, dukto_handler, restart=False, no_quit=False, super_menu=True): super().__init__() flags = ( @@ -344,6 +346,9 @@ class MainWindow(QtWidgets.QMainWindow): self.setMask(mask) self.super_menu = super_menu + self.dukto_handler = dukto_handler + self.dukto_handler.on_receive_request = self.on_dukto_receive_request + self.receive_request_signal.connect(self.show_receive_confirmation) self.tray = QtWidgets.QSystemTrayIcon(self) self.tray.setIcon(QtGui.QIcon(str(ASSET))) @@ -418,6 +423,22 @@ class MainWindow(QtWidgets.QMainWindow): def toggle_visible(self): self.setVisible(not self.isVisible()) + def on_dukto_receive_request(self, sender_ip: str): + self.receive_request_signal.emit(sender_ip) + + def show_receive_confirmation(self, sender_ip: str): + reply = QtWidgets.QMessageBox.question( + self, + "Incoming Transfer", + f"You have an incoming transfer from {sender_ip}.\nDo you want to accept it?", + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.No + ) + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + self.dukto_handler.approve_transfer() + else: + self.dukto_handler.reject_transfer() + def start_app_launcher(self): self.app_launcher_dialog = AppLauncherDialog(self) self.app_launcher_dialog.move(QtGui.QCursor.pos()) @@ -519,6 +540,7 @@ class MainWindow(QtWidgets.QMainWindow): def restart_application(self): presence.end() + self.dukto_handler.shutdown() args = [sys.executable] + sys.argv @@ -534,9 +556,16 @@ def main(): restart = "--restart" in sys.argv no_quit = "--no-quit" in sys.argv super_menu = not "--no-super" in sys.argv - pet = MainWindow(restart=restart, no_quit=no_quit, super_menu=super_menu) + + dukto_handler = DuktoProtocol() + + pet = MainWindow(dukto_handler=dukto_handler, restart=restart, no_quit=no_quit, super_menu=super_menu) + presence.start() + dukto_handler.initialize() + dukto_handler.say_hello() + # bottom right corner screen_geometry = app.primaryScreen().availableGeometry() pet_geometry = pet.frameGeometry() @@ -547,6 +576,7 @@ def main(): pet.show() app.aboutToQuit.connect(presence.end) + app.aboutToQuit.connect(dukto_handler.shutdown) sys.exit(app.exec()) From 081baa52467a2b8eaac6ae44feedad5f31d41fe2 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:39:19 +0200 Subject: [PATCH 09/13] RECEIVING FILES WORKS --- core/dukto.py | 38 +++++++++++++++++++--------- main.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 1f11d65..35b1d77 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -234,6 +234,10 @@ class DuktoProtocol: def _receive_files(self, conn: socket.socket, sender_ip: str): self.is_receiving = True + # Define the receive directory + receive_dir = Path.home() / "Received" + receive_dir.mkdir(parents=True, exist_ok=True) + if self.on_receive_start: self.on_receive_start(sender_ip) @@ -279,16 +283,24 @@ class DuktoProtocol: # Find unique name i = 2 original_name = name - while os.path.exists(name): - name = f"{original_name} ({i})" + dest_path = receive_dir / name + while dest_path.exists(): + dest_path = receive_dir / f"{original_name} ({i})" i += 1 + root_folder_name = original_name - root_folder_renamed = name - received_files.append(name) + root_folder_renamed = dest_path.name + + final_path = dest_path + received_files.append(str(final_path)) + elif root_folder_name != root_folder_renamed: name = name.replace(root_folder_name, root_folder_renamed, 1) + final_path = receive_dir / name + else: + final_path = receive_dir / name - os.makedirs(name, exist_ok=True) + final_path.mkdir(parents=True, exist_ok=True) elif name == "___DUKTO___TEXT___": # Text transfer receiving_text = True @@ -308,21 +320,23 @@ class DuktoProtocol: self.on_transfer_progress(total_size, total_received) else: # Regular file + dest_name = name if '/' in name and name.split('/')[0] == root_folder_name: - name = name.replace(root_folder_name, root_folder_renamed, 1) + dest_name = dest_name.replace(root_folder_name, root_folder_renamed, 1) # Find unique filename i = 2 - original_name = name - base_path = Path(name) - while os.path.exists(name): - name = f"{base_path.stem} ({i}){base_path.suffix}" + original_path = receive_dir / dest_name + dest_path = original_path + while dest_path.exists(): + dest_path = original_path.with_name(f"{original_path.stem} ({i}){original_path.suffix}") i += 1 - received_files.append(name) + received_files.append(str(dest_path)) + dest_path.parent.mkdir(parents=True, exist_ok=True) # Receive file data - with open(name, 'wb') as f: + with open(dest_path, 'wb') as f: received = 0 while received < element_size: chunk = conn.recv(min(8192, element_size - received)) diff --git a/main.py b/main.py index ae31d44..ba7999c 100644 --- a/main.py +++ b/main.py @@ -319,7 +319,15 @@ class WebSearchResults(QtWidgets.QDialog): class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() + + # Dukto signals receive_request_signal = QtCore.Signal(str) + progress_update_signal = QtCore.Signal(int, int) + receive_start_signal = QtCore.Signal(str) + receive_complete_signal = QtCore.Signal(list, int) + receive_text_signal = QtCore.Signal(str, int) + dukto_error_signal = QtCore.Signal(str) + def __init__(self, dukto_handler, restart=False, no_quit=False, super_menu=True): super().__init__() @@ -347,8 +355,23 @@ class MainWindow(QtWidgets.QMainWindow): self.super_menu = super_menu self.dukto_handler = dukto_handler - self.dukto_handler.on_receive_request = self.on_dukto_receive_request + self.progress_dialog = None + + # Connect Dukto callbacks to emit signals + self.dukto_handler.on_receive_request = lambda ip: self.receive_request_signal.emit(ip) + self.dukto_handler.on_transfer_progress = lambda total, rec: self.progress_update_signal.emit(total, rec) + self.dukto_handler.on_receive_start = lambda ip: self.receive_start_signal.emit(ip) + self.dukto_handler.on_receive_complete = lambda files, size: self.receive_complete_signal.emit(files, size) + self.dukto_handler.on_receive_text = lambda text, size: self.receive_text_signal.emit(text, size) + self.dukto_handler.on_error = lambda msg: self.dukto_error_signal.emit(msg) + + # Connect signals to GUI slots self.receive_request_signal.connect(self.show_receive_confirmation) + self.progress_update_signal.connect(self.update_progress_dialog) + self.receive_start_signal.connect(self.handle_receive_start) + self.receive_complete_signal.connect(self.handle_receive_complete) + self.receive_text_signal.connect(self.handle_receive_text) + self.dukto_error_signal.connect(self.handle_dukto_error) self.tray = QtWidgets.QSystemTrayIcon(self) self.tray.setIcon(QtGui.QIcon(str(ASSET))) @@ -423,9 +446,6 @@ class MainWindow(QtWidgets.QMainWindow): def toggle_visible(self): self.setVisible(not self.isVisible()) - def on_dukto_receive_request(self, sender_ip: str): - self.receive_request_signal.emit(sender_ip) - def show_receive_confirmation(self, sender_ip: str): reply = QtWidgets.QMessageBox.question( self, @@ -439,6 +459,47 @@ class MainWindow(QtWidgets.QMainWindow): else: self.dukto_handler.reject_transfer() + @QtCore.Slot(str) + def handle_receive_start(self, sender_ip: str): + self.progress_dialog = QtWidgets.QProgressDialog("Receiving file...", "Cancel", 0, 100, self) + self.progress_dialog.setWindowTitle(f"Receiving from {sender_ip}") + self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore + self.progress_dialog.show() + + @QtCore.Slot(int, int) + def update_progress_dialog(self, total_size: int, received: int): + if self.progress_dialog: + self.progress_dialog.setMaximum(total_size) + self.progress_dialog.setValue(received) + + @QtCore.Slot(list, int) + def handle_receive_complete(self, received_files: list, total_size: int): + if self.progress_dialog: + self.progress_dialog.setValue(total_size) + self.progress_dialog.close() + self.progress_dialog = None + + QtWidgets.QMessageBox.information(self, "Transfer Complete", f"Successfully received {len(received_files)} items to ~/Received.") + + # Open the directory + receive_dir = str(Path.home() / "Received") + url = QtCore.QUrl.fromLocalFile(receive_dir) + QtGui.QDesktopServices.openUrl(url) + + @QtCore.Slot(str, int) + def handle_receive_text(self, text: str, total_size: int): + if self.progress_dialog: + self.progress_dialog.close() + self.progress_dialog = None + QtWidgets.QMessageBox.information(self, "Text Received", text) + + @QtCore.Slot(str) + def handle_dukto_error(self, error_msg: str): + if self.progress_dialog: + self.progress_dialog.close() + self.progress_dialog = None + QtWidgets.QMessageBox.critical(self, "Transfer Error", error_msg) + def start_app_launcher(self): self.app_launcher_dialog = AppLauncherDialog(self) self.app_launcher_dialog.move(QtGui.QCursor.pos()) From cde018210176d8ba56a4a57020380698f9c01971 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:42:35 +0200 Subject: [PATCH 10/13] faster shutdown + text receiving --- core/discord_presence.py | 2 +- main.py | 52 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/core/discord_presence.py b/core/discord_presence.py index 0b201c7..bde3d7c 100644 --- a/core/discord_presence.py +++ b/core/discord_presence.py @@ -75,7 +75,7 @@ class DiscordPresence: print("Stopping Discord presence...") self._stop_thread.set() if self._thread and self._thread.is_alive(): - self._thread.join(timeout=5) + self._thread.join(timeout=1) self.running = False self.presence = None diff --git a/main.py b/main.py index ba7999c..290c6d5 100644 --- a/main.py +++ b/main.py @@ -317,6 +317,38 @@ class WebSearchResults(QtWidgets.QDialog): QtWidgets.QApplication.restoreOverrideCursor() +class TextViewerDialog(QtWidgets.QDialog): + def __init__(self, text, parent=None): + super().__init__(parent) + self.setWindowTitle("Text Received") + self.setMinimumSize(400, 300) + + self.text_to_copy = text + layout = QtWidgets.QVBoxLayout(self) + + self.text_edit = QtWidgets.QTextEdit() + self.text_edit.setPlainText(text) + self.text_edit.setReadOnly(True) + layout.addWidget(self.text_edit) + + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() + + copy_button = QtWidgets.QPushButton("Copy to Clipboard") + copy_button.clicked.connect(self.copy_text) + button_layout.addWidget(copy_button) + + close_button = QtWidgets.QPushButton("Close") + close_button.clicked.connect(self.accept) + button_layout.addWidget(close_button) + + layout.addLayout(button_layout) + + def copy_text(self): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self.text_to_copy) + + class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() @@ -481,17 +513,27 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QMessageBox.information(self, "Transfer Complete", f"Successfully received {len(received_files)} items to ~/Received.") - # Open the directory - receive_dir = str(Path.home() / "Received") - url = QtCore.QUrl.fromLocalFile(receive_dir) - QtGui.QDesktopServices.openUrl(url) + reply = QtWidgets.QMessageBox.question( + self, + "Open Directory", + "Do you want to open the folder now?", + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.Yes + ) + + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + receive_dir = str(Path.home() / "Received") + url = QtCore.QUrl.fromLocalFile(receive_dir) + QtGui.QDesktopServices.openUrl(url) @QtCore.Slot(str, int) def handle_receive_text(self, text: str, total_size: int): if self.progress_dialog: self.progress_dialog.close() self.progress_dialog = None - QtWidgets.QMessageBox.information(self, "Text Received", text) + + dialog = TextViewerDialog(text, self) + dialog.exec() @QtCore.Slot(str) def handle_dukto_error(self, error_msg: str): From 1189b2f53eb7d553c777feef1763fa15b708bd85 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:47:28 +0200 Subject: [PATCH 11/13] identify as CLARA --- core/dukto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 35b1d77..84edb7c 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -58,10 +58,10 @@ class DuktoProtocol: self.local_tcp_port = tcp_port def get_system_signature(self) -> str: - username = getpass.getuser() + name = getpass.getuser() + "'s CLARA" hostname = socket.gethostname() system = platform.system() - return f"{username} at {hostname} ({system})" + return f"{name} at {hostname} ({system})" def initialize(self): # UDP Socket for peer discovery From 1b86d5ad4708bdd89c7146d11412364461798cf1 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:49:56 +0200 Subject: [PATCH 12/13] sending 1 --- core/dukto.py | 7 ++++ main.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/core/dukto.py b/core/dukto.py index 84edb7c..6812bd4 100644 --- a/core/dukto.py +++ b/core/dukto.py @@ -49,6 +49,7 @@ class DuktoProtocol: self.on_receive_request: Optional[Callable[[str], None]] = None self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None self.on_receive_text: Optional[Callable[[str, int], None]] = None + self.on_send_start: Optional[Callable[[str], None]] = None self.on_send_complete: Optional[Callable[[List[str]], None]] = None self.on_transfer_progress: Optional[Callable[[int, int], None]] = None self.on_error: Optional[Callable[[str], None]] = None @@ -367,6 +368,9 @@ class DuktoProtocol: def _send_file_thread(self, ip_dest: str, port: int, files: List[str]): try: + if self.on_send_start: + self.on_send_start(ip_dest) + # Expand file tree expanded_files = self._expand_tree(files) @@ -426,6 +430,9 @@ class DuktoProtocol: def _send_text_thread(self, ip_dest: str, port: int, text: str): try: + if self.on_send_start: + self.on_send_start(ip_dest) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((ip_dest, port)) diff --git a/main.py b/main.py index 290c6d5..0f68f10 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ from core.web_search import MullvadLetaWrapper from core.discord_presence import presence from core.app_launcher import list_apps, launch from core.updater import update_repository, is_update_available -from core.dukto import DuktoProtocol +from core.dukto import DuktoProtocol, Peer ASSET = Path(__file__).parent / "assets" / "2ktan.png" @@ -353,11 +353,15 @@ class MainWindow(QtWidgets.QMainWindow): show_menu_signal = QtCore.Signal() # Dukto signals + peer_added_signal = QtCore.Signal(Peer) + peer_removed_signal = QtCore.Signal(Peer) receive_request_signal = QtCore.Signal(str) progress_update_signal = QtCore.Signal(int, int) receive_start_signal = QtCore.Signal(str) receive_complete_signal = QtCore.Signal(list, int) receive_text_signal = QtCore.Signal(str, int) + send_start_signal = QtCore.Signal(str) + send_complete_signal = QtCore.Signal(list) dukto_error_signal = QtCore.Signal(str) @@ -390,19 +394,27 @@ class MainWindow(QtWidgets.QMainWindow): self.progress_dialog = None # Connect Dukto callbacks to emit signals + self.dukto_handler.on_peer_added = lambda peer: self.peer_added_signal.emit(peer) + self.dukto_handler.on_peer_removed = lambda peer: self.peer_removed_signal.emit(peer) self.dukto_handler.on_receive_request = lambda ip: self.receive_request_signal.emit(ip) self.dukto_handler.on_transfer_progress = lambda total, rec: self.progress_update_signal.emit(total, rec) self.dukto_handler.on_receive_start = lambda ip: self.receive_start_signal.emit(ip) self.dukto_handler.on_receive_complete = lambda files, size: self.receive_complete_signal.emit(files, size) self.dukto_handler.on_receive_text = lambda text, size: self.receive_text_signal.emit(text, size) + self.dukto_handler.on_send_start = lambda ip: self.send_start_signal.emit(ip) + self.dukto_handler.on_send_complete = lambda files: self.send_complete_signal.emit(files) self.dukto_handler.on_error = lambda msg: self.dukto_error_signal.emit(msg) # Connect signals to GUI slots + self.peer_added_signal.connect(self.handle_peer_added) + self.peer_removed_signal.connect(self.handle_peer_removed) self.receive_request_signal.connect(self.show_receive_confirmation) self.progress_update_signal.connect(self.update_progress_dialog) self.receive_start_signal.connect(self.handle_receive_start) self.receive_complete_signal.connect(self.handle_receive_complete) self.receive_text_signal.connect(self.handle_receive_text) + self.send_start_signal.connect(self.handle_send_start) + self.send_complete_signal.connect(self.handle_send_complete) self.dukto_error_signal.connect(self.handle_dukto_error) self.tray = QtWidgets.QSystemTrayIcon(self) @@ -414,6 +426,8 @@ class MainWindow(QtWidgets.QMainWindow): right_menu.addAction("Search Files", self.start_file_search) right_menu.addAction("Search Web", self.start_web_search) right_menu.addSeparator() + self.send_menu_right = right_menu.addMenu("Send to") + right_menu.addSeparator() right_menu.addAction("Check for updates", self.update_git) if restart: right_menu.addAction("Restart", self.restart_application) @@ -430,6 +444,10 @@ class MainWindow(QtWidgets.QMainWindow): self.left_menu.addAction("Launch App", self.start_app_launcher) self.left_menu.addAction("Search Files", self.start_file_search) self.left_menu.addAction("Search Web", self.start_web_search) + self.left_menu.addSeparator() + self.send_menu_left = self.left_menu.addMenu("Send to") + + self.update_peer_menus() # always on top timer self.stay_on_top_timer = QtCore.QTimer(self) @@ -478,6 +496,60 @@ class MainWindow(QtWidgets.QMainWindow): def toggle_visible(self): self.setVisible(not self.isVisible()) + @QtCore.Slot(Peer) + def handle_peer_added(self, peer: Peer): + self.update_peer_menus() + + @QtCore.Slot(Peer) + def handle_peer_removed(self, peer: Peer): + self.update_peer_menus() + + def update_peer_menus(self): + self.send_menu_left.clear() + self.send_menu_right.clear() + + peers = list(self.dukto_handler.peers.values()) + + if not peers: + self.send_menu_left.addAction("No peers found").setEnabled(False) + self.send_menu_right.addAction("No peers found").setEnabled(False) + return + + for peer in sorted(peers, key=lambda p: p.signature): + peer_submenu_left = self.send_menu_left.addMenu(peer.signature) + peer_submenu_right = self.send_menu_right.addMenu(peer.signature) + + send_files_action_left = peer_submenu_left.addAction("Send File(s)") + send_text_action_left = peer_submenu_left.addAction("Send Text") + + send_files_action_right = peer_submenu_right.addAction("Send File(s)") + send_text_action_right = peer_submenu_right.addAction("Send Text") + + # Use a lambda with a default argument to capture the correct peer object + send_files_action_left.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p)) + send_text_action_left.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p)) + + send_files_action_right.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p)) + send_text_action_right.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p)) + + def start_file_send(self, peer: Peer): + file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames( + self, + f"Select files to send to {peer.signature}", + str(Path.home()), + ) + if file_paths: + self.dukto_handler.send_file(peer.address, file_paths, peer.port) + + def start_text_send(self, peer: Peer): + text, ok = QtWidgets.QInputDialog.getMultiLineText( + self, + f"Send Text to {peer.signature}", + "Enter text to send:" + ) + if ok and text: + self.dukto_handler.send_text(peer.address, text, peer.port) + def show_receive_confirmation(self, sender_ip: str): reply = QtWidgets.QMessageBox.question( self, @@ -493,10 +565,17 @@ class MainWindow(QtWidgets.QMainWindow): @QtCore.Slot(str) def handle_receive_start(self, sender_ip: str): - self.progress_dialog = QtWidgets.QProgressDialog("Receiving file...", "Cancel", 0, 100, self) + self.progress_dialog = QtWidgets.QProgressDialog("Receiving data...", "Cancel", 0, 100, self) self.progress_dialog.setWindowTitle(f"Receiving from {sender_ip}") self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore self.progress_dialog.show() + + @QtCore.Slot(str) + def handle_send_start(self, dest_ip: str): + self.progress_dialog = QtWidgets.QProgressDialog("Sending data...", "Cancel", 0, 100, self) + self.progress_dialog.setWindowTitle(f"Sending to {dest_ip}") + self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore + self.progress_dialog.show() @QtCore.Slot(int, int) def update_progress_dialog(self, total_size: int, received: int): @@ -525,6 +604,19 @@ class MainWindow(QtWidgets.QMainWindow): receive_dir = str(Path.home() / "Received") url = QtCore.QUrl.fromLocalFile(receive_dir) QtGui.QDesktopServices.openUrl(url) + + @QtCore.Slot(list) + def handle_send_complete(self, sent_files: list): + if self.progress_dialog: + if self.progress_dialog.maximum() > 0: + self.progress_dialog.setValue(self.progress_dialog.maximum()) + self.progress_dialog.close() + self.progress_dialog = None + + if sent_files and sent_files[0] == "___DUKTO___TEXT___": + QtWidgets.QMessageBox.information(self, "Transfer Complete", "Text sent successfully.") + else: + QtWidgets.QMessageBox.information(self, "Transfer Complete", f"Successfully sent {len(sent_files)} items.") @QtCore.Slot(str, int) def handle_receive_text(self, text: str, total_size: int): From a31a32e03dcfc69f1c81fc448896e89a5f4bf6ec Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Fri, 24 Oct 2025 19:52:36 +0200 Subject: [PATCH 13/13] sending 2 --- main.py | 61 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/main.py b/main.py index 0f68f10..d54acc3 100644 --- a/main.py +++ b/main.py @@ -406,8 +406,8 @@ class MainWindow(QtWidgets.QMainWindow): self.dukto_handler.on_error = lambda msg: self.dukto_error_signal.emit(msg) # Connect signals to GUI slots - self.peer_added_signal.connect(self.handle_peer_added) - self.peer_removed_signal.connect(self.handle_peer_removed) + self.peer_added_signal.connect(self.update_peer_menus) + self.peer_removed_signal.connect(self.update_peer_menus) self.receive_request_signal.connect(self.show_receive_confirmation) self.progress_update_signal.connect(self.update_progress_dialog) self.receive_start_signal.connect(self.handle_receive_start) @@ -426,7 +426,9 @@ class MainWindow(QtWidgets.QMainWindow): right_menu.addAction("Search Files", self.start_file_search) right_menu.addAction("Search Web", self.start_web_search) right_menu.addSeparator() - self.send_menu_right = right_menu.addMenu("Send to") + send_menu_right = right_menu.addMenu("Send") + self.send_files_submenu_right = send_menu_right.addMenu("Send File(s)") + self.send_text_submenu_right = send_menu_right.addMenu("Send Text") right_menu.addSeparator() right_menu.addAction("Check for updates", self.update_git) if restart: @@ -445,7 +447,9 @@ class MainWindow(QtWidgets.QMainWindow): self.left_menu.addAction("Search Files", self.start_file_search) self.left_menu.addAction("Search Web", self.start_web_search) self.left_menu.addSeparator() - self.send_menu_left = self.left_menu.addMenu("Send to") + send_menu_left = self.left_menu.addMenu("Send") + self.send_files_submenu_left = send_menu_left.addMenu("Send File(s)") + self.send_text_submenu_left = send_menu_left.addMenu("Send Text") self.update_peer_menus() @@ -496,41 +500,38 @@ class MainWindow(QtWidgets.QMainWindow): def toggle_visible(self): self.setVisible(not self.isVisible()) - @QtCore.Slot(Peer) - def handle_peer_added(self, peer: Peer): - self.update_peer_menus() - - @QtCore.Slot(Peer) - def handle_peer_removed(self, peer: Peer): - self.update_peer_menus() - def update_peer_menus(self): - self.send_menu_left.clear() - self.send_menu_right.clear() - + self.send_files_submenu_left.clear() + self.send_text_submenu_left.clear() + self.send_files_submenu_right.clear() + self.send_text_submenu_right.clear() + peers = list(self.dukto_handler.peers.values()) - + if not peers: - self.send_menu_left.addAction("No peers found").setEnabled(False) - self.send_menu_right.addAction("No peers found").setEnabled(False) + no_peers_action_left_files = self.send_files_submenu_left.addAction("No peers found") + no_peers_action_left_files.setEnabled(False) + no_peers_action_left_text = self.send_text_submenu_left.addAction("No peers found") + no_peers_action_left_text.setEnabled(False) + + no_peers_action_right_files = self.send_files_submenu_right.addAction("No peers found") + no_peers_action_right_files.setEnabled(False) + no_peers_action_right_text = self.send_text_submenu_right.addAction("No peers found") + no_peers_action_right_text.setEnabled(False) return for peer in sorted(peers, key=lambda p: p.signature): - peer_submenu_left = self.send_menu_left.addMenu(peer.signature) - peer_submenu_right = self.send_menu_right.addMenu(peer.signature) - - send_files_action_left = peer_submenu_left.addAction("Send File(s)") - send_text_action_left = peer_submenu_left.addAction("Send Text") + file_action_left = self.send_files_submenu_left.addAction(peer.signature) + file_action_right = self.send_files_submenu_right.addAction(peer.signature) - send_files_action_right = peer_submenu_right.addAction("Send File(s)") - send_text_action_right = peer_submenu_right.addAction("Send Text") + text_action_left = self.send_text_submenu_left.addAction(peer.signature) + text_action_right = self.send_text_submenu_right.addAction(peer.signature) - # Use a lambda with a default argument to capture the correct peer object - send_files_action_left.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p)) - send_text_action_left.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p)) + file_action_left.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p)) + file_action_right.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p)) - send_files_action_right.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p)) - send_text_action_right.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p)) + text_action_left.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p)) + text_action_right.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p)) def start_file_send(self, peer: Peer): file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(