diff --git a/TODO.md b/TODO.md index 7a109d3a..98b9e6c3 100644 --- a/TODO.md +++ b/TODO.md @@ -167,11 +167,6 @@ - Add upload keybindings (close failed upload, pause, resume) - Handle errors when setting an avatar -- Show proper progress ring for mxc thumbnails loading - -- Sentinel function to report local file paths for already downloaded media, - without having to click and try downloading first -- EventFile "Save as..." context menu entry - Show a reason or HTTP error code for thumbnails that fail to load - Support `m.file` thumbnails diff --git a/src/backend/media_cache.py b/src/backend/media_cache.py index e2e49c23..25274cf0 100644 --- a/src/backend/media_cache.py +++ b/src/backend/media_cache.py @@ -47,13 +47,37 @@ class MediaCache: self.downloads_dir.mkdir(parents=True, exist_ok=True) + async def get_local_media(self, mxc: str, title: str) -> Optional[Path]: + """Return `Media.get_local()`'s result for QML.""" + + media = Media(self, mxc, title, None) + + try: + return await media.get_local() + except FileNotFoundError: + return None + + + async def get_local_thumbnail( + self, mxc: str, title: str, width: int, height: int, + ) -> Optional[Path]: + """Return `Thumbnail.get_local()`'s result for QML.""" + + th = Thumbnail(self, mxc, title, None, (round(width), round(height))) + + try: + return await th.get_local() + except FileNotFoundError: + return None + + async def get_media( self, mxc: str, title: str, crypt_dict: CryptDict = None, ) -> Path: - """Return a `Media` object. Method intended for QML convenience.""" + """Return `Media.get()`'s result. Intended for QML.""" return await Media(self, mxc, title, crypt_dict).get() @@ -66,7 +90,7 @@ class MediaCache: height: int, crypt_dict: CryptDict = None, ) -> Path: - """Return a `Thumbnail` object. Method intended for QML convenience.""" + """Return `Thumbnail.get()`'s result. Intended for QML.""" thumb = Thumbnail( # QML sometimes pass float sizes, which matrix API doesn't like. @@ -116,13 +140,13 @@ class Media: async with ACCESS_LOCKS[self.mxc]: try: - return await self._get_local_existing_file() + return await self.get_local() except FileNotFoundError: return await self.create() - async def _get_local_existing_file(self) -> Path: - """Return the cached file's path.""" + async def get_local(self) -> Path: + """Return a cached local existing path for this media or raise.""" if not self.local_path.exists(): raise FileNotFoundError() @@ -286,7 +310,7 @@ class Thumbnail(Media): return self.cache.thumbs_dir / parsed.netloc / size_dir / filename - async def _get_local_existing_file(self) -> Path: + async def get_local(self) -> Path: """Return an existing thumbnail path or raise `FileNotFoundError`. If we have a bigger size thumbnail downloaded than the `wanted_size` diff --git a/src/gui/Pages/Chat/Timeline/EventAudio.qml b/src/gui/Pages/Chat/Timeline/EventAudio.qml index 7361a2cc..1c1da909 100644 --- a/src/gui/Pages/Chat/Timeline/EventAudio.qml +++ b/src/gui/Pages/Chat/Timeline/EventAudio.qml @@ -13,6 +13,6 @@ AudioPlayer { HoverHandler { onHoveredChanged: eventDelegate.hoveredMediaTypeUrl = - hovered ? [Utils.Media.Audio, audio.source] : [] + hovered ? [Utils.Media.Audio, audio.source, loader.title] : [] } } diff --git a/src/gui/Pages/Chat/Timeline/EventDelegate.qml b/src/gui/Pages/Chat/Timeline/EventDelegate.qml index f2f7c0fb..c6a11077 100644 --- a/src/gui/Pages/Chat/Timeline/EventDelegate.qml +++ b/src/gui/Pages/Chat/Timeline/EventDelegate.qml @@ -5,11 +5,12 @@ import QtQuick.Layouts 1.12 import Clipboard 0.1 import "../../.." import "../../../Base" +import "../../../PythonBridge" HColumnLayout { id: eventDelegate - property var hoveredMediaTypeUrl: [] + property var hoveredMediaTypeUrl: [] // [] or [mediaType, url, title] property var fetchProfilesFuture: null @@ -41,7 +42,7 @@ HColumnLayout { combine readonly property int cursorShape: - eventContent.hoveredLink || hoveredMediaTypeUrl.length > 0 ? + eventContent.hoveredLink || hoveredMediaTypeUrl.length === 3 ? Qt.PointingHandCursor : eventContent.hoveredSelectable ? Qt.IBeamCursor : @@ -140,8 +141,25 @@ HColumnLayout { property var media: [] property string link: "" + property var localPath: null + property Future getLocalFuture: null - onClosed: { media = []; link = "" } + readonly property bool isEncryptedMedia: + Object.keys(JSON.parse(model.media_crypt_dict)).length > 0 + + onClosed: { + if (getLocalFuture) getLocalFuture.cancel() + media = [] + link = "" + } + + onOpened: if (media.length === 3 && media[1].startsWith("mxc://")) { + getLocalFuture = py.callCoro( + "media_cache.get_local_media", + [media[1], media[2]], + path => { localPath = path; getLocalFuture = null }, + ) + } HMenuItem { icon.name: "toggle-select-message" @@ -163,14 +181,21 @@ HColumnLayout { onTriggered: eventList.checkFromLastToHere(model.index) } + HMenuItem { + icon.name: "copy-local-path" + text: qsTr("Copy local path") + visible: Boolean(contextMenu.localPath) + onTriggered: + Clipboard.text = + contextMenu.localPath.replace(/^file:\/\//, "") + } + HMenuItem { id: copyMedia icon.name: "copy-link" text: - contextMenu.media.length < 1 ? "" : - - contextMenu.media[0] === Utils.Media.Page ? - qsTr("Copy page address") : + contextMenu.media.length === 0 || isEncryptedMedia ? + "" : contextMenu.media[0] === Utils.Media.File ? qsTr("Copy file address") : @@ -181,17 +206,13 @@ HColumnLayout { contextMenu.media[0] === Utils.Media.Video ? qsTr("Copy video address") : - contextMenu.media[0] === Utils.Media.Audio ? - qsTr("Copy audio address") : - - qsTr("Copy media address") + qsTr("Copy audio address") visible: Boolean(text) onTriggered: Clipboard.text = contextMenu.media[1] } HMenuItem { - id: copyLink icon.name: "copy-link" text: qsTr("Copy link address") visible: Boolean(contextMenu.link) @@ -201,12 +222,8 @@ HColumnLayout { HMenuItem { icon.name: "copy-text" text: - eventList.selectedCount ? - qsTr("Copy selection") : - - copyMedia.visible ? - qsTr("Copy filename") : - + eventList.selectedCount ? qsTr("Copy selection") : + contextMenu.media.length > 0 ? qsTr("Copy filename") : qsTr("Copy text") onTriggered: { diff --git a/src/gui/Pages/Chat/Timeline/EventFile.qml b/src/gui/Pages/Chat/Timeline/EventFile.qml index b2d5b0f5..0d3fa4b1 100644 --- a/src/gui/Pages/Chat/Timeline/EventFile.qml +++ b/src/gui/Pages/Chat/Timeline/EventFile.qml @@ -60,12 +60,8 @@ HTile { return } - eventDelegate.hoveredMediaTypeUrl = [ - Utils.Media.File, - // XXX - // loader.downloadedPath.replace(/^file:\/\//, "") || - loader.mediaUrl - ] + eventDelegate.hoveredMediaTypeUrl = + [Utils.Media.File, loader.mediaUrl, loader.title] } Binding on backgroundColor { diff --git a/src/gui/Pages/Chat/Timeline/EventImage.qml b/src/gui/Pages/Chat/Timeline/EventImage.qml index 1defc1d0..3a29b864 100644 --- a/src/gui/Pages/Chat/Timeline/EventImage.qml +++ b/src/gui/Pages/Chat/Timeline/EventImage.qml @@ -99,12 +99,8 @@ HMxcImage { return } - eventDelegate.hoveredMediaTypeUrl = [ - Utils.Media.Image, - // XXX - // loader.downloadedPath.replace(/^file:\/\//, "") || - loader.mediaUrl - ] + eventDelegate.hoveredMediaTypeUrl = + [Utils.Media.Image, loader.mediaUrl, loader.title] } } diff --git a/src/gui/Pages/Chat/Timeline/EventMediaLoader.qml b/src/gui/Pages/Chat/Timeline/EventMediaLoader.qml index 71c258f0..797d217e 100644 --- a/src/gui/Pages/Chat/Timeline/EventMediaLoader.qml +++ b/src/gui/Pages/Chat/Timeline/EventMediaLoader.qml @@ -27,6 +27,8 @@ HLoader { eventList.getMediaType(singleMediaInfo) : utils.getLinkType(mediaUrl) + readonly property string cachedLocalPath: "" + readonly property string thumbnailMxc: singleMediaInfo.thumbnail_url diff --git a/src/gui/Pages/Chat/Timeline/EventVideo.qml b/src/gui/Pages/Chat/Timeline/EventVideo.qml index 36a663f0..58de0829 100644 --- a/src/gui/Pages/Chat/Timeline/EventVideo.qml +++ b/src/gui/Pages/Chat/Timeline/EventVideo.qml @@ -12,5 +12,5 @@ VideoPlayer { onHoveredChanged: eventDelegate.hoveredMediaTypeUrl = - hovered ? [Utils.Media.Video, video.source] : [] + hovered ? [Utils.Media.Video, video.source, loader.title] : [] } diff --git a/src/icons/thin/copy-local-path.svg b/src/icons/thin/copy-local-path.svg new file mode 100644 index 00000000..9263bb9c --- /dev/null +++ b/src/icons/thin/copy-local-path.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +