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
|
- Media
|
||||||
|
- SVG uploads
|
||||||
- Uploading progress bar (+local echo)
|
- Uploading progress bar (+local echo)
|
||||||
- Directly create cache files for our uploads before actually uploading
|
- Directly create cache files for our uploads before actually uploading
|
||||||
- Downloading
|
- Downloading
|
||||||
|
@ -9,7 +9,7 @@ import nio
|
|||||||
|
|
||||||
from .app import App
|
from .app import App
|
||||||
from .matrix_client import MatrixClient
|
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
|
from .models.model_store import ModelStore
|
||||||
|
|
||||||
ProfileResponse = Union[nio.ProfileGetResponse, nio.ProfileGetError]
|
ProfileResponse = Union[nio.ProfileGetResponse, nio.ProfileGetError]
|
||||||
@ -29,6 +29,7 @@ class Backend:
|
|||||||
(Device, str), # Devices of user_id
|
(Device, str), # Devices of user_id
|
||||||
(Room, str), # Rooms for user_id
|
(Room, str), # Rooms for user_id
|
||||||
(Member, str), # Members in room_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
|
(Event, str, str), # Events for account user_id for room_id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -25,7 +25,9 @@ import nio
|
|||||||
|
|
||||||
from . import __about__, utils
|
from . import __about__, utils
|
||||||
from .html_filter import HTML_FILTER
|
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 .models.model_store import ModelStore
|
||||||
from .pyotherside_events import AlertRequested
|
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:
|
async def send_file(self, room_id: str, path: Union[Path, str]) -> None:
|
||||||
path = Path(path)
|
path = Path(path)
|
||||||
|
size = path.resolve().stat().st_size
|
||||||
encrypt = room_id in self.encrypted_rooms
|
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]
|
kind = (mime or "").split("/")[0]
|
||||||
|
|
||||||
@ -234,7 +242,7 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
"body": path.name,
|
"body": path.name,
|
||||||
"info": {
|
"info": {
|
||||||
"mimetype": mime,
|
"mimetype": mime,
|
||||||
"size": path.resolve().stat().st_size,
|
"size": size,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +262,9 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
thumb_url, thumb_info, thumb_crypt_dict = \
|
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):
|
except (UneededThumbnail, UnthumbnailableError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@ -302,6 +312,9 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
content["msgtype"] = "m.file"
|
content["msgtype"] = "m.file"
|
||||||
content["filename"] = path.name
|
content["filename"] = path.name
|
||||||
|
|
||||||
|
upload_item.status = UploadStatus.Success
|
||||||
|
del self.models[Upload, room_id]
|
||||||
|
|
||||||
uuid = str(uuid4())
|
uuid = str(uuid4())
|
||||||
|
|
||||||
await self._local_echo(
|
await self._local_echo(
|
||||||
@ -435,7 +448,10 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
|
|
||||||
|
|
||||||
async def upload_thumbnail(
|
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]:
|
) -> Tuple[str, Dict[str, Any], CryptDict]:
|
||||||
|
|
||||||
png_modes = ("1", "L", "P", "RGBA")
|
png_modes = ("1", "L", "P", "RGBA")
|
||||||
@ -450,6 +466,9 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
if small and is_jpg_png and not jpgable_png:
|
if small and is_jpg_png and not jpgable_png:
|
||||||
raise UneededThumbnail()
|
raise UneededThumbnail()
|
||||||
|
|
||||||
|
if item:
|
||||||
|
item.status = UploadStatus.CreatingThumbnail
|
||||||
|
|
||||||
if not small:
|
if not small:
|
||||||
thumb.thumbnail((800, 600), PILImage.LANCZOS)
|
thumb.thumbnail((800, 600), PILImage.LANCZOS)
|
||||||
|
|
||||||
@ -464,11 +483,17 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
data = out.getvalue()
|
data = out.getvalue()
|
||||||
|
|
||||||
if encrypt:
|
if encrypt:
|
||||||
|
if item:
|
||||||
|
item.status = UploadStatus.EncryptingThumbnail
|
||||||
|
|
||||||
data, crypt_dict = await self.encrypt_attachment(data)
|
data, crypt_dict = await self.encrypt_attachment(data)
|
||||||
upload_mime = "application/octet-stream"
|
upload_mime = "application/octet-stream"
|
||||||
else:
|
else:
|
||||||
crypt_dict, upload_mime = {}, mime
|
crypt_dict, upload_mime = {}, mime
|
||||||
|
|
||||||
|
if item:
|
||||||
|
item.status = UploadStatus.UploadingThumbnail
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await self.upload(data, upload_mime, Path(path).name),
|
await self.upload(data, upload_mime, Path(path).name),
|
||||||
{
|
{
|
||||||
@ -485,8 +510,13 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
raise UnthumbnailableError(err)
|
raise UnthumbnailableError(err)
|
||||||
|
|
||||||
|
|
||||||
async def upload_file(self, path: Union[Path, str], encrypt: bool = False,
|
async def upload_file(
|
||||||
) -> Tuple[str, str, CryptDict]:
|
self,
|
||||||
|
path: Union[Path, str],
|
||||||
|
item: Optional[Upload] = None,
|
||||||
|
encrypt: bool = False,
|
||||||
|
) -> Tuple[str, str, CryptDict]:
|
||||||
|
|
||||||
with open(path, "rb") as file:
|
with open(path, "rb") as file:
|
||||||
mime = utils.guess_mime(file)
|
mime = utils.guess_mime(file)
|
||||||
file.seek(0, 0)
|
file.seek(0, 0)
|
||||||
@ -494,11 +524,17 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
data: Union[BinaryIO, bytes]
|
data: Union[BinaryIO, bytes]
|
||||||
|
|
||||||
if encrypt:
|
if encrypt:
|
||||||
|
if item:
|
||||||
|
item.status = UploadStatus.Encrypting
|
||||||
|
|
||||||
data, crypt_dict = await self.encrypt_attachment(file.read())
|
data, crypt_dict = await self.encrypt_attachment(file.read())
|
||||||
upload_mime = "application/octet-stream"
|
upload_mime = "application/octet-stream"
|
||||||
else:
|
else:
|
||||||
data, crypt_dict, upload_mime = file, {}, mime
|
data, crypt_dict, upload_mime = file, {}, mime
|
||||||
|
|
||||||
|
if item:
|
||||||
|
item.status = UploadStatus.Uploading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await self.upload(data, upload_mime, Path(path).name),
|
await self.upload(data, upload_mime, Path(path).name),
|
||||||
mime,
|
mime,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import lxml # nosec
|
import lxml # nosec
|
||||||
|
|
||||||
@ -101,6 +103,38 @@ class Member(ModelItem):
|
|||||||
return self.display_name
|
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):
|
class TypeSpecifier(AutoStrEnum):
|
||||||
none = auto()
|
none = auto()
|
||||||
profile_change = auto()
|
profile_change = auto()
|
||||||
|
@ -25,6 +25,10 @@ HSplitView {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UploadsBar {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
InviteBanner {
|
InviteBanner {
|
||||||
id: inviteBanner
|
id: inviteBanner
|
||||||
visible: ! chatPage.roomInfo.left && inviterId
|
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