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())