1
0
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:
Kangheng Liu
2026-03-25 09:14:59 -04:00
committed by GitHub
parent 86d8fe4fa4
commit 906c6a2501
3 changed files with 101 additions and 6 deletions

View File

@@ -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`

View File

@@ -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)
}
}

View File

@@ -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))
}
}