diff --git a/components/filedialog/FileDialog.qml b/components/filedialog/FileDialog.qml index 45f814a47..f20a07d61 100644 --- a/components/filedialog/FileDialog.qml +++ b/components/filedialog/FileDialog.qml @@ -37,7 +37,8 @@ LazyLoader { readonly property bool selectionValid: { const file = folderContents.currentItem?.modelData; - return (file && !file.isDir && (filters.includes("*") || filters.includes(file.suffix))) ?? false; + const suffix = file?.suffix.toLowerCase(); + return (file && !file.isDir && (filters.includes("*") || filters.some(f => f.toLowerCase() === suffix))) ?? false; } function accepted(path: string): void { diff --git a/components/images/ProfileImage.qml b/components/images/ProfileImage.qml new file mode 100644 index 000000000..b6ada3f23 --- /dev/null +++ b/components/images/ProfileImage.qml @@ -0,0 +1,53 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Caelestia + +Item { + id: root + + property string path + property int _reloadToken + readonly property string _format: { + _reloadToken; + return CUtils.imageFormat(path); + } + + readonly property int status: loader.item?.status ?? Image.Null // qmllint disable missing-property + + function reload(): void { + _reloadToken++; + loader.active = false; + loader.active = true; + } + + Loader { + id: loader + + anchors.fill: parent + sourceComponent: root._format === "gif" ? animatedComponent : cachingComponent + } + + Component { + id: animatedComponent + + AnimatedImage { + anchors.fill: parent + fillMode: AnimatedImage.PreserveAspectCrop + asynchronous: true + cache: false + playing: true + source: Qt.resolvedUrl(root.path) + } + } + + Component { + id: cachingComponent + + CachingImage { + anchors.fill: parent + cache: false + path: root.path + } + } +} diff --git a/flake.nix b/flake.nix index 7058c6cc4..5129650fd 100644 --- a/flake.nix +++ b/flake.nix @@ -30,13 +30,23 @@ nixpkgs.lib.genAttrs nixpkgs.lib.platforms.linux ( system: fn nixpkgs.legacyPackages.${system} ); + sourceRev = + if self ? rev + then self.rev + else if self ? dirtyRev + then self.dirtyRev + else if self ? shortRev + then self.shortRev + else if self ? lastModifiedDate + then self.lastModifiedDate + else "unknown"; in { formatter = forAllSystems (pkgs: pkgs.alejandra); packages = forAllSystems (pkgs: rec { caelestia-shell = pkgs.callPackage ./nix { inherit (inputs) m3shapes; - rev = self.rev or self.dirtyRev; + rev = sourceRev; stdenv = pkgs.clangStdenv; quickshell = inputs.quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default.override { withX11 = false; diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 5debd7adb..d965e54de 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -18,7 +18,7 @@ Item { readonly property FileDialog facePicker: FileDialog { title: qsTr("Select a profile picture") filterLabel: qsTr("Image files") - filters: Images.validImageExtensions + filters: Images.validProfileImageExtensions onAccepted: path => { if (CUtils.copyFile(Qt.resolvedUrl(path), Qt.resolvedUrl(`${Paths.home}/.face`))) Quickshell.execDetached(["notify-send", "-a", "caelestia-shell", "-u", "low", "-h", `STRING:image-path:${path}`, "Profile picture changed", `Profile picture changed to ${Paths.shortenHome(path)}`]); diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index d8d561286..5dd5ae038 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -84,7 +84,7 @@ Item { } } - CachingImage { + ProfileImage { id: pfp anchors.fill: parent @@ -129,6 +129,14 @@ Item { } } } + + Connections { + function onAccepted(): void { + Qt.callLater(pfp.reload); + } + + target: root.facePicker + } } MaterialShape { diff --git a/modules/lock/center/ProfilePic.qml b/modules/lock/center/ProfilePic.qml index dafd7ba83..b2884b84e 100644 --- a/modules/lock/center/ProfilePic.qml +++ b/modules/lock/center/ProfilePic.qml @@ -42,7 +42,7 @@ Item { visible: pfp.status !== Image.Ready } - CachingImage { + ProfileImage { id: pfp anchors.fill: shape diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index b6080f6ea..3d621279f 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -138,6 +139,19 @@ QString CUtils::toLocalFile(const QUrl& url) { return url.toLocalFile(); } +QString CUtils::imageFormat(const QUrl& url) { + if (!url.isLocalFile()) { + qCWarning(lcCUtils) << "imageFormat: url" << url << "is not a local file"; + return QString(); + } + + return imageFormat(url.toLocalFile()); +} + +QString CUtils::imageFormat(const QString& path) { + return QString::fromLatin1(QImageReader::imageFormat(path)); +} + qreal CUtils::clamp(qreal value, qreal min, qreal max) { return qBound(min, value, max); } diff --git a/plugin/src/Caelestia/cutils.hpp b/plugin/src/Caelestia/cutils.hpp index 7fb4163a9..51a106919 100644 --- a/plugin/src/Caelestia/cutils.hpp +++ b/plugin/src/Caelestia/cutils.hpp @@ -27,6 +27,8 @@ class CUtils : public QObject { Q_INVOKABLE static bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true); Q_INVOKABLE static bool deleteFile(const QUrl& path); Q_INVOKABLE static QString toLocalFile(const QUrl& url); + Q_INVOKABLE static QString imageFormat(const QUrl& url); + Q_INVOKABLE static QString imageFormat(const QString& path); Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max); diff --git a/utils/Images.qml b/utils/Images.qml index ac76f5118..ebc0481af 100644 --- a/utils/Images.qml +++ b/utils/Images.qml @@ -5,8 +5,16 @@ import Quickshell Singleton { readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] + readonly property list validProfileImageTypes: validImageTypes.concat(["gif"]) + readonly property list validProfileImageExtensions: validImageExtensions.concat(["gif"]) function isValidImageByName(name: string): bool { - return validImageExtensions.some(t => name.endsWith(`.${t}`)); + const lowerName = name.toLowerCase(); + return validImageExtensions.some(t => lowerName.endsWith(`.${t}`)); + } + + function isValidProfileImageByName(name: string): bool { + const lowerName = name.toLowerCase(); + return validProfileImageExtensions.some(t => lowerName.endsWith(`.${t}`)); } }