1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18:28 -04:00

Squashed commit of the following:

commit 990d86d481
Author: bbedward <bbedward@gmail.com>
Date:   Sat Jul 18 10:43:22 2026 -0400

    flake: update-common

commit 526cb157fd
Author: bbedward <bbedward@gmail.com>
Date:   Thu Jul 16 17:56:40 2026 -0400

    i18n: update sync scrirpt for dank-qml-common

commit 92ba96d9f9
Author: bbedward <bbedward@gmail.com>
Date:   Thu Jul 16 09:24:37 2026 -0400

    qs: integrate with dank-qml-common
This commit is contained in:
bbedward
2026-07-18 10:44:31 -04:00
parent 5dfd875b9e
commit 235f0668b8
128 changed files with 577 additions and 14706 deletions
+2 -128
View File
@@ -1,129 +1,3 @@
import QtQuick
import Quickshell.Widgets
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
required property string iconValue
required property int iconSize
property string fallbackText: "A"
property color iconColor: Theme.surfaceText
property color colorOverride: "transparent"
property real brightnessOverride: 0.0
property real contrastOverride: 0.0
property real saturationOverride: 0.0
property color fallbackBackgroundColor: Theme.surfaceLight
property color fallbackTextColor: Theme.primary
property real materialIconSizeAdjustment: Theme.spacingM
property real unicodeIconScale: 0.7
property real fallbackTextScale: 0.4
property real iconMargins: 0
property real fallbackLeftMargin: 0
property real fallbackRightMargin: 0
property real fallbackTopMargin: 0
property real fallbackBottomMargin: 0
readonly property bool isMaterial: iconValue && iconValue.startsWith("material:")
readonly property bool isUnicode: iconValue && iconValue.startsWith("unicode:")
readonly property bool isSvgCorner: iconValue && iconValue.startsWith("svg+corner:")
readonly property bool isSvg: iconValue && !isSvgCorner && iconValue.startsWith("svg:")
readonly property bool isImage: iconValue && iconValue.startsWith("image:")
readonly property bool hasColorOverride: colorOverride.a > 0
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
readonly property string svgSource: {
if (isSvgCorner) {
const parts = iconValue.substring(11).split("|");
return parts[0] || "";
}
if (isSvg)
return iconValue.substring(4);
return "";
}
readonly property string svgCornerIcon: isSvgCorner ? (iconValue.substring(11).split("|")[1] || "") : ""
readonly property bool hasSpecialPrefix: isMaterial || isUnicode || isSvg || isSvgCorner || isImage
readonly property string iconPath: {
if (hasSpecialPrefix || !iconValue)
return "";
return Paths.resolveIconPath(iconValue);
}
visible: iconValue !== undefined && iconValue !== ""
DankIcon {
anchors.centerIn: parent
name: root.materialName
size: root.iconSize - root.materialIconSizeAdjustment
color: root.hasColorOverride ? root.colorOverride : root.iconColor
visible: root.isMaterial
}
StyledText {
anchors.centerIn: parent
text: root.unicodeChar
font.pixelSize: root.iconSize * root.unicodeIconScale
color: root.hasColorOverride ? root.colorOverride : root.iconColor
visible: root.isUnicode
}
DankSVGIcon {
anchors.centerIn: parent
source: root.svgSource
size: root.iconSize
cornerIcon: root.svgCornerIcon
colorOverride: root.colorOverride
brightnessOverride: root.brightnessOverride
contrastOverride: root.contrastOverride
saturationOverride: root.saturationOverride
visible: root.isSvg || root.isSvgCorner
}
CachingImage {
id: cachingImg
anchors.fill: parent
imagePath: root.imagePath
maxCacheSize: root.iconSize * 2
animate: false
visible: root.isImage && status === Image.Ready
}
Loader {
id: iconImgLoader
anchors.fill: parent
anchors.margins: root.iconMargins
active: !root.hasSpecialPrefix && root.iconPath !== ""
sourceComponent: IconImage {
anchors.fill: parent
source: root.iconPath
backer.sourceSize: Qt.size(root.iconSize * 2, root.iconSize * 2)
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
}
Rectangle {
id: fallbackRect
anchors.fill: parent
anchors.leftMargin: root.fallbackLeftMargin
anchors.rightMargin: root.fallbackRightMargin
anchors.topMargin: root.fallbackTopMargin
anchors.bottomMargin: root.fallbackBottomMargin
visible: !root.hasSpecialPrefix && (root.iconPath === "" || !iconImgLoader.item || iconImgLoader.item.status !== Image.Ready)
color: root.fallbackBackgroundColor
radius: Theme.cornerRadius
border.width: 0
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: root.fallbackText
font.pixelSize: root.iconSize * root.fallbackTextScale
color: root.fallbackTextColor
font.weight: Font.Bold
}
}
}
DankCommon.AppIconRenderer {}
+2 -131
View File
@@ -1,132 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string imagePath: ""
property int maxCacheSize: 512
property int status: isAnimated ? animatedImg.status : staticImg.status
property int fillMode: Image.PreserveAspectCrop
// AnimatedImage decodes full-size on the GUI thread and is never cached;
// disable for thumbnail grids
property bool animate: true
property bool _fromCache: false
readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
readonly property bool isAnimated: {
if (!animate || !imagePath)
return false;
const lower = imagePath.toLowerCase();
return lower.endsWith(".gif") || lower.endsWith(".webp");
}
readonly property string normalizedPath: {
if (!imagePath)
return "";
if (isRemoteUrl)
return imagePath;
if (imagePath.startsWith("file://"))
return imagePath.substring(7);
return imagePath;
}
function djb2Hash(str) {
if (!str)
return "";
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i);
hash = hash & 0x7FFFFFFF;
}
return hash.toString(16).padStart(8, '0');
}
readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
readonly property string cacheFileName: imageHash && !isRemoteUrl && !isAnimated ? `${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
readonly property string cachePath: cacheFileName ? `${Paths.stringify(Paths.imagecache)}/${cacheFileName}` : ""
readonly property string encodedImagePath: {
if (!normalizedPath)
return "";
if (isRemoteUrl)
return normalizedPath;
return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
}
AnimatedImage {
id: animatedImg
anchors.fill: parent
visible: root.isAnimated
asynchronous: true
fillMode: root.fillMode
source: root.isAnimated ? root.imagePath : ""
playing: visible && status === AnimatedImage.Ready
}
Image {
id: staticImg
anchors.fill: parent
visible: !root.isAnimated
asynchronous: true
fillMode: root.fillMode
sourceSize.width: root.maxCacheSize
sourceSize.height: root.maxCacheSize
smooth: true
onStatusChanged: {
switch (status) {
case Image.Error:
if (!root._fromCache)
return;
root._fromCache = false;
source = root.encodedImagePath;
return;
case Image.Ready:
if (root._fromCache || root.isRemoteUrl || !root.cachePath)
return;
if (!visible || width <= 0 || height <= 0 || !Window.window?.visible)
return;
const grabPath = root.cachePath;
grabToImage(res => {
res.saveToFile(grabPath);
});
return;
}
}
}
// Derives everything from a local snapshot of imagePath: sibling property
// bindings (isRemoteUrl, encodedImagePath, ...) are still stale when
// onImagePathChanged runs, so reading them here routes remote URLs down
// the local-file branch on the first path change
function resolveSource() {
const path = imagePath;
if (!path) {
_fromCache = false;
staticImg.source = "";
return;
}
const lower = path.toLowerCase();
if (animate && (lower.endsWith(".gif") || lower.endsWith(".webp")))
return;
if (path.startsWith("http://") || path.startsWith("https://")) {
_fromCache = false;
staticImg.source = path;
return;
}
const stripped = path.startsWith("file://") ? path.substring(7) : path;
const encoded = "file://" + stripped.split('/').map(s => encodeURIComponent(s)).join('/');
const hash = djb2Hash(stripped);
if (!hash) {
_fromCache = false;
staticImg.source = encoded;
return;
}
// Cache-first; a miss errors and falls back to encodedImagePath
_fromCache = true;
staticImg.source = `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png`;
}
onImagePathChanged: resolveSource()
// During creation onImagePathChanged fires before sibling properties (maxCacheSize) initialize
onCachePathChanged: resolveSource()
}
DankCommon.CachingImage {}
+2 -44
View File
@@ -1,45 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
StyledRect {
id: root
property string iconName: ""
property int iconSize: Theme.iconSize - 4
property color iconColor: Theme.surfaceText
property color backgroundColor: "transparent"
property bool circular: true
property int buttonSize: 32
property var tooltipText: null
property string tooltipSide: "bottom"
readonly property alias pressed: stateLayer.pressed
signal clicked
signal entered
signal exited
width: buttonSize
height: buttonSize
radius: Theme.cornerRadius
color: backgroundColor
DankIcon {
anchors.centerIn: parent
name: root.iconName
size: root.iconSize
color: root.iconColor
}
StateLayer {
id: stateLayer
disabled: !root.enabled
stateColor: Theme.primary
cornerRadius: root.radius
onClicked: root.clicked()
onEntered: root.entered()
onExited: root.exited()
tooltipText: root.tooltipText
tooltipSide: root.tooltipSide
}
}
DankCommon.DankActionButton {}
+2 -27
View File
@@ -1,28 +1,3 @@
import QtQuick
import qs.DankCommon.Widgets as DankCommon
SequentialAnimation {
id: root
property Item target
property real minOpacity: 0.3
property int pulseDuration: 600
loops: Animation.Infinite
NumberAnimation {
target: root.target
property: "opacity"
to: root.minOpacity
duration: root.pulseDuration
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.target
property: "opacity"
to: 1.0
duration: root.pulseDuration
easing.type: Easing.InOutQuad
}
onStopped: if (root.target) root.target.opacity = 1.0
}
DankCommon.DankBlink {}
+2 -98
View File
@@ -1,99 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property string text: ""
property string iconName: ""
property int iconSize: Theme.iconSizeSmall
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property color backgroundColor: Theme.buttonBg
property color textColor: Theme.buttonText
property int buttonHeight: 40
property int horizontalPadding: Theme.spacingL
property bool enableScaleAnimation: false
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
signal clicked
width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
height: buttonHeight
radius: Theme.cornerRadius
color: backgroundColor
opacity: enabled ? 1 : 0.4
scale: (enableScaleAnimation && pressed) ? 0.98 : 1.0
Behavior on scale {
enabled: enableScaleAnimation && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: 100
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
Rectangle {
id: stateLayer
anchors.fill: parent
radius: parent.radius
color: {
if (pressed)
return Theme.withAlpha(root.textColor, 0.20);
if (hovered)
return Theme.withAlpha(root.textColor, 0.12);
return Theme.withAlpha(root.textColor, 0);
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: rippleLayer
rippleColor: root.textColor
cornerRadius: root.radius
enableRipple: root.enableRipple
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: root.iconSize
color: root.textColor
visible: root.iconName !== ""
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.text
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: root.textColor
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.enabled
onPressed: mouse => {
if (root.enableRipple)
rippleLayer.trigger(mouse.x, mouse.y);
}
onClicked: root.clicked()
}
}
DankCommon.DankButton {}
+2 -270
View File
@@ -1,271 +1,3 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Row {
id: root
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property var model: []
property int currentIndex: -1
property string selectionMode: "single"
property bool multiSelect: selectionMode === "multi"
property var initialSelection: []
property var currentSelection: initialSelection
property bool checkEnabled: true
property string size: "medium"
property int buttonHeight: size === "small" ? 32 : 40
property int minButtonWidth: size === "small" ? 56 : 64
property int buttonPadding: size === "small" ? Theme.spacingM : Theme.spacingL
property int checkIconSize: size === "small" ? Theme.iconSizeSmall - 2 : Theme.iconSizeSmall
property int textSize: size === "small" ? Theme.fontSizeSmall : Theme.fontSizeMedium
property bool userInteracted: false
property bool usePopupTransparency: !checkParentDisablesTransparency()
property real maximumWidth: -1
readonly property real _segmentCap: {
const count = model?.length ?? 0;
if (maximumWidth <= 0 || count === 0)
return -1;
return (maximumWidth - spacing * (count - 1)) / count - 4;
}
signal selectionChanged(int index, bool selected)
signal animationCompleted
spacing: Theme.spacingXS
Timer {
id: animationTimer
interval: Theme.shortDuration
onTriggered: {
root.userInteracted = false;
root.animationCompleted();
}
}
function isSelected(index) {
if (multiSelect) {
return repeater.itemAt(index)?.selected || false;
}
return index === currentIndex;
}
function selectItem(index) {
userInteracted = true;
if (multiSelect) {
const modelValue = model[index];
let newSelection = [...currentSelection];
const isCurrentlySelected = newSelection.includes(modelValue);
if (isCurrentlySelected) {
newSelection = newSelection.filter(item => item !== modelValue);
} else {
newSelection.push(modelValue);
}
currentSelection = newSelection;
selectionChanged(index, !isCurrentlySelected);
animationTimer.restart();
} else {
const oldIndex = currentIndex;
selectionChanged(index, true);
if (oldIndex !== index && oldIndex >= 0) {
selectionChanged(oldIndex, false);
}
animationTimer.restart();
}
}
Repeater {
id: repeater
model: ScriptModel {
values: root.model
}
delegate: Rectangle {
id: segment
property bool selected: multiSelect ? root.currentSelection.includes(modelData) : (index === root.currentIndex)
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property bool isFirst: index === 0
property bool isLast: index === repeater.count - 1
property bool visualFirst: I18n.isRtl ? isLast : isFirst
property bool visualLast: I18n.isRtl ? isFirst : isLast
property bool prevSelected: index > 0 ? root.isSelected(index - 1) : false
property bool nextSelected: index < repeater.count - 1 ? root.isSelected(index + 1) : false
readonly property real contentNaturalWidth: (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0) + buttonText.implicitWidth
width: {
const natural = Math.max(contentNaturalWidth + root.buttonPadding * 2, root.minButtonWidth);
const capped = root._segmentCap > 0 ? Math.min(natural, Math.max(root._segmentCap, root.minButtonWidth)) : natural;
return capped + (selected ? 4 : 0);
}
height: root.buttonHeight
color: selected ? Theme.buttonBg : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency) : Theme.surfaceVariant)
border.color: "transparent"
border.width: 0
topLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
bottomLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
topRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
bottomRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
Behavior on width {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on topLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on topRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on bottomLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on bottomRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
enabled: root.userInteracted
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: stateLayer
anchors.fill: parent
topLeftRadius: parent.topLeftRadius
bottomLeftRadius: parent.bottomLeftRadius
topRightRadius: parent.topRightRadius
bottomRightRadius: parent.bottomRightRadius
color: {
if (pressed)
return selected ? Theme.buttonPressed : Theme.surfaceTextHover;
if (hovered)
return selected ? Theme.buttonHover : Theme.surfaceTextHover;
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: segmentRipple
cornerRadius: Theme.cornerRadius
rippleColor: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
}
Item {
id: contentItem
anchors.centerIn: parent
implicitWidth: contentRow.implicitWidth
implicitHeight: contentRow.implicitHeight
Row {
id: contentRow
spacing: Theme.spacingS
DankIcon {
id: checkIcon
name: "check"
size: root.checkIconSize
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
visible: root.checkEnabled && segment.selected
opacity: segment.selected ? 1 : 0
scale: segment.selected ? 1 : 0.6
anchors.verticalCenter: parent.verticalCenter
Behavior on opacity {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on scale {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
StyledText {
id: buttonText
readonly property real capAvailable: {
if (root._segmentCap <= 0)
return -1;
const cap = Math.max(root._segmentCap, root.minButtonWidth);
return Math.max(0, cap - root.buttonPadding * 2 - (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0));
}
text: typeof modelData === "string" ? modelData : modelData.text || ""
font.pixelSize: root.textSize
font.weight: segment.selected ? Font.Medium : Font.Normal
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
width: capAvailable < 0 ? implicitWidth : Math.min(implicitWidth, capAvailable)
maximumLineCount: 1
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => segmentRipple.trigger(mouse.x, mouse.y)
onClicked: root.selectItem(index)
}
}
}
}
DankCommon.DankButtonGroup {}
+2 -130
View File
@@ -1,131 +1,3 @@
import QtQuick
import QtQuick.Window
import Quickshell.Widgets
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property string imageSource: ""
property string fallbackIcon: "notifications"
property string fallbackText: ""
property bool cacheImages: true
property bool hasImage: imageSource !== ""
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error
readonly property var activeImage: {
if (isAnimated)
return probe;
if (staticImage.status === Image.Ready)
return staticImage;
if (probe.status === Image.Ready && probe.source !== "")
return probe;
return 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: Theme.primaryHover
border.color: "transparent"
border.width: 0
ClippingRectangle {
anchors.fill: parent
anchors.margins: 2
radius: Math.min(width, height) / 2
color: "transparent"
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
AnimatedImage {
id: probe
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== ""
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
}
// Takes over once the probe settles on a non-animated image, then latches.
Image {
id: staticImage
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== ""
sourceSize.width: Math.max(width * 2, 128)
sourceSize.height: Math.max(height * 2, 128)
source: {
if (!root.shouldProbe)
return root.imageSource;
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
return root.imageSource;
return "";
}
}
}
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
}
}
DankCommon.DankCircularImage {}
+2 -132
View File
@@ -1,133 +1,3 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
ColumnLayout {
id: root
required property string title
property string description: ""
property bool expanded: false
property bool showBackground: false
property alias headerColor: headerRect.color
signal toggleRequested
spacing: Theme.spacingS
Layout.fillWidth: true
Rectangle {
id: headerRect
Layout.fillWidth: true
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Theme.paddingM * 2, 48)
radius: Theme.cornerRadius
color: "transparent"
RowLayout {
id: titleRow
anchors.fill: parent
anchors.leftMargin: Theme.paddingM
anchors.rightMargin: Theme.paddingM
spacing: Theme.spacingM
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
Layout.fillWidth: true
}
DankIcon {
name: "expand_more"
size: Theme.iconSizeSmall
rotation: root.expanded ? 180 : 0
Behavior on rotation {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
}
StateLayer {
anchors.fill: parent
onClicked: {
root.toggleRequested();
root.expanded = !root.expanded;
}
}
}
default property alias content: contentColumn.data
Item {
id: contentWrapper
Layout.fillWidth: true
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Theme.spacingS * 2) : 0
clip: true
Behavior on Layout.preferredHeight {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankAnim {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
Rectangle {
id: backgroundRect
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
visible: root.showBackground
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
ColumnLayout {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
y: Theme.spacingS
anchors.leftMargin: Theme.paddingM
anchors.rightMargin: Theme.paddingM
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingS
opacity: root.expanded ? 1.0 : 0.0
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
StyledText {
id: descriptionText
Layout.fillWidth: true
Layout.topMargin: root.description !== "" ? Theme.spacingXS : 0
Layout.bottomMargin: root.description !== "" ? Theme.spacingS : 0
visible: root.description !== ""
text: root.description
color: Theme.surfaceTextSecondary
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
}
}
}
DankCommon.DankCollapsibleSection {}
+2 -61
View File
@@ -1,62 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
// Premultiplied-alpha color tween: bind `to`, read `value`. Plain
// ColorAnimation lerps raw RGBA and flashes when the endpoints differ
// in both color and alpha (translucent <-> opaque).
QtObject {
id: root
property color to
property bool animated: true
property int duration: Theme.mediumDuration
property int easingType: Theme.emphasizedEasing
property color _from: to
property color _target: to
property real _mix: 1
property bool _ready: false
readonly property color value: {
const from = _from;
const target = _target;
const alpha = from.a + (target.a - from.a) * _mix;
if (alpha <= 0)
return Qt.rgba(target.r, target.g, target.b, 0);
const mix = (a, b) => (a * from.a + (b * target.a - a * from.a) * _mix) / alpha;
return Qt.rgba(mix(from.r, target.r), mix(from.g, target.g), mix(from.b, target.b), alpha);
}
readonly property NumberAnimation _anim: NumberAnimation {
target: root
property: "_mix"
from: 0
to: 1
duration: root.duration
easing.type: root.easingType
}
onToChanged: {
if (!_ready || !animated) {
_anim.stop();
_from = to;
_target = to;
_mix = 1;
return;
}
if (Qt.colorEqual(to, _target))
return;
const current = value;
_anim.stop();
_from = current;
_target = to;
_mix = 0;
_anim.restart();
}
Component.onCompleted: {
_from = to;
_target = to;
_ready = true;
}
}
DankCommon.DankColorAnimation {}
+2 -44
View File
@@ -1,45 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property color swatchColor: "transparent"
property color ringColor: Theme.outline
property real minPreviewAlpha: 0.4
readonly property bool translucent: swatchColor.a > 0 && swatchColor.a < 1
readonly property color displayColor: translucent ? Theme.withAlpha(swatchColor, Math.max(swatchColor.a, minPreviewAlpha)) : swatchColor
Loader {
anchors.fill: parent
active: root.translucent
sourceComponent: Component {
Canvas {
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.beginPath();
ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI);
ctx.clip();
const s = Math.max(2, Math.round(width / 4));
for (let y = 0; y < height; y += s) {
for (let x = 0; x < width; x += s) {
ctx.fillStyle = (((x / s) + (y / s)) % 2 === 0) ? "#ffffff" : "#bdbdbd";
ctx.fillRect(x, y, s, s);
}
}
}
onVisibleChanged: if (visible)
requestPaint()
}
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: root.displayColor
border.color: root.ringColor
border.width: 1
}
}
DankCommon.DankColorSwatch {}
+2 -554
View File
@@ -1,555 +1,3 @@
import "../Common/fzf.js" as Fzf
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property string text: ""
property string description: ""
property string currentValue: ""
property var options: []
property var optionIcons: []
property bool enableFuzzySearch: false
property var optionIconMap: ({})
property var optionColorMap: ({})
function rebuildIconMap() {
const map = {};
for (let i = 0; i < options.length; i++) {
if (optionIcons.length > i)
map[options[i]] = optionIcons[i];
}
optionIconMap = map;
}
onOptionsChanged: rebuildIconMap()
onOptionIconsChanged: rebuildIconMap()
property int popupWidthOffset: 0
property int maxPopupHeight: 400
property bool openUpwards: false
property int popupWidth: 0
property bool alignPopupRight: false
property int dropdownWidth: 200
property bool compactMode: text === "" && description === ""
property bool showTrigger: true
property Item popupAnchorItem: null
property bool addHorizontalPadding: false
property string emptyText: ""
property bool usePopupTransparency: !checkParentDisablesTransparency()
property var transientSurfaceTracker: null
signal valueChanged(string value)
property bool menuOpen: false
onMenuOpenChanged: transientSurfaceTracker?.setActive(root, menuOpen, null)
function closeDropdownMenu() {
if (!root.menuOpen && !dropdownMenu.opened && !dropdownMenu.visible)
return;
root.menuOpen = false;
dropdownMenu.close();
}
function positionDropdownMenu() {
let currentIndex = root.options.indexOf(root.currentValue);
listView.positionViewAtIndex(currentIndex >= 0 ? currentIndex : 0, ListView.Beginning);
const anchorItem = root.popupAnchorItem || dropdown;
const pos = anchorItem.mapToItem(Overlay.overlay, 0, 0);
const popupW = dropdownMenu.width;
const popupH = dropdownMenu.height;
const overlayH = Overlay.overlay.height;
const goUp = root.openUpwards || pos.y + anchorItem.height + popupH + 4 > overlayH;
dropdownMenu.x = root.alignPopupRight ? pos.x + anchorItem.width - popupW : pos.x - (root.popupWidthOffset / 2);
dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + anchorItem.height + 4;
}
function showDropdownMenu() {
if (root.options.length === 0)
return;
if (root.menuOpen)
return;
root.menuOpen = true;
dropdownMenu.open();
positionDropdownMenu();
if (root.enableFuzzySearch)
searchField.forceActiveFocus();
}
function openDropdownMenu() {
if (root.menuOpen) {
closeDropdownMenu();
return;
}
showDropdownMenu();
}
function resetSearch() {
searchField.text = "";
dropdownMenu.fzfFinder = null;
dropdownMenu.searchQuery = "";
dropdownMenu.selectedIndex = -1;
}
width: !showTrigger ? 0 : (compactMode ? dropdownWidth : parent.width)
implicitHeight: !showTrigger ? 0 : (compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM))
Component.onDestruction: {
transientSurfaceTracker?.unregister(root);
if (root.menuOpen || dropdownMenu.opened || dropdownMenu.visible)
dropdownMenu.close();
}
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
root.closeDropdownMenu();
}
}
Column {
id: labelColumn
anchors.left: parent.left
anchors.right: dropdown.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: root.addHorizontalPadding ? Theme.spacingM : 0
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingXS
visible: !root.compactMode && root.showTrigger
StyledText {
text: root.text
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: description.length > 0
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
Rectangle {
id: dropdown
visible: root.showTrigger
width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth))
height: 40
anchors.right: parent.right
anchors.rightMargin: root.addHorizontalPadding && !root.compactMode ? Theme.spacingM : 0
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : Theme.surfaceContainer)
border.color: dropdownMenu.visible ? Theme.primary : Theme.outlineHeavy
border.width: dropdownMenu.visible ? 2 : 1
MouseArea {
id: dropdownArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openDropdownMenu()
}
Row {
id: contentRow
anchors.left: parent.left
anchors.right: expandIcon.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankColorSwatch {
id: triggerSwatch
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
visible: root.optionColorMap[root.currentValue] !== undefined
swatchColor: visible ? root.optionColorMap[root.currentValue] : "transparent"
}
DankIcon {
id: triggerIcon
name: root.optionIconMap[root.currentValue] ?? ""
size: 18
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.currentValue !== "" ? root.currentValue : root.emptyText
font.pixelSize: Theme.fontSizeMedium
color: root.currentValue !== "" ? Theme.surfaceText : Theme.outline
width: contentRow.width - (triggerSwatch.visible ? triggerSwatch.width + contentRow.spacing : 0) - (triggerIcon.visible ? triggerIcon.width + contentRow.spacing : 0)
elide: Text.ElideRight
wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
}
}
DankIcon {
id: expandIcon
name: dropdownMenu.visible ? "expand_less" : "expand_more"
size: 20
color: Theme.surfaceText
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Popup {
id: dropdownMenu
property string searchQuery: ""
property var filteredOptions: {
if (!root.enableFuzzySearch || searchQuery.length === 0)
return root.options;
if (!fzfFinder)
return root.options;
return fzfFinder.find(searchQuery).map(r => r.item);
}
property int selectedIndex: -1
property var fzfFinder: null
function initFinder() {
fzfFinder = new Fzf.Finder(root.options, {
"selector": option => option,
"limit": 50,
"casing": "case-insensitive",
"sort": true,
"tiebreakers": [(a, b, selector) => selector(a.item).length - selector(b.item).length]
});
}
function selectNext() {
if (filteredOptions.length === 0)
return;
selectedIndex = (selectedIndex + 1) % filteredOptions.length;
listView.positionViewAtIndex(selectedIndex, ListView.Contain);
}
function selectPrevious() {
if (filteredOptions.length === 0)
return;
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1;
listView.positionViewAtIndex(selectedIndex, ListView.Contain);
}
function selectCurrent() {
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length)
return;
root.currentValue = filteredOptions[selectedIndex];
root.valueChanged(filteredOptions[selectedIndex]);
close();
}
onOpened: {
root.menuOpen = true;
selectedIndex = -1;
if (searchField.text.length > 0) {
initFinder();
searchQuery = searchField.text;
} else {
fzfFinder = null;
searchQuery = "";
}
}
onClosed: root.menuOpen = false
parent: root.Overlay.overlay
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
height: {
let h = root.enableFuzzySearch ? 54 : 0;
if (root.options.length === 0 && root.emptyText !== "")
h += 32;
else
h += Math.min(filteredOptions.length, 10) * 36;
return Math.min(root.maxPopupHeight, h + 16);
}
padding: 0
modal: true
dim: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition {
NumberAnimation {
property: "scale"
from: 0.9
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
exit: Transition {
NumberAnimation {
property: "scale"
from: 1
to: 0.9
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
id: contentSurface
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
color: Theme.withAlpha(Theme.surfaceContainer, 1)
border.color: Theme.primary
border.width: 2
radius: Theme.cornerRadius
ElevationShadow {
id: shadowLayer
anchors.fill: parent
z: -1
level: Theme.elevationLevel2
fallbackOffset: 4
targetRadius: contentSurface.radius
targetColor: contentSurface.color
borderColor: contentSurface.border.color
borderWidth: contentSurface.border.width
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
Rectangle {
id: searchContainer
width: parent.width
height: 42
visible: root.enableFuzzySearch
radius: Theme.cornerRadius
color: root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
DankTextField {
id: searchField
anchors.fill: parent
anchors.margins: 1
placeholderText: I18n.tr("Search...")
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
onTextChanged: searchDebounce.restart()
Keys.onDownPressed: dropdownMenu.selectNext()
Keys.onUpPressed: dropdownMenu.selectPrevious()
Keys.onReturnPressed: dropdownMenu.selectCurrent()
Keys.onEnterPressed: dropdownMenu.selectCurrent()
Keys.onPressed: event => {
if (!(event.modifiers & Qt.ControlModifier))
return;
switch (event.key) {
case Qt.Key_N:
case Qt.Key_J:
dropdownMenu.selectNext();
event.accepted = true;
break;
case Qt.Key_P:
case Qt.Key_K:
dropdownMenu.selectPrevious();
event.accepted = true;
break;
}
}
Timer {
id: searchDebounce
interval: 50
onTriggered: {
if (!dropdownMenu.fzfFinder)
dropdownMenu.initFinder();
dropdownMenu.searchQuery = searchField.text;
dropdownMenu.selectedIndex = -1;
}
}
}
}
Item {
width: 1
height: Theme.spacingXS
visible: root.enableFuzzySearch
}
Item {
width: parent.width
height: 32
visible: root.options.length === 0 && root.emptyText !== ""
StyledText {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.emptyText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
}
DankListView {
id: listView
width: parent.width
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) - (root.options.length === 0 && root.emptyText !== "" ? 32 : 0)
clip: true
visible: root.options.length > 0
model: ScriptModel {
values: dropdownMenu.filteredOptions
}
spacing: Theme.spacingXXS
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
delegate: Rectangle {
id: delegateRoot
required property var modelData
required property int index
property bool isSelected: dropdownMenu.selectedIndex === index
property bool isCurrentValue: root.currentValue === modelData
property string iconName: root.optionIconMap[modelData] ?? ""
property var swatchColor: root.optionColorMap[modelData]
width: ListView.view.width
height: 32
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankColorSwatch {
id: optionSwatch
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
visible: delegateRoot.swatchColor !== undefined
swatchColor: visible ? delegateRoot.swatchColor : Theme.withAlpha(delegateRoot.swatchColor, 0)
ringColor: delegateRoot.isCurrentValue ? Theme.primary : Theme.outline
}
DankIcon {
name: delegateRoot.iconName
size: 18
color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: delegateRoot.modelData
font.pixelSize: Theme.fontSizeMedium
color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
font.weight: delegateRoot.isCurrentValue ? Font.Medium : Font.Normal
width: root.popupWidth > 0 ? undefined : (delegateRoot.width - parent.x - Theme.spacingS * 2 - (optionSwatch.visible ? optionSwatch.width + parent.spacing : 0))
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
}
}
MouseArea {
id: optionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentValue = delegateRoot.modelData;
root.valueChanged(delegateRoot.modelData);
root.closeDropdownMenu();
}
}
}
}
}
}
}
}
DankCommon.DankDropdown {}
+2 -107
View File
@@ -1,108 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Flow {
id: root
property var model: []
property int currentIndex: 0
property int chipHeight: 32
property int chipPadding: Theme.spacingM
property bool showCheck: true
property bool showCounts: true
signal selectionChanged(int index)
spacing: Theme.spacingS
width: parent ? parent.width : 400
Repeater {
model: root.model
Rectangle {
id: chip
required property var modelData
required property int index
property bool selected: index === root.currentIndex
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property string label: typeof modelData === "string" ? modelData : (modelData.label || "")
property int count: typeof modelData === "object" ? (modelData.count || 0) : 0
property bool showCount: root.showCounts && count > 0
width: contentRow.implicitWidth + root.chipPadding * 2
height: root.chipHeight
radius: height / 2
color: selected ? Theme.primary : Theme.surfaceVariant
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: {
if (pressed)
return chip.selected ? Theme.primaryPressed : Theme.surfaceTextHover;
if (hovered)
return chip.selected ? Theme.primaryHover : Theme.surfaceTextHover;
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: chipRipple
cornerRadius: chip.radius
rippleColor: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "check"
size: 16
anchors.verticalCenter: parent.verticalCenter
color: Theme.primaryText
visible: root.showCheck && chip.selected
}
StyledText {
text: chip.label + (chip.showCount ? " (" + chip.count + ")" : "")
font.pixelSize: Theme.fontSizeSmall
font.weight: chip.selected ? Font.Medium : Font.Normal
color: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => chipRipple.trigger(mouse.x, mouse.y)
onClicked: {
root.currentIndex = chip.index;
root.selectionChanged(chip.index);
}
}
}
}
}
DankCommon.DankFilterChips {}
+2 -178
View File
@@ -1,179 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Widgets
import "ScrollConstants.js" as Scroll
import qs.DankCommon.Widgets as DankCommon
Flickable {
id: flickable
property alias verticalScrollBar: vbar
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: Scroll.friction
property bool _scrollBarActive: false
flickDeceleration: Scroll.flickDeceleration
maximumFlickVelocity: Scroll.maximumFlickVelocity
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
WheelHandler {
id: wheelHandler
property real touchpadSpeed: Scroll.touchpadSpeed
property real momentumRetention: Scroll.momentumRetention
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
property bool sessionUsedMouseWheel: false
function startMomentum() {
flickable.isMomentumActive = true;
momentumAnim.running = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
vbar._scrollBarActive = true;
vbar.hideTimer.restart();
const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
flickable.isMomentumActive = false;
velocitySamples = [];
momentum = 0;
flickable.momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
let newY = flickable.contentY + scrollAmount;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
} else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
flickable.isMomentumActive = false;
velocitySamples = [];
momentum = 0;
flickable.momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false;
momentumAnim.running = false;
flickable.isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) {
flickable.momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
}
}
if (timeDelta < Scroll.momentumTimeThreshold) {
momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
delta += momentum;
} else {
momentum = 0;
}
let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
}
event.accepted = true;
}
onActiveChanged: {
if (!active) {
if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= Scroll.minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
flickable.momentumVelocity = 0;
}
}
}
}
onMovementStarted: {
vbar._scrollBarActive = true;
vbar.hideTimer.stop();
}
onMovementEnded: vbar.hideTimer.restart()
FrameAnimation {
id: momentumAnim
running: false
onTriggered: {
const dt = frameTime;
const newY = flickable.contentY - flickable.momentumVelocity * dt;
const maxY = Math.max(0, flickable.contentHeight - flickable.height);
if (newY < 0 || newY > maxY) {
flickable.contentY = newY < 0 ? 0 : maxY;
running = false;
flickable.isMomentumActive = false;
flickable.momentumVelocity = 0;
return;
}
flickable.contentY = newY;
flickable.momentumVelocity *= Math.pow(flickable.friction, dt / 0.016);
if (Math.abs(flickable.momentumVelocity) < Scroll.momentumStopThreshold) {
running = false;
flickable.isMomentumActive = false;
flickable.momentumVelocity = 0;
}
}
}
ScrollBar.vertical: DankScrollbar {
id: vbar
}
}
DankCommon.DankFlickable {}
+2 -174
View File
@@ -1,175 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Widgets
import "ScrollConstants.js" as Scroll
import qs.DankCommon.Widgets as DankCommon
GridView {
id: gridView
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: Scroll.friction
flickDeceleration: Scroll.flickDeceleration
maximumFlickVelocity: Scroll.maximumFlickVelocity
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
onMovementStarted: {
vbar._scrollBarActive = true;
vbar.hideTimer.stop();
}
onMovementEnded: vbar.hideTimer.restart()
WheelHandler {
id: wheelHandler
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
property real touchpadSpeed: Scroll.touchpadSpeed
property real momentumRetention: Scroll.momentumRetention
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
property bool sessionUsedMouseWheel: false
function startMomentum() {
isMomentumActive = true;
momentumAnim.running = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
vbar._scrollBarActive = true;
vbar.hideTimer.restart();
const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
let newY = contentY + scrollAmount;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
} else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 120 * cellHeight * 1.2;
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false;
momentumAnim.running = false;
isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) {
momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
}
}
if (timeDelta < Scroll.momentumTimeThreshold) {
momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
delta += momentum;
} else {
momentum = 0;
}
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
}
event.accepted = true;
}
onActiveChanged: {
if (!active) {
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
momentumVelocity = 0;
}
}
}
}
FrameAnimation {
id: momentumAnim
running: false
onTriggered: {
const dt = frameTime;
const newY = contentY - momentumVelocity * dt;
const maxY = Math.max(0, contentHeight - height);
if (newY < 0 || newY > maxY) {
contentY = newY < 0 ? 0 : maxY;
running = false;
isMomentumActive = false;
momentumVelocity = 0;
return;
}
contentY = newY;
momentumVelocity *= Math.pow(friction, dt / 0.016);
if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
running = false;
isMomentumActive = false;
momentumVelocity = 0;
}
}
}
ScrollBar.vertical: DankScrollbar {
id: vbar
}
}
DankCommon.DankGridView {}
+2 -71
View File
@@ -1,72 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property alias name: icon.text
property alias size: icon.font.pixelSize
property alias color: icon.color
property bool filled: false
property real fill: filled ? 1.0 : 0.0
property int grade: Theme.isLightMode ? 0 : -25
property int weight: filled ? 500 : 400
property bool smoothTransform: false
implicitWidth: Math.round(size)
implicitHeight: Math.round(size)
signal rotationCompleted
FontLoader {
id: materialSymbolsFont
source: Qt.resolvedUrl("../assets/fonts/material-design-icons/variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf")
}
StyledText {
id: icon
anchors.fill: parent
font.family: materialSymbolsFont.name
font.pixelSize: Math.round(Theme.fontSizeMedium)
font.weight: root.weight
font.hintingPreference: Font.PreferNoHinting
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: root.smoothTransform ? Text.QtRendering : Text.NativeRendering
font.variableAxes: {
"FILL": root.fill.toFixed(1),
"GRAD": root.grade,
"opsz": 24,
"wght": root.weight
}
Behavior on font.weight {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on fill {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Timer {
id: rotationTimer
interval: 16
repeat: false
onTriggered: root.rotationCompleted()
}
onRotationChanged: {
rotationTimer.restart();
}
}
DankCommon.DankIcon {}
+2 -258
View File
@@ -1,259 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property string currentIcon: ""
property string iconType: "icon"
signal iconSelected(string iconName, string iconType)
width: 240
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: iconPopup.visible ? Theme.primary : Theme.outline
border.width: 1
property var iconCategories: [
{
"name": I18n.tr("Numbers"),
"icons": ["looks_one", "looks_two", "looks_3", "looks_4", "looks_5", "looks_6", "filter_1", "filter_2", "filter_3", "filter_4", "filter_5", "filter_6", "filter_7", "filter_8", "filter_9", "filter_9_plus", "plus_one", "exposure_plus_1", "exposure_plus_2"]
},
{
"name": I18n.tr("Workspace"),
"icons": ["work", "laptop", "desktop_windows", "folder", "view_module", "dashboard", "apps", "grid_view"]
},
{
"name": I18n.tr("Development"),
"icons": ["code", "terminal", "bug_report", "build", "engineering", "integration_instructions", "data_object", "schema", "api", "webhook"]
},
{
"name": I18n.tr("Communication"),
"icons": ["chat", "mail", "forum", "message", "video_call", "call", "contacts", "group", "notifications", "campaign"]
},
{
"name": I18n.tr("Media"),
"icons": ["music_note", "headphones", "mic", "videocam", "photo", "movie", "library_music", "album", "radio", "volume_up"]
},
{
"name": I18n.tr("System"),
"icons": ["memory", "storage", "developer_board", "monitor", "keyboard", "mouse", "battery_std", "wifi", "bluetooth", "security", "settings"]
},
{
"name": I18n.tr("Navigation"),
"icons": ["home", "arrow_forward", "arrow_back", "expand_more", "expand_less", "menu", "close", "search", "filter_list", "sort"]
},
{
"name": I18n.tr("Actions"),
"icons": ["add", "remove", "edit", "delete", "save", "download", "upload", "share", "content_copy", "content_paste", "content_cut", "undo", "redo"]
},
{
"name": I18n.tr("Status"),
"icons": ["check", "error", "warning", "info", "done", "pending", "schedule", "update", "sync", "offline_bolt"]
},
{
"name": I18n.tr("Fun"),
"icons": ["celebration", "cake", "star", "favorite", "pets", "sports_esports", "local_fire_department", "bolt", "auto_awesome", "diamond"]
}
]
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (iconPopup.visible) {
iconPopup.close();
return;
}
const pos = root.mapToItem(Overlay.overlay, 0, 0);
const popupHeight = 500;
const overlayHeight = Overlay.overlay?.height ?? 800;
iconPopup.x = pos.x;
if (pos.y + root.height + popupHeight + 4 > overlayHeight) {
iconPopup.y = pos.y - popupHeight - 4;
} else {
iconPopup.y = pos.y + root.height + 4;
}
iconPopup.open();
}
}
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: (root.iconType === "icon" && root.currentIcon) ? root.currentIcon : (root.iconType === "text" ? "text_fields" : "add")
size: 16
color: root.currentIcon ? Theme.surfaceText : Theme.outline
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.currentIcon ? root.currentIcon : I18n.tr("Choose icon")
font.pixelSize: Theme.fontSizeSmall
color: root.currentIcon ? Theme.surfaceText : Theme.outline
anchors.verticalCenter: parent.verticalCenter
width: 160
elide: Text.ElideRight
}
}
DankIcon {
name: iconPopup.visible ? "expand_less" : "expand_more"
size: 16
color: Theme.outline
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
}
Popup {
id: iconPopup
parent: root.Overlay.overlay
width: 320
height: Math.min(500, dropdownContent.implicitHeight + 32)
padding: 0
modal: true
dim: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
id: contentSurface
color: Theme.surface
radius: Theme.cornerRadius
ElevationShadow {
id: shadowLayer
anchors.fill: parent
z: -1
level: Theme.elevationLevel2
fallbackOffset: 4
targetRadius: contentSurface.radius
targetColor: contentSurface.color
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
shadowEnabled: Theme.elevationEnabled
}
Rectangle {
width: 24
height: 24
radius: 12
color: closeMouseArea.containsMouse ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0)
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
z: 1
DankIcon {
name: "close"
size: 16
color: closeMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: iconPopup.close()
}
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingS
contentHeight: dropdownContent.height
clip: true
pressDelay: 0
Column {
id: dropdownContent
width: parent.width
spacing: Theme.spacingM
Repeater {
model: root.iconCategories
Column {
required property var modelData
width: parent.width
spacing: Theme.spacingS
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
Flow {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: modelData.icons
Rectangle {
required property string modelData
width: 36
height: 36
radius: Theme.cornerRadius
color: iconMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
border.color: root.currentIcon === modelData ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: 2
DankIcon {
name: parent.modelData
size: 20
color: root.currentIcon === parent.modelData ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: iconMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.iconSelected(parent.modelData, "icon");
iconPopup.close();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}
}
}
function setIcon(iconName, type) {
root.iconType = type;
root.iconType = "icon";
root.currentIcon = iconName;
}
}
DankCommon.DankIconPicker {}
+2 -276
View File
@@ -1,277 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
import "ScrollConstants.js" as Scroll
import qs.DankCommon.Widgets as DankCommon
ListView {
id: listView
property real scrollBarTopMargin: 0
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
property real savedY: 0
property bool justChanged: false
property bool isUserScrolling: false
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: Scroll.friction
flickDeceleration: Scroll.flickDeceleration
maximumFlickVelocity: Scroll.maximumFlickVelocity
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
add: ListViewTransitions.add
remove: ListViewTransitions.remove
displaced: ListViewTransitions.displaced
move: ListViewTransitions.move
// QQmlDelegateModel can release a delegate back to its cache without hiding it,
// leaving a stale row painted over live ones. Hide anything the view no longer claims.
Connections {
target: listView.model?.objectName !== undefined ? listView.model : null
ignoreUnknownSignals: true
function onRowsInserted() {
orphanSweep.arm();
}
function onRowsRemoved() {
orphanSweep.arm();
}
function onRowsMoved() {
orphanSweep.arm();
}
function onModelReset() {
orphanSweep.arm();
}
}
FrameAnimation {
id: orphanSweep
property int frames: 0
function arm() {
frames = 0;
running = true;
}
onTriggered: {
const kids = listView.contentItem.children;
const rows = [];
for (let i = 0; i < kids.length; i++) {
const c = kids[i];
if (!c || c.index === undefined)
continue;
const claimed = listView.itemAtIndex(c.index) === c;
if (claimed && !c.visible) {
c.visible = true;
continue;
}
if (c.visible)
rows.push({
item: c,
claimed: claimed
});
}
for (let a = 0; a < rows.length; a++) {
if (rows[a].claimed)
continue;
for (let b = 0; b < rows.length; b++) {
if (a === b || !rows[b].claimed)
continue;
if (Math.abs(rows[a].item.y - rows[b].item.y) < rows[b].item.height / 2) {
rows[a].item.visible = false;
break;
}
}
}
if (++frames >= 3)
running = false;
}
}
onMovementStarted: {
isUserScrolling = true;
vbar._scrollBarActive = true;
vbar.hideTimer.stop();
}
onMovementEnded: {
isUserScrolling = false;
vbar.hideTimer.restart();
}
onContentYChanged: {
if (!justChanged && isUserScrolling) {
savedY = contentY;
}
justChanged = false;
}
onModelChanged: {
justChanged = true;
contentY = savedY;
}
WheelHandler {
id: wheelHandler
property real touchpadSpeed: Scroll.touchpadSpeed
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
property bool sessionUsedMouseWheel: false
function startMomentum() {
isMomentumActive = true;
momentumAnim.running = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
isUserScrolling = true;
vbar._scrollBarActive = true;
vbar.hideTimer.restart();
const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
let newY = listView.contentY + scrollAmount;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
} else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = listView.contentY - delta;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false;
momentumAnim.running = false;
isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) {
momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
}
}
if (timeDelta < Scroll.momentumTimeThreshold) {
momentum = momentum * Scroll.momentumRetention + delta * Scroll.momentumDeltaFactor;
delta += momentum;
} else {
momentum = 0;
}
let newY = listView.contentY - delta;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
}
event.accepted = true;
}
onActiveChanged: {
if (!active) {
isUserScrolling = false;
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
momentumVelocity = 0;
}
}
}
}
FrameAnimation {
id: momentumAnim
running: false
onTriggered: {
const dt = frameTime;
const newY = contentY - momentumVelocity * dt;
const maxY = Math.max(0, contentHeight - height + originY);
const minY = originY;
if (newY < minY || newY > maxY) {
contentY = newY < minY ? minY : maxY;
savedY = contentY;
running = false;
isMomentumActive = false;
momentumVelocity = 0;
return;
}
contentY = newY;
savedY = newY;
momentumVelocity *= Math.pow(friction, dt / 0.016);
if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
running = false;
isMomentumActive = false;
momentumVelocity = 0;
}
}
}
ScrollBar.vertical: DankScrollbar {
id: vbar
topPadding: listView.scrollBarTopMargin
}
}
DankCommon.DankListView {}
+2 -270
View File
@@ -1,271 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
activeFocusOnTab: true
KeyNavigation.tab: keyNavigationTab
KeyNavigation.backtab: keyNavigationBacktab
onActiveFocusChanged: {
if (activeFocus) {
locationInput.forceActiveFocus();
}
}
property string currentLocation: ""
property string placeholderText: I18n.tr("Search for a location...")
property bool _internalChange: false
property bool isLoading: false
property string currentSearchText: ""
property Item keyNavigationTab: null
property Item keyNavigationBacktab: null
signal locationSelected(string displayName, string coordinates)
function resetSearchState() {
locationSearchTimer.stop();
dropdownHideTimer.stop();
isLoading = false;
searchResultsModel.clear();
}
// CJK city names are commonly two code points (北京, 東京, 서울).
function canSearch(t) {
return t.length > 2 || (t.length >= 2 && /[぀-ヿ㐀-䶿一-鿿豈-﫿가-힯]/.test(t));
}
width: parent.width
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
ListModel {
id: searchResultsModel
}
Timer {
id: locationSearchTimer
interval: 500
running: false
repeat: false
onTriggered: {
if (root.canSearch(locationInput.text)) {
searchResultsModel.clear();
root.isLoading = true;
const searchLocation = locationInput.text;
root.currentSearchText = searchLocation;
const encodedLocation = encodeURIComponent(searchLocation);
const searchUrl = "https://nominatim.openstreetmap.org/search?q=" + encodedLocation + "&format=json&limit=5&addressdetails=1";
Proc.runCommand("locationSearch", [Proc.dmsBin, "dl", "-4", "--timeout", "10", searchUrl], (output, exitCode) => {
root.isLoading = false;
if (exitCode !== 0) {
searchResultsModel.clear();
return;
}
if (root.currentSearchText !== locationInput.text)
return;
const raw = output.trim();
searchResultsModel.clear();
if (!raw || raw[0] !== "[") {
return;
}
try {
const data = JSON.parse(raw);
if (data.length === 0) {
return;
}
for (var i = 0; i < Math.min(data.length, 5); i++) {
const location = data[i];
if (location.display_name && location.lat && location.lon) {
const parts = location.display_name.split(', ');
let cleanName = parts[0];
if (parts.length > 1) {
const state = parts[parts.length - 2];
if (state && state !== cleanName)
cleanName += `, ${state}`;
}
const query = `${location.lat},${location.lon}`;
searchResultsModel.append({
"name": cleanName,
"query": query
});
}
}
} catch (e) {}
});
}
}
}
Timer {
id: dropdownHideTimer
interval: 200
running: false
repeat: false
onTriggered: {
if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
root.resetSearchState();
}
}
Item {
id: searchInputField
width: parent.width
height: 48
DankTextField {
id: locationInput
width: parent.width
height: parent.height
leftIconName: "search"
placeholderText: root.placeholderText
text: ""
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
keyNavigationTab: root.keyNavigationTab
keyNavigationBacktab: root.keyNavigationBacktab
onTextEdited: {
if (root._internalChange)
return;
if (getActiveFocus()) {
if (root.canSearch(text)) {
root.isLoading = true;
locationSearchTimer.restart();
} else {
root.resetSearchState();
}
}
}
onFocusStateChanged: hasFocus => {
if (hasFocus) {
dropdownHideTimer.stop();
} else {
dropdownHideTimer.start();
}
}
}
DankIcon {
name: root.isLoading ? "hourglass_empty" : (searchResultsModel.count > 0 ? "check_circle" : "error")
size: Theme.iconSize - 4
color: root.isLoading ? Theme.surfaceVariantText : (searchResultsModel.count > 0 ? Theme.primary : Theme.error)
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
opacity: (locationInput.getActiveFocus() && root.canSearch(locationInput.text)) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
StyledRect {
id: searchDropdown
property bool hovered: false
width: parent.width
height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
y: searchInputField.height
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.primarySelected
border.width: 1
visible: locationInput.getActiveFocus() && root.canSearch(locationInput.text) && (searchResultsModel.count > 0 || root.isLoading)
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
parent.hovered = true;
dropdownHideTimer.stop();
}
onExited: {
parent.hovered = false;
if (!locationInput.getActiveFocus())
dropdownHideTimer.start();
}
acceptedButtons: Qt.NoButton
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
DankListView {
id: searchResultsList
anchors.fill: parent
clip: true
model: searchResultsModel
spacing: Theme.spacingXXS
delegate: StyledRect {
width: searchResultsList.width
height: 36
radius: Theme.cornerRadius
color: resultMouseArea.containsMouse ? Theme.surfaceLight : Theme.withAlpha(Theme.surfaceLight, 0)
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: "place"
size: Theme.iconSize - 6
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: model.name || "Unknown"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - 30
}
}
MouseArea {
id: resultMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root._internalChange = true;
const selectedName = model.name;
const selectedQuery = model.query;
locationInput.text = selectedName;
root.locationSelected(selectedName, selectedQuery);
root.resetSearchState();
locationInput.setFocus(false);
root._internalChange = false;
}
}
}
}
StyledText {
anchors.centerIn: parent
text: root.isLoading ? "Searching..." : "No locations found"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: searchResultsList.count === 0 && root.canSearch(locationInput.text)
}
}
}
}
DankCommon.DankLocationSearch {}
+2 -163
View File
@@ -1,164 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string name: ""
property int size: Theme.fontSizeMedium
property alias color: icon.color
width: size
height: size
visible: text.length > 0
// This is for file browser, particularly - might want another map later for app IDs
readonly property var iconMap: ({
// --- Distribution logos ---
"debian": "\u{f08da}",
"arch": "\u{f08c7}",
"archcraft": "\u{f345}",
"guix": "\u{f325}",
"fedora": "\u{f08db}",
"nixos": "\u{f1105}",
"ubuntu": "\u{f0548}",
"gentoo": "\u{f08e8}",
"endeavouros": "\u{f322}",
"manjaro": "\u{f160a}",
"opensuse": "\u{f314}",
"artix": "\u{f31f}",
"void": "\u{f32e}",
// --- special types ---
"folder": "\u{F024B}",
"file": "\u{F0214}",
// --- special filenames (no extension) ---
"docker": "\u{F0868}",
"makefile": "\u{F09EE}",
"license": "\u{F09EE}",
"readme": "\u{F0354}",
// --- programming languages ---
"rs": "\u{F1617}",
"dart": "\u{e798}",
"go": "\u{F07D3}",
"py": "\u{F0320}",
"js": "\u{F031E}",
"jsx": "\u{F031E}",
"ts": "\u{F06E6}",
"tsx": "\u{F06E6}",
"java": "\u{F0B37}",
"c": "\u{F0671}",
"cpp": "\u{F0672}",
"cxx": "\u{F0672}",
"h": "\u{F0672}",
"hpp": "\u{F0672}",
"cs": "\u{F031B}",
"html": "\u{e60e}",
"htm": "\u{e60e}",
"css": "\u{E6b8}",
"scss": "\u{F031C}",
"less": "\u{F031C}",
"md": "\u{F0354}",
"markdown": "\u{F0354}",
"json": "\u{eb0f}",
"jsonc": "\u{eb0f}",
"yaml": "\u{e8eb}",
"yml": "\u{e8eb}",
"xml": "\u{F09EE}",
"sql": "\u{f1c0}",
// --- scripts / shells ---
"sh": "\u{f0bc1}",
"bash": "\u{f0bc1}",
"zsh": "\u{f0bc1}",
"fish": "\u{f0bc1}",
"ps1": "\u{f0bc1}",
"bat": "\u{f0bc1}",
// --- data / config ---
"toml": "\u{e6b2}",
"ini": "\u{F09EE}",
"conf": "\u{F09EE}",
"cfg": "\u{F09EE}",
"csv": "\u{eefc}",
"tsv": "\u{F021C}",
// --- docs / office ---
"pdf": "\u{F0226}",
"doc": "\u{F09EE}",
"docx": "\u{F09EE}",
"rtf": "\u{F09EE}",
"ppt": "\u{F09EE}",
"pptx": "\u{F09EE}",
"log": "\u{F09EE}",
"xls": "\u{F021C}",
"xlsx": "\u{F021C}",
// --- images ---
"ico": "\u{F021F}",
// --- audio / video ---
"mp3": "\u{e638}",
"wav": "\u{e638}",
"flac": "\u{e638}",
"ogg": "\u{e638}",
"mp4": "\u{f0567}",
"mkv": "\u{f0567}",
"webm": "\u{f0567}",
"mov": "\u{f0567}",
// --- archives / packages ---
"zip": "\u{e6aa}",
"tar": "\u{f003c}",
"gz": "\u{f003c}",
"bz2": "\u{f003c}",
"7z": "\u{f003c}",
// --- containers / infra / cloud ---
"dockerfile": "\u{F0868}",
"yml.k8s": "\u{F09EE}",
"yaml.k8s": "\u{F09EE}",
"tf": "\u{F09EE}",
"tfvars": "\u{F09EE}",
// --- moon phases
"moon_new": "\u{F0F64}",
"moon_waxing_crescent": "\u{F0F67}",
"moon_first_quarter": "\u{F0F61}",
"moon_waxing_gibbous": "\u{F0F68}",
"moon_full": "\u{F0F62}",
"moon_waning_gibbous": "\u{F0F66}",
"moon_last_quarter": "\u{F0F63}",
"moon_waning_crescent": "\u{F0F65}"
})
readonly property string text: iconMap[name] || iconMap["file"] || ""
function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile")) {
return "docker";
}
const ext = fileName.split('.').pop();
return ext || "";
}
FontLoader {
id: firaCodeFont
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
}
StyledText {
id: icon
anchors.centerIn: parent
font.family: firaCodeFont.name
font.pixelSize: root.size
color: Theme.surfaceText
text: root.text
antialiasing: true
}
}
DankCommon.DankNFIcon {}
+2 -72
View File
@@ -1,73 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Column {
id: root
property string text: ""
property var incrementTooltipText: ""
property var decrementTooltipText: ""
property var onIncrement: undefined
property var onDecrement: undefined
property string incrementIconName: "keyboard_arrow_up"
property string decrementIconName: "keyboard_arrow_down"
property bool incrementEnabled: true
property bool decrementEnabled: true
property color textColor: Theme.surfaceText
property color iconColor: Theme.withAlpha(Theme.surfaceText, 0.5)
property color backgroundColor: Theme.primary
property int textSize: Theme.fontSizeSmall
property var iconSize: 12
property int buttonSize: 20
property int horizontalPadding: Theme.spacingL
readonly property bool effectiveIncrementEnabled: root.onIncrement ? root.incrementEnabled : false
readonly property bool effectiveDecrementEnabled: root.onDecrement ? root.decrementEnabled : false
width: Math.max(buttonSize * 2, root.implicitWidth + horizontalPadding * 2)
spacing: Theme.spacingXS
DankActionButton {
anchors.horizontalCenter: parent.horizontalCenter
enabled: root.effectiveIncrementEnabled
iconColor: root.effectiveIncrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
iconSize: root.iconSize
buttonSize: root.buttonSize
iconName: root.incrementIconName
onClicked: if (typeof root.onIncrement === 'function')
root.onIncrement()
tooltipText: root.incrementTooltipText
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: 5
height: 1
}
StyledText {
isMonospace: true
text: root.text
font.pixelSize: root.textSize
color: root.textColor
}
Item {
width: 5
height: 1
}
}
DankActionButton {
anchors.horizontalCenter: parent.horizontalCenter
enabled: root.effectiveDecrementEnabled
iconColor: root.effectiveDecrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
iconSize: root.iconSize
buttonSize: root.buttonSize
iconName: root.decrementIconName
onClicked: if (typeof root.onDecrement === 'function')
root.onDecrement()
tooltipText: root.decrementTooltipText
}
}
DankCommon.DankNumberStepper {}
+2 -104
View File
@@ -1,105 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property color rippleColor: Theme.primary
property real cornerRadius: 0
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
property real _rippleX: 0
property real _rippleY: 0
property real _rippleMaxRadius: 0
readonly property alias animating: rippleAnim.running
anchors.fill: parent
function trigger(x, y) {
if (!enableRipple || Theme.currentAnimationSpeed === SettingsData.AnimationSpeed.None)
return;
_rippleX = x;
_rippleY = y;
const dist = (ox, oy) => ox * ox + oy * oy;
_rippleMaxRadius = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y)));
rippleAnim.restart();
}
SequentialAnimation {
id: rippleAnim
PropertyAction {
target: rippleFx
property: "rippleCenterX"
value: root._rippleX
}
PropertyAction {
target: rippleFx
property: "rippleCenterY"
value: root._rippleY
}
PropertyAction {
target: rippleFx
property: "rippleRadius"
value: 0
}
PropertyAction {
target: rippleFx
property: "rippleOpacity"
value: 0.10
}
ParallelAnimation {
DankAnim {
target: rippleFx
property: "rippleRadius"
from: 0
to: root._rippleMaxRadius
duration: root.animationDuration
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
SequentialAnimation {
PauseAnimation {
duration: Math.round(root.animationDuration * 0.6)
}
DankAnim {
target: rippleFx
property: "rippleOpacity"
to: 0
duration: root.animationDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
}
ShaderEffect {
id: rippleFx
visible: rippleAnim.running
property real rippleCenterX: 0
property real rippleCenterY: 0
property real rippleRadius: 0
property real rippleOpacity: 0
x: Math.max(0, rippleCenterX - rippleRadius)
y: Math.max(0, rippleCenterY - rippleRadius)
width: Math.max(0, Math.min(root.width, rippleCenterX + rippleRadius) - x)
height: Math.max(0, Math.min(root.height, rippleCenterY + rippleRadius) - y)
property real widthPx: width
property real heightPx: height
property real cornerRadiusPx: root.cornerRadius
property real offsetX: x
property real offsetY: y
property real parentWidth: root.width
property real parentHeight: root.height
property vector4d rippleCol: Qt.vector4d(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, root.rippleColor.a)
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/ripple.frag.qsb")
}
}
DankCommon.DankRipple {}
+2 -80
View File
@@ -1,81 +1,3 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
required property string source
property int size: 24
property string cornerIcon: ""
property int cornerIconSize: Math.max(10, size * 0.4)
property color cornerIconColor: Theme.surfaceText
property color cornerIconBackground: Theme.surface
property color colorOverride: "transparent"
property real brightnessOverride: 0.0
property real contrastOverride: 0.0
property real saturationOverride: 0.0
readonly property bool hasCornerIcon: cornerIcon !== ""
readonly property bool hasColorOverride: colorOverride.a > 0
readonly property bool hasColorEffect: hasColorOverride || brightnessOverride !== 0.0 || contrastOverride !== 0.0 || saturationOverride !== 0.0
readonly property string resolvedSource: {
if (!source)
return "";
if (source.startsWith("file://"))
return source;
if (source.startsWith("/"))
return "file://" + source;
if (source.startsWith("qrc:"))
return source;
return source;
}
implicitWidth: size
implicitHeight: size
IconImage {
id: iconImage
anchors.fill: parent
source: root.resolvedSource
smooth: true
mipmap: true
asynchronous: true
implicitSize: root.size * 2
backer.sourceSize.width: root.size * 2
backer.sourceSize.height: root.size * 2
backer.cache: true
layer.enabled: root.hasColorEffect
layer.smooth: true
layer.mipmap: true
layer.effect: MultiEffect {
saturation: root.saturationOverride
colorization: root.hasColorOverride ? 1 : 0
colorizationColor: root.colorOverride
brightness: root.brightnessOverride
contrast: root.contrastOverride
}
}
Rectangle {
visible: root.hasCornerIcon
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: -2
anchors.bottomMargin: -2
width: root.cornerIconSize + 4
height: root.cornerIconSize + 4
radius: width / 2
color: root.cornerIconBackground
border.width: 1
border.color: Theme.surfaceLight
DankIcon {
anchors.centerIn: parent
name: root.cornerIcon
size: root.cornerIconSize
color: root.cornerIconColor
}
}
}
DankCommon.DankSVGIcon {}
+2 -45
View File
@@ -1,46 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.DankCommon.Widgets as DankCommon
ScrollBar {
id: scrollbar
property var targetFlickable: null
readonly property var _target: targetFlickable ?? parent
property bool _scrollBarActive: false
property alias hideTimer: hideScrollBarTimer
property bool _isParentMoving: _target && (_target.moving || _target.flicking || _target.isMomentumActive)
property bool _shouldShow: pressed || hovered || active || _isParentMoving || _scrollBarActive
policy: (_target && _target.contentHeight > _target.height) ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
minimumSize: 0.08
implicitWidth: 10
interactive: true
hoverEnabled: true
z: 1000
opacity: (policy !== ScrollBar.AlwaysOff && _shouldShow) ? 1.0 : 0.0
visible: policy !== ScrollBar.AlwaysOff
Behavior on opacity {
NumberAnimation {
duration: 160
easing.type: Easing.OutQuad
}
}
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: scrollbar.pressed ? Theme.primary : scrollbar._shouldShow ? Theme.outline : Theme.outlineMedium
opacity: scrollbar.pressed ? 1.0 : scrollbar._shouldShow ? 1.0 : 0.6
}
background: Item {}
Timer {
id: hideScrollBarTimer
interval: 1200
onTriggered: scrollbar._scrollBarActive = false
}
}
DankCommon.DankScrollbar {}
+2 -307
View File
@@ -1,308 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: slider
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property int value: 50
property int minimum: 0
property int maximum: 100
property int step: 1
property string leftIcon: ""
property string rightIcon: ""
property string unit: "%"
property bool showValue: true
property bool isDragging: false
property bool wheelEnabled: true
property bool centerMinimum: false
property real valueOverride: -1
property bool alwaysShowValue: false
readonly property bool containsMouse: sliderMouseArea.containsMouse
property color thumbOutlineColor: Theme.surfaceContainer
property color trackColor: enabled ? Theme.outline : Theme.outline
property bool usePopupTransparency: !checkParentDisablesTransparency()
property real trackOpacity: usePopupTransparency ? Theme.popupTransparency : 1.0
signal sliderValueChanged(int newValue)
signal sliderDragFinished(int finalValue)
height: 48
function updateValueFromPosition(x) {
let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width)));
if (centerMinimum)
ratio = Math.max(0, (ratio - 0.5) * 2);
let rawValue = minimum + ratio * (maximum - minimum);
let newValue = step > 1 ? Math.round(rawValue / step) * step : Math.round(rawValue);
newValue = Math.max(minimum, Math.min(maximum, newValue));
if (newValue !== value) {
value = newValue;
sliderValueChanged(newValue);
}
}
Row {
anchors.centerIn: parent
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: slider.leftIcon
size: Theme.iconSize
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
anchors.verticalCenter: parent.verticalCenter
visible: slider.leftIcon.length > 0
}
StyledRect {
id: sliderTrack
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
height: 12
radius: Theme.cornerRadius
color: Theme.withAlpha(slider.trackColor, slider.trackOpacity)
anchors.verticalCenter: parent.verticalCenter
clip: false
StyledRect {
id: sliderFill
height: parent.height
radius: Theme.cornerRadius
topRightRadius: 0
bottomRightRadius: 0
width: {
const range = slider.maximum - slider.minimum;
const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
const travel = sliderTrack.width - sliderHandle.width;
const handleLeft = travel * ratio;
const endPoint = handleLeft - 3;
return Math.max(0, Math.min(sliderTrack.width, endPoint));
}
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
}
StyledRect {
id: sliderHandle
property bool active: sliderMouseArea.containsMouse || sliderMouseArea.pressed || slider.isDragging
width: 4
height: 20
radius: Theme.cornerRadius
x: {
const range = slider.maximum - slider.minimum;
const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
const travel = sliderTrack.width - width;
return Math.max(0, Math.min(travel, travel * ratio));
}
anchors.verticalCenter: parent.verticalCenter
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
border.width: 0
border.color: slider.thumbOutlineColor
StyledRect {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.onPrimary
opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0
visible: opacity > 0
}
StyledRect {
anchors.centerIn: parent
width: parent.width + 20
height: parent.height + 20
radius: width / 2
color: "transparent"
border.width: 2
border.color: Theme.primary
opacity: slider.enabled && slider.focus ? 0.3 : 0
visible: opacity > 0
}
Rectangle {
id: ripple
anchors.centerIn: parent
width: 0
height: 0
radius: width / 2
color: Theme.onPrimary
opacity: 0
function start() {
opacity = 0.16;
width = 0;
height = 0;
rippleAnimation.start();
}
SequentialAnimation {
id: rippleAnimation
NumberAnimation {
target: ripple
properties: "width,height"
to: 28
duration: 180
}
NumberAnimation {
target: ripple
property: "opacity"
to: 0
duration: 150
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onPressedChanged: {
if (pressed && slider.enabled) {
ripple.start();
}
}
}
scale: active ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Item {
id: sliderContainer
anchors.fill: parent
MouseArea {
id: sliderMouseArea
property bool isDragging: false
anchors.fill: parent
anchors.topMargin: -10
anchors.bottomMargin: -10
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: slider.enabled
preventStealing: true
acceptedButtons: Qt.LeftButton
onWheel: wheelEvent => {
if (!slider.wheelEnabled) {
wheelEvent.accepted = false;
return;
}
let wheelStep = slider.step > 1 ? slider.step : Math.max(1, (maximum - minimum) / 100);
let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + wheelStep) : Math.max(minimum, value - wheelStep);
if (slider.step > 1)
newValue = Math.round(newValue / slider.step) * slider.step;
newValue = Math.round(newValue);
if (newValue !== value) {
value = newValue;
sliderValueChanged(newValue);
}
wheelEvent.accepted = true;
}
onPressed: mouse => {
if (slider.enabled) {
slider.isDragging = true;
sliderMouseArea.isDragging = true;
updateValueFromPosition(mouse.x);
}
}
onReleased: {
if (slider.enabled) {
slider.isDragging = false;
sliderMouseArea.isDragging = false;
slider.sliderDragFinished(slider.value);
}
}
onPositionChanged: mouse => {
if (pressed && slider.isDragging && slider.enabled) {
updateValueFromPosition(mouse.x);
}
}
onClicked: mouse => {
if (slider.enabled && !slider.isDragging) {
updateValueFromPosition(mouse.x);
}
}
}
}
StyledRect {
id: valueTooltip
width: tooltipText.reservedWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingM
x: Math.max(0, Math.min(parent.width - width, sliderHandle.x + sliderHandle.width / 2 - width / 2))
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
opacity: visible ? 1 : 0
NumericText {
id: tooltipText
text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
reserveText: {
let widest = "";
const samples = [slider.minimum, slider.maximum];
if (slider.valueOverride >= 0)
samples.push(slider.valueOverride);
for (let i = 0; i < samples.length; i++) {
const candidate = Math.round(samples[i]) + slider.unit;
if (candidate.length > widest.length)
widest = candidate;
}
return widest;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
font.hintingPreference: Font.PreferFullHinting
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
DankIcon {
name: slider.rightIcon
size: Theme.iconSize
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
anchors.verticalCenter: parent.verticalCenter
visible: slider.rightIcon.length > 0
}
}
}
DankCommon.DankSlider {}
+2 -107
View File
@@ -1,108 +1,3 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property real size: 48
property real strokeWidth: Math.max(2, size / 12)
property color color: Theme.primary
property bool running: visible
implicitWidth: size
implicitHeight: size
onRunningChanged: {
if (running)
return;
arc.rotation = 0;
arc.startAngle = 0;
arc.sweepAngle = 16;
}
Item {
id: rotator
anchors.fill: parent
RotationAnimator on rotation {
from: 0
to: 360
duration: 1568
loops: Animation.Infinite
running: root.running
}
Shape {
id: arc
property real startAngle: 0
property real sweepAngle: 16
anchors.fill: parent
antialiasing: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: root.color
strokeWidth: root.strokeWidth
fillColor: "transparent"
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: arc.width / 2
centerY: arc.height / 2
radiusX: Math.max(1, (Math.min(arc.width, arc.height) - root.strokeWidth) / 2)
radiusY: radiusX
startAngle: arc.startAngle
sweepAngle: arc.sweepAngle
}
}
}
SequentialAnimation {
running: root.running
loops: Animation.Infinite
NumberAnimation {
target: arc
property: "sweepAngle"
from: 16
to: 270
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
ParallelAnimation {
NumberAnimation {
target: arc
property: "startAngle"
from: 0
to: 254
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
NumberAnimation {
target: arc
property: "sweepAngle"
from: 270
to: 16
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
ScriptAction {
script: {
arc.rotation = (arc.rotation + 254) % 360;
arc.startAngle = 0;
}
}
}
}
}
DankCommon.DankSpinner {}
+2 -269
View File
@@ -1,270 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
FocusScope {
id: tabBar
property alias model: tabRepeater.model
property int currentIndex: 0
property int spacing: Theme.spacingL
property int tabHeight: 56
property bool showIcons: true
property bool equalWidthTabs: true
property bool enableArrowNavigation: true
property Item nextFocusTarget: null
property Item previousFocusTarget: null
signal tabClicked(int index)
signal actionTriggered(int index)
focus: false
activeFocusOnTab: true
height: tabHeight
KeyNavigation.tab: nextFocusTarget
KeyNavigation.down: nextFocusTarget
KeyNavigation.backtab: previousFocusTarget
KeyNavigation.up: previousFocusTarget
Keys.onPressed: event => {
if (!tabBar.activeFocus || tabRepeater.count === 0)
return;
function findSelectableIndex(startIndex, step) {
let idx = startIndex;
for (let i = 0; i < tabRepeater.count; i++) {
idx = (idx + step + tabRepeater.count) % tabRepeater.count;
const item = tabRepeater.itemAt(idx);
if (item && !item.isAction)
return idx;
}
return -1;
}
const goToIndex = nextIndex => {
if (nextIndex >= 0 && nextIndex !== tabBar.currentIndex) {
tabBar.currentIndex = nextIndex;
tabBar.tabClicked(nextIndex);
}
};
const resolveTarget = item => {
if (!item)
return null;
if (item.focusTarget)
return resolveTarget(item.focusTarget);
return item;
};
const focusItem = item => {
const target = resolveTarget(item);
if (!target)
return false;
if (target.requestFocus) {
Qt.callLater(() => target.requestFocus());
return true;
}
if (target.forceActiveFocus) {
Qt.callLater(() => target.forceActiveFocus());
return true;
}
return false;
};
if (event.key === Qt.Key_Right && tabBar.enableArrowNavigation) {
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : -1;
const nextIndex = findSelectableIndex(baseIndex, 1);
if (nextIndex >= 0) {
goToIndex(nextIndex);
event.accepted = true;
}
} else if (event.key === Qt.Key_Left && tabBar.enableArrowNavigation) {
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : 0;
const nextIndex = findSelectableIndex(baseIndex, -1);
if (nextIndex >= 0) {
goToIndex(nextIndex);
event.accepted = true;
}
} else if (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) {
if (focusItem(tabBar.previousFocusTarget)) {
event.accepted = true;
}
} else if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down) {
if (focusItem(tabBar.nextFocusTarget)) {
event.accepted = true;
}
} else if (event.key === Qt.Key_Up) {
if (focusItem(tabBar.previousFocusTarget)) {
event.accepted = true;
}
}
}
Row {
id: tabRow
anchors.fill: parent
spacing: tabBar.spacing
Repeater {
id: tabRepeater
Item {
id: tabItem
property bool isAction: modelData && modelData.isAction === true
property bool isActive: !isAction && tabBar.currentIndex === index
property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0
property bool hasText: modelData && modelData.text && modelData.text.length > 0
width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * Math.max(0, tabRepeater.count - 1)) / Math.max(1, tabRepeater.count) : Math.max(contentCol.implicitWidth + Theme.spacingXL, 64)
height: tabBar.tabHeight
Column {
id: contentCol
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: modelData.icon || ""
anchors.horizontalCenter: parent.horizontalCenter
size: Theme.iconSize
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
visible: hasIcon
}
StyledText {
text: modelData.text || ""
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Theme.fontSizeMedium
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
visible: hasText
}
}
Rectangle {
id: stateLayer
anchors.fill: parent
color: Theme.surfaceTint
opacity: tabArea.pressed ? 0.12 : (tabArea.containsMouse ? 0.08 : 0)
visible: opacity > 0
radius: Theme.cornerRadius
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: tabRipple
cornerRadius: Theme.cornerRadius
}
MouseArea {
id: tabArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => tabRipple.trigger(mouse.x, mouse.y)
onClicked: {
if (tabItem.isAction) {
tabBar.actionTriggered(index);
} else {
tabBar.tabClicked(index);
}
}
}
}
}
}
Rectangle {
id: indicator
y: parent.height + 7
height: 3
width: 60
topLeftRadius: Theme.cornerRadius
topRightRadius: Theme.cornerRadius
bottomLeftRadius: 0
bottomRightRadius: 0
color: Theme.primary
visible: false
property bool animationEnabled: false
property bool initialSetupComplete: false
Behavior on x {
enabled: indicator.animationEnabled
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
enabled: indicator.animationEnabled
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: 1
y: parent.height + 10
color: Theme.outlineStrong
}
function updateIndicator() {
if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) {
indicator.visible = false;
indicator.initialSetupComplete = false;
return;
}
const item = tabRepeater.itemAt(currentIndex);
if (!item || item.isAction) {
return;
}
const tabPos = item.mapToItem(tabBar, 0, 0);
const tabCenterX = tabPos.x + item.width / 2;
const indicatorWidth = 60;
if (tabPos.x < 10 && currentIndex > 0) {
Qt.callLater(updateIndicator);
return;
}
if (!indicator.initialSetupComplete) {
indicator.animationEnabled = false;
indicator.width = indicatorWidth;
indicator.x = tabCenterX - indicatorWidth / 2;
indicator.visible = true;
indicator.initialSetupComplete = true;
indicator.animationEnabled = true;
} else {
indicator.width = indicatorWidth;
indicator.x = tabCenterX - indicatorWidth / 2;
indicator.visible = true;
}
}
function snapIndicator() {
indicator.initialSetupComplete = false;
updateIndicator();
}
onCurrentIndexChanged: {
Qt.callLater(updateIndicator);
}
onWidthChanged: Qt.callLater(updateIndicator)
}
DankCommon.DankTabBar {}
+2 -30
View File
@@ -1,31 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property bool shown: true
property bool blinkOn: true
readonly property int flashTime: Qt.styleHints.cursorFlashTime
function resetBlink() {
blinkOn = true;
blinkTimer.restart();
}
width: 2
radius: 1
color: Theme.primary
visible: shown && blinkOn
onShownChanged: resetBlink()
Timer {
id: blinkTimer
running: root.shown && root.flashTime > 1
interval: root.flashTime / 2
repeat: true
onTriggered: root.blinkOn = !root.blinkOn
}
}
DankCommon.DankTextCursor {}
+2 -289
View File
@@ -1,290 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
StyledRect {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
KeyNavigation.tab: keyNavigationTab
KeyNavigation.backtab: keyNavigationBacktab
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property alias text: textInput.text
property alias cursorPosition: textInput.cursorPosition
property string placeholderText: ""
property string labelText: ""
property alias font: textInput.font
property alias textColor: textInput.color
property alias echoMode: textInput.echoMode
property alias validator: textInput.validator
property alias maximumLength: textInput.maximumLength
property string leftIconName: ""
property int leftIconSize: Theme.iconSize
property color leftIconColor: Theme.surfaceVariantText
property color leftIconFocusedColor: Theme.primary
property bool showClearButton: false
property bool showPasswordToggle: false
property real rightAccessoryWidth: 0
property bool passwordVisible: false
property bool usePopupTransparency: !checkParentDisablesTransparency()
property color backgroundColor: usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
property color focusedBorderColor: Theme.primary
property color normalBorderColor: Theme.outlineMedium
property color placeholderColor: Theme.outlineButton
property real borderWidth: 1
property real focusedBorderWidth: 2
property real cornerRadius: Theme.cornerRadius
readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0)
readonly property real rightPadding: {
let p = Theme.spacingS + rightAccessoryWidth;
if (showPasswordToggle)
p += 20 + Theme.spacingXS;
if (showClearButton && text.length > 0)
p += 20 + Theme.spacingXS;
return p;
}
property real topPadding: Theme.spacingS
property real bottomPadding: Theme.spacingS
property bool ignoreLeftRightKeys: false
property bool ignoreUpDownKeys: false
property bool ignoreTabKeys: false
property var keyForwardTargets: []
property Item keyNavigationTab: null
property Item keyNavigationBacktab: null
signal textEdited
signal editingFinished
signal accepted
signal focusStateChanged(bool hasFocus)
function getActiveFocus() {
return textInput.activeFocus;
}
function setFocus(value) {
textInput.focus = value;
}
function forceActiveFocus() {
textInput.forceActiveFocus();
}
function selectAll() {
textInput.selectAll();
}
function clear() {
textInput.clear();
}
function insertText(str) {
textInput.insert(textInput.cursorPosition, str);
}
readonly property real labelBandHeight: Math.round(Theme.fontSizeSmall * 1.4) + Theme.spacingXS * 2
width: 200
height: labelText !== "" ? Math.round(Theme.fontSizeMedium * 3) + labelBandHeight : Math.round(Theme.fontSizeMedium * 3)
radius: cornerRadius
color: backgroundColor
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
border.width: textInput.activeFocus ? focusedBorderWidth : borderWidth
DankIcon {
id: leftIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: textInput.verticalCenter
name: leftIconName
size: leftIconSize
color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor
visible: leftIconName !== ""
}
StyledText {
id: fieldLabel
anchors.left: textInput.left
anchors.right: textInput.right
anchors.top: parent.top
anchors.topMargin: Theme.spacingXS
text: root.labelText
visible: root.labelText !== ""
font.pixelSize: Theme.fontSizeSmall
color: textInput.activeFocus ? Theme.primary : Theme.surfaceVariantText
elide: Text.ElideRight
}
TextInput {
id: textInput
anchors.left: leftIcon.visible ? leftIcon.right : parent.left
anchors.leftMargin: Theme.spacingM
anchors.right: rightButtonsRow.left
anchors.rightMargin: rightButtonsRow.visible ? Theme.spacingS : Theme.spacingM
anchors.top: parent.top
anchors.topMargin: root.labelText !== "" ? root.labelBandHeight : root.topPadding
anchors.bottom: parent.bottom
anchors.bottomMargin: root.bottomPadding
font.pixelSize: Theme.fontSizeMedium
font.family: Theme.fontFamily
color: Theme.surfaceText
selectionColor: Theme.primaryContainer
selectedTextColor: Theme.primary
horizontalAlignment: TextInput.AlignLeft
verticalAlignment: TextInput.AlignVCenter
selectByMouse: !root.ignoreLeftRightKeys
clip: true
activeFocusOnTab: true
KeyNavigation.tab: root.keyNavigationTab
KeyNavigation.backtab: root.keyNavigationBacktab
onTextChanged: root.textEdited()
onEditingFinished: root.editingFinished()
onAccepted: root.accepted()
onActiveFocusChanged: root.focusStateChanged(activeFocus)
Keys.forwardTo: root.keyForwardTargets
Keys.onLeftPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
// Allow normal TextInput cursor movement
event.accepted = false;
}
}
Keys.onRightPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
event.accepted = false;
}
}
Keys.onPressed: event => {
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false;
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event);
}
return;
}
if (root.ignoreUpDownKeys && (event.key === Qt.Key_Up || event.key === Qt.Key_Down)) {
event.accepted = false;
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event);
}
return;
}
if ((event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) && root.keyForwardTargets.length > 0) {
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event);
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
}
Row {
id: rightButtonsRow
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS + root.rightAccessoryWidth
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: showPasswordToggle || (showClearButton && text.length > 0)
StyledRect {
id: passwordToggleButton
width: 20
height: 20
radius: 10
color: passwordToggleArea.containsMouse ? Theme.outlineStrong : Theme.withAlpha(Theme.outlineStrong, 0)
visible: showPasswordToggle
DankIcon {
anchors.centerIn: parent
name: passwordVisible ? "visibility_off" : "visibility"
size: 14
color: passwordToggleArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
}
MouseArea {
id: passwordToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: passwordVisible = !passwordVisible
}
}
StyledRect {
id: clearButton
width: 20
height: 20
radius: 10
color: clearArea.containsMouse ? Theme.outlineStrong : Theme.withAlpha(Theme.outlineStrong, 0)
visible: showClearButton && text.length > 0
DankIcon {
anchors.centerIn: parent
name: "close"
size: 14
color: clearArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: textInput.text = ""
}
}
}
StyledText {
id: placeholderLabel
anchors.fill: textInput
text: root.placeholderText
font: textInput.font
color: placeholderColor
horizontalAlignment: Text.AlignLeft
verticalAlignment: textInput.verticalAlignment
visible: textInput.text.length === 0 && !textInput.activeFocus
elide: I18n.isRtl ? Text.ElideLeft : Text.ElideRight
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankCommon.DankTextField {}
+2 -193
View File
@@ -1,194 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: toggle
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
// API
property bool checked: false
property bool toggling: false
property string text: ""
property string description: ""
property color descriptionColor: Theme.surfaceVariantText
property bool hideText: false
signal clicked
signal toggled(bool checked)
signal toggleCompleted(bool checked)
readonly property bool showText: text && !hideText
readonly property int trackWidth: 52
readonly property int trackHeight: 30
readonly property int insetCircle: 24
width: showText ? parent.width : trackWidth
height: showText ? Math.max(trackHeight, textColumn.implicitHeight + Theme.spacingM * 2) : trackHeight
function handleClick() {
if (!enabled)
return;
clicked();
toggled(!checked);
}
StyledRect {
id: background
anchors.fill: parent
radius: showText ? Theme.cornerRadius : 0
color: "transparent"
visible: showText
StateLayer {
visible: showText
disabled: !toggle.enabled
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: toggle.handleClick()
}
}
Row {
anchors.left: parent.left
anchors.right: toggleTrack.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingXS
visible: showText
Column {
id: textColumn
width: parent.width
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: toggle.text
font.pixelSize: Appearance.fontSize.normal
font.weight: Font.Medium
opacity: toggle.enabled ? 1 : 0.4
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: toggle.description
font.pixelSize: Appearance.fontSize.small
color: toggle.descriptionColor
wrapMode: Text.WordWrap
width: parent.width
visible: toggle.description.length > 0
horizontalAlignment: Text.AlignLeft
}
}
}
StyledRect {
id: toggleTrack
width: showText ? trackWidth : Math.max(parent.width, trackWidth)
height: showText ? trackHeight : Math.max(parent.height, trackHeight)
anchors.right: parent.right
anchors.rightMargin: showText ? Theme.spacingM : 0
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
// Distinguish disabled checked vs unchecked so unchecked disabled switches don't look enabled
color: !toggle.enabled ? (toggle.checked ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.withAlpha(Qt.alpha(Theme.surfaceText, 0.12), 0)) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha)
opacity: toggle.toggling ? 0.6 : 1
// M3 disabled unchecked border: on surface 12% opacity
border.color: toggle.checked ? Theme.withAlpha(Theme.outline, 0) : (!toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.outline)
readonly property int pad: Math.round((height - thumb.width) / 2)
readonly property int edgeLeft: pad
readonly property int edgeRight: width - thumb.width - pad
StyledRect {
id: thumb
width: toggle.checked ? insetCircle : insetCircle - 4
height: toggle.checked ? insetCircle : insetCircle - 4
radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter
// M3 disabled thumb:
// checked = solid surface | unchecked = outlined off-state thumb
color: !toggle.enabled ? (toggle.checked ? Theme.surface : Theme.withAlpha(Theme.surface, 0)) : (toggle.checked ? Theme.surface : Theme.outline)
border.color: !toggle.enabled ? (toggle.checked ? Theme.withAlpha(Qt.alpha(Theme.surfaceText, 0.38), 0) : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
border.width: (toggle.checked && toggle.enabled) ? 1 : 2
x: toggle.checked ? toggleTrack.edgeRight : toggleTrack.edgeLeft
Behavior on x {
SequentialAnimation {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
ScriptAction {
script: {
toggle.toggleCompleted(toggle.checked);
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on border.width {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
DankIcon {
id: checkIcon
anchors.centerIn: parent
name: "check"
size: 20
// M3 disabled icon: on surface 38%
color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
filled: true
opacity: (toggle.checked && toggle.enabled) ? 1 : 0
scale: (toggle.checked && toggle.enabled) ? 1 : 0.6
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
}
}
StateLayer {
disabled: !toggle.enabled
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: toggle.handleClick()
}
}
}
DankCommon.DankToggle {}
+2 -153
View File
@@ -1,154 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string text: ""
function show(text, item, offsetX, offsetY, preferredSide) {
if (!item)
return;
let windowContentItem = item.Window?.window?.contentItem;
if (!windowContentItem) {
let current = item;
while (current) {
if (current.Window?.window?.contentItem) {
windowContentItem = current.Window.window.contentItem;
break;
}
current = current.parent;
}
}
if (!windowContentItem)
return;
tooltip.parent = windowContentItem;
tooltip.text = text;
const itemPos = item.mapToItem(windowContentItem, 0, 0);
const parentWidth = windowContentItem.width;
const parentHeight = windowContentItem.height;
const tooltipWidth = tooltip.implicitWidth;
const tooltipHeight = tooltip.implicitHeight;
const side = preferredSide || _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight);
let targetX = 0;
let targetY = 0;
switch (side) {
case "left":
targetX = itemPos.x - tooltipWidth - 8;
targetY = itemPos.y + (item.height - tooltipHeight) / 2;
break;
case "right":
targetX = itemPos.x + item.width + 8;
targetY = itemPos.y + (item.height - tooltipHeight) / 2;
break;
case "top":
targetX = itemPos.x + (item.width - tooltipWidth) / 2;
targetY = itemPos.y - tooltipHeight - 8;
break;
case "bottom":
default:
targetX = itemPos.x + (item.width - tooltipWidth) / 2;
targetY = itemPos.y + item.height + 8;
break;
}
tooltip.x = Math.max(4, Math.min(parentWidth - tooltipWidth - 4, targetX + (offsetX || 0)));
tooltip.y = Math.max(4, Math.min(parentHeight - tooltipHeight - 4, targetY + (offsetY || 0)));
tooltip.open();
}
function _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight) {
const itemCenterX = itemPos.x + item.width / 2;
const itemCenterY = itemPos.y + item.height / 2;
const spaceLeft = itemPos.x;
const spaceRight = parentWidth - (itemPos.x + item.width);
const spaceTop = itemPos.y;
const spaceBottom = parentHeight - (itemPos.y + item.height);
if (spaceRight >= tooltipWidth + 16) {
return "right";
}
if (spaceLeft >= tooltipWidth + 16) {
return "left";
}
if (spaceBottom >= tooltipHeight + 16) {
return "bottom";
}
if (spaceTop >= tooltipHeight + 16) {
return "top";
}
if (itemCenterX > parentWidth / 2) {
return "left";
}
return "right";
}
function hide() {
tooltip.close();
}
Popup {
id: tooltip
property string text: ""
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
closePolicy: Popup.NoAutoClose
modal: false
dim: false
background: Rectangle {
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
}
contentItem: Text {
id: textContent
width: Math.min(implicitWidth, 500)
text: tooltip.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.NoWrap
maximumLineCount: 1
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
}
DankCommon.DankTooltipV2 {}
+2 -114
View File
@@ -1,115 +1,3 @@
import QtQuick
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
readonly property real edgeSize: 8
required property var targetWindow
readonly property bool canMaximize: targetWindow.minimumSize.width !== targetWindow.maximumSize.width || targetWindow.minimumSize.height !== targetWindow.maximumSize.height
anchors.fill: parent
function tryStartMove() {
targetWindow.startSystemMove();
}
function tryStartResize(edges) {
if (!canMaximize)
return;
targetWindow.startSystemResize(edges);
}
function tryToggleMaximize() {
if (!canMaximize)
return;
targetWindow.maximized = !targetWindow.maximized;
}
MouseArea {
visible: root.canMaximize
height: root.edgeSize
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: 6
anchors.rightMargin: 6
cursorShape: Qt.SizeVerCursor
onPressed: root.tryStartResize(Qt.TopEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 6
anchors.bottomMargin: 6
cursorShape: Qt.SizeHorCursor
onPressed: root.tryStartResize(Qt.LeftEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 6
anchors.bottomMargin: 6
cursorShape: Qt.SizeHorCursor
onPressed: root.tryStartResize(Qt.RightEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.left: parent.left
anchors.top: parent.top
cursorShape: Qt.SizeFDiagCursor
onPressed: root.tryStartResize(Qt.LeftEdge | Qt.TopEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.right: parent.right
anchors.top: parent.top
cursorShape: Qt.SizeBDiagCursor
onPressed: root.tryStartResize(Qt.RightEdge | Qt.TopEdge)
}
MouseArea {
visible: root.canMaximize
height: root.edgeSize
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 6
anchors.rightMargin: 6
cursorShape: Qt.SizeVerCursor
onPressed: root.tryStartResize(Qt.BottomEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.left: parent.left
anchors.bottom: parent.bottom
cursorShape: Qt.SizeBDiagCursor
onPressed: root.tryStartResize(Qt.LeftEdge | Qt.BottomEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.right: parent.right
anchors.bottom: parent.bottom
cursorShape: Qt.SizeFDiagCursor
onPressed: root.tryStartResize(Qt.RightEdge | Qt.BottomEdge)
}
}
DankCommon.FloatingWindowControls {}
+2 -63
View File
@@ -1,64 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
// Wave progress indicator: track, animated fill, seek preview and playhead are
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
Item {
id: root
property real value: 0
property real actualValue: value
property bool showActualPlaybackState: false
property real lineWidth: 2
property real wavelength: 20
property real amp: 1.6
property real phase: 0.0
property bool isPlaying: false
property real currentAmp: 1.6
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
property color actualProgressColor: Theme.onSurface_38
Behavior on currentAmp {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
Component.onCompleted: currentAmp = isPlaying ? amp : 0
ShaderEffect {
anchors.fill: parent
blending: true
readonly property real widthPx: width
readonly property real heightPx: height
readonly property real value: root.value
readonly property real actualValue: root.actualValue
readonly property real phase: root.phase
readonly property real ampPx: root.currentAmp
readonly property real wavelengthPx: root.wavelength
readonly property real lineWidthPx: root.lineWidth
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
readonly property color fillColor: root.fillColor
readonly property color trackColor: root.trackColor
readonly property color playheadColor: root.playheadColor
readonly property color actualColor: root.actualProgressColor
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
}
signal frameTicked
FrameAnimation {
running: root.visible && (root.isPlaying || root.currentAmp > 0) && (root.Window.window?.visible ?? false)
onTriggered: {
if (!root.isPlaying)
return;
root.phase = (root.phase + 0.03 * frameTime * 60) % 6.28318530718;
root.frameTicked();
}
}
}
DankCommon.M3WaveProgress {}
+2 -21
View File
@@ -1,22 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
StyledText {
id: root
property string reserveText: ""
readonly property real reservedWidth: reserveText !== "" ? Math.max(contentWidth, reserveMetrics.width) : contentWidth
isMonospace: true
wrapMode: Text.NoWrap
StyledTextMetrics {
id: reserveMetrics
isMonospace: root.isMonospace
font.pixelSize: root.font.pixelSize
font.family: root.font.family
font.weight: root.font.weight
font.hintingPreference: root.font.hintingPreference
text: root.reserveText
}
}
DankCommon.NumericText {}
-14
View File
@@ -1,14 +0,0 @@
.pragma library
const friction = 0.96;
const touchpadSpeed = 3.5;
const mouseWheelSpeed = 60;
const momentumRetention = 0.92;
const momentumDeltaFactor = 0.15;
const maxMomentumVelocity = 2500;
const minMomentumVelocity = 50;
const momentumStopThreshold = 5;
const velocitySampleWindowMs = 100;
const momentumTimeThreshold = 50;
const flickDeceleration = 1500;
const maximumFlickVelocity = 2000;
+2 -73
View File
@@ -1,74 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
MouseArea {
id: root
property bool disabled: false
property color stateColor: Theme.surfaceText
property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius
property var tooltipText: null
property string tooltipSide: "bottom"
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0
anchors.fill: parent
cursorShape: disabled ? undefined : Qt.PointingHandCursor
hoverEnabled: true
onPressed: mouse => {
if (!disabled && enableRipple) {
rippleLayer.trigger(mouse.x, mouse.y);
}
}
Rectangle {
id: stateRect
anchors.fill: parent
radius: root.cornerRadius
color: Theme.withAlpha(stateColor, stateOpacity)
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankColorAnim {
duration: Theme.shorterDuration
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
}
}
DankRipple {
id: rippleLayer
anchors.fill: parent
rippleColor: root.stateColor
cornerRadius: root.cornerRadius
enableRipple: root.enableRipple
}
Timer {
id: hoverDelay
interval: 400
repeat: false
onTriggered: {
tooltip.show(root.tooltipText, root, 0, 0, root.tooltipSide);
}
}
onEntered: {
if (!tooltipText)
return;
hoverDelay.restart();
}
onExited: {
if (!tooltipText)
return;
hoverDelay.stop();
tooltip.hide();
}
DankTooltipV2 {
id: tooltip
}
}
DankCommon.StateLayer {}
+2 -28
View File
@@ -1,29 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Rectangle {
color: "transparent"
radius: Appearance.rounding.normal
readonly property var standardAnimation: {
"duration": Appearance.anim.durations.normal,
"easing.type": Easing.BezierSpline,
"easing.bezierCurve": Appearance.anim.curves.standard
}
Behavior on radius {
NumberAnimation {
duration: standardAnimation.duration
easing.type: standardAnimation["easing.type"]
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
}
}
Behavior on opacity {
NumberAnimation {
duration: standardAnimation.duration
easing.type: standardAnimation["easing.type"]
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
}
}
}
DankCommon.StyledRect {}
+2 -75
View File
@@ -1,76 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Text {
property bool isMonospace: false
FontLoader {
id: interFont
source: Qt.resolvedUrl("../assets/fonts/inter/InterVariable.ttf")
}
FontLoader {
id: firaCodeFont
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
}
readonly property string resolvedFontFamily: {
const requestedFont = isMonospace ? Theme.monoFontFamily : Theme.fontFamily;
const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
if (requestedFont === defaultFont) {
return isMonospace ? firaCodeFont.name : interFont.name;
}
return requestedFont;
}
readonly property int resolvedRenderType: {
switch (SettingsData.textRenderType) {
case SettingsData.TextRenderType.Qt:
return Text.QtRendering;
case SettingsData.TextRenderType.Curve:
return Text.CurveRendering;
default:
return Text.NativeRendering;
}
}
readonly property int resolvedRenderQuality: {
switch (SettingsData.textRenderQuality) {
case SettingsData.TextRenderQuality.Low:
return Text.LowRenderTypeQuality;
case SettingsData.TextRenderQuality.Normal:
return Text.NormalRenderTypeQuality;
case SettingsData.TextRenderQuality.High:
return Text.HighRenderTypeQuality;
case SettingsData.TextRenderQuality.VeryHigh:
return Text.VeryHighRenderTypeQuality;
default:
return Text.DefaultRenderTypeQuality;
}
}
readonly property var standardAnimation: {
"duration": Appearance.anim.durations.normal,
"easing.type": Easing.BezierSpline,
"easing.bezierCurve": Appearance.anim.curves.standard
}
color: Theme.surfaceText
font.pixelSize: Appearance.fontSize.normal
font.family: resolvedFontFamily
font.weight: Theme.fontWeight
textFormat: Text.PlainText
wrapMode: Text.WordWrap
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
renderType: resolvedRenderType
renderTypeQuality: resolvedRenderQuality
Behavior on opacity {
NumberAnimation {
duration: standardAnimation.duration
easing.type: standardAnimation["easing.type"]
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
}
}
}
DankCommon.StyledText {}
+2 -23
View File
@@ -1,24 +1,3 @@
import QtQuick
import qs.Common
import qs.Services
import qs.DankCommon.Widgets as DankCommon
TextMetrics {
property bool isMonospace: false
readonly property string resolvedFontFamily: {
const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily;
const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
if (requestedFont === defaultFont) {
const availableFonts = Qt.fontFamilies();
if (!availableFonts.includes(requestedFont)) {
return isMonospace ? "Monospace" : "DejaVu Sans";
}
}
return requestedFont;
}
font.pixelSize: Appearance.fontSize.normal
font.family: resolvedFontFamily
font.weight: SettingsData.fontWeight
}
DankCommon.StyledTextMetrics {}
+2 -80
View File
@@ -1,81 +1,3 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string colorOverride: ""
property real brightnessOverride: 0.5
property real contrastOverride: 1
readonly property bool hasColorOverride: colorOverride !== ""
property bool useNerdFont: false
property string nerdFontIcon: ""
IconImage {
id: iconImage
anchors.fill: parent
visible: !root.useNerdFont
smooth: true
asynchronous: true
layer.enabled: hasColorOverride
layer.effect: MultiEffect {
colorization: 1
colorizationColor: colorOverride
brightness: brightnessOverride
contrast: contrastOverride
}
}
DankNFIcon {
id: nfIcon
anchors.centerIn: parent
visible: root.useNerdFont
name: root.nerdFontIcon
size: Math.min(root.width, root.height)
color: hasColorOverride ? colorOverride : Theme.surfaceText
}
Component.onCompleted: {
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $ID"], (output, exitCode) => {
if (!root || exitCode !== 0 || !output) return
const distroId = output.trim()
if (!distroId) return
const supportedDistroNFs = ["debian", "arch", "archcraft", "fedora", "nixos", "ubuntu", "guix", "gentoo", "endeavouros", "manjaro", "opensuse"]
if (supportedDistroNFs.includes(distroId)) {
if (!root) return
root.useNerdFont = true
root.nerdFontIcon = distroId
return
}
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $LOGO"], (logoOutput, logoExitCode) => {
if (!root || !iconImage || logoExitCode !== 0 || !logoOutput) return
const logo = logoOutput.trim()
if (!logo) return
if (logo === "cachyos") {
iconImage.source = "file:///usr/share/icons/cachyos.svg"
return
}
if (logo === "guix-icon") {
iconImage.source = "file:///run/current-system/profile/share/icons/hicolor/scalable/apps/guix-icon.svg"
return
}
if (logo === "zirconium") {
iconImage.source = "file:///usr/share/zirconium/pixmaps/logo-z.svg"
return
}
iconImage.source = Quickshell.iconPath(logo, true)
}, 0)
}, 0)
}
}
DankCommon.SystemLogo {}