1
0
mirror of https://github.com/bpozdena/OneDriveGUI.git synced 2026-10-24 04:59:17 +03:00

Enhance file transfer UI with timestamp display #97

This commit is contained in:
Boris Pozdena
2025-11-21 16:19:09 +01:00
parent 80f6513b21
commit 09a9bb3400
5 changed files with 254 additions and 13 deletions

View File

@@ -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 <a href=file:///{absolute_path}>{shortened_path}</a>")
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 <a href=file:///{absolute_path}>{shortened_path}</a>")
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 <a href=file:///{absolute_path}>{shortened_path}</a>")
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 <a href=file:///{absolute_path}>{shortened_path}</a>")
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()

View File

@@ -67,11 +67,11 @@
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<item row="0" column="1">
<widget class="QLabel" name="ls_label_file_name">
<property name="maximumSize">
<size>
<width>340</width>
<width>270</width>
<height>16777215</height>
</size>
</property>
@@ -85,6 +85,43 @@
<property name="text">
<string>FileName</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="ls_label_timestamp">
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>50</weight>
</font>
</property>
<property name="styleSheet">
<string notr="true">color: rgba(128, 128, 128, 200);</string>
</property>
<property name="text">
<string></string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignVCenter</set>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item row="2" column="2" rowspan="2">
@@ -107,6 +144,12 @@
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="1" rowspan="2">

View File

@@ -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

View File

@@ -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"

View File

@@ -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 = {}