mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
feat: FileBrowser video thumbnail (#2077)
* feat(filebrowser): add filebrowser video thumbnails display - Find cached thumbnails first - If not found, generate with ffmpegthumbnailer - Fallback to placeholder icon if dependency not met * fix(filebrowser): create thumbnail cache dir if not exists * refactor(filebrowser): prefer using Paths lib * fix(filebrowser): only check filetype once for each file * fix(filebrowser): early test for thumbnails * feat: add xdgCache path
This commit is contained in:
@@ -10,6 +10,7 @@ Singleton {
|
||||
|
||||
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||
readonly property url xdgCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
|
||||
|
||||
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||
|
||||
@@ -75,6 +75,50 @@ StyledRect {
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(delegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(delegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
|
||||
property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
|
||||
property string videoThumbnailPath: {
|
||||
if (!delegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + delegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
|
||||
onVideoThumbnailPathChanged: {
|
||||
_videoThumb = "";
|
||||
if (!videoThumbnailPath)
|
||||
return;
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||
const size = _thumbnailPx;
|
||||
const fp = delegateRoot.filePath;
|
||||
Paths.mkdir(thumbDir);
|
||||
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
|
||||
if (exitCode === 0) {
|
||||
_videoThumb = thumbPath;
|
||||
} else {
|
||||
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function(output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
@@ -124,7 +168,11 @@ StyledRect {
|
||||
property string imagePath: {
|
||||
if (weMode && delegateRoot.fileIsDir)
|
||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? delegateRoot.filePath : "";
|
||||
if (!delegateRoot.fileIsDir && isImage)
|
||||
return delegateRoot.filePath;
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
onStatusChanged: {
|
||||
@@ -149,7 +197,7 @@ StyledRect {
|
||||
source: gridPreviewImage
|
||||
maskEnabled: true
|
||||
maskSource: gridImageMask
|
||||
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir))
|
||||
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && (isImage || isVideo)) || (weMode && delegateRoot.fileIsDir))
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
@@ -175,7 +223,7 @@ StyledRect {
|
||||
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
||||
size: iconSizes[iconSizeIndex] * 0.45
|
||||
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: (!delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)) || (delegateRoot.fileIsDir && !weMode)
|
||||
visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,46 @@ StyledRect {
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(listDelegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(listDelegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string videoThumbnailPath: {
|
||||
if (!listDelegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + listDelegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
|
||||
onVideoThumbnailPathChanged: {
|
||||
_videoThumb = "";
|
||||
if (!videoThumbnailPath)
|
||||
return;
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const fp = listDelegateRoot.filePath;
|
||||
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
|
||||
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
|
||||
if (exitCode === 0) {
|
||||
_videoThumb = thumbPath;
|
||||
} else {
|
||||
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function(output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
@@ -127,7 +167,13 @@ StyledRect {
|
||||
Image {
|
||||
id: listPreviewImage
|
||||
anchors.fill: parent
|
||||
property string imagePath: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? listDelegateRoot.filePath : ""
|
||||
property string imagePath: {
|
||||
if (!listDelegateRoot.fileIsDir && isImage)
|
||||
return listDelegateRoot.filePath;
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: 32
|
||||
@@ -141,7 +187,7 @@ StyledRect {
|
||||
source: listPreviewImage
|
||||
maskEnabled: true
|
||||
maskSource: listImageMask
|
||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)
|
||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo)
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
@@ -166,7 +212,7 @@ StyledRect {
|
||||
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
||||
size: Theme.iconSize - 2
|
||||
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: listDelegateRoot.fileIsDir || !isImageFile(listDelegateRoot.fileName)
|
||||
visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user