This commit is contained in:
N0\A
2025-10-24 14:29:04 +02:00
parent 3b6b40fa88
commit b9e2da327a
2 changed files with 157 additions and 148 deletions

View File

@@ -46,6 +46,7 @@ class DuktoProtocol:
self.on_send_complete: Optional[Callable[[List[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_transfer_progress: Optional[Callable[[int, int], None]] = None
self.on_error: Optional[Callable[[str], 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): def set_ports(self, udp_port: int, tcp_port: int):
self.local_udp_port = udp_port self.local_udp_port = udp_port
@@ -192,11 +193,21 @@ class DuktoProtocol:
conn.close() conn.close()
continue continue
threading.Thread(target=self._receive_files, if self.on_incoming_connection:
args=(conn, addr[0]), daemon=True).start() # 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: except Exception as e:
if self.running: if self.running:
print(f"TCP listener error: {e}") 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): def _receive_files(self, conn: socket.socket, sender_ip: str):
self.is_receiving = True self.is_receiving = True

290
main.py
View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sys, os, subprocess import sys, os, subprocess, socket
from pathlib import Path from pathlib import Path
from PySide6 import QtCore, QtGui, QtWidgets from PySide6 import QtCore, QtGui, QtWidgets
from pynput import keyboard from pynput import keyboard
@@ -13,140 +13,6 @@ from core.dukto import DuktoProtocol, Peer
ASSET = Path(__file__).parent / "assets" / "2ktan.png" 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): class AppLauncherDialog(QtWidgets.QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -453,6 +319,14 @@ class WebSearchResults(QtWidgets.QDialog):
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
show_menu_signal = QtCore.Signal() 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): def __init__(self, restart=False, no_quit=False, super_menu=True):
super().__init__() super().__init__()
@@ -479,6 +353,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.setMask(mask) self.setMask(mask)
self.super_menu = super_menu self.super_menu = super_menu
self.progress_dialog = None
self.tray = QtWidgets.QSystemTrayIcon(self) self.tray = QtWidgets.QSystemTrayIcon(self)
self.tray.setIcon(QtGui.QIcon(str(ASSET))) 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("Launch App", self.start_app_launcher)
right_menu.addAction("Search Files", self.start_file_search) right_menu.addAction("Search Files", self.start_file_search)
right_menu.addAction("Search Web", self.start_web_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.addSeparator()
right_menu.addAction("Check for updates", self.update_git) right_menu.addAction("Check for updates", self.update_git)
if restart: if restart:
@@ -496,7 +372,7 @@ class MainWindow(QtWidgets.QMainWindow):
right_menu.addAction("Hide/Show", self.toggle_visible) right_menu.addAction("Hide/Show", self.toggle_visible)
right_menu.addSeparator() right_menu.addSeparator()
if not no_quit: 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.setContextMenu(right_menu)
self.tray.activated.connect(self.handle_tray_activated) self.tray.activated.connect(self.handle_tray_activated)
self.tray.show() 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("Launch App", self.start_app_launcher)
self.left_menu.addAction("Search Files", self.start_file_search) self.left_menu.addAction("Search Files", self.start_file_search)
self.left_menu.addAction("Search Web", self.start_web_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 # always on top timer
self.stay_on_top_timer = QtCore.QTimer(self) 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.show_menu_signal.connect(self.show_menu)
self.start_hotkey_listener() 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): def show_menu(self):
self.left_menu.popup(QtGui.QCursor.pos()) 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.move(QtGui.QCursor.pos())
self.app_launcher_dialog.show() 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): def start_file_search(self):
dialog = QtWidgets.QInputDialog(self) dialog = QtWidgets.QInputDialog(self)
dialog.setWindowTitle("File Search") dialog.setWindowTitle("File Search")
@@ -660,13 +572,98 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QMessageBox.critical(self, "Update Failed", message) QtWidgets.QMessageBox.critical(self, "Update Failed", message)
def restart_application(self): def restart_application(self):
self.dukto.shutdown()
presence.end() presence.end()
args = [sys.executable] + sys.argv args = [sys.executable] + sys.argv
subprocess.Popen(args) subprocess.Popen(args)
QtWidgets.QApplication.quit() 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(): def main():
@@ -689,6 +686,7 @@ def main():
pet.show() pet.show()
app.aboutToQuit.connect(presence.end) app.aboutToQuit.connect(presence.end)
app.aboutToQuit.connect(pet.dukto.shutdown)
sys.exit(app.exec()) sys.exit(app.exec())