refactor, focus fixes, stable
- SHA
a5baae6bd5b1e93b9a25de85edc45a0ac17158a2- Parents
-
7ad6d3f - Tree
70ad7d9
a5baae6
a5baae6bd5b1e93b9a25de85edc45a0ac17158a27ad6d3f
70ad7d9| Status | File | + | - |
|---|---|---|---|
| M |
src/wulftp/core/file_transfer.py
|
27 | 13 |
| M |
src/wulftp/ui/__init__.py
|
12 | 0 |
| A |
src/wulftp/ui/dialogs.py
|
376 | 0 |
| M |
src/wulftp/ui/main_window.py
|
22 | 82 |
src/wulftp/core/file_transfer.pymodified@@ -3,6 +3,7 @@ | ||
| 3 | 3 | import os |
| 4 | 4 | import stat |
| 5 | 5 | from typing import Optional |
| 6 | +import posixpath # For proper remote path handling | |
| 6 | 7 | |
| 7 | 8 | from PyQt6.QtCore import QObject, QRunnable, pyqtSignal |
| 8 | 9 | |
@@ -81,7 +82,7 @@ class DirectoryTransferWorker(QRunnable): | ||
| 81 | 82 | count = 0 |
| 82 | 83 | try: |
| 83 | 84 | for item in self.sftp.listdir_attr(remote_path): |
| 84 | - item_path = os.path.join(remote_path, item.filename) | |
| 85 | + item_path = posixpath.join(remote_path, item.filename) | |
| 85 | 86 | if stat.S_ISDIR(item.st_mode): |
| 86 | 87 | count += self._count_remote_files(item_path) |
| 87 | 88 | else: |
@@ -96,6 +97,9 @@ class DirectoryTransferWorker(QRunnable): | ||
| 96 | 97 | if self.total_files == 0: |
| 97 | 98 | self.total_files = self._count_files(local_path) |
| 98 | 99 | |
| 100 | + # Ensure remote_path uses forward slashes | |
| 101 | + remote_path = remote_path.replace('\\', '/') | |
| 102 | + | |
| 99 | 103 | # Create remote directory |
| 100 | 104 | try: |
| 101 | 105 | self.sftp.mkdir(remote_path) |
@@ -106,17 +110,22 @@ class DirectoryTransferWorker(QRunnable): | ||
| 106 | 110 | # Upload contents |
| 107 | 111 | for item in os.listdir(local_path): |
| 108 | 112 | local_item = os.path.join(local_path, item) |
| 109 | - remote_item = os.path.join(remote_path, item).replace('\\', '/') | |
| 113 | + # Use posixpath for remote paths to ensure forward slashes | |
| 114 | + remote_item = posixpath.join(remote_path, item) | |
| 110 | 115 | |
| 111 | 116 | if os.path.isdir(local_item): |
| 112 | 117 | self._upload_directory(local_item, remote_item) |
| 113 | 118 | else: |
| 114 | 119 | # Upload file |
| 115 | - self.sftp.put(local_item, remote_item) | |
| 116 | - self.processed_files += 1 | |
| 117 | - if self.total_files > 0: | |
| 118 | - progress = int((self.processed_files / self.total_files) * 100) | |
| 119 | - self.signals.progress.emit(progress) | |
| 120 | + try: | |
| 121 | + self.sftp.put(local_item, remote_item) | |
| 122 | + self.processed_files += 1 | |
| 123 | + if self.total_files > 0: | |
| 124 | + progress = int((self.processed_files / self.total_files) * 100) | |
| 125 | + self.signals.progress.emit(progress) | |
| 126 | + except Exception as e: | |
| 127 | + # Re-raise with more context | |
| 128 | + raise Exception(f"Failed to upload {local_item} to {remote_item}: {str(e)}") | |
| 120 | 129 | |
| 121 | 130 | def _download_directory(self, remote_path: str, local_path: str): |
| 122 | 131 | """Recursively download a directory""" |
@@ -129,18 +138,23 @@ class DirectoryTransferWorker(QRunnable): | ||
| 129 | 138 | |
| 130 | 139 | # Download contents |
| 131 | 140 | for item in self.sftp.listdir_attr(remote_path): |
| 132 | - remote_item = os.path.join(remote_path, item.filename).replace('\\', '/') | |
| 141 | + # Use posixpath for remote paths | |
| 142 | + remote_item = posixpath.join(remote_path, item.filename) | |
| 133 | 143 | local_item = os.path.join(local_path, item.filename) |
| 134 | 144 | |
| 135 | 145 | if stat.S_ISDIR(item.st_mode): |
| 136 | 146 | self._download_directory(remote_item, local_item) |
| 137 | 147 | else: |
| 138 | 148 | # Download file |
| 139 | - self.sftp.get(remote_item, local_item) | |
| 140 | - self.processed_files += 1 | |
| 141 | - if self.total_files > 0: | |
| 142 | - progress = int((self.processed_files / self.total_files) * 100) | |
| 143 | - self.signals.progress.emit(progress) | |
| 149 | + try: | |
| 150 | + self.sftp.get(remote_item, local_item) | |
| 151 | + self.processed_files += 1 | |
| 152 | + if self.total_files > 0: | |
| 153 | + progress = int((self.processed_files / self.total_files) * 100) | |
| 154 | + self.signals.progress.emit(progress) | |
| 155 | + except Exception as e: | |
| 156 | + # Re-raise with more context | |
| 157 | + raise Exception(f"Failed to download {remote_item} to {local_item}: {str(e)}") | |
| 144 | 158 | |
| 145 | 159 | |
| 146 | 160 | class FileTransferManager: |
src/wulftp/ui/__init__.pymodified@@ -9,6 +9,13 @@ from .components import ( | ||
| 9 | 9 | FileTransferToolbar, |
| 10 | 10 | FileContextMenu |
| 11 | 11 | ) |
| 12 | +from .dialogs import ( | |
| 13 | + FastConfirmDialog, | |
| 14 | + FastMessageDialog, | |
| 15 | + InlineConfirmBar, | |
| 16 | + fast_confirm, | |
| 17 | + fast_message | |
| 18 | +) | |
| 12 | 19 | from .main_window import WulFTPClient |
| 13 | 20 | |
| 14 | 21 | __all__ = [ |
@@ -19,5 +26,10 @@ __all__ = [ | ||
| 19 | 26 | "RemoteFilePane", |
| 20 | 27 | "FileTransferToolbar", |
| 21 | 28 | "FileContextMenu", |
| 29 | + "FastConfirmDialog", | |
| 30 | + "FastMessageDialog", | |
| 31 | + "InlineConfirmBar", | |
| 32 | + "fast_confirm", | |
| 33 | + "fast_message", | |
| 22 | 34 | "WulFTPClient" |
| 23 | 35 | ] |
src/wulftp/ui/dialogs.pyadded@@ -0,0 +1,376 @@ | ||
| 1 | +"""Fast custom dialogs for wulFTP.""" | |
| 2 | + | |
| 3 | +from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QPropertyAnimation, QRect | |
| 4 | +from PyQt6.QtWidgets import ( | |
| 5 | + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, | |
| 6 | + QWidget, QFrame, QGraphicsOpacityEffect, QSizePolicy | |
| 7 | +) | |
| 8 | +from PyQt6.QtGui import QKeyEvent | |
| 9 | +import qtawesome as qta | |
| 10 | + | |
| 11 | + | |
| 12 | +class FastConfirmDialog(QDialog): | |
| 13 | + """Fast, lightweight confirmation dialog.""" | |
| 14 | + | |
| 15 | + def __init__(self, title: str, message: str, parent=None): | |
| 16 | + super().__init__(parent) | |
| 17 | + self.setWindowTitle(title) | |
| 18 | + self.setModal(True) | |
| 19 | + self.setWindowFlags( | |
| 20 | + Qt.WindowType.Dialog | | |
| 21 | + Qt.WindowType.FramelessWindowHint | | |
| 22 | + Qt.WindowType.WindowStaysOnTopHint | |
| 23 | + ) | |
| 24 | + | |
| 25 | + # Make it slightly transparent for modern look | |
| 26 | + self.setWindowOpacity(0.95) | |
| 27 | + | |
| 28 | + # Don't set size yet - let it adjust after setup | |
| 29 | + self._setup_ui(title, message) | |
| 30 | + | |
| 31 | + # Now adjust size based on content | |
| 32 | + self.adjustSize() | |
| 33 | + # Set minimum size to ensure content fits | |
| 34 | + self.setMinimumSize(400, 150) | |
| 35 | + # But don't let it get too big | |
| 36 | + self.setMaximumSize(600, 400) | |
| 37 | + | |
| 38 | + # Position at center of parent | |
| 39 | + if parent: | |
| 40 | + parent_rect = parent.geometry() | |
| 41 | + x = parent_rect.x() + (parent_rect.width() - self.width()) // 2 | |
| 42 | + y = parent_rect.y() + (parent_rect.height() - self.height()) // 2 | |
| 43 | + self.move(x, y) | |
| 44 | + | |
| 45 | + def _setup_ui(self, title: str, message: str): | |
| 46 | + """Set up the dialog UI.""" | |
| 47 | + # Main layout | |
| 48 | + layout = QVBoxLayout(self) | |
| 49 | + layout.setContentsMargins(0, 0, 0, 0) | |
| 50 | + | |
| 51 | + # Create frame for border | |
| 52 | + frame = QFrame() | |
| 53 | + frame.setObjectName("confirmFrame") | |
| 54 | + frame.setStyleSheet(""" | |
| 55 | + #confirmFrame { | |
| 56 | + background-color: #2b2b2b; | |
| 57 | + border: 2px solid #555; | |
| 58 | + border-radius: 8px; | |
| 59 | + } | |
| 60 | + """) | |
| 61 | + | |
| 62 | + frame_layout = QVBoxLayout(frame) | |
| 63 | + frame_layout.setContentsMargins(20, 20, 20, 20) | |
| 64 | + frame_layout.setSpacing(10) | |
| 65 | + | |
| 66 | + # Title | |
| 67 | + title_label = QLabel(title) | |
| 68 | + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #fff;") | |
| 69 | + frame_layout.addWidget(title_label) | |
| 70 | + | |
| 71 | + # Message | |
| 72 | + message_label = QLabel(message) | |
| 73 | + message_label.setWordWrap(True) | |
| 74 | + message_label.setStyleSheet("color: #ddd; margin: 10px 0;") | |
| 75 | + # Ensure the label can expand as needed | |
| 76 | + message_label.setSizePolicy( | |
| 77 | + QSizePolicy.Policy.Expanding, | |
| 78 | + QSizePolicy.Policy.Expanding | |
| 79 | + ) | |
| 80 | + frame_layout.addWidget(message_label) | |
| 81 | + | |
| 82 | + frame_layout.addStretch() | |
| 83 | + | |
| 84 | + # Buttons | |
| 85 | + button_layout = QHBoxLayout() | |
| 86 | + button_layout.addStretch() | |
| 87 | + | |
| 88 | + # Yes button (default) | |
| 89 | + self.yes_btn = QPushButton("Yes") | |
| 90 | + self.yes_btn.setDefault(True) | |
| 91 | + self.yes_btn.setStyleSheet(""" | |
| 92 | + QPushButton { | |
| 93 | + background-color: #0d7377; | |
| 94 | + color: white; | |
| 95 | + border: none; | |
| 96 | + padding: 8px 20px; | |
| 97 | + border-radius: 4px; | |
| 98 | + font-weight: bold; | |
| 99 | + min-width: 80px; | |
| 100 | + } | |
| 101 | + QPushButton:hover { | |
| 102 | + background-color: #14b8bd; | |
| 103 | + } | |
| 104 | + QPushButton:pressed { | |
| 105 | + background-color: #0a5d61; | |
| 106 | + } | |
| 107 | + """) | |
| 108 | + self.yes_btn.clicked.connect(self.accept) | |
| 109 | + button_layout.addWidget(self.yes_btn) | |
| 110 | + | |
| 111 | + # No button | |
| 112 | + self.no_btn = QPushButton("No") | |
| 113 | + self.no_btn.setStyleSheet(""" | |
| 114 | + QPushButton { | |
| 115 | + background-color: #555; | |
| 116 | + color: white; | |
| 117 | + border: none; | |
| 118 | + padding: 8px 20px; | |
| 119 | + border-radius: 4px; | |
| 120 | + font-weight: bold; | |
| 121 | + min-width: 80px; | |
| 122 | + } | |
| 123 | + QPushButton:hover { | |
| 124 | + background-color: #666; | |
| 125 | + } | |
| 126 | + QPushButton:pressed { | |
| 127 | + background-color: #444; | |
| 128 | + } | |
| 129 | + """) | |
| 130 | + self.no_btn.clicked.connect(self.reject) | |
| 131 | + button_layout.addWidget(self.no_btn) | |
| 132 | + | |
| 133 | + frame_layout.addLayout(button_layout) | |
| 134 | + layout.addWidget(frame) | |
| 135 | + | |
| 136 | + # Focus on Yes button | |
| 137 | + self.yes_btn.setFocus() | |
| 138 | + | |
| 139 | + def keyPressEvent(self, event: QKeyEvent): | |
| 140 | + """Handle key presses for quick response.""" | |
| 141 | + if event.key() == Qt.Key.Key_Y: | |
| 142 | + self.accept() | |
| 143 | + elif event.key() == Qt.Key.Key_N or event.key() == Qt.Key.Key_Escape: | |
| 144 | + self.reject() | |
| 145 | + else: | |
| 146 | + super().keyPressEvent(event) | |
| 147 | + | |
| 148 | + | |
| 149 | +class InlineConfirmBar(QWidget): | |
| 150 | + """Inline confirmation bar that slides in from top.""" | |
| 151 | + | |
| 152 | + confirmed = pyqtSignal() | |
| 153 | + cancelled = pyqtSignal() | |
| 154 | + | |
| 155 | + def __init__(self, message: str, parent=None): | |
| 156 | + super().__init__(parent) | |
| 157 | + self.setAutoFillBackground(True) | |
| 158 | + self.setFixedHeight(50) | |
| 159 | + | |
| 160 | + # Style | |
| 161 | + self.setStyleSheet(""" | |
| 162 | + InlineConfirmBar { | |
| 163 | + background-color: #3a3a3a; | |
| 164 | + border-bottom: 2px solid #0d7377; | |
| 165 | + } | |
| 166 | + """) | |
| 167 | + | |
| 168 | + # Layout | |
| 169 | + layout = QHBoxLayout(self) | |
| 170 | + layout.setContentsMargins(20, 10, 20, 10) | |
| 171 | + | |
| 172 | + # Icon | |
| 173 | + icon_label = QLabel() | |
| 174 | + icon_label.setPixmap( | |
| 175 | + qta.icon("fa5s.question-circle", color="#0d7377").pixmap(24, 24) | |
| 176 | + ) | |
| 177 | + layout.addWidget(icon_label) | |
| 178 | + | |
| 179 | + # Message | |
| 180 | + message_label = QLabel(message) | |
| 181 | + message_label.setStyleSheet("color: #fff; font-size: 14px; margin-left: 10px;") | |
| 182 | + layout.addWidget(message_label) | |
| 183 | + | |
| 184 | + layout.addStretch() | |
| 185 | + | |
| 186 | + # Buttons | |
| 187 | + confirm_btn = QPushButton("Confirm") | |
| 188 | + confirm_btn.setStyleSheet(""" | |
| 189 | + QPushButton { | |
| 190 | + background-color: #0d7377; | |
| 191 | + color: white; | |
| 192 | + border: none; | |
| 193 | + padding: 6px 16px; | |
| 194 | + border-radius: 3px; | |
| 195 | + font-size: 13px; | |
| 196 | + } | |
| 197 | + QPushButton:hover { | |
| 198 | + background-color: #14b8bd; | |
| 199 | + } | |
| 200 | + """) | |
| 201 | + confirm_btn.clicked.connect(self._on_confirm) | |
| 202 | + layout.addWidget(confirm_btn) | |
| 203 | + | |
| 204 | + cancel_btn = QPushButton("Cancel") | |
| 205 | + cancel_btn.setStyleSheet(""" | |
| 206 | + QPushButton { | |
| 207 | + background-color: transparent; | |
| 208 | + color: #ccc; | |
| 209 | + border: 1px solid #555; | |
| 210 | + padding: 6px 16px; | |
| 211 | + border-radius: 3px; | |
| 212 | + font-size: 13px; | |
| 213 | + margin-left: 8px; | |
| 214 | + } | |
| 215 | + QPushButton:hover { | |
| 216 | + background-color: #444; | |
| 217 | + } | |
| 218 | + """) | |
| 219 | + cancel_btn.clicked.connect(self._on_cancel) | |
| 220 | + layout.addWidget(cancel_btn) | |
| 221 | + | |
| 222 | + # Set initial position (hidden above parent) | |
| 223 | + self.move(0, -self.height()) | |
| 224 | + | |
| 225 | + def _on_confirm(self): | |
| 226 | + """Handle confirm button.""" | |
| 227 | + self.confirmed.emit() | |
| 228 | + self.hide_animated() | |
| 229 | + | |
| 230 | + def _on_cancel(self): | |
| 231 | + """Handle cancel button.""" | |
| 232 | + self.cancelled.emit() | |
| 233 | + self.hide_animated() | |
| 234 | + | |
| 235 | + def show_animated(self): | |
| 236 | + """Slide in from top.""" | |
| 237 | + self.show() | |
| 238 | + self.raise_() | |
| 239 | + | |
| 240 | + # Animate slide in | |
| 241 | + self.animation = QPropertyAnimation(self, b"geometry") | |
| 242 | + self.animation.setDuration(150) # Fast animation | |
| 243 | + self.animation.setStartValue(QRect(0, -self.height(), self.parent().width(), self.height())) | |
| 244 | + self.animation.setEndValue(QRect(0, 0, self.parent().width(), self.height())) | |
| 245 | + self.animation.start() | |
| 246 | + | |
| 247 | + def hide_animated(self): | |
| 248 | + """Slide out to top.""" | |
| 249 | + self.animation = QPropertyAnimation(self, b"geometry") | |
| 250 | + self.animation.setDuration(150) | |
| 251 | + self.animation.setStartValue(self.geometry()) | |
| 252 | + self.animation.setEndValue(QRect(0, -self.height(), self.parent().width(), self.height())) | |
| 253 | + self.animation.finished.connect(self.hide) | |
| 254 | + self.animation.start() | |
| 255 | + | |
| 256 | + | |
| 257 | +class FastMessageDialog(QDialog): | |
| 258 | + """Fast message/error dialog.""" | |
| 259 | + | |
| 260 | + def __init__(self, title: str, message: str, icon_name: str = "fa5s.info-circle", | |
| 261 | + icon_color: str = "#17a2b8", parent=None): | |
| 262 | + super().__init__(parent) | |
| 263 | + self.setWindowTitle(title) | |
| 264 | + self.setModal(True) | |
| 265 | + self.setWindowFlags( | |
| 266 | + Qt.WindowType.Dialog | | |
| 267 | + Qt.WindowType.FramelessWindowHint | | |
| 268 | + Qt.WindowType.WindowStaysOnTopHint | |
| 269 | + ) | |
| 270 | + | |
| 271 | + self.setWindowOpacity(0.95) | |
| 272 | + | |
| 273 | + self._setup_ui(title, message, icon_name, icon_color) | |
| 274 | + | |
| 275 | + # Adjust size based on content | |
| 276 | + self.adjustSize() | |
| 277 | + self.setMinimumSize(350, 120) | |
| 278 | + self.setMaximumSize(500, 300) | |
| 279 | + | |
| 280 | + # Center on parent | |
| 281 | + if parent: | |
| 282 | + parent_rect = parent.geometry() | |
| 283 | + x = parent_rect.x() + (parent_rect.width() - self.width()) // 2 | |
| 284 | + y = parent_rect.y() + (parent_rect.height() - self.height()) // 2 | |
| 285 | + self.move(x, y) | |
| 286 | + | |
| 287 | + def _setup_ui(self, title: str, message: str, icon_name: str, icon_color: str): | |
| 288 | + """Set up the dialog UI.""" | |
| 289 | + layout = QVBoxLayout(self) | |
| 290 | + layout.setContentsMargins(0, 0, 0, 0) | |
| 291 | + | |
| 292 | + # Frame | |
| 293 | + frame = QFrame() | |
| 294 | + frame.setStyleSheet(""" | |
| 295 | + QFrame { | |
| 296 | + background-color: #2b2b2b; | |
| 297 | + border: 2px solid #555; | |
| 298 | + border-radius: 8px; | |
| 299 | + } | |
| 300 | + """) | |
| 301 | + | |
| 302 | + frame_layout = QVBoxLayout(frame) | |
| 303 | + frame_layout.setContentsMargins(20, 20, 20, 20) | |
| 304 | + | |
| 305 | + # Header with icon | |
| 306 | + header_layout = QHBoxLayout() | |
| 307 | + | |
| 308 | + icon_label = QLabel() | |
| 309 | + icon_label.setPixmap(qta.icon(icon_name, color=icon_color).pixmap(24, 24)) | |
| 310 | + header_layout.addWidget(icon_label) | |
| 311 | + | |
| 312 | + title_label = QLabel(title) | |
| 313 | + title_label.setStyleSheet("font-size: 15px; font-weight: bold; color: #fff; margin-left: 8px;") | |
| 314 | + header_layout.addWidget(title_label) | |
| 315 | + header_layout.addStretch() | |
| 316 | + | |
| 317 | + frame_layout.addLayout(header_layout) | |
| 318 | + | |
| 319 | + # Message | |
| 320 | + message_label = QLabel(message) | |
| 321 | + message_label.setWordWrap(True) | |
| 322 | + message_label.setStyleSheet("color: #ddd; margin: 15px 0;") | |
| 323 | + frame_layout.addWidget(message_label) | |
| 324 | + | |
| 325 | + frame_layout.addStretch() | |
| 326 | + | |
| 327 | + # OK button | |
| 328 | + ok_btn = QPushButton("OK") | |
| 329 | + ok_btn.setDefault(True) | |
| 330 | + ok_btn.setStyleSheet(""" | |
| 331 | + QPushButton { | |
| 332 | + background-color: #555; | |
| 333 | + color: white; | |
| 334 | + border: none; | |
| 335 | + padding: 6px 20px; | |
| 336 | + border-radius: 4px; | |
| 337 | + } | |
| 338 | + QPushButton:hover { | |
| 339 | + background-color: #666; | |
| 340 | + } | |
| 341 | + """) | |
| 342 | + ok_btn.clicked.connect(self.accept) | |
| 343 | + | |
| 344 | + btn_layout = QHBoxLayout() | |
| 345 | + btn_layout.addStretch() | |
| 346 | + btn_layout.addWidget(ok_btn) | |
| 347 | + frame_layout.addLayout(btn_layout) | |
| 348 | + | |
| 349 | + layout.addWidget(frame) | |
| 350 | + | |
| 351 | + # Auto-close after 3 seconds | |
| 352 | + QTimer.singleShot(3000, self.accept) | |
| 353 | + | |
| 354 | + def keyPressEvent(self, event: QKeyEvent): | |
| 355 | + """Close on any key press.""" | |
| 356 | + self.accept() | |
| 357 | + | |
| 358 | + | |
| 359 | +def fast_confirm(parent, title: str, message: str) -> bool: | |
| 360 | + """Show fast confirmation dialog and return result.""" | |
| 361 | + dialog = FastConfirmDialog(title, message, parent) | |
| 362 | + return dialog.exec() == QDialog.DialogCode.Accepted | |
| 363 | + | |
| 364 | + | |
| 365 | +def fast_message(parent, title: str, message: str, message_type: str = "info"): | |
| 366 | + """Show fast message dialog.""" | |
| 367 | + icon_configs = { | |
| 368 | + "info": ("fa5s.info-circle", "#17a2b8"), | |
| 369 | + "warning": ("fa5s.exclamation-triangle", "#ffc107"), | |
| 370 | + "error": ("fa5s.times-circle", "#dc3545"), | |
| 371 | + "success": ("fa5s.check-circle", "#28a745") | |
| 372 | + } | |
| 373 | + | |
| 374 | + icon_name, icon_color = icon_configs.get(message_type, icon_configs["info"]) | |
| 375 | + dialog = FastMessageDialog(title, message, icon_name, icon_color, parent) | |
| 376 | + dialog.exec() | |
src/wulftp/ui/main_window.pymodified@@ -39,6 +39,7 @@ from ..ui.components import ( | ||
| 39 | 39 | RemoteFilePane, |
| 40 | 40 | FileContextMenu |
| 41 | 41 | ) |
| 42 | +from ..ui.dialogs import fast_confirm, fast_message | |
| 42 | 43 | |
| 43 | 44 | |
| 44 | 45 | class WulFTPClient(QMainWindow): |
@@ -162,7 +163,7 @@ class WulFTPClient(QMainWindow): | ||
| 162 | 163 | self.remote_pane.navigateHome.connect(self.remote_pane.navigate_home) |
| 163 | 164 | self.remote_pane.createDirectory.connect(lambda: self._create_directory(False)) |
| 164 | 165 | self.remote_pane.model.errorOccurred.connect( |
| 165 | - lambda err: QMessageBox.warning(self, "Error", err) | |
| 166 | + lambda err: fast_message(self, "Error", err, "error") | |
| 166 | 167 | ) |
| 167 | 168 | self.remote_pane.model.filesDropped.connect(self._handle_files_dropped) |
| 168 | 169 | self.remote_pane.tree_view.selectionModel().selectionChanged.connect(self._update_button_states) |
@@ -242,14 +243,8 @@ class WulFTPClient(QMainWindow): | ||
| 242 | 243 | )) |
| 243 | 244 | |
| 244 | 245 | except Exception as e: |
| 245 | - QMessageBox.critical( | |
| 246 | - self, | |
| 247 | - "Connection Error", | |
| 248 | - str(e) | |
| 249 | - ) | |
| 246 | + fast_message(self, "Connection Error", str(e), "error") | |
| 250 | 247 | self._update_connection_state(False) |
| 251 | - # Restore focus after error dialog | |
| 252 | - self._restore_focus() | |
| 253 | 248 | |
| 254 | 249 | def disconnect(self): |
| 255 | 250 | """Close SFTP connection.""" |
@@ -323,18 +318,10 @@ class WulFTPClient(QMainWindow): | ||
| 323 | 318 | |
| 324 | 319 | # Handle directories first |
| 325 | 320 | if directories: |
| 326 | - reply = QMessageBox.question( | |
| 327 | - self, | |
| 328 | - "Upload Directories", | |
| 329 | - format_status_message("Upload", len(directories), "directory") + "?\n\n" + | |
| 330 | - "This will recursively upload all contents.", | |
| 331 | - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, | |
| 332 | - QMessageBox.StandardButton.Yes | |
| 333 | - ) | |
| 321 | + msg = format_status_message("Upload", len(directories), "directory") + "?\n\n" + \ | |
| 322 | + "This will recursively upload all contents." | |
| 334 | 323 | |
| 335 | - self._restore_focus() # Restore focus after dialog | |
| 336 | - | |
| 337 | - if reply == QMessageBox.StandardButton.Yes: | |
| 324 | + if fast_confirm(self, "Upload Directories", msg): | |
| 338 | 325 | for dir_path in directories: |
| 339 | 326 | dirname = os.path.basename(dir_path) |
| 340 | 327 | remote_base = self.remote_pane.model.rootPath() |
@@ -374,16 +361,10 @@ class WulFTPClient(QMainWindow): | ||
| 374 | 361 | |
| 375 | 362 | # Handle directories first |
| 376 | 363 | if directories: |
| 377 | - reply = QMessageBox.question( | |
| 378 | - self, | |
| 379 | - "Download Directories", | |
| 380 | - format_status_message("Download", len(directories), "directory") + "?\n\n" + | |
| 381 | - "This will recursively download all contents.", | |
| 382 | - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, | |
| 383 | - QMessageBox.StandardButton.Yes | |
| 384 | - ) | |
| 364 | + msg = format_status_message("Download", len(directories), "directory") + "?\n\n" + \ | |
| 365 | + "This will recursively download all contents." | |
| 385 | 366 | |
| 386 | - if reply == QMessageBox.StandardButton.Yes: | |
| 367 | + if fast_confirm(self, "Download Directories", msg): | |
| 387 | 368 | for dir_path, dirname in directories: |
| 388 | 369 | local_base = self.local_pane.model.filePath(self.local_pane.tree_view.rootIndex()) |
| 389 | 370 | local_path = os.path.join(local_base, dirname) |
@@ -458,11 +439,7 @@ class WulFTPClient(QMainWindow): | ||
| 458 | 439 | def _transfer_error(self, error): |
| 459 | 440 | """Handle transfer error.""" |
| 460 | 441 | self.progress_bar.setVisible(False) |
| 461 | - QMessageBox.critical( | |
| 462 | - self, | |
| 463 | - "Transfer Error", | |
| 464 | - error | |
| 465 | - ) | |
| 442 | + fast_message(self, "Transfer Error", error, "error") | |
| 466 | 443 | |
| 467 | 444 | def _handle_files_dropped(self, local_paths: List[str], target_directory: str): |
| 468 | 445 | """Handle files and directories dropped onto remote view.""" |
@@ -483,16 +460,10 @@ class WulFTPClient(QMainWindow): | ||
| 483 | 460 | |
| 484 | 461 | # Handle directories |
| 485 | 462 | if directories: |
| 486 | - reply = QMessageBox.question( | |
| 487 | - self, | |
| 488 | - "Upload Directories", | |
| 489 | - format_status_message("Upload", len(directories), "directory") + "?\n\n" + | |
| 490 | - "This will recursively upload all contents.", | |
| 491 | - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, | |
| 492 | - QMessageBox.StandardButton.Yes | |
| 493 | - ) | |
| 463 | + msg = format_status_message("Upload", len(directories), "directory") + "?\n\n" + \ | |
| 464 | + "This will recursively upload all contents." | |
| 494 | 465 | |
| 495 | - if reply == QMessageBox.StandardButton.Yes: | |
| 466 | + if fast_confirm(self, "Upload Directories", msg): | |
| 496 | 467 | for dir_path in directories: |
| 497 | 468 | dirname = os.path.basename(dir_path) |
| 498 | 469 | remote_path = os.path.join(target_directory, dirname).replace('\\', '/') |
@@ -546,16 +517,10 @@ class WulFTPClient(QMainWindow): | ||
| 546 | 517 | |
| 547 | 518 | # Handle directories |
| 548 | 519 | if directories: |
| 549 | - reply = QMessageBox.question( | |
| 550 | - self, | |
| 551 | - "Download Directories", | |
| 552 | - format_status_message("Download", len(directories), "directory") + "?\n\n" + | |
| 553 | - "This will recursively download all contents.", | |
| 554 | - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, | |
| 555 | - QMessageBox.StandardButton.Yes | |
| 556 | - ) | |
| 520 | + msg = format_status_message("Download", len(directories), "directory") + "?\n\n" + \ | |
| 521 | + "This will recursively download all contents." | |
| 557 | 522 | |
| 558 | - if reply == QMessageBox.StandardButton.Yes: | |
| 523 | + if fast_confirm(self, "Download Directories", msg): | |
| 559 | 524 | for remote_path, dirname in directories: |
| 560 | 525 | local_path = os.path.join(local_directory, dirname) |
| 561 | 526 | |
@@ -622,11 +587,7 @@ class WulFTPClient(QMainWindow): | ||
| 622 | 587 | AppConfig.STATUS_MESSAGE_TIMEOUT |
| 623 | 588 | ) |
| 624 | 589 | except Exception as e: |
| 625 | - QMessageBox.warning( | |
| 626 | - self, | |
| 627 | - "Error", | |
| 628 | - f"Failed to create directory: {str(e)}" | |
| 629 | - ) | |
| 590 | + fast_message(self, "Error", f"Failed to create directory: {str(e)}", "error") | |
| 630 | 591 | |
| 631 | 592 | def _delete_selected(self, is_local: bool): |
| 632 | 593 | """Delete selected files/directories.""" |
@@ -647,18 +608,7 @@ class WulFTPClient(QMainWindow): | ||
| 647 | 608 | if len(items) > 5: |
| 648 | 609 | msg += f"... and {len(items) - 5} more" |
| 649 | 610 | |
| 650 | - reply = QMessageBox.question( | |
| 651 | - self, | |
| 652 | - "Confirm Delete", | |
| 653 | - msg, | |
| 654 | - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, | |
| 655 | - QMessageBox.StandardButton.No | |
| 656 | - ) | |
| 657 | - | |
| 658 | - # Always restore focus after the dialog, regardless of the answer | |
| 659 | - self._restore_focus() | |
| 660 | - | |
| 661 | - if reply == QMessageBox.StandardButton.Yes: | |
| 611 | + if fast_confirm(self, "Confirm Delete", msg): | |
| 662 | 612 | errors = [] |
| 663 | 613 | for path, is_dir, name in items: |
| 664 | 614 | try: |
@@ -677,13 +627,9 @@ class WulFTPClient(QMainWindow): | ||
| 677 | 627 | errors.append(f"{name}: {str(e)}") |
| 678 | 628 | |
| 679 | 629 | if errors: |
| 680 | - QMessageBox.warning( | |
| 681 | - self, | |
| 682 | - "Delete Errors", | |
| 683 | - "Some items could not be deleted:\n\n" + "\n".join(errors[:10]) | |
| 684 | - ) | |
| 685 | - # Restore focus after error dialog | |
| 686 | - self._restore_focus() | |
| 630 | + fast_message(self, "Delete Errors", | |
| 631 | + "Some items could not be deleted:\n\n" + "\n".join(errors[:10]), | |
| 632 | + "error") | |
| 687 | 633 | else: |
| 688 | 634 | self.status_bar.showMessage( |
| 689 | 635 | AppConfig.MSG_DELETE_COMPLETE.format(len(items)), |
@@ -721,10 +667,4 @@ class WulFTPClient(QMainWindow): | ||
| 721 | 667 | elif remote_selection: |
| 722 | 668 | self._delete_selected(False) |
| 723 | 669 | else: |
| 724 | - QMessageBox.information( | |
| 725 | - self, | |
| 726 | - "Delete", | |
| 727 | - "Please select items to delete" | |
| 728 | - ) | |
| 729 | - # Restore focus after info dialog | |
| 730 | - self._restore_focus() | |
| 670 | + fast_message(self, "Delete", "Please select items to delete", "info") | |