Add visible indicator when downloading files

Downloading file messages will now show a transfer control above the
composer, similar to uploads. Measuring the progress or pausing the
operation is not possible yet.
This commit is contained in:
miruka 2021-01-20 15:50:04 -04:00
parent 86f0a8a6a0
commit 7af1456c1d
16 changed files with 167 additions and 141 deletions

View File

@ -68,8 +68,8 @@ class Backend:
- `("<user_id>", "rooms")`: rooms our account `user_id` is part of; - `("<user_id>", "rooms")`: rooms our account `user_id` is part of;
- `("<user_id>", "uploads")`: ongoing or failed file uploads for - `("<user_id>", "transfers")`: ongoing or failed file
our account `user_id`; uploads/downloads for our account `user_id`;
- `("<user_id>", "<room_id>", "members")`: members in the room - `("<user_id>", "<room_id>", "members")`: members in the room
`room_id` that our account `user_id` is part of; `room_id` that our account `user_id` is part of;
@ -322,7 +322,7 @@ class Backend:
self.models["accounts"].pop(user_id, None) self.models["accounts"].pop(user_id, None)
self.models["matching_accounts"].pop(user_id, None) self.models["matching_accounts"].pop(user_id, None)
self.models[user_id, "uploads"].clear() self.models[user_id, "transfers"].clear()
for room_id in self.models[user_id, "rooms"]: for room_id in self.models[user_id, "rooms"]:
self.models["all_rooms"].pop(room_id, None) self.models["all_rooms"].pop(room_id, None)

View File

@ -90,6 +90,11 @@ class MatrixTooLarge(MatrixError):
m_code: str = "M_TOO_LARGE" m_code: str = "M_TOO_LARGE"
@dataclass
class MatrixBadGateway(MatrixError):
http_code: int = 502
m_code: Optional[str] = None
@dataclass @dataclass
class MatrixBadGateway(MatrixError): class MatrixBadGateway(MatrixError):
http_code: int = 502 http_code: int = 502

View File

@ -9,7 +9,6 @@ import io
import logging as log import logging as log
import platform import platform
import re import re
import sys
import textwrap import textwrap
import traceback import traceback
from contextlib import suppress from contextlib import suppress
@ -42,8 +41,8 @@ from .errors import (
from .html_markdown import HTML_PROCESSOR as HTML from .html_markdown import HTML_PROCESSOR as HTML
from .media_cache import Media, Thumbnail from .media_cache import Media, Thumbnail
from .models.items import ( from .models.items import (
ZERO_DATE, Account, Event, Member, Room, TypeSpecifier, Upload, ZERO_DATE, Account, Event, Member, Room, Transfer, TransferStatus,
UploadStatus, TypeSpecifier,
) )
from .models.model_store import ModelStore from .models.model_store import ModelStore
from .nio_callbacks import NioCallbacks from .nio_callbacks import NioCallbacks
@ -55,11 +54,6 @@ from .pyotherside_events import (
if TYPE_CHECKING: if TYPE_CHECKING:
from .backend import Backend from .backend import Backend
if sys.version_info >= (3, 7):
current_task = asyncio.current_task
else:
current_task = asyncio.Task.current_task
CryptDict = Dict[str, Any] CryptDict = Dict[str, Any]
PathCallable = Union[ PathCallable = Union[
str, Path, Callable[[], Coroutine[None, None, Union[str, Path]]], str, Path, Callable[[], Coroutine[None, None, Union[str, Path]]],
@ -190,8 +184,8 @@ class MatrixClient(nio.AsyncClient):
self.sync_task: Optional[asyncio.Future] = None self.sync_task: Optional[asyncio.Future] = None
self.start_task: Optional[asyncio.Future] = None self.start_task: Optional[asyncio.Future] = None
self.upload_monitors: Dict[UUID, nio.TransferMonitor] = {} self.transfer_monitors: Dict[UUID, nio.TransferMonitor] = {}
self.upload_tasks: Dict[UUID, asyncio.Task] = {} self.transfer_tasks: Dict[UUID, asyncio.Task] = {}
self.send_message_tasks: Dict[UUID, asyncio.Task] = {} self.send_message_tasks: Dict[UUID, asyncio.Task] = {}
self._presence: str = "" self._presence: str = ""
@ -628,23 +622,23 @@ class MatrixClient(nio.AsyncClient):
await self._send_message(room_id, content, tx_id) await self._send_message(room_id, content, tx_id)
async def toggle_pause_upload( async def toggle_pause_transfer(
self, room_id: str, uuid: Union[str, UUID], self, room_id: str, uuid: Union[str, UUID],
) -> None: ) -> None:
if isinstance(uuid, str): if isinstance(uuid, str):
uuid = UUID(uuid) uuid = UUID(uuid)
pause = not self.upload_monitors[uuid].pause pause = not self.transfer_monitors[uuid].pause
self.upload_monitors[uuid].pause = pause self.transfer_monitors[uuid].pause = pause
self.models[room_id, "uploads"][str(uuid)].paused = pause self.models[room_id, "transfers"][str(uuid)].paused = pause
async def cancel_upload(self, uuid: Union[str, UUID]) -> None: async def cancel_transfer(self, uuid: Union[str, UUID]) -> None:
if isinstance(uuid, str): if isinstance(uuid, str):
uuid = UUID(uuid) uuid = UUID(uuid)
self.upload_tasks[uuid].cancel() self.transfer_tasks[uuid].cancel()
async def send_clipboard_image( async def send_clipboard_image(
@ -691,9 +685,9 @@ class MatrixClient(nio.AsyncClient):
try: try:
await self._send_file(item_uuid, room_id, path, reply_to_event_id) await self._send_file(item_uuid, room_id, path, reply_to_event_id)
except (nio.TransferCancelledError, asyncio.CancelledError): except (nio.TransferCancelledError, asyncio.CancelledError):
self.upload_monitors.pop(item_uuid, None) self.transfer_monitors.pop(item_uuid, None)
self.upload_tasks.pop(item_uuid, None) self.transfer_tasks.pop(item_uuid, None)
self.models[room_id, "uploads"].pop(str(item_uuid), None) self.models[room_id, "transfers"].pop(str(item_uuid), None)
async def _send_file( async def _send_file(
@ -708,10 +702,10 @@ class MatrixClient(nio.AsyncClient):
# TODO: this function is way too complex, and most of it should be # TODO: this function is way too complex, and most of it should be
# refactored into nio. # refactored into nio.
self.upload_tasks[item_uuid] = current_task() # type: ignore self.transfer_tasks[item_uuid] = utils.current_task() # type: ignore
upload_item = Upload(item_uuid) transfer = Transfer(item_uuid, is_upload=True)
self.models[room_id, "uploads"][str(item_uuid)] = upload_item self.models[room_id, "transfers"][str(item_uuid)] = transfer
transaction_id = uuid4() transaction_id = uuid4()
path = Path(await path() if callable(path) else path) path = Path(await path() if callable(path) else path)
@ -726,18 +720,18 @@ class MatrixClient(nio.AsyncClient):
# This error will be caught again by the try block later below # This error will be caught again by the try block later below
size = 0 size = 0
upload_item.set_fields( transfer.set_fields(
status=UploadStatus.Uploading, filepath=path, total_size=size, status=TransferStatus.Transfering, filepath=path, total_size=size,
) )
monitor = nio.TransferMonitor(size) monitor = nio.TransferMonitor(size)
self.upload_monitors[item_uuid] = monitor self.transfer_monitors[item_uuid] = monitor
def on_transferred(transferred: int) -> None: def on_transferred(transferred: int) -> None:
upload_item.uploaded = transferred transfer.transferred = transferred
def on_speed_changed(speed: float) -> None: def on_speed_changed(speed: float) -> None:
upload_item.set_fields( transfer.set_fields(
speed = speed, speed = speed,
time_left = monitor.remaining_time or timedelta(0), time_left = monitor.remaining_time or timedelta(0),
) )
@ -761,8 +755,8 @@ class MatrixClient(nio.AsyncClient):
raise nio.TransferCancelledError() raise nio.TransferCancelledError()
except (MatrixError, OSError) as err: except (MatrixError, OSError) as err:
upload_item.set_fields( transfer.set_fields(
status = UploadStatus.Error, status = TransferStatus.Error,
error = type(err), error = type(err),
error_args = err.args, error_args = err.args,
) )
@ -771,7 +765,7 @@ class MatrixClient(nio.AsyncClient):
while True: while True:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
upload_item.status = UploadStatus.Caching transfer.status = TransferStatus.Caching
local_media = await Media.from_existing_file( local_media = await Media.from_existing_file(
self.backend.media_cache, self.user_id, url, path, self.backend.media_cache, self.user_id, url, path,
) )
@ -787,7 +781,7 @@ class MatrixClient(nio.AsyncClient):
"body": path.name, "body": path.name,
"info": { "info": {
"mimetype": mime, "mimetype": mime,
"size": upload_item.total_size, "size": transfer.total_size,
}, },
} }
@ -822,20 +816,20 @@ class MatrixClient(nio.AsyncClient):
thumb_ext = "png" if thumb_info.mime == "image/png" else "jpg" thumb_ext = "png" if thumb_info.mime == "image/png" else "jpg"
thumb_name = f"{path.stem}_thumbnail.{thumb_ext}" thumb_name = f"{path.stem}_thumbnail.{thumb_ext}"
upload_item.set_fields( transfer.set_fields(
status = UploadStatus.Uploading, status = TransferStatus.Transfering,
filepath = Path(thumb_name), filepath = Path(thumb_name),
total_size = len(thumb_data), total_size = len(thumb_data),
) )
try: try:
upload_item.total_size = thumb_info.size transfer.total_size = thumb_info.size
monitor = nio.TransferMonitor(thumb_info.size) monitor = nio.TransferMonitor(thumb_info.size)
monitor.on_transferred = on_transferred monitor.on_transferred = on_transferred
monitor.on_speed_changed = on_speed_changed monitor.on_speed_changed = on_speed_changed
self.upload_monitors[item_uuid] = monitor self.transfer_monitors[item_uuid] = monitor
thumb_url, _, thumb_crypt_dict = await self.upload( thumb_url, _, thumb_crypt_dict = await self.upload(
lambda *_: thumb_data, lambda *_: thumb_data,
@ -851,7 +845,7 @@ class MatrixClient(nio.AsyncClient):
except MatrixError as err: except MatrixError as err:
log.warning(f"Failed uploading thumbnail {path}: {err}") log.warning(f"Failed uploading thumbnail {path}: {err}")
else: else:
upload_item.status = UploadStatus.Caching transfer.status = TransferStatus.Caching
await Thumbnail.from_bytes( await Thumbnail.from_bytes(
self.backend.media_cache, self.backend.media_cache,
@ -907,9 +901,9 @@ class MatrixClient(nio.AsyncClient):
content["msgtype"] = "m.file" content["msgtype"] = "m.file"
content["filename"] = path.name content["filename"] = path.name
del self.upload_monitors[item_uuid] del self.transfer_monitors[item_uuid]
del self.upload_tasks[item_uuid] del self.transfer_tasks[item_uuid]
del self.models[room_id, "uploads"][str(upload_item.id)] del self.models[room_id, "transfers"][str(transfer.id)]
if reply_to_event_id: if reply_to_event_id:
await self.send_text( await self.send_text(
@ -1004,7 +998,7 @@ class MatrixClient(nio.AsyncClient):
"""Send a message event with `content` dict to a room.""" """Send a message event with `content` dict to a room."""
self.send_message_tasks[transaction_id] = \ self.send_message_tasks[transaction_id] = \
current_task() # type: ignore utils.current_task() # type: ignore
async with self.backend.send_locks[room_id]: async with self.backend.send_locks[room_id]:
await self.room_send( await self.room_send(
@ -2068,13 +2062,13 @@ class MatrixClient(nio.AsyncClient):
avatar_size = (48, 48) avatar_size = (48, 48)
avatar_path = await self.backend.media_cache.get_thumbnail( avatar_path = await Thumbnail(
cache = self.backend.media_cache,
client_user_id = self.user_id, client_user_id = self.user_id,
mxc = mxc, mxc = mxc,
title = f"user_{user_id}.notification", title = f"user_{user_id}.notification",
width = avatar_size[0], wanted_size = avatar_size,
height = avatar_size[1], ).get()
)
image_data = None image_data = None
create = False create = False

View File

@ -13,11 +13,14 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import uuid4
import nio import nio
from PIL import Image as PILImage from PIL import Image as PILImage
from .utils import Size, atomic_write from .models.items import Transfer, TransferStatus
from .models.model import Model
from .utils import Size, atomic_write, current_task
if TYPE_CHECKING: if TYPE_CHECKING:
from .backend import Backend from .backend import Backend
@ -25,8 +28,6 @@ if TYPE_CHECKING:
if sys.version_info < (3, 8): if sys.version_info < (3, 8):
import pyfastcopy # noqa import pyfastcopy # noqa
CryptDict = Optional[Dict[str, Any]]
CONCURRENT_DOWNLOADS_LIMIT = asyncio.BoundedSemaphore(8) CONCURRENT_DOWNLOADS_LIMIT = asyncio.BoundedSemaphore(8)
ACCESS_LOCKS: DefaultDict[str, asyncio.Lock] = DefaultDict(asyncio.Lock) ACCESS_LOCKS: DefaultDict[str, asyncio.Lock] = DefaultDict(asyncio.Lock)
@ -47,45 +48,35 @@ class MediaCache:
self.downloads_dir.mkdir(parents=True, exist_ok=True) self.downloads_dir.mkdir(parents=True, exist_ok=True)
async def get_media( async def get_media(self, *args) -> Path:
self, """Return `Media(self, ...).get()`'s result. Intended for QML."""
client_user_id: str, return await Media(self, *args).get()
mxc: str,
title: str,
crypt_dict: CryptDict = None,
) -> Path:
"""Return `Media.get()`'s result. Intended for QML."""
return await Media(self, client_user_id, mxc, title, crypt_dict).get()
async def get_thumbnail( async def get_thumbnail(self, width: float, height: float, *args) -> Path:
self, """Return `Thumbnail(self, ...).get()`'s result. Intended for QML."""
client_user_id: str,
mxc: str,
title: str,
width: int,
height: int,
crypt_dict: CryptDict = None,
) -> Path:
"""Return `Thumbnail.get()`'s result. Intended for QML."""
# QML sometimes pass float sizes, which matrix API doesn't like. # QML sometimes pass float sizes, which matrix API doesn't like.
size = (round(width), round(height)) size = (round(width), round(height))
return await Thumbnail(
thumb = Thumbnail(self, client_user_id, mxc, title, crypt_dict, size) self, *args, wanted_size=size, # type: ignore
return await thumb.get() ).get()
@dataclass @dataclass
class Media: class Media:
"""A matrix media file.""" """A matrix media file that is downloaded or has yet to be.
If the `room_id` is not set, no `Transfer` model item will be registered
while this media is being downloaded.
"""
cache: "MediaCache" = field() cache: "MediaCache" = field()
client_user_id: str = field() client_user_id: str = field()
mxc: str = field() mxc: str = field()
title: str = field() title: str = field()
crypt_dict: CryptDict = field(repr=False) room_id: Optional[str] = None
filesize: Optional[int] = None
crypt_dict: Optional[Dict[str, Any]] = field(default=None, repr=False)
def __post_init__(self) -> None: def __post_init__(self) -> None:
@ -155,12 +146,39 @@ class Media:
async def _get_remote_data(self) -> bytes: async def _get_remote_data(self) -> bytes:
"""Return the file's data from the matrix server, decrypt if needed.""" """Return the file's data from the matrix server, decrypt if needed."""
parsed = urlparse(self.mxc) client = self.cache.backend.clients[self.client_user_id]
resp = await self.cache.backend.clients[self.client_user_id].download( transfer: Optional[Transfer] = None
model: Optional[Model] = None
if self.room_id:
model = self.cache.backend.models[self.room_id, "transfers"]
transfer = Transfer(
id = uuid4(),
is_upload = False,
filepath = self.local_path,
total_size = self.filesize or 0,
status = TransferStatus.Transfering,
)
assert model is not None
client.transfer_tasks[transfer.id] = current_task() # type: ignore
model[str(transfer.id)] = transfer
try:
parsed = urlparse(self.mxc)
resp = await client.download(
server_name = parsed.netloc, server_name = parsed.netloc,
media_id = parsed.path.lstrip("/"), media_id = parsed.path.lstrip("/"),
) )
except (nio.TransferCancelledError, asyncio.CancelledError):
if transfer and model:
del model[str(transfer.id)]
del client.transfer_tasks[transfer.id]
raise
if transfer and model:
del model[str(transfer.id)]
del client.transfer_tasks[transfer.id]
return await self._decrypt(resp.body) return await self._decrypt(resp.body)
@ -196,8 +214,13 @@ class Media:
"""Copy an existing file to cache and return a `Media` for it.""" """Copy an existing file to cache and return a `Media` for it."""
media = cls( media = cls(
cache, client_user_id, mxc, existing.name, {}, **kwargs, cache = cache,
) # type: ignore client_user_id = client_user_id,
mxc = mxc,
title = existing.name,
filesize = existing.stat().st_size,
**kwargs,
)
media.local_path.parent.mkdir(parents=True, exist_ok=True) media.local_path.parent.mkdir(parents=True, exist_ok=True)
if not media.local_path.exists() or overwrite: if not media.local_path.exists() or overwrite:
@ -221,8 +244,8 @@ class Media:
"""Create a cached file from bytes data and return a `Media` for it.""" """Create a cached file from bytes data and return a `Media` for it."""
media = cls( media = cls(
cache, client_user_id, mxc, filename, {}, **kwargs, cache, client_user_id, mxc, filename, filesize=len(data), **kwargs,
) # type: ignore )
media.local_path.parent.mkdir(parents=True, exist_ok=True) media.local_path.parent.mkdir(parents=True, exist_ok=True)
if not media.local_path.exists() or overwrite: if not media.local_path.exists() or overwrite:
@ -237,14 +260,9 @@ class Media:
@dataclass @dataclass
class Thumbnail(Media): class Thumbnail(Media):
"""The thumbnail of a matrix media, which is a media itself.""" """A matrix media's thumbnail, which is downloaded or has yet to be."""
cache: "MediaCache" = field() wanted_size: Size = (800, 600)
client_user_id: str = field()
mxc: str = field()
title: str = field()
crypt_dict: CryptDict = field(repr=False)
wanted_size: Size = field()
server_size: Optional[Size] = field(init=False, repr=False, default=None) server_size: Optional[Size] = field(init=False, repr=False, default=None)

View File

@ -286,37 +286,38 @@ class Member(ModelItem):
) )
class UploadStatus(AutoStrEnum): class TransferStatus(AutoStrEnum):
"""Enum describing the status of an upload operation.""" """Enum describing the status of an upload operation."""
Preparing = auto() Preparing = auto()
Uploading = auto() Transfering = auto()
Caching = auto() Caching = auto()
Error = auto() Error = auto()
@dataclass(eq=False) @dataclass(eq=False)
class Upload(ModelItem): class Transfer(ModelItem):
"""Represent a running or failed file upload operation.""" """Represent a running or failed file upload/download operation."""
id: UUID = field() id: UUID = field()
is_upload: bool = field()
filepath: Path = Path("-") filepath: Path = Path("-")
total_size: int = 0 total_size: int = 0
uploaded: int = 0 transferred: int = 0
speed: float = 0 speed: float = 0
time_left: timedelta = timedelta(0) time_left: timedelta = timedelta(0)
paused: bool = False paused: bool = False
status: UploadStatus = UploadStatus.Preparing status: TransferStatus = TransferStatus.Preparing
error: OptionalExceptionType = type(None) error: OptionalExceptionType = type(None)
error_args: Tuple[Any, ...] = () error_args: Tuple[Any, ...] = ()
start_date: datetime = field(init=False, default_factory=datetime.now) start_date: datetime = field(init=False, default_factory=datetime.now)
def __lt__(self, other: "Upload") -> bool: def __lt__(self, other: "Transfer") -> bool:
"""Sort by the start date, from newest upload to oldest.""" """Sort by the start date, from newest transfer to oldest."""
return (self.start_date, self.id) > (other.start_date, other.id) return (self.start_date, self.id) > (other.start_date, other.id)

View File

@ -197,6 +197,8 @@ class NioCallbacks:
client_user_id = self.user_id, client_user_id = self.user_id,
mxc = ev.url, mxc = ev.url,
title = ev.body, title = ev.body,
room_id = room.room_id,
filesize = info.get("size") or 0,
crypt_dict = media_crypt_dict, crypt_dict = media_crypt_dict,
).get_local() ).get_local()
except FileNotFoundError: except FileNotFoundError:

View File

@ -38,8 +38,10 @@ from .pcn.section import Section
if sys.version_info >= (3, 7): if sys.version_info >= (3, 7):
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
current_task = asyncio.current_task
else: else:
from async_generator import asynccontextmanager from async_generator import asynccontextmanager
current_task = asyncio.Task.current_task
Size = Tuple[int, int] Size = Tuple[int, int]
BytesOrPIL = Union[bytes, PILImage.Image] BytesOrPIL = Union[bytes, PILImage.Image]

View File

@ -9,6 +9,9 @@ HImage {
property string clientUserId property string clientUserId
property string mxc property string mxc
property string title property string title
property var roomId: undefined // undefined or string
property var fileSize: undefined // undefined or int (bytes)
property string sourceOverride: "" property string sourceOverride: ""
property bool thumbnail: true property bool thumbnail: true
property var cryptDict: ({}) property var cryptDict: ({})
@ -39,10 +42,10 @@ HImage {
} }
const method = image.thumbnail ? "get_thumbnail" : "get_media" const method = image.thumbnail ? "get_thumbnail" : "get_media"
const args = let args = [
image.thumbnail ? clientUserId, image.mxc, image.title, roomId, fileSize, cryptDict,
[clientUserId, image.mxc, image.title, w, h, cryptDict] : ]
[clientUserId, image.mxc, image.title, cryptDict] if (image.thumbnail) args = [w, h, ...args]
getFutureId = py.callCoro("media_cache." + method, args, path => { getFutureId = py.callCoro("media_cache." + method, args, path => {
if (! image) return if (! image) return

View File

@ -12,7 +12,7 @@ HColumnLayout {
property bool cancelPending: false property bool cancelPending: false
property int msLeft: model.time_left property int msLeft: model.time_left
property int uploaded: model.uploaded property int transferred: model.transferred
readonly property int speed: model.speed readonly property int speed: model.speed
readonly property int totalSize: model.total_size readonly property int totalSize: model.total_size
readonly property string status: model.status readonly property string status: model.status
@ -21,12 +21,12 @@ HColumnLayout {
function cancel() { function cancel() {
cancelPending = true cancelPending = true
// Python will delete this model item on cancel // Python will delete this model item on cancel
py.callClientCoro(chat.userId, "cancel_upload", [model.id]) py.callClientCoro(chat.userId, "cancel_transfer", [model.id])
} }
function toggle_pause() { function toggle_pause() {
py.callClientCoro( py.callClientCoro(
chat.userId, "toggle_pause_upload", [chat.roomId, model.id], chat.userId, "toggle_pause_transfer", [chat.roomId, model.id],
) )
} }
@ -36,7 +36,7 @@ HColumnLayout {
HRowLayout { HRowLayout {
HIcon { HIcon {
svgName: "uploading" svgName: model.is_upload ? "uploading" : "downloading"
colorize: colorize:
cancelPending || transfer.status === "Error" ? cancelPending || transfer.status === "Error" ?
theme.colors.negativeBackground : theme.colors.negativeBackground :
@ -70,7 +70,7 @@ HColumnLayout {
status === "Preparing" ? status === "Preparing" ?
qsTr("Preparing file...") : qsTr("Preparing file...") :
status === "Uploading" ? status === "Transfering" ?
fileName : fileName :
status === "Caching" ? status === "Caching" ?
@ -120,8 +120,12 @@ HColumnLayout {
speed ? qsTr("%1/s").arg(CppUtils.formattedBytes(speed)) : "", speed ? qsTr("%1/s").arg(CppUtils.formattedBytes(speed)) : "",
qsTr("%1/%2").arg(CppUtils.formattedBytes(uploaded)) transferred && totalSize ?
.arg(CppUtils.formattedBytes(totalSize)), qsTr("%1/%2").arg(CppUtils.formattedBytes(transferred))
.arg(CppUtils.formattedBytes(totalSize)) :
transferred || totalSize ?
CppUtils.formattedBytes(transferred || totalSize) :
"",
] ]
HLabel { HLabel {
@ -131,7 +135,7 @@ HColumnLayout {
rightPadding: leftPadding rightPadding: leftPadding
Layout.preferredWidth: Layout.preferredWidth:
status === "Uploading" ? implicitWidth : 0 status === "Transfering" ? implicitWidth : 0
Behavior on Layout.preferredWidth { HNumberAnimation {} } Behavior on Layout.preferredWidth { HNumberAnimation {} }
} }
@ -142,7 +146,7 @@ HColumnLayout {
padded: false padded: false
icon.name: transfer.paused ? icon.name: transfer.paused ?
"upload-resume" : "upload-pause" "transfer-resume" : "transfer-pause"
icon.color: transfer.paused ? icon.color: transfer.paused ?
theme.colors.positiveBackground : theme.colors.positiveBackground :
@ -153,8 +157,9 @@ HColumnLayout {
onClicked: transfer.toggle_pause() onClicked: transfer.toggle_pause()
// TODO: pausing downloads
Layout.preferredWidth: Layout.preferredWidth:
status === "Uploading" ? status === "Transfering" && model.is_upload ?
theme.baseElementsHeight : 0 theme.baseElementsHeight : 0
Layout.fillHeight: true Layout.fillHeight: true
@ -163,7 +168,7 @@ HColumnLayout {
} }
HButton { HButton {
icon.name: "upload-cancel" icon.name: "transfer-cancel"
icon.color: theme.colors.negativeBackground icon.color: theme.colors.negativeBackground
onClicked: transfer.cancel() onClicked: transfer.cancel()
padded: false padded: false
@ -183,8 +188,8 @@ HColumnLayout {
HProgressBar { HProgressBar {
id: progressBar id: progressBar
visible: Layout.maximumHeight !== 0 visible: Layout.maximumHeight !== 0
indeterminate: status !== "Uploading" indeterminate: status !== "Transfering" || ! totalSize || ! transferred
value: uploaded value: transferred
to: totalSize to: totalSize
// TODO: bake this in hprogressbar // TODO: bake this in hprogressbar

View File

@ -25,7 +25,7 @@ Rectangle {
id: transferList id: transferList
anchors.fill: parent anchors.fill: parent
model: ModelStore.get(chat.roomId, "uploads") model: ModelStore.get(chat.roomId, "transfers")
delegate: Transfer { width: transferList.width } delegate: Transfer { width: transferList.width }
} }
} }

View File

@ -489,19 +489,16 @@ Rectangle {
return return
} }
print("Downloading " + event.media_url + " ...")
const args = [ const args = [
chat.userId, chat.userId,
event.media_url, event.media_url,
event.media_title, event.media_title,
chat.roomId,
event.media_size,
JSON.parse(event.media_crypt_dict), JSON.parse(event.media_crypt_dict),
] ]
py.callCoro("media_cache.get_media", args, path => { py.callCoro("media_cache.get_media", args, callback)
print("Done: " + path)
callback(path)
})
} }
function openMediaExternally(event) { function openMediaExternally(event) {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 21l-8-9h6v-12h4v12h6l-8 9zm9-1v2h-18v-2h-2v4h22v-4h-2z"/></svg>

After

Width:  |  Height:  |  Size: 159 B

View File

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 246 B

View File

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 247 B

View File

Before

Width:  |  Height:  |  Size: 124 B

After

Width:  |  Height:  |  Size: 124 B

View File

@ -1,3 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 9h-6l8-9 8 9h-6v11h-4v-11zm11 11v2h-18v-2h-2v4h22v-4h-2z"/></svg>
<path d="m16 16h-3v5h-2v-5h-3l4-4zm3.479-5.908c-.212-3.951-3.473-7.092-7.479-7.092s-7.267 3.141-7.479 7.092c-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h3.5v-2h-3.5c-1.93 0-3.5-1.57-3.5-3.5 0-2.797 2.479-3.833 4.433-3.72-.167-4.218 2.208-6.78 5.567-6.78 3.453 0 5.891 2.797 5.567 6.78 1.745-.046 4.433.751 4.433 3.72 0 1.93-1.57 3.5-3.5 3.5h-3.5v2h3.5c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408z"/>
</svg>

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 161 B