diff --git a/src/main_window.py b/src/main_window.py
index c55073e..65c3833 100644
--- a/src/main_window.py
+++ b/src/main_window.py
@@ -39,7 +39,7 @@ from options import (
version,
)
-from utils.utils import humanize_file_size, shorten_path
+from utils.utils import humanize_file_size, shorten_path, format_relative_time
from workers import WorkerThread, MaintenanceWorker, TaskList, workers
from gui_settings_window import gui_settings_window
@@ -131,6 +131,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.profile_status_pages[profile].quit_gui_signal.connect(self.graceful_shutdown)
self.stackedLayout.addWidget(self.profile_status_pages[profile])
+ # Connect scroll events for timestamp updates
+ listWidget = self.profile_status_pages[profile].listWidget
+ scrollbar = listWidget.verticalScrollBar()
+ scrollbar.valueChanged.connect(self.on_list_scrolled)
+
self.verticalLayout_2.addLayout(self.stackedLayout)
# Ensure all profiles in comboBox are aligned to center
@@ -203,6 +208,18 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.auto_sync.timeout.connect(self.autostart_monitor)
self.auto_sync.start(1000)
+ # Timer to refresh relative times for completed transfers
+ self.refresh_relative_times = QTimer()
+ self.refresh_relative_times.setSingleShot(False)
+ self.refresh_relative_times.timeout.connect(self.update_relative_times)
+ self.refresh_relative_times.start(60000) # Update every 60 seconds
+
+ # Debounce timer for scroll-triggered updates
+ self.scroll_update_timer = QTimer()
+ self.scroll_update_timer.setSingleShot(True)
+ self.scroll_update_timer.timeout.connect(self.update_relative_times)
+ self.scroll_update_timer.setInterval(200) # 200ms debounce delay
+
def mousePressEvent(self, event):
if gui_settings.get("frameless_window") == "True":
self.dragPos = event.globalPosition().toPoint()
@@ -841,20 +858,48 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if file_operation == "Deleting":
item_widget.set_label_1(f"Deleted from {parent_dir}")
item_widget.set_label_2(f"")
+ # Store timestamp and display for deleted files
+ if "timestamp" in data and data["timestamp"]:
+ item_widget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ item_widget.set_timestamp(relative_time)
+ else:
+ item_widget.set_timestamp("")
elif file_operation == "Moving":
item_widget.set_label_1(f"Trashed from {parent_dir}")
item_widget.set_label_2(f"")
+ # Store timestamp and display for moved files
+ if "timestamp" in data and data["timestamp"]:
+ item_widget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ item_widget.set_timestamp(relative_time)
+ else:
+ item_widget.set_timestamp("")
elif transfer_complete and file_operation == "Uploading":
shortened_path = shorten_path(relative_path_display, 32)
item_widget.set_label_1(f"Uploaded from {shortened_path}")
item_widget.set_label_2(f"{file_size_human}")
+ # Store timestamp and display in separate timestamp label
+ if "timestamp" in data and data["timestamp"]:
+ item_widget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ item_widget.set_timestamp(relative_time)
+ else:
+ item_widget.set_timestamp("")
elif transfer_complete and file_operation == "Downloading":
shortened_path = shorten_path(relative_path_display, 32)
item_widget.set_label_1(f"Downloaded from {shortened_path}")
item_widget.set_label_2(f"{file_size_human}")
+ # Store timestamp and display in separate timestamp label
+ if "timestamp" in data and data["timestamp"]:
+ item_widget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ item_widget.set_timestamp(relative_time)
+ else:
+ item_widget.set_timestamp("")
elif file_operation == "Downloading":
# Estimate final size of file before download completes
@@ -881,20 +926,48 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if file_operation == "Deleting":
myQCustomQWidget.set_label_1(f"Deleted from {parent_dir}")
myQCustomQWidget.set_label_2(f"")
+ # Store timestamp and display for deleted files
+ if "timestamp" in data and data["timestamp"]:
+ myQCustomQWidget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ myQCustomQWidget.set_timestamp(relative_time)
+ else:
+ myQCustomQWidget.set_timestamp("")
elif file_operation == "Moving":
myQCustomQWidget.set_label_1(f"Trashed from {parent_dir}")
myQCustomQWidget.set_label_2(f"")
+ # Store timestamp and display for moved files
+ if "timestamp" in data and data["timestamp"]:
+ myQCustomQWidget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ myQCustomQWidget.set_timestamp(relative_time)
+ else:
+ myQCustomQWidget.set_timestamp("")
elif transfer_complete and file_operation == "Uploading":
shortened_path = shorten_path(relative_path_display, 32)
myQCustomQWidget.set_label_1(f"Uploaded from {shortened_path}")
myQCustomQWidget.set_label_2(f"{file_size_human}")
+ # Store timestamp and display in separate timestamp label
+ if "timestamp" in data and data["timestamp"]:
+ myQCustomQWidget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ myQCustomQWidget.set_timestamp(relative_time)
+ else:
+ myQCustomQWidget.set_timestamp("")
elif transfer_complete and file_operation == "Downloading":
shortened_path = shorten_path(relative_path_display, 32)
myQCustomQWidget.set_label_1(f"Downloaded from {shortened_path}")
myQCustomQWidget.set_label_2(f"{file_size_human}")
+ # Store timestamp and display in separate timestamp label
+ if "timestamp" in data and data["timestamp"]:
+ myQCustomQWidget.set_completion_timestamp(data["timestamp"])
+ relative_time = format_relative_time(data["timestamp"])
+ myQCustomQWidget.set_timestamp(relative_time)
+ else:
+ myQCustomQWidget.set_timestamp("")
elif file_operation == "Downloading":
# Estimate final size of file before download completes
@@ -926,6 +999,54 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if listWidget.count() > 10_000:
listWidget.takeItem(listWidget.count() - 1)
+ def on_list_scrolled(self, value):
+ """
+ Debounced scroll handler - triggers timestamp update after scrolling stops.
+ Called when scrollbar position changes (user scroll or automatic scroll from new items).
+ """
+ # Restart the debounce timer - only triggers after 200ms of no scrolling
+ self.scroll_update_timer.start()
+
+ def update_relative_times(self):
+ """
+ Update relative time display only for visible items in the viewport.
+ Optimized to update only 10-20 visible items instead of all 10,000 items.
+ Called every 60 seconds by QTimer and when user scrolls the list.
+ """
+ for profile in self.profile_status_pages:
+ listWidget = self.profile_status_pages[profile].listWidget
+
+ # Get the visible viewport area
+ rect = listWidget.viewport().contentsRect()
+ top_index = listWidget.indexAt(rect.topLeft())
+
+ # Skip if no items are visible
+ if not top_index.isValid():
+ continue
+
+ # Get the bottom visible index
+ bottom_index = listWidget.indexAt(rect.bottomLeft())
+
+ # Handle case where bottom index is invalid (last item is visible)
+ if not bottom_index.isValid():
+ bottom_row = listWidget.count() - 1
+ else:
+ bottom_row = bottom_index.row()
+
+ # Only iterate through visible rows (typically 10-20 items instead of 10,000)
+ for row in range(top_index.row(), bottom_row + 1):
+ item = listWidget.item(row)
+ if item is not None:
+ item_widget = listWidget.itemWidget(item)
+ if item_widget is not None:
+ # Get the completion timestamp
+ timestamp = item_widget.get_completion_timestamp()
+
+ # If item has a timestamp, update the relative time in the timestamp label
+ if timestamp is not None:
+ relative_time = format_relative_time(timestamp)
+ item_widget.set_timestamp(relative_time)
+
def show_login(self, profile):
# Show login window with QT WebEngine
self.window1 = QWidget()
diff --git a/src/ui/list_item_widget.ui b/src/ui/list_item_widget.ui
index 7db301d..a039a85 100644
--- a/src/ui/list_item_widget.ui
+++ b/src/ui/list_item_widget.ui
@@ -67,11 +67,11 @@
- -
+
-
- 340
+ 270
16777215
@@ -85,6 +85,43 @@
FileName
+
+ Qt::PlainText
+
+
+
+ -
+
+
+
+ 70
+ 0
+
+
+
+
+ 70
+ 16777215
+
+
+
+
+ 8
+ 50
+
+
+
+ color: rgba(128, 128, 128, 200);
+
+
+
+
+
+ Qt::AlignRight|Qt::AlignVCenter
+
+
+ Qt::PlainText
+
-
@@ -107,6 +144,12 @@
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+ Qt::PlainText
+
+
+ false
+
-
diff --git a/src/ui/ui_list_item_widget.py b/src/ui/ui_list_item_widget.py
index 50d5781..ecb850b 100644
--- a/src/ui/ui_list_item_widget.py
+++ b/src/ui/ui_list_item_widget.py
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'list_item_widget.ui'
##
-## Created by: Qt User Interface Compiler version 6.2.2
+## Created by: Qt User Interface Compiler version 6.10.0
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -35,7 +35,7 @@ class Ui_list_item_widget(object):
self.toolButton.setMinimumSize(QSize(0, 0))
self.toolButton.setAutoFillBackground(False)
icon = QIcon()
- icon.addFile(u"../resources/images/icons8-clouds-48.png", QSize(), QIcon.Normal, QIcon.Off)
+ icon.addFile(u"../resources/images/icons8-clouds-48.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.toolButton.setIcon(icon)
self.toolButton.setIconSize(QSize(30, 40))
@@ -43,29 +43,45 @@ class Ui_list_item_widget(object):
self.ls_label_file_name = QLabel(list_item_widget)
self.ls_label_file_name.setObjectName(u"ls_label_file_name")
- self.ls_label_file_name.setMaximumSize(QSize(340, 16777215))
+ self.ls_label_file_name.setMaximumSize(QSize(270, 16777215))
font = QFont()
font.setPointSize(10)
font.setBold(True)
self.ls_label_file_name.setFont(font)
+ self.ls_label_file_name.setTextFormat(Qt.PlainText)
- self.gridLayout.addWidget(self.ls_label_file_name, 0, 1, 1, 2)
+ self.gridLayout.addWidget(self.ls_label_file_name, 0, 1, 1, 1)
+
+ self.ls_label_timestamp = QLabel(list_item_widget)
+ self.ls_label_timestamp.setObjectName(u"ls_label_timestamp")
+ self.ls_label_timestamp.setMinimumSize(QSize(70, 0))
+ self.ls_label_timestamp.setMaximumSize(QSize(70, 16777215))
+ font1 = QFont()
+ font1.setPointSize(8)
+ self.ls_label_timestamp.setFont(font1)
+ self.ls_label_timestamp.setStyleSheet(u"color: rgba(128, 128, 128, 200);")
+ self.ls_label_timestamp.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+ self.ls_label_timestamp.setTextFormat(Qt.PlainText)
+
+ self.gridLayout.addWidget(self.ls_label_timestamp, 0, 2, 1, 1)
self.ls_label_2 = QLabel(list_item_widget)
self.ls_label_2.setObjectName(u"ls_label_2")
- sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
+ sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ls_label_2.sizePolicy().hasHeightForWidth())
self.ls_label_2.setSizePolicy(sizePolicy)
self.ls_label_2.setMaximumSize(QSize(170, 16777215))
self.ls_label_2.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
+ self.ls_label_2.setTextFormat(Qt.PlainText)
+ self.ls_label_2.setWordWrap(False)
self.gridLayout.addWidget(self.ls_label_2, 2, 2, 2, 1)
self.ls_label_1 = QLabel(list_item_widget)
self.ls_label_1.setObjectName(u"ls_label_1")
- sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.ls_label_1.sizePolicy().hasHeightForWidth())
@@ -92,6 +108,7 @@ class Ui_list_item_widget(object):
list_item_widget.setWindowTitle(QCoreApplication.translate("list_item_widget", u"Form", None))
self.toolButton.setText(QCoreApplication.translate("list_item_widget", u"...", None))
self.ls_label_file_name.setText(QCoreApplication.translate("list_item_widget", u"FileName", None))
+ self.ls_label_timestamp.setText("")
self.ls_label_2.setText(QCoreApplication.translate("list_item_widget", u"9960KB of 9100KB", None))
self.ls_label_1.setText(QCoreApplication.translate("list_item_widget", u"Downloading/Uploading ", None))
# retranslateUi
diff --git a/src/utils/utils.py b/src/utils/utils.py
index 720808c..1c0d091 100644
--- a/src/utils/utils.py
+++ b/src/utils/utils.py
@@ -1,6 +1,7 @@
import os
import subprocess
import re
+from datetime import datetime
import logging
# from logger import logger
@@ -105,3 +106,28 @@ def config_client_bin_path() -> str:
return "onedrive"
else:
return gui_settings.get("client_bin_path")
+
+
+def format_relative_time(timestamp):
+ """
+ Format a datetime timestamp into compact relative time string.
+ Examples: "now", "2m ago", "3h ago", "2d ago"
+ """
+ if timestamp is None:
+ return ""
+
+ now = datetime.now()
+ delta = now - timestamp
+ total_seconds = int(delta.total_seconds())
+
+ if total_seconds < 60:
+ return "now"
+ elif total_seconds < 3600: # Less than 1 hour
+ minutes = total_seconds // 60
+ return f"{minutes}m ago"
+ elif total_seconds < 86400: # Less than 1 day
+ hours = total_seconds // 3600
+ return f"{hours}h ago"
+ else: # 1 day or more
+ days = total_seconds // 86400
+ return f"{days}d ago"
diff --git a/src/workers.py b/src/workers.py
index 1801d8e..af56277 100644
--- a/src/workers.py
+++ b/src/workers.py
@@ -1,6 +1,7 @@
-from PySide6.QtCore import QThread, Signal, QFileInfo
+from PySide6.QtCore import QThread, Signal, QFileInfo, Qt
from PySide6.QtWidgets import QWidget, QFileIconProvider
+from PySide6.QtGui import QFontMetrics
# Imports for main window.
@@ -10,6 +11,7 @@ from ui.ui_list_item_widget import Ui_list_item_widget
import re
import time
import subprocess
+from datetime import datetime
from global_config import save_global_config
from options import (
@@ -260,7 +262,7 @@ class WorkerThread(QThread):
self.profile_status["status_message"] = "Initializing the OneDrive API"
self.update_profile_status.emit(self.profile_status, self.profile_name)
- elif "Processing:" in stdout or "Number of items to download from Microsoft OneDrive" in stdout:
+ elif "Processing:" in stdout or "Number of items to download from Microsoft OneDrive" in stdout or "OneDrive Client requested to create" in stdout:
items_left = re.match(r"^Processing\s([0-9]+)\sOneDrive\sitems", stdout)
if items_left != None:
self.profile_status["status_message"] = f"OneDrive is processing {items_left.group(1)} items..."
@@ -300,6 +302,7 @@ class WorkerThread(QThread):
"file_path": "unknown file name" if self.file_path is None else self.file_path.group(1),
"progress": progress,
"transfer_complete": transfer_complete,
+ "timestamp": datetime.now() if transfer_complete else None,
}
# Update file transfer list
@@ -333,6 +336,7 @@ class WorkerThread(QThread):
"file_path": file_path,
"progress": progress,
"transfer_complete": transfer_complete,
+ "timestamp": datetime.now() if transfer_complete else None,
}
logging.info(transfer_progress_new)
@@ -648,6 +652,14 @@ class TaskList(QWidget, Ui_list_item_widget):
# Set up the user interface from Designer.
self.setupUi(self)
+ # Store completion timestamp for relative time display
+ self.completion_timestamp = None
+
+ # Enable text eliding for ls_label_2 to prevent horizontal overflow
+ self.ls_label_2.setWordWrap(False)
+ self.ls_label_2.setSizePolicy(self.ls_label_2.sizePolicy().horizontalPolicy(), self.ls_label_2.sizePolicy().verticalPolicy())
+ self.ls_label_2_max_width = 170 # Store max width for eliding
+
def set_icon(self, file_path):
self.fileInfo = QFileInfo(file_path)
self.iconProvider = QFileIconProvider()
@@ -656,7 +668,10 @@ class TaskList(QWidget, Ui_list_item_widget):
self.toolButton.setIcon(self.icon)
def set_file_name(self, file_path):
- self.ls_label_file_name.setText(file_path)
+ # Use font metrics to elide long filenames
+ font_metrics = QFontMetrics(self.ls_label_file_name.font())
+ elided_text = font_metrics.elidedText(file_path, Qt.ElideMiddle, 270)
+ self.ls_label_file_name.setText(elided_text)
def get_file_name(self):
return self.ls_label_file_name.text()
@@ -669,7 +684,18 @@ class TaskList(QWidget, Ui_list_item_widget):
self.ls_label_1.setText(text)
def set_label_2(self, text):
- self.ls_label_2.setText(text)
+ # Use font metrics to elide text if it exceeds maximum width
+ font_metrics = QFontMetrics(self.ls_label_2.font())
+ elided_text = font_metrics.elidedText(text, Qt.ElideRight, self.ls_label_2_max_width)
+ self.ls_label_2.setText(elided_text)
+
+ def set_timestamp(self, text):
+ """Set the timestamp label text"""
+ self.ls_label_timestamp.setText(text)
+
+ def get_timestamp_text(self):
+ """Get current timestamp label text"""
+ return self.ls_label_timestamp.text()
def hide_progress_bar(self, transfer_status: bool):
if transfer_status:
@@ -677,5 +703,13 @@ class TaskList(QWidget, Ui_list_item_widget):
else:
self.ls_progressBar.show()
+ def set_completion_timestamp(self, timestamp):
+ """Store the completion timestamp for this transfer"""
+ self.completion_timestamp = timestamp
+
+ def get_completion_timestamp(self):
+ """Get the completion timestamp for this transfer"""
+ return self.completion_timestamp
+
workers = {}