From 332d07373ec8204e2cca4ad330ff33cdd88abbaf Mon Sep 17 00:00:00 2001 From: Boris Pozdena Date: Fri, 21 Nov 2025 14:35:32 +0100 Subject: [PATCH] Refactor OneDrive sync error handling and update image resources - Enhanced error reporting for failed file uploads/downloads in the WorkerThread class. - Introduced a mechanism to collect and display detailed information about failed files. - Removed unused cloud icon images and added new icons for better visual representation. - Updated the OneDrive sync completion status messages to reflect the number of failed files. --- README.md | 3 + src/OneDriveGUI.py | 2 +- src/main_window.py | 169 +++++++++++++++++- src/resources/images/OneDriveGUI.png | Bin 1041 -> 1749 bytes src/resources/images/icons8-cloud-80.png | Bin 0 -> 1749 bytes .../images/icons8-cloud-cross-40_2.png | Bin 1239 -> 0 bytes .../images/icons8-cloud-done-40_2.png | Bin 1208 -> 0 bytes src/resources/images/icons8-cloud-done-80.png | Bin 0 -> 2072 bytes .../images/icons8-cloud-error-80.png | Bin 0 -> 2626 bytes src/resources/images/icons8-cloud-stop-80.png | Bin 0 -> 2192 bytes .../images/icons8-cloud-sync-40_2.png | Bin 1463 -> 0 bytes src/resources/images/icons8-cloud-sync-80.png | Bin 0 -> 2966 bytes src/resources/images/icons8-clouds-48.png | Bin 654 -> 0 bytes .../images/icons8-clouds-80-dark-edge.png | Bin 1887 -> 0 bytes src/workers.py | 63 ++++++- 15 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 src/resources/images/icons8-cloud-80.png delete mode 100644 src/resources/images/icons8-cloud-cross-40_2.png delete mode 100644 src/resources/images/icons8-cloud-done-40_2.png create mode 100644 src/resources/images/icons8-cloud-done-80.png create mode 100644 src/resources/images/icons8-cloud-error-80.png create mode 100644 src/resources/images/icons8-cloud-stop-80.png delete mode 100644 src/resources/images/icons8-cloud-sync-40_2.png create mode 100644 src/resources/images/icons8-cloud-sync-80.png delete mode 100644 src/resources/images/icons8-clouds-48.png delete mode 100644 src/resources/images/icons8-clouds-80-dark-edge.png 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 b93231828b985a924a95408a4b6dab55a62d9960..c408513cc6be1b4838038d117b31496cb39a5448 100644 GIT binary patch literal 1749 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 BTyOvY 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 37a8ee9e33aaad010a1f87f0c4282c57abe2c4e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1208 zcmV;p1V{UcP)twq2Riqh6>L{Lu# zLrKJoi3AdBA|fES^`;?62!8|{^ld;ca$J#505C=)0QR_2 ze6(T3{B!>cF#K7ImT>g#AXr*fR^llv4l=KyK?s3p)Wo&R-PXnPU5)@g)vucGgu@}{ zKLh@Q(|*d!&zUi+rcCFY4Li=V9UM7$CVJ;q|1tHd8A=98lL4Bx_HkZ*c6Ci{#fUfn zP!*1+>#L2dpl^1(MBk9iwZ=VztG2Yw&@|fq{EKtE55o=4wjAvH;izfbE+d3UM(K4S zk`PjUt}A9! z`1O!EZs3~jt@TXh>!c9#g)4oG(HNo&U6o$FAmEK?4PtWYWjx7L| z4;?e?A~B1#=B#X^alyhGqjX9k(R3A*QW%DYl8Lz>Des>5A_SOSGfgLyyt-~{>l32_ zS8qMgz&Kk{`%KlC7eT5iD#v4G#VS?#TcZLMjjf$tH7TvQSlq(#6PF%3rm!f7gJRLB zKq!V$+(LJ(S5qXo15%dKLqWdL~}aP5xPIRr#nrk!@&(#5qf49`Q4Q5BAP zb=5{7+c(=R(l!AD`=FF$eRKOxLdde|)usC6(tJW`Qg6Yeaf7pL2mAIOi8)d{$pGNJ zk{7o5{odD}eQuT!oDd*n4ExCO6PFMO=unjV7t>%|?gT%UzA_JnckDO-V2r}H9C^F9 zUuA2y{`?eImCX&InZBVH8>wR_JF##7DO~8hf%0iZcacMJjf-pH)zxv~22xbcF8_6- zh;oneensUZeXKYDs35@2j?_G8KZqxBB`(O z`;h6lt%K{j*6T`%?>M3KKqS)pQt6a}q~0C_$bsMK$gy3VKYJx%3ws;26V3-`IxfW= z$4&Z#UR_NaPDw($? z01N;Y)GYfxs>-PsFLaf2##Gg#fOCeiY}hHI92b3eV(7dOagQE4W5#3Vt{$m0ezIbY z4Gj6@U%johPEq;0Qp$SQb#ow7|6@N|l#&EvG$Mrj){)NkjVl{E9`!XdWy+LkP=5o^ Wa@Kk5_Sbp<0000 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..849f3c72a3e5ab8f82ba43a9e610c637a2e7cea4 GIT binary patch literal 2192 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 Rb9Mj#002ovPDHLkV1mV6wXOgF 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 0000000000000000000000000000000000000000..ddbcf174dbdbce42be0644ff00c9687df7eeeadd GIT binary patch literal 2966 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 M07*qoM6N<$f_~|p#sB~S literal 0 HcmV?d00001 diff --git a/src/resources/images/icons8-clouds-48.png b/src/resources/images/icons8-clouds-48.png deleted file mode 100644 index 7b7533c51380c17a80b35807fa693b05a9e63ab3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 654 zcmV;90&)F`P)2a3?`rhzKri^XF3e~~l>SzOZL#-Mu%WJ;C8Kn_?J;iIa)ayGM%CXUKRlD_*Mp!UV9 zsOnuH-$C{bWTsqno|*QaNp7t>^L6)@s?O5cJirL(liG@V({zl)D^RPuUjIMs)%0g(Cq`zv%rXH|G?VLrZYNrNPfl< z8*icR<^l8y$;<=fHwWB{am7Xf=IZXT?&f=eOvP0i1<0V&=4KLcB(BhK3(1(NlZ5Pv zD>OX=IYWIy;3W|X zpMtfh0B=(8k_ZuA25X@JzVB3lSa0!^`8xE=3Ecys0F$G#Cdg$#Q?V6~rV5p)kDSOu ze4^;Q5ahf-qCW+_MOW45_{l={T~uqwr^4IMy4T5gw==>8gx#vCJS}`+Ly!-M|9l78 o#}fr&a?2qI0y+o-0_CdQ;!qTUay_65LqX2rro_pXZQs6bOIx;^ z-IDLj_rKT9FQ3gze);|K08mm=Qc_Y455uxcu2o?jNlA*CYII0(K)H#7S z>pGG*>^O{DTYtn~uGiiKkGX;*+AfQYy$*`10zz2`B-#;ov#470?!5nm#r)@O2l(MzMk1o!cg}`a>UZG39Xea+@Px&N6xCz2Iaizl{7H~#JB@tTmSMpJm=0_ zt@(%ClC>6IFI^-4dEm>k)M@oX#%VxvoL_6j^pgV5=}~ijF>ff7Gb%o;TcFk6vIJ=x z1bj)5u;#{qN6uO)72uAPbEdp7+leI|V!9h))(MbqPs`>95YfIe2DmZ#v{Zn?;c)|V zt>$DJ(-UI-mJE=tbq4fmWM?UxhuNQuUzCqh8BK?Teku#G81pt^^$e* zF1?6|VhC$@odI+jI!!v9yjm}XA2dZenEd*K zk+V!`5B;uC;fZ|849@A+J-?6+B)>kKc8k|&)stH?+XeKt#oFd2kAo{&m6bqPTWkbo zEUgDe_2LQNtdI_*AfB*jy(zt6y*&yK5*7dn-D?IXDlYvdEar*HC#6Gp6dyc$gO>_( z-I*bwMZ0V+C>4Vh^ z!6|9C9W7K*$L#j$OkOdfqr0Zn%|J|B_vsyI zWz8Nw`>ZV1_=izF$4DQPF2TEwo_93HOc2u^n(7!!^p;S@lRVs()ctIi7FBi=#I-rX zD;dhlWUhUFnjq^Q_P$eB_!~{Txm0o>v4`g{`&gWj_O#_&r4K;&0WVy)H*d30v;>V0 z?m7OS^x$GPJlSNxAdTOAG3HX4G@3Kwa+UG2C@Uzeayt(+jv_x$;~cI zCmN^efiu^o2d(>%gIo5c zdcBam`Oj2y-Eg$qTo7Tp&-qZBHzIFECpiw7zP;Na#O~Vgk{4kS(Uj)&liBT9mg;Np$x0&o!N4T{0^& zggJ_gk84#XG5I7f7as3Y7YquqSIHG1VC*T!_h|;|q9P%#OUNFO?nUoBlE$ky?`}SU zOa*z7I4qmLHY!uQt>c@IV;Ap9ADmvuUXgTwS9<$hQ`=KJkgp~avc|qCZoX&zlZ+%X zl=kIwR_%236LcRKtS2p&s&)e=WhBhT+gw~|JixR+T^5C zHQSU1o)8SBZy}}!sM8*AIl-vj%#v33DsIv-i79aLq{ z0I#TWv*KcAiaW;^8*7D##{9|RvV34jba3e!(WSftOi4*eNl8gbNl8gbNl8gbNl6LF Z)c*(z(%V8WY~=s|002ovPDHLkV1j0nh`s;- 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