diff --git a/README.md b/README.md index 2b116bd..11b568a 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,6 @@ A GUI for [Linux OneDrive Client](https://github.com/abraunegg/onedrive) with mu - The list of managed OneDrive profiles is stored in `~/.config/onedrive-gui/profiles`. You can manually rename your profile or path to the config file there. - Newly created OneDrive config files are stored in `~/.config/onedrive/accounts/`. - Debug logs are saved in ``. Logging can be changed in the GUI settings. + +# Attribution +- Icons by Icons8 diff --git a/src/OneDriveGUI.py b/src/OneDriveGUI.py index caaf834..64a6303 100644 --- a/src/OneDriveGUI.py +++ b/src/OneDriveGUI.py @@ -19,7 +19,7 @@ from global_config import DIR_PATH app = QApplication(sys.argv) app.setApplicationName("OneDriveGUI") app.setDesktopFileName("OneDriveGUI") -app.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png")) +app.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png")) from options import gui_settings, global_config, version diff --git a/src/main_window.py b/src/main_window.py index 4886409..c55073e 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -74,7 +74,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): super(MainWindow, self).__init__() self.setupUi(self) self.setWindowTitle(f"OneDriveGUI v{version}") - self.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png")) + self.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png")) if gui_settings.get("frameless_window") == "True": self.setWindowFlags(Qt.FramelessWindowHint) @@ -150,7 +150,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): # System Tray self.tray = QSystemTrayIcon() if self.tray.isSystemTrayAvailable(): - icon = QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png") + # Initial icon will be updated by update_tray_icon() after profiles are loaded + icon = QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png") menu = QMenu() show_action = menu.addAction("Show/Hide") @@ -165,6 +166,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.tray.setIcon(icon) self.tray.setContextMenu(menu) self.tray.show() + # Initial tooltip - will be updated by update_tray_icon() self.tray.setToolTip("OneDriveGUI") else: @@ -510,6 +512,162 @@ class MainWindow(QMainWindow, Ui_MainWindow): profile_status_page.pushButton_start_stop.clicked.disconnect() profile_status_page.pushButton_start_stop.clicked.connect(profile_status_page.stop_monitor) + # Update system tray icon based on aggregate status + self.update_tray_icon() + + def is_ignorable_error(self, error_text): + """ + Check if an error message should be ignored for tray icon status. + Returns True if the error should be ignored. + """ + if not error_text: + return True + + # List of error patterns to ignore + ignored_patterns = ["inotify_add_watch failed", "The local file system returned an error with the following message"] + + error_lower = error_text.lower() + for pattern in ignored_patterns: + if pattern.lower() in error_lower: + return True + + return False + + def aggregate_tray_status(self): + """ + Aggregate status across all profiles to determine system tray icon state. + Returns: tuple (status_state, running_count, total_count, profile_statuses) + + status_state: 'ERROR', 'STOPPED', 'SYNCING', or 'IDLE' + Priority: ERROR > STOPPED > SYNCING > IDLE + """ + if not global_config: + return ("IDLE", 0, 0, {}) + + total_count = len(global_config) + running_count = 0 + profile_statuses = {} + + has_error = False + has_stopped = False + has_syncing = False + + for profile_name in global_config: + # Check if worker is running + is_running = profile_name in workers and workers[profile_name].isRunning() + + # Get status message from profile status page + if profile_name in self.profile_status_pages: + status_msg = self.profile_status_pages[profile_name].label_onedrive_status.text() + error_tooltip = self.profile_status_pages[profile_name].label_error_icon.toolTip() + else: + status_msg = "" + error_tooltip = "" + + # Categorize this profile's state + if not is_running: + profile_state = "STOPPED" + has_stopped = True + elif (not self.is_ignorable_error(error_tooltip) and not self.is_ignorable_error(status_msg)) and ( + error_tooltip or "Error:" in status_msg or "error" in status_msg.lower() or "failed" in status_msg.lower() or "crashed" in status_msg.lower() + ): + profile_state = "ERROR" + has_error = True + running_count += 1 + elif "sync in progress" in status_msg.lower() or "processing" in status_msg.lower() or "starting" in status_msg.lower() or "initializing" in status_msg.lower(): + profile_state = "SYNCING" + has_syncing = True + running_count += 1 + elif is_running: + profile_state = "IDLE" + running_count += 1 + else: + profile_state = "STOPPED" + has_stopped = True + + profile_statuses[profile_name] = {"state": profile_state, "status_msg": status_msg if status_msg else "stopped", "is_running": is_running} + + # Determine overall state with priority + if has_error: + overall_state = "ERROR" + elif has_stopped: + overall_state = "STOPPED" + elif has_syncing: + overall_state = "SYNCING" + else: + overall_state = "IDLE" + + return (overall_state, running_count, total_count, profile_statuses) + + def generate_tray_tooltip(self, overall_state, running_count, total_count, profile_statuses): + """ + Generate detailed tooltip showing status of all profiles. + """ + if not profile_statuses: + return "OneDriveGUI - No profiles configured" + + # Summary line + if running_count == total_count and overall_state != "ERROR": + summary = f"All {total_count} profile(s) running" + elif running_count == 0: + summary = f"All {total_count} profile(s) stopped" + else: + summary = f"{running_count} of {total_count} profile(s) running" + + # Add overall state indicator + state_indicators = {"ERROR": "Sync error detected", "STOPPED": "Sync stopped", "SYNCING": "Syncing...", "IDLE": "All syncs complete"} + summary += f" - {state_indicators.get(overall_state, '')}" + + # Build detailed list + lines = [summary, ""] + for profile_name, status_info in profile_statuses.items(): + state = status_info["state"] + status_msg = status_info["status_msg"] + + # Add text indicator for each profile + if state == "ERROR": + indicator = "[ERROR]" + elif state == "STOPPED": + indicator = "[STOPPED]" + elif state == "SYNCING": + indicator = "[SYNCING]" + else: + indicator = "[IDLE]" + + # Shorten status message if too long + if len(status_msg) > 50: + status_msg = status_msg[:47] + "..." + + lines.append(f"{indicator} {profile_name}: {status_msg}") + + return "\n".join(lines) + + def update_tray_icon(self): + """ + Update system tray icon and tooltip based on current sync status. + """ + if not self.tray: + return + + # Get aggregated status + overall_state, running_count, total_count, profile_statuses = self.aggregate_tray_status() + + # Select icon based on state + icon_map = { + "ERROR": DIR_PATH + "/resources/images/icons8-cloud-error-80.png", + "STOPPED": DIR_PATH + "/resources/images/icons8-cloud-stop-80.png", + "SYNCING": DIR_PATH + "/resources/images/icons8-cloud-sync-80.png", + "IDLE": DIR_PATH + "/resources/images/icons8-cloud-done-80.png", + } + + icon_path = icon_map.get(overall_state, DIR_PATH + "/resources/images/icons8-cloud-80.png") + icon = QIcon(icon_path) + self.tray.setIcon(icon) + + # Generate and set tooltip + tooltip = self.generate_tray_tooltip(overall_state, running_count, total_count, profile_statuses) + self.tray.setToolTip(tooltip) + def autostart_monitor(self): # Auto-start sync if compatible version of OneDrive client is installed. for profile_name in global_config: @@ -632,6 +790,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.profile_status_pages[profile].label_error_icon.clear() self.profile_status_pages[profile].label_error_icon.setToolTip("") + # Update system tray icon when profile status changes + self.update_tray_icon() + def event_update_progress(self, data, profile): """ data: @@ -768,7 +929,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): def show_login(self, profile): # Show login window with QT WebEngine self.window1 = QWidget() - self.window1.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png")) + self.window1.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png")) self.lw = Ui_LoginWindow() self.lw.setupUi(self.window1) self.window1.show() @@ -792,7 +953,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): def show_external_login(self, profile): # Show external login window self.window2 = QWidget() - self.window2.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png")) + self.window2.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png")) self.lw2 = Ui_ExternalLoginWindow() self.lw2.setupUi(self.window2) self.window2.show() diff --git a/src/resources/images/OneDriveGUI.png b/src/resources/images/OneDriveGUI.png index b932318..c408513 100644 Binary files a/src/resources/images/OneDriveGUI.png and b/src/resources/images/OneDriveGUI.png differ diff --git a/src/resources/images/icons8-cloud-80.png b/src/resources/images/icons8-cloud-80.png new file mode 100644 index 0000000..c408513 Binary files /dev/null and b/src/resources/images/icons8-cloud-80.png differ diff --git a/src/resources/images/icons8-cloud-cross-40_2.png b/src/resources/images/icons8-cloud-cross-40_2.png deleted file mode 100644 index 76f3a6b..0000000 Binary files a/src/resources/images/icons8-cloud-cross-40_2.png and /dev/null differ diff --git a/src/resources/images/icons8-cloud-done-40_2.png b/src/resources/images/icons8-cloud-done-40_2.png deleted file mode 100644 index 37a8ee9..0000000 Binary files a/src/resources/images/icons8-cloud-done-40_2.png and /dev/null differ diff --git a/src/resources/images/icons8-cloud-done-80.png b/src/resources/images/icons8-cloud-done-80.png new file mode 100644 index 0000000..5eb1e2a Binary files /dev/null and b/src/resources/images/icons8-cloud-done-80.png differ diff --git a/src/resources/images/icons8-cloud-error-80.png b/src/resources/images/icons8-cloud-error-80.png new file mode 100644 index 0000000..a6e6d5d Binary files /dev/null and b/src/resources/images/icons8-cloud-error-80.png differ diff --git a/src/resources/images/icons8-cloud-stop-80.png b/src/resources/images/icons8-cloud-stop-80.png new file mode 100644 index 0000000..849f3c7 Binary files /dev/null and b/src/resources/images/icons8-cloud-stop-80.png differ diff --git a/src/resources/images/icons8-cloud-sync-40_2.png b/src/resources/images/icons8-cloud-sync-40_2.png deleted file mode 100644 index f1d310a..0000000 Binary files a/src/resources/images/icons8-cloud-sync-40_2.png and /dev/null differ diff --git a/src/resources/images/icons8-cloud-sync-80.png b/src/resources/images/icons8-cloud-sync-80.png new file mode 100644 index 0000000..ddbcf17 Binary files /dev/null and b/src/resources/images/icons8-cloud-sync-80.png differ diff --git a/src/resources/images/icons8-clouds-48.png b/src/resources/images/icons8-clouds-48.png deleted file mode 100644 index 7b7533c..0000000 Binary files a/src/resources/images/icons8-clouds-48.png and /dev/null differ diff --git a/src/resources/images/icons8-clouds-80-dark-edge.png b/src/resources/images/icons8-clouds-80-dark-edge.png deleted file mode 100644 index d26daad..0000000 Binary files a/src/resources/images/icons8-clouds-80-dark-edge.png and /dev/null differ diff --git a/src/workers.py b/src/workers.py index 59e4a19..c53fa3c 100644 --- a/src/workers.py +++ b/src/workers.py @@ -95,6 +95,11 @@ class WorkerThread(QThread): # Track pending error for multi-line error messages self.pending_error = None + # Track failed files for detailed error reporting + self.failed_files = [] + self.failed_files_count = 0 + self.collecting_failed_files = False # Flag to indicate we're collecting failed file lines + self.profile_status["status_message"] = "OneDrive sync is starting..." self.update_profile_status.emit(self.profile_status, self.profile_name) @@ -159,6 +164,32 @@ class WorkerThread(QThread): self.profile_status["error_message"] = full_error_message # Full error for tooltip self.update_profile_status.emit(self.profile_status, self.profile_name) + def _emit_failed_files_status(self): + """Format and emit status for failed file uploads/downloads.""" + if not self.failed_files and self.failed_files_count == 0: + return + + # Use the actual count from the summary line if available, otherwise use list length + count = self.failed_files_count if self.failed_files_count > 0 else len(self.failed_files) + + # Status message (truncated for main UI) + status_msg = f"Sync completed with {count} failed file(s)" + + # Tooltip message (detailed list) + tooltip_lines = [f"Failed items to upload to/from Microsoft OneDrive: {count}"] + tooltip_lines.extend(self.failed_files) + + # If there are more failures than we captured, add indicator + if count > len(self.failed_files): + remaining = count - len(self.failed_files) + tooltip_lines.append(f"... and {remaining} more") + + full_tooltip = "\n".join(tooltip_lines) + + self.profile_status["status_message"] = status_msg + self.profile_status["error_message"] = full_tooltip + self.update_profile_status.emit(self.profile_status, self.profile_name) + def read_stdout(self): stdout = self.onedrive_process.stdout.readline().strip() if stdout != "": @@ -170,6 +201,12 @@ class WorkerThread(QThread): self._emit_error_status(self.pending_error) self.pending_error = None + # Check if we were collecting failed files and this line is not a "Failed to" line + if self.collecting_failed_files and "Failed to upload:" not in stdout and "Failed to download:" not in stdout: + # We're done collecting, emit the status + self._emit_failed_files_status() + self.collecting_failed_files = False + if "Calling Function: testNetwork()" in stdout: self.profile_status["status_message"] = "Cannot connect to Microsoft OneDrive Service" self.update_profile_status.emit(self.profile_status, self.profile_name) @@ -180,7 +217,7 @@ class WorkerThread(QThread): self.update_profile_status.emit(self.profile_status, self.profile_name) self.update_credentials.emit(self.profile_name) - elif any(msg in stdout for msg in ["Sync with Microsoft OneDrive is complete", "Total number of local file(s) added or changed", "Scanning the local file system"]): + elif any(msg in stdout for msg in ["Sync with Microsoft OneDrive is complete", "Total number of local file(s) added or changed"]): self.profile_status["status_message"] = "OneDrive sync is complete" self.update_profile_status.emit(self.profile_status, self.profile_name) @@ -216,6 +253,10 @@ class WorkerThread(QThread): # save_global_config() elif "Initializing the OneDrive API" in stdout: + # Clear failed files list at start of new sync cycle + self.failed_files = [] + self.failed_files_count = 0 + self.collecting_failed_files = False self.profile_status["status_message"] = "Initializing the OneDrive API" self.update_profile_status.emit(self.profile_status, self.profile_name) @@ -365,6 +406,26 @@ class WorkerThread(QThread): # Store as pending error to check if next line has "Error Message:" self.pending_error = error_text + elif "Failed items to upload to/from Microsoft OneDrive:" in stdout: + # Extract the count of failed items + match = re.search(r"Failed items .+?: (\d+)", stdout) + if match: + self.failed_files_count = int(match.group(1)) + # Start collecting failed file lines (they come after this summary line) + self.collecting_failed_files = True + + elif "Failed to upload:" in stdout or "Failed to download:" in stdout: + # Extract the operation and file path + match = re.search(r"Failed to (upload|download): (.+)$", stdout) + if match: + operation = match.group(1) + file_path = match.group(2).strip() + failed_entry = f"Failed to {operation}: {file_path}" + + # Add to list, limiting to 25 entries + if len(self.failed_files) < 25: + self.failed_files.append(failed_entry) + else: # logging.debug(f"No rule matched: {stdout}") pass