Show an uploads bar in chats when uploading files
This commit is contained in:
parent
91064fc625
commit
078cf61b7e
1
TODO.md
1
TODO.md
|
@ -1,4 +1,5 @@
|
|||
- Media
|
||||
- SVG uploads
|
||||
- Uploading progress bar (+local echo)
|
||||
- Directly create cache files for our uploads before actually uploading
|
||||
- Downloading
|
||||
|
|
|
@ -9,7 +9,7 @@ import nio
|
|||
|
||||
from .app import App
|
||||
from .matrix_client import MatrixClient
|
||||
from .models.items import Account, Device, Event, Member, Room
|
||||
from .models.items import Account, Device, Event, Member, Room, Upload
|
||||
from .models.model_store import ModelStore
|
||||
|
||||
ProfileResponse = Union[nio.ProfileGetResponse, nio.ProfileGetError]
|
||||
|
@ -29,6 +29,7 @@ class Backend:
|
|||
(Device, str), # Devices of user_id
|
||||
(Room, str), # Rooms for user_id
|
||||
(Member, str), # Members in room_id
|
||||
(Upload, str), # Uploads running in room_id
|
||||
(Event, str, str), # Events for account user_id for room_id
|
||||
})
|
||||
|
||||
|
|
|
@ -25,7 +25,9 @@ import nio
|
|||
|
||||
from . import __about__, utils
|
||||
from .html_filter import HTML_FILTER
|
||||
from .models.items import Account, Event, Member, Room, TypeSpecifier
|
||||
from .models.items import (
|
||||
Account, Event, Member, Room, TypeSpecifier, Upload, UploadStatus,
|
||||
)
|
||||
from .models.model_store import ModelStore
|
||||
from .pyotherside_events import AlertRequested
|
||||
|
||||
|
@ -224,9 +226,15 @@ class MatrixClient(nio.AsyncClient):
|
|||
|
||||
async def send_file(self, room_id: str, path: Union[Path, str]) -> None:
|
||||
path = Path(path)
|
||||
size = path.resolve().stat().st_size
|
||||
encrypt = room_id in self.encrypted_rooms
|
||||
|
||||
url, mime, crypt_dict = await self.upload_file(path, encrypt=encrypt)
|
||||
upload_item = Upload(str(path), total_size=size)
|
||||
self.models[Upload, room_id][upload_item.uuid] = upload_item
|
||||
|
||||
url, mime, crypt_dict = await self.upload_file(
|
||||
path, upload_item, encrypt=encrypt,
|
||||
)
|
||||
|
||||
kind = (mime or "").split("/")[0]
|
||||
|
||||
|
@ -234,7 +242,7 @@ class MatrixClient(nio.AsyncClient):
|
|||
"body": path.name,
|
||||
"info": {
|
||||
"mimetype": mime,
|
||||
"size": path.resolve().stat().st_size,
|
||||
"size": size,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -254,7 +262,9 @@ class MatrixClient(nio.AsyncClient):
|
|||
|
||||
try:
|
||||
thumb_url, thumb_info, thumb_crypt_dict = \
|
||||
await self.upload_thumbnail(path, encrypt=encrypt)
|
||||
await self.upload_thumbnail(
|
||||
path, upload_item, encrypt=encrypt,
|
||||
)
|
||||
except (UneededThumbnail, UnthumbnailableError):
|
||||
pass
|
||||
else:
|
||||
|
@ -302,6 +312,9 @@ class MatrixClient(nio.AsyncClient):
|
|||
content["msgtype"] = "m.file"
|
||||
content["filename"] = path.name
|
||||
|
||||
upload_item.status = UploadStatus.Success
|
||||
del self.models[Upload, room_id]
|
||||
|
||||
uuid = str(uuid4())
|
||||
|
||||
await self._local_echo(
|
||||
|
@ -435,7 +448,10 @@ class MatrixClient(nio.AsyncClient):
|
|||
|
||||
|
||||
async def upload_thumbnail(
|
||||
self, path: Union[Path, str], encrypt: bool = False,
|
||||
self,
|
||||
path: Union[Path, str],
|
||||
item: Optional[Upload] = None,
|
||||
encrypt: bool = False,
|
||||
) -> Tuple[str, Dict[str, Any], CryptDict]:
|
||||
|
||||
png_modes = ("1", "L", "P", "RGBA")
|
||||
|
@ -450,6 +466,9 @@ class MatrixClient(nio.AsyncClient):
|
|||
if small and is_jpg_png and not jpgable_png:
|
||||
raise UneededThumbnail()
|
||||
|
||||
if item:
|
||||
item.status = UploadStatus.CreatingThumbnail
|
||||
|
||||
if not small:
|
||||
thumb.thumbnail((800, 600), PILImage.LANCZOS)
|
||||
|
||||
|
@ -464,11 +483,17 @@ class MatrixClient(nio.AsyncClient):
|
|||
data = out.getvalue()
|
||||
|
||||
if encrypt:
|
||||
if item:
|
||||
item.status = UploadStatus.EncryptingThumbnail
|
||||
|
||||
data, crypt_dict = await self.encrypt_attachment(data)
|
||||
upload_mime = "application/octet-stream"
|
||||
else:
|
||||
crypt_dict, upload_mime = {}, mime
|
||||
|
||||
if item:
|
||||
item.status = UploadStatus.UploadingThumbnail
|
||||
|
||||
return (
|
||||
await self.upload(data, upload_mime, Path(path).name),
|
||||
{
|
||||
|
@ -485,8 +510,13 @@ class MatrixClient(nio.AsyncClient):
|
|||
raise UnthumbnailableError(err)
|
||||
|
||||
|
||||
async def upload_file(self, path: Union[Path, str], encrypt: bool = False,
|
||||
async def upload_file(
|
||||
self,
|
||||
path: Union[Path, str],
|
||||
item: Optional[Upload] = None,
|
||||
encrypt: bool = False,
|
||||
) -> Tuple[str, str, CryptDict]:
|
||||
|
||||
with open(path, "rb") as file:
|
||||
mime = utils.guess_mime(file)
|
||||
file.seek(0, 0)
|
||||
|
@ -494,11 +524,17 @@ class MatrixClient(nio.AsyncClient):
|
|||
data: Union[BinaryIO, bytes]
|
||||
|
||||
if encrypt:
|
||||
if item:
|
||||
item.status = UploadStatus.Encrypting
|
||||
|
||||
data, crypt_dict = await self.encrypt_attachment(file.read())
|
||||
upload_mime = "application/octet-stream"
|
||||
else:
|
||||
data, crypt_dict, upload_mime = file, {}, mime
|
||||
|
||||
if item:
|
||||
item.status = UploadStatus.Uploading
|
||||
|
||||
return (
|
||||
await self.upload(data, upload_mime, Path(path).name),
|
||||
mime,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from uuid import uuid4
|
||||
|
||||
import lxml # nosec
|
||||
|
||||
|
@ -101,6 +103,38 @@ class Member(ModelItem):
|
|||
return self.display_name
|
||||
|
||||
|
||||
class UploadStatus(AutoStrEnum):
|
||||
Starting = auto()
|
||||
Encrypting = auto()
|
||||
Uploading = auto()
|
||||
CreatingThumbnail = auto()
|
||||
EncryptingThumbnail = auto()
|
||||
UploadingThumbnail = auto()
|
||||
Success = auto()
|
||||
Failure = auto() # TODO
|
||||
|
||||
|
||||
@dataclass
|
||||
class Upload(ModelItem):
|
||||
filepath: str = field()
|
||||
status: UploadStatus = UploadStatus.Starting
|
||||
total_size: int = 0
|
||||
uploaded: int = 0
|
||||
|
||||
uuid: str = field(init=False, default_factory=lambda: str(uuid4()))
|
||||
start_date: datetime = field(init=False, default_factory=datetime.now)
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.total_size:
|
||||
self.total_size = Path(self.filepath).resolve().stat().st_size
|
||||
|
||||
|
||||
def __lt__(self, other: "Upload") -> bool:
|
||||
# TODO
|
||||
return self.start_date > other.start_date
|
||||
|
||||
|
||||
class TypeSpecifier(AutoStrEnum):
|
||||
none = auto()
|
||||
profile_change = auto()
|
||||
|
|
|
@ -25,6 +25,10 @@ HSplitView {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
UploadsBar {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
InviteBanner {
|
||||
id: inviteBanner
|
||||
visible: ! chatPage.roomInfo.left && inviterId
|
||||
|
|
105
src/qml/Chat/UploadsBar.qml
Normal file
105
src/qml/Chat/UploadsBar.qml
Normal file
|
@ -0,0 +1,105 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../Base"
|
||||
import "../utils.js" as Utils
|
||||
|
||||
Rectangle {
|
||||
id: uploadsBar
|
||||
implicitWidth: 800
|
||||
implicitHeight: firstDelegate ? firstDelegate.height : 0
|
||||
color: theme.chat.typingMembers.background
|
||||
opacity: implicitHeight ? 1 : 0
|
||||
|
||||
|
||||
property int delegateHeight: 0
|
||||
property int maxShownDelegates: 1
|
||||
|
||||
readonly property var firstDelegate:
|
||||
uploadsList.contentItem.visibleChildren[0]
|
||||
|
||||
|
||||
Behavior on implicitHeight { HNumberAnimation {} }
|
||||
|
||||
HListView {
|
||||
id: uploadsList
|
||||
enableFlicking: false
|
||||
width: parent.width
|
||||
|
||||
model: HListModel {
|
||||
keyField: "uuid"
|
||||
source: modelSources[["Upload", chatPage.roomId]] || []
|
||||
}
|
||||
|
||||
delegate: HColumnLayout {
|
||||
id: delegate
|
||||
width: uploadsList.width
|
||||
Component.onCompleted: Utils.debug(delegate)
|
||||
|
||||
HRowLayout {
|
||||
HLabel {
|
||||
id: filenameLabel
|
||||
elide: Text.ElideRight
|
||||
text:
|
||||
model.status === "Starting" ?
|
||||
qsTr("Preparing %1...").arg(fileName) :
|
||||
|
||||
model.status === "Encrypting" ?
|
||||
qsTr("Encrypting %1...").arg(fileName) :
|
||||
|
||||
model.status === "Uploading" ?
|
||||
qsTr("Uploading %1...").arg(fileName) :
|
||||
|
||||
model.status === "CreatingThumbnail" ?
|
||||
qsTr("Generating thumbnail for %1...").arg(fileName) :
|
||||
|
||||
model.status === "EncryptingThumbnail" ?
|
||||
qsTr("Encrypting thumbnail for %1...").arg(fileName) :
|
||||
|
||||
model.status === "UploadingThumbnail" ?
|
||||
qsTr("Uploading thumbnail for %1...").arg(fileName) :
|
||||
|
||||
model.status === "Failure" ?
|
||||
qsTr("Failed uploading %1.").arg(fileName) :
|
||||
|
||||
qsTr("Invalid status for %1: %2")
|
||||
.arg(fileName, model.status)
|
||||
|
||||
topPadding: theme.spacing / 2
|
||||
bottomPadding: topPadding
|
||||
leftPadding: theme.spacing / 1.5
|
||||
rightPadding: leftPadding
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property string fileName:
|
||||
model.filepath.split("/").slice(-1)[0]
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
HLabel {
|
||||
id: uploadCountLabel
|
||||
visible: Layout.preferredWidth > 0
|
||||
text: qsTr("%1/%2")
|
||||
.arg(model.index + 1).arg(uploadsList.model.count)
|
||||
|
||||
topPadding: theme.spacing / 2
|
||||
bottomPadding: topPadding
|
||||
leftPadding: theme.spacing / 1.5
|
||||
rightPadding: leftPadding
|
||||
|
||||
Layout.preferredWidth:
|
||||
uploadsList.model.count < 2 ? 0 : implicitWidth
|
||||
|
||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||
}
|
||||
}
|
||||
|
||||
HProgressBar {
|
||||
id: progressBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user