Real "copy URL" & "copy path" context menu entries

Replace the poorly implemented 2-in-1 "copy address" media event
menu option with:

- Copy <mediaType> address: visible for non-encrypted media, always
  copies the http URL

- Copy local path: always visible for already downloaded media, even if
  they were downloaded before mirage was started
This commit is contained in:
miruka 2020-07-20 00:22:12 -04:00
parent 37579fc664
commit 30ce271ebc
9 changed files with 88 additions and 43 deletions

View File

@ -167,11 +167,6 @@
- Add upload keybindings (close failed upload, pause, resume) - Add upload keybindings (close failed upload, pause, resume)
- Handle errors when setting an avatar - 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 - Show a reason or HTTP error code for thumbnails that fail to load
- Support `m.file` thumbnails - Support `m.file` thumbnails

View File

@ -47,13 +47,37 @@ class MediaCache:
self.downloads_dir.mkdir(parents=True, exist_ok=True) 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( async def get_media(
self, self,
mxc: str, mxc: str,
title: str, title: str,
crypt_dict: CryptDict = None, crypt_dict: CryptDict = None,
) -> Path: ) -> 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() return await Media(self, mxc, title, crypt_dict).get()
@ -66,7 +90,7 @@ class MediaCache:
height: int, height: int,
crypt_dict: CryptDict = None, crypt_dict: CryptDict = None,
) -> Path: ) -> Path:
"""Return a `Thumbnail` object. Method intended for QML convenience.""" """Return `Thumbnail.get()`'s result. Intended for QML."""
thumb = Thumbnail( thumb = Thumbnail(
# QML sometimes pass float sizes, which matrix API doesn't like. # QML sometimes pass float sizes, which matrix API doesn't like.
@ -116,13 +140,13 @@ class Media:
async with ACCESS_LOCKS[self.mxc]: async with ACCESS_LOCKS[self.mxc]:
try: try:
return await self._get_local_existing_file() return await self.get_local()
except FileNotFoundError: except FileNotFoundError:
return await self.create() return await self.create()
async def _get_local_existing_file(self) -> Path: async def get_local(self) -> Path:
"""Return the cached file's path.""" """Return a cached local existing path for this media or raise."""
if not self.local_path.exists(): if not self.local_path.exists():
raise FileNotFoundError() raise FileNotFoundError()
@ -286,7 +310,7 @@ class Thumbnail(Media):
return self.cache.thumbs_dir / parsed.netloc / size_dir / filename 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`. """Return an existing thumbnail path or raise `FileNotFoundError`.
If we have a bigger size thumbnail downloaded than the `wanted_size` If we have a bigger size thumbnail downloaded than the `wanted_size`

View File

@ -13,6 +13,6 @@ AudioPlayer {
HoverHandler { HoverHandler {
onHoveredChanged: onHoveredChanged:
eventDelegate.hoveredMediaTypeUrl = eventDelegate.hoveredMediaTypeUrl =
hovered ? [Utils.Media.Audio, audio.source] : [] hovered ? [Utils.Media.Audio, audio.source, loader.title] : []
} }
} }

View File

@ -5,11 +5,12 @@ import QtQuick.Layouts 1.12
import Clipboard 0.1 import Clipboard 0.1
import "../../.." import "../../.."
import "../../../Base" import "../../../Base"
import "../../../PythonBridge"
HColumnLayout { HColumnLayout {
id: eventDelegate id: eventDelegate
property var hoveredMediaTypeUrl: [] property var hoveredMediaTypeUrl: [] // [] or [mediaType, url, title]
property var fetchProfilesFuture: null property var fetchProfilesFuture: null
@ -41,7 +42,7 @@ HColumnLayout {
combine combine
readonly property int cursorShape: readonly property int cursorShape:
eventContent.hoveredLink || hoveredMediaTypeUrl.length > 0 ? eventContent.hoveredLink || hoveredMediaTypeUrl.length === 3 ?
Qt.PointingHandCursor : Qt.PointingHandCursor :
eventContent.hoveredSelectable ? Qt.IBeamCursor : eventContent.hoveredSelectable ? Qt.IBeamCursor :
@ -140,8 +141,25 @@ HColumnLayout {
property var media: [] property var media: []
property string link: "" 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 { HMenuItem {
icon.name: "toggle-select-message" icon.name: "toggle-select-message"
@ -163,14 +181,21 @@ HColumnLayout {
onTriggered: eventList.checkFromLastToHere(model.index) 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 { HMenuItem {
id: copyMedia id: copyMedia
icon.name: "copy-link" icon.name: "copy-link"
text: text:
contextMenu.media.length < 1 ? "" : contextMenu.media.length === 0 || isEncryptedMedia ?
"" :
contextMenu.media[0] === Utils.Media.Page ?
qsTr("Copy page address") :
contextMenu.media[0] === Utils.Media.File ? contextMenu.media[0] === Utils.Media.File ?
qsTr("Copy file address") : qsTr("Copy file address") :
@ -181,17 +206,13 @@ HColumnLayout {
contextMenu.media[0] === Utils.Media.Video ? contextMenu.media[0] === Utils.Media.Video ?
qsTr("Copy video address") : qsTr("Copy video address") :
contextMenu.media[0] === Utils.Media.Audio ? qsTr("Copy audio address")
qsTr("Copy audio address") :
qsTr("Copy media address")
visible: Boolean(text) visible: Boolean(text)
onTriggered: Clipboard.text = contextMenu.media[1] onTriggered: Clipboard.text = contextMenu.media[1]
} }
HMenuItem { HMenuItem {
id: copyLink
icon.name: "copy-link" icon.name: "copy-link"
text: qsTr("Copy link address") text: qsTr("Copy link address")
visible: Boolean(contextMenu.link) visible: Boolean(contextMenu.link)
@ -201,12 +222,8 @@ HColumnLayout {
HMenuItem { HMenuItem {
icon.name: "copy-text" icon.name: "copy-text"
text: text:
eventList.selectedCount ? eventList.selectedCount ? qsTr("Copy selection") :
qsTr("Copy selection") : contextMenu.media.length > 0 ? qsTr("Copy filename") :
copyMedia.visible ?
qsTr("Copy filename") :
qsTr("Copy text") qsTr("Copy text")
onTriggered: { onTriggered: {

View File

@ -60,12 +60,8 @@ HTile {
return return
} }
eventDelegate.hoveredMediaTypeUrl = [ eventDelegate.hoveredMediaTypeUrl =
Utils.Media.File, [Utils.Media.File, loader.mediaUrl, loader.title]
// XXX
// loader.downloadedPath.replace(/^file:\/\//, "") ||
loader.mediaUrl
]
} }
Binding on backgroundColor { Binding on backgroundColor {

View File

@ -99,12 +99,8 @@ HMxcImage {
return return
} }
eventDelegate.hoveredMediaTypeUrl = [ eventDelegate.hoveredMediaTypeUrl =
Utils.Media.Image, [Utils.Media.Image, loader.mediaUrl, loader.title]
// XXX
// loader.downloadedPath.replace(/^file:\/\//, "") ||
loader.mediaUrl
]
} }
} }

View File

@ -27,6 +27,8 @@ HLoader {
eventList.getMediaType(singleMediaInfo) : eventList.getMediaType(singleMediaInfo) :
utils.getLinkType(mediaUrl) utils.getLinkType(mediaUrl)
readonly property string cachedLocalPath: ""
readonly property string thumbnailMxc: singleMediaInfo.thumbnail_url readonly property string thumbnailMxc: singleMediaInfo.thumbnail_url

View File

@ -12,5 +12,5 @@ VideoPlayer {
onHoveredChanged: onHoveredChanged:
eventDelegate.hoveredMediaTypeUrl = eventDelegate.hoveredMediaTypeUrl =
hovered ? [Utils.Media.Video, video.source] : [] hovered ? [Utils.Media.Video, video.source, loader.title] : []
} }

View File

@ -0,0 +1,15 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.4683867 0 0 1.4683867 -6.00608 -7.949919)">
<path d="m14.089135 6.966652h1.915602v1.291619h-1.915602z" fill="none"/>
<path d="m15.046936 7.612462h2.465696v1.08719h-2.465696z" fill="none"/>
<path d="m4.090259 7.128954h16.344467v1.016837h-16.344467z"/>
<path d="m4.090259 19.162901h16.344467v.97476h-16.344467z"/>
<path d="m7.128954-5.107096h13.008707v1.016837h-13.008707z" transform="rotate(90)"/>
<path d="m7.128954-20.434727h13.008707v1.016837h-13.008707z" transform="rotate(90)"/>
<g transform="translate(.106056 -.00001)">
<path d="m12.465338 7.077473h1.539704v7.582061h-1.539704z" stroke-width=".119289" transform="matrix(.96607107 .25827638 -.35321141 .93554353 0 0)"/>
<circle cx="16.285589" cy="16.168999" r="1.162796"/>
<circle cx="12.156437" cy="16.168999" r="1.162796"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1013 B