summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/DankCircularImage.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/DankCircularImage.qml')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/DankCircularImage.qml171
1 files changed, 171 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/DankCircularImage.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/DankCircularImage.qml
new file mode 100644
index 0000000..36cbfe4
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/DankCircularImage.qml
@@ -0,0 +1,171 @@
+import QtQuick
+import QtQuick.Window
+import QtQuick.Effects
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string imageSource: ""
+ property string fallbackIcon: "notifications"
+ property string fallbackText: ""
+ property bool hasImage: imageSource !== ""
+ readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
+ // Probe with AnimatedImage first; once loaded, check frameCount to decide.
+ readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
+ readonly property var activeImage: isAnimated ? probe : staticImage
+ property int imageStatus: activeImage.status
+
+ signal imageSaved(string filePath)
+
+ property string _pendingSavePath: ""
+ property var _attachedWindow: root.Window.window
+
+ on_AttachedWindowChanged: {
+ if (_attachedWindow && _pendingSavePath !== "") {
+ Qt.callLater(function () {
+ if (root._pendingSavePath !== "") {
+ let path = root._pendingSavePath;
+ root._pendingSavePath = "";
+ root.saveImageToFile(path);
+ }
+ });
+ }
+ }
+
+ function saveImageToFile(filePath) {
+ if (activeImage.status !== Image.Ready)
+ return false;
+
+ if (!activeImage.Window.window) {
+ _pendingSavePath = filePath;
+ return true;
+ }
+
+ activeImage.grabToImage(function (result) {
+ if (result && result.saveToFile(filePath)) {
+ root.imageSaved(filePath);
+ }
+ });
+ return true;
+ }
+
+ radius: width / 2
+ color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
+ border.color: "transparent"
+ border.width: 0
+
+ // Probe: loads as AnimatedImage to detect frame count.
+ AnimatedImage {
+ id: probe
+ anchors.fill: parent
+ anchors.margins: 2
+ asynchronous: true
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ mipmap: true
+ cache: true
+ visible: false
+ source: root.shouldProbe ? root.imageSource : ""
+ }
+
+ // Static fallback: used once probe confirms the image is not animated.
+ Image {
+ id: staticImage
+ anchors.fill: parent
+ anchors.margins: 2
+ asynchronous: true
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ mipmap: true
+ cache: true
+ visible: false
+ sourceSize.width: Math.max(width * 2, 128)
+ sourceSize.height: Math.max(height * 2, 128)
+ source: !root.shouldProbe ? root.imageSource : ""
+ }
+
+ // Once the probe loads, if not animated, hand off to Image and unload probe.
+ Connections {
+ target: probe
+ function onStatusChanged() {
+ if (!root.shouldProbe)
+ return;
+ switch (probe.status) {
+ case Image.Ready:
+ if (probe.frameCount <= 1) {
+ staticImage.source = root.imageSource;
+ probe.source = "";
+ }
+ break;
+ case Image.Error:
+ staticImage.source = root.imageSource;
+ probe.source = "";
+ break;
+ }
+ }
+ }
+
+ // If imageSource changes, reset: re-probe with AnimatedImage.
+ onImageSourceChanged: {
+ if (root.shouldProbe) {
+ staticImage.source = "";
+ probe.source = root.imageSource;
+ } else {
+ probe.source = "";
+ staticImage.source = root.imageSource;
+ }
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ anchors.margins: 2
+ source: root.activeImage
+ maskEnabled: true
+ maskSource: circularMask
+ visible: root.activeImage.status === Image.Ready && root.imageSource !== ""
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1
+ }
+
+ Item {
+ id: circularMask
+ anchors.centerIn: parent
+ width: parent.width - 4
+ height: parent.height - 4
+ layer.enabled: true
+ layer.smooth: true
+ visible: false
+
+ Rectangle {
+ anchors.fill: parent
+ radius: width / 2
+ color: "black"
+ antialiasing: true
+ }
+ }
+
+ AppIconRenderer {
+ anchors.centerIn: parent
+ width: Math.round(parent.width * 0.75)
+ height: width
+ visible: (root.activeImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== ""
+ iconValue: root.fallbackIcon
+ iconSize: width
+ iconColor: Theme.surfaceVariantText
+ materialIconSizeAdjustment: 0
+ fallbackText: root.fallbackText
+ fallbackBackgroundColor: "transparent"
+ fallbackTextColor: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""
+ text: root.fallbackText
+ font.pixelSize: Math.max(12, parent.width * 0.5)
+ font.weight: Font.Bold
+ color: Theme.surfaceVariantText
+ }
+}