diff --git a/src/python/matrix_client.py b/src/python/matrix_client.py index 186901e0..13831e9c 100644 --- a/src/python/matrix_client.py +++ b/src/python/matrix_client.py @@ -215,8 +215,8 @@ class MatrixClient(nio.AsyncClient): try: await self._send_file(item_uuid, room_id, path) - except asyncio.CancelledError: - del self.models[Upload, room_id][item_uuid] + except (nio.TransferCancelledError, asyncio.CancelledError): + del self.models[Upload, room_id][str(item_uuid)] async def _send_file( @@ -235,18 +235,27 @@ class MatrixClient(nio.AsyncClient): task = asyncio.Task.current_task() upload_item = Upload(item_uuid, task, path, total_size=size) - self.models[Upload, room_id][upload_item.uuid] = upload_item + self.models[Upload, room_id][str(item_uuid)] = upload_item + + def on_transfered(transfered: int) -> None: + upload_item.uploaded = transfered + upload_item.time_left = monitor.remaining_time + + def on_speed_change(speed: float) -> None: + upload_item.speed = speed + + monitor = nio.TransferMonitor(size, on_transfered, on_speed_change) try: url, mime, crypt_dict = await self.upload( - path, filename=path.name, encrypt=encrypt, + path, filename=path.name, encrypt=encrypt, monitor=monitor, ) except (MatrixError, OSError) as err: upload_item.status = UploadStatus.Error upload_item.error = type(err) upload_item.error_args = err.args - # Wait for cancellation, see parent send_file() method + # Wait for cancellation from UI, see parent send_file() method while True: await asyncio.sleep(0.1) @@ -606,9 +615,10 @@ class MatrixClient(nio.AsyncClient): async def upload( self, data: UploadData, - mime: Optional[str] = None, - filename: Optional[str] = None, - encrypt: bool = False, + mime: Optional[str] = None, + filename: Optional[str] = None, + encrypt: bool = False, + monitor: Optional[nio.TransferMonitor] = None, ) -> UploadReturn: mime = mime or await utils.guess_mime(data) @@ -618,6 +628,7 @@ class MatrixClient(nio.AsyncClient): "application/octet-stream" if encrypt else mime, filename, encrypt, + monitor, ) if isinstance(response, nio.UploadError): diff --git a/src/python/models/items.py b/src/python/models/items.py index ee59f0a1..43bb7cba 100644 --- a/src/python/models/items.py +++ b/src/python/models/items.py @@ -1,7 +1,7 @@ import asyncio import re from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Type, Union from uuid import UUID @@ -114,12 +114,16 @@ class UploadStatus(AutoStrEnum): @dataclass class Upload(ModelItem): - uuid: UUID = field() - task: asyncio.Task = field() - filepath: Path = field() + uuid: UUID = field() + task: asyncio.Task = field() + filepath: Path = field() + + total_size: int = 0 + uploaded: int = 0 + speed: float = 0 + time_left: Optional[timedelta] = None + status: UploadStatus = UploadStatus.Uploading - total_size: int = 0 - uploaded: int = 0 error: OptionalExceptionType = type(None) error_args: Tuple[Any, ...] = () diff --git a/src/python/utils.py b/src/python/utils.py index 757f7ee0..0c786049 100644 --- a/src/python/utils.py +++ b/src/python/utils.py @@ -7,6 +7,7 @@ import inspect import io import logging as log import xml.etree.cElementTree as xml_etree # FIXME: bandit warning +from datetime import timedelta from enum import Enum from enum import auto as autostr from pathlib import Path @@ -140,6 +141,9 @@ def serialize_value_for_qml(value: Any) -> Any: if isinstance(value, UUID): return str(value) + if isinstance(value, timedelta): + return value.total_seconds() * 1000 + if inspect.isclass(value): return value.__name__ diff --git a/src/qml/Chat/UploadsBar.qml b/src/qml/Chat/UploadsBar.qml index b269b3a3..1c570e24 100644 --- a/src/qml/Chat/UploadsBar.qml +++ b/src/qml/Chat/UploadsBar.qml @@ -94,7 +94,7 @@ Rectangle { .arg(fileName) : model.error === "IsADirectoryError" ? - qsTr("Can't upload folders: %1") + qsTr("Can't upload folders, need a file: %1") .arg(filePath) : model.error === "FileNotFoundError" ? @@ -128,6 +128,13 @@ Rectangle { readonly property string filePath: model.filepath.replace(/^file:\/\//, "") + + HoverHandler { id: statusLabelHover } + + HToolTip { + text: parent.truncated ? parent.text : "" + visible: text && statusLabelHover.hovered + } } HSpacer {} @@ -135,7 +142,11 @@ Rectangle { HLabel { id: uploadCountLabel visible: Layout.preferredWidth > 0 - text: qsTr("%1") + text: qsTr("-%1 %2/s %3/%4") + .arg(model.time_left ? + Utils.formatDuration(msLeft) : "∞") + .arg(CppUtils.formattedBytes(model.speed)) + .arg(CppUtils.formattedBytes(uploaded)) .arg(CppUtils.formattedBytes(model.total_size)) topPadding: theme.spacing / 2 @@ -146,6 +157,12 @@ Rectangle { Layout.preferredWidth: model.status === "Uploading" ? implicitWidth : 0 + property int msLeft: model.time_left || -1 + property int uploaded: model.uploaded + + Behavior on msLeft { HNumberAnimation { duration: 1000 } } + Behavior on uploaded { HNumberAnimation { duration: 1000 }} + Behavior on Layout.preferredWidth { HNumberAnimation {} } } @@ -153,20 +170,15 @@ Rectangle { onTapped: if (model.status !== "Error") statusLabel.expand = ! statusLabel.expand } - - HoverHandler { id: infoRowHover } - - HToolTip { - id: statusToolTip - text: statusLabel.truncated ? statusLabel.text : "" - visible: text && infoRowHover.hovered - } } HProgressBar { id: progressBar visible: Layout.maximumHeight !== 0 - indeterminate: true + indeterminate: model.status !== "Uploading" + value: model.uploaded + to: model.total_size + foregroundColor: model.status === "Error" ? theme.controls.progressBar.errorForeground : @@ -176,6 +188,7 @@ Rectangle { Layout.maximumHeight: model.status === "Error" && indeterminate ? 0 : -1 + Behavior on value { HNumberAnimation { duration: 1000 } } Behavior on Layout.maximumHeight { HNumberAnimation {} } } }