forked from Shinonome/dots-hyprland
Rearrange for tidier structure (#2212)
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var directory
|
||||
property bool showBreadcrumb: true
|
||||
onShowBreadcrumbChanged: {
|
||||
addressInput.text = root.directory;
|
||||
}
|
||||
|
||||
signal navigateToDirectory(string path)
|
||||
|
||||
property real padding: 6
|
||||
implicitWidth: mainLayout.implicitWidth + padding * 2
|
||||
implicitHeight: mainLayout.implicitHeight + padding * 2
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
function focusBreadcrumb() {
|
||||
root.showBreadcrumb = false;
|
||||
addressInput.forceActiveFocus();
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: mainLayout
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.padding
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
RippleButton {
|
||||
id: parentDirButton
|
||||
downAction: () => root.navigateToDirectory(FileUtils.parentDirectory(root.directory))
|
||||
contentItem: MaterialSymbol {
|
||||
text: "drive_folder_upload"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Rectangle {
|
||||
id: directoryEntry
|
||||
visible: !root.showBreadcrumb
|
||||
anchors.fill: parent
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: addressInput.implicitWidth
|
||||
implicitHeight: addressInput.implicitHeight
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (directoryEntry.visible && event.key === Qt.Key_Escape) {
|
||||
root.showBreadcrumb = true;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
StyledTextInput {
|
||||
id: addressInput
|
||||
anchors.fill: parent
|
||||
padding: 10
|
||||
text: root.directory
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.navigateToDirectory(text);
|
||||
root.showBreadcrumb = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// I-beam cursor
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: breadcrumbLoader
|
||||
active: root.showBreadcrumb
|
||||
visible: root.showBreadcrumb
|
||||
anchors.fill: parent
|
||||
sourceComponent: AddressBreadcrumb {
|
||||
directory: root.directory
|
||||
onNavigateToDirectory: dir => {
|
||||
root.navigateToDirectory(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RippleButton {
|
||||
id: dirEditButton
|
||||
toggled: !root.showBreadcrumb
|
||||
downAction: () => root.showBreadcrumb = !root.showBreadcrumb
|
||||
contentItem: MaterialSymbol {
|
||||
text: "edit"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Edit directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
required property var directory
|
||||
property var breadcrumbDirectory: ""
|
||||
Component.onCompleted: breadcrumbDirectory = directory;
|
||||
onDirectoryChanged: {
|
||||
if (breadcrumbDirectory.startsWith(directory)) return;
|
||||
breadcrumbDirectory = directory
|
||||
}
|
||||
|
||||
signal navigateToDirectory(string path)
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
clip: true
|
||||
spacing: 2
|
||||
|
||||
model: breadcrumbDirectory.split("/")
|
||||
delegate: SelectionGroupButton {
|
||||
id: folderButton
|
||||
required property var modelData
|
||||
required property int index
|
||||
buttonText: index === 0 ? "/" : modelData
|
||||
toggled: {
|
||||
if (directory.trim() === "/") return index === 0;
|
||||
return index === directory.split("/").length - 1
|
||||
}
|
||||
leftmost: index === 0
|
||||
rightmost: index === breadcrumbDirectory.split("/").length - 1
|
||||
|
||||
onClicked: {
|
||||
root.navigateToDirectory(breadcrumbDirectory.split("/").slice(0, index + 1).join("/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
/**
|
||||
* A container that supports GroupButton children for bounciness.
|
||||
* See https://m3.material.io/components/button-groups/overview
|
||||
*/
|
||||
Rectangle {
|
||||
id: root
|
||||
default property alias data: rowLayout.data
|
||||
property alias uniformCellSizes: rowLayout.uniformCellSizes
|
||||
property real spacing: 5
|
||||
property real padding: 0
|
||||
property int clickIndex: rowLayout.clickIndex
|
||||
|
||||
property real contentWidth: {
|
||||
let total = 0;
|
||||
for (let i = 0; i < rowLayout.children.length; ++i) {
|
||||
const child = rowLayout.children[i];
|
||||
if (!child.visible) continue;
|
||||
total += child.baseWidth ?? child.implicitWidth ?? child.width;
|
||||
}
|
||||
return total + rowLayout.spacing * (rowLayout.children.length - 1);
|
||||
}
|
||||
|
||||
topLeftRadius: rowLayout.children.length > 0 ? (rowLayout.children[0].radius + padding) :
|
||||
Appearance?.rounding?.small
|
||||
bottomLeftRadius: topLeftRadius
|
||||
topRightRadius: rowLayout.children.length > 0 ? (rowLayout.children[rowLayout.children.length - 1].radius + padding) :
|
||||
Appearance?.rounding?.small
|
||||
bottomRightRadius: topRightRadius
|
||||
|
||||
color: "transparent"
|
||||
width: root.contentWidth + padding * 2
|
||||
implicitHeight: rowLayout.implicitHeight + padding * 2
|
||||
implicitWidth: root.contentWidth + padding * 2
|
||||
|
||||
children: [RowLayout {
|
||||
id: rowLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
spacing: root.spacing
|
||||
property int clickIndex: -1
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.modules.common
|
||||
|
||||
/**
|
||||
* Material 3 circular progress. See https://m3.material.io/components/progress-indicators/specs
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int implicitSize: 30
|
||||
property int lineWidth: 2
|
||||
property real value: 0
|
||||
property color colPrimary: Appearance.m3colors.m3onSecondaryContainer
|
||||
property color colSecondary: Appearance.colors.colSecondaryContainer
|
||||
property real gapAngle: 360 / 18
|
||||
property bool fill: false
|
||||
property int fillOverflow: 2
|
||||
property bool enableAnimation: true
|
||||
property int animationDuration: 800
|
||||
property var easingType: Easing.OutCubic
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
property real degree: value * 360
|
||||
property real centerX: root.width / 2
|
||||
property real centerY: root.height / 2
|
||||
property real arcRadius: root.implicitSize / 2 - root.lineWidth
|
||||
property real startAngle: -90
|
||||
|
||||
Behavior on degree {
|
||||
enabled: root.enableAnimation
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.easingType
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.fill
|
||||
anchors.fill: parent
|
||||
|
||||
sourceComponent: Rectangle {
|
||||
radius: 9999
|
||||
color: root.colSecondary
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
id: secondaryPath
|
||||
strokeColor: root.colSecondary
|
||||
strokeWidth: root.lineWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
PathAngleArc {
|
||||
centerX: root.centerX
|
||||
centerY: root.centerY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle - root.gapAngle
|
||||
sweepAngle: -(360 - root.degree - 2 * root.gapAngle)
|
||||
}
|
||||
}
|
||||
ShapePath {
|
||||
id: primaryPath
|
||||
strokeColor: root.colPrimary
|
||||
strokeWidth: root.lineWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
PathAngleArc {
|
||||
centerX: root.centerX
|
||||
centerY: root.centerY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle
|
||||
sweepAngle: root.degree
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property string entry
|
||||
property real maxWidth
|
||||
property real maxHeight
|
||||
property bool blur: false
|
||||
property string blurText: "Image hidden"
|
||||
|
||||
property string imageDecodePath: Directories.cliphistDecode
|
||||
property string imageDecodeFileName: `${entryNumber}`
|
||||
property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}`
|
||||
property string source
|
||||
|
||||
property int entryNumber: {
|
||||
if (!root.entry)
|
||||
return 0;
|
||||
const match = root.entry.match(/^(\d+)\t/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
}
|
||||
property int imageWidth: {
|
||||
if (!root.entry)
|
||||
return 0;
|
||||
const match = root.entry.match(/(\d+)x(\d+)/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
}
|
||||
property int imageHeight: {
|
||||
if (!root.entry)
|
||||
return 0;
|
||||
const match = root.entry.match(/(\d+)x(\d+)/);
|
||||
return match ? parseInt(match[2]) : 0;
|
||||
}
|
||||
property real scale: {
|
||||
return Math.min(root.maxWidth / imageWidth, root.maxHeight / imageHeight, 1);
|
||||
}
|
||||
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: Appearance.rounding.small
|
||||
implicitHeight: imageHeight * scale
|
||||
implicitWidth: imageWidth * scale
|
||||
|
||||
Component.onCompleted: {
|
||||
decodeImageProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: decodeImageProcess
|
||||
command: ["bash", "-c", `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | ${Cliphist.cliphistBinary} decode > '${imageDecodeFilePath}'`]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 0) {
|
||||
root.source = imageDecodeFilePath;
|
||||
} else {
|
||||
console.error("[CliphistImage] Failed to decode image for entry:", root.entry);
|
||||
root.source = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
Quickshell.execDetached(["bash", "-c", `[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'`]);
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: image.width
|
||||
height: image.height
|
||||
radius: root.radius
|
||||
}
|
||||
}
|
||||
|
||||
StyledImage {
|
||||
id: image
|
||||
anchors.fill: parent
|
||||
|
||||
source: Qt.resolvedUrl(root.source)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
|
||||
width: root.imageWidth * root.scale
|
||||
height: root.imageHeight * root.scale
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: blurLoader
|
||||
active: root.blur
|
||||
anchors.fill: image
|
||||
sourceComponent: GaussianBlur {
|
||||
source: image
|
||||
radius: 35
|
||||
samples: radius * 2 + 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: ColorUtils.transparentize(Appearance.colors.colLayer0, 0.5)
|
||||
|
||||
Column {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
MaterialSymbol {
|
||||
visible: width <= image.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "visibility_off"
|
||||
font.pixelSize: 28
|
||||
}
|
||||
StyledText {
|
||||
visible: width <= image.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.blurText
|
||||
color: Appearance.colors.colOnSurface
|
||||
font.pixelSize: Appearance.font.pixelSize.smallie
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int implicitSize: 18
|
||||
property int lineWidth: 2
|
||||
property real value: 0
|
||||
property color colPrimary: Appearance?.colors.colOnSecondaryContainer ?? "#685496"
|
||||
property color colSecondary: ColorUtils.transparentize(colPrimary, 0.5) ?? "#F1D3F9"
|
||||
property real gapAngle: 360 / 18
|
||||
property bool fill: true
|
||||
property int fillOverflow: 2
|
||||
property bool enableAnimation: true
|
||||
property int animationDuration: 800
|
||||
property var easingType: Easing.OutCubic
|
||||
property bool accountForLightBleeding: true
|
||||
default property Item textMask: Item {
|
||||
width: implicitSize
|
||||
height: implicitSize
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: Math.round(root.value * 100)
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
property real degree: value * 360
|
||||
property real centerX: root.width / 2
|
||||
property real centerY: root.height / 2
|
||||
property real arcRadius: root.implicitSize / 2 - root.lineWidth / 2 - (0.5 * root.accountForLightBleeding)
|
||||
property real startAngle: -90
|
||||
|
||||
Behavior on degree {
|
||||
enabled: root.enableAnimation
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.easingType
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
radius: implicitSize / 2
|
||||
color: root.colSecondary
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: primaryPath
|
||||
pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting
|
||||
strokeColor: root.colPrimary
|
||||
strokeWidth: root.lineWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: root.colPrimary
|
||||
|
||||
startX: root.centerX
|
||||
startY: root.centerY
|
||||
|
||||
PathAngleArc {
|
||||
moveToStart: false
|
||||
centerX: root.centerX
|
||||
centerY: root.centerY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle
|
||||
sweepAngle: root.degree
|
||||
}
|
||||
PathLine {
|
||||
x: primaryPath.startX
|
||||
y: primaryPath.startY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
anchors.fill: parent
|
||||
source: contentItem
|
||||
invert: true
|
||||
maskSource: root.textMask
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
/**
|
||||
* A progress bar with both ends rounded and text acts as clipping like OneUI 7's battery indicator.
|
||||
*/
|
||||
ProgressBar {
|
||||
id: root
|
||||
property bool vertical: false
|
||||
property real valueBarWidth: 30
|
||||
property real valueBarHeight: 18
|
||||
property color highlightColor: Appearance?.colors.colOnSecondaryContainer ?? "#685496"
|
||||
property color trackColor: ColorUtils.transparentize(highlightColor, 0.5) ?? "#F1D3F9"
|
||||
property alias radius: contentItem.radius
|
||||
property string text
|
||||
default property Item textMask: Item {
|
||||
width: valueBarWidth
|
||||
height: valueBarHeight
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
font: root.font
|
||||
text: root.text
|
||||
}
|
||||
}
|
||||
|
||||
text: Math.round(value * 100)
|
||||
font {
|
||||
pixelSize: 13
|
||||
weight: text.length > 2 ? Font.Medium : Font.DemiBold
|
||||
}
|
||||
|
||||
background: Item {
|
||||
implicitHeight: valueBarHeight
|
||||
implicitWidth: valueBarWidth
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
radius: 9999
|
||||
color: root.trackColor
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
id: progressFill
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: undefined
|
||||
}
|
||||
width: parent.width * root.visualPosition
|
||||
height: parent.height
|
||||
|
||||
states: State {
|
||||
name: "vertical"
|
||||
when: root.vertical
|
||||
AnchorChanges {
|
||||
target: progressFill
|
||||
anchors {
|
||||
top: undefined
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: progressFill
|
||||
width: parent.width
|
||||
height: parent.height * root.visualPosition
|
||||
}
|
||||
}
|
||||
|
||||
radius: Appearance.rounding.unsharpen
|
||||
color: root.highlightColor
|
||||
}
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
id: roundingMask
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
source: contentItem
|
||||
maskSource: Rectangle {
|
||||
width: contentItem.width
|
||||
height: contentItem.height
|
||||
radius: contentItem.radius
|
||||
}
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
anchors.fill: parent
|
||||
source: roundingMask
|
||||
invert: true
|
||||
maskSource: root.textMask
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
property bool uniform: false
|
||||
spacing: 4
|
||||
uniformCellSizes: uniform
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
property list<var> options: [
|
||||
{
|
||||
"displayName": "Option 1",
|
||||
"icon": "check",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"displayName": "Option 2",
|
||||
"icon": "close",
|
||||
"value": 2
|
||||
},
|
||||
]
|
||||
property var currentValue: null
|
||||
|
||||
signal selected(var newValue)
|
||||
|
||||
Repeater {
|
||||
model: root.options
|
||||
delegate: SelectionGroupButton {
|
||||
id: paletteButton
|
||||
required property var modelData
|
||||
required property int index
|
||||
onYChanged: {
|
||||
if (index === 0) {
|
||||
paletteButton.leftmost = true
|
||||
} else {
|
||||
var prev = root.children[index - 1]
|
||||
var thisIsOnNewLine = prev && prev.y !== paletteButton.y
|
||||
paletteButton.leftmost = thisIsOnNewLine
|
||||
prev.rightmost = thisIsOnNewLine
|
||||
}
|
||||
}
|
||||
leftmost: index === 0
|
||||
rightmost: index === root.options.length - 1
|
||||
buttonIcon: modelData.icon || ""
|
||||
buttonText: modelData.displayName
|
||||
toggled: root.currentValue === modelData.value
|
||||
onClicked: {
|
||||
root.selected(modelData.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
property string text: ""
|
||||
property string icon
|
||||
property alias value: spinBoxWidget.value
|
||||
property alias stepSize: spinBoxWidget.stepSize
|
||||
property alias from: spinBoxWidget.from
|
||||
property alias to: spinBoxWidget.to
|
||||
spacing: 10
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
OptionalMaterialSymbol {
|
||||
icon: root.icon
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
}
|
||||
StyledText {
|
||||
id: labelWidget
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
}
|
||||
}
|
||||
|
||||
StyledSpinBox {
|
||||
id: spinBoxWidget
|
||||
Layout.fillWidth: false
|
||||
value: root.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property string buttonIcon
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentItem.implicitHeight + 8 * 2
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
|
||||
onClicked: checked = !checked
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 10
|
||||
OptionalMaterialSymbol {
|
||||
icon: root.buttonIcon
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
StyledText {
|
||||
id: labelWidget
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
font: root.font
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
}
|
||||
StyledSwitch {
|
||||
id: switchWidget
|
||||
down: root.down
|
||||
scale: 0.6
|
||||
Layout.fillWidth: false
|
||||
checked: root.checked
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
StyledFlickable {
|
||||
id: root
|
||||
property real baseWidth: 600
|
||||
property bool forceWidth: false
|
||||
property real bottomContentPadding: 100
|
||||
|
||||
default property alias data: contentColumn.data
|
||||
|
||||
clip: true
|
||||
contentHeight: contentColumn.implicitHeight + root.bottomContentPadding // Add some padding at the bottom
|
||||
implicitWidth: contentColumn.implicitWidth
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: root.forceWidth ? root.baseWidth : Math.max(root.baseWidth, implicitWidth)
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
margins: 20
|
||||
}
|
||||
spacing: 30
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property string title
|
||||
property string icon: ""
|
||||
default property alias data: sectionContent.data
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
spacing: 6
|
||||
OptionalMaterialSymbol {
|
||||
icon: root.icon
|
||||
iconSize: Appearance.font.pixelSize.hugeass
|
||||
}
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
font.weight: Font.Medium
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sectionContent
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property string title: ""
|
||||
property string tooltip: ""
|
||||
default property alias data: sectionContent.data
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
ContentSubsectionLabel {
|
||||
visible: root.title && root.title.length > 0
|
||||
text: root.title
|
||||
}
|
||||
MaterialSymbol {
|
||||
visible: root.tooltip && root.tooltip.length > 0
|
||||
text: "info"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
|
||||
color: Appearance.colors.colSubtext
|
||||
MouseArea {
|
||||
id: infoMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.WhatsThisCursor
|
||||
StyledToolTip {
|
||||
extraVisibleCondition: false
|
||||
alternativeVisibleCondition: infoMouseArea.containsMouse
|
||||
text: root.tooltip
|
||||
}
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
ColumnLayout {
|
||||
id: sectionContent
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
StyledText {
|
||||
text: "Subsection"
|
||||
color: Appearance.colors.colSubtext
|
||||
Layout.leftMargin: 2
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
MaterialCookie {
|
||||
id: root
|
||||
property alias text: symbol.text
|
||||
property alias iconSize: symbol.iconSize
|
||||
property alias font: symbol.font
|
||||
property alias colSymbol: symbol.color
|
||||
property real padding: 6
|
||||
|
||||
color: Appearance.colors.colSecondaryContainer
|
||||
colSymbol: Appearance.colors.colOnSecondaryContainer
|
||||
|
||||
sides: 5
|
||||
|
||||
implicitSize: Math.max(symbol.implicitWidth, symbol.implicitHeight) + padding * 2
|
||||
|
||||
MaterialSymbol {
|
||||
id: symbol
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool colorize: false
|
||||
property color color
|
||||
property string source: ""
|
||||
property string iconFolder: Qt.resolvedUrl(Quickshell.shellPath("assets/icons")) // The folder to check first
|
||||
width: 30
|
||||
height: 30
|
||||
|
||||
IconImage {
|
||||
id: iconImage
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
const fullPathWhenSourceIsIconName = iconFolder + "/" + root.source;
|
||||
if (iconFolder && fullPathWhenSourceIsIconName) {
|
||||
return fullPathWhenSourceIsIconName
|
||||
}
|
||||
return root.source
|
||||
}
|
||||
implicitSize: root.height
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.colorize
|
||||
anchors.fill: iconImage
|
||||
sourceComponent: ColorOverlay {
|
||||
source: iconImage
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
|
||||
*/
|
||||
RippleButton {
|
||||
id: root
|
||||
|
||||
property string buttonText
|
||||
padding: 14
|
||||
implicitHeight: 36
|
||||
implicitWidth: buttonTextWidget.implicitWidth + padding * 2
|
||||
buttonRadius: Appearance?.rounding.full ?? 9999
|
||||
|
||||
property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96"
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
property alias colText: buttonTextWidget.color
|
||||
|
||||
contentItem: StyledText {
|
||||
id: buttonTextWidget
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.padding
|
||||
anchors.rightMargin: root.padding
|
||||
text: buttonText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance?.font.pixelSize.small ?? 12
|
||||
color: root.enabled ? root.colEnabled : root.colDisabled
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property bool active: false
|
||||
|
||||
horizontalPadding: Appearance.rounding.large
|
||||
verticalPadding: 12
|
||||
|
||||
clip: true
|
||||
pointingHandCursor: !active
|
||||
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: contentItem.implicitHeight + verticalPadding * 2
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: active ? colBackground : Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
buttonRadius: 0
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
// From https://github.com/caelestia-dots/shell with modifications.
|
||||
// License: GPLv3
|
||||
|
||||
Image {
|
||||
id: root
|
||||
required property var fileModelData
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: {
|
||||
if (!fileModelData.fileIsDir)
|
||||
return Quickshell.iconPath("application-x-zerosize");
|
||||
|
||||
if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].some(dir => FileUtils.trimFileProtocol(dir) === fileModelData.filePath))
|
||||
return Quickshell.iconPath(`folder-${fileModelData.fileName.toLowerCase()}`);
|
||||
|
||||
return Quickshell.iconPath("inode-directory");
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = Quickshell.iconPath("error");
|
||||
}
|
||||
|
||||
Process {
|
||||
running: !fileModelData.fileIsDir
|
||||
command: ["file", "--mime", "-b", fileModelData.filePath]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const mime = text.split(";")[0].replace("/", "-");
|
||||
root.source = Images.validImageTypes.some(t => mime === `image-${t}`) ? fileModelData.fileUrl : Quickshell.iconPath(mime, "image-missing");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import qs.modules.common
|
||||
import qs.services
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* A convenience MouseArea for handling drag events.
|
||||
*/
|
||||
MouseArea {
|
||||
id: root
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
property bool interactive: true
|
||||
property bool automaticallyReset: true
|
||||
readonly property real dragDiffX: _dragDiffX
|
||||
readonly property real dragDiffY: _dragDiffY
|
||||
|
||||
signal dragPressed(diffX: real, diffY: real)
|
||||
signal dragReleased(diffX: real, diffY: real)
|
||||
|
||||
property real startX: 0
|
||||
property real startY: 0
|
||||
property bool dragging: false
|
||||
property real _dragDiffX: 0
|
||||
property real _dragDiffY: 0
|
||||
|
||||
function resetDrag() {
|
||||
_dragDiffX = 0
|
||||
_dragDiffY = 0
|
||||
}
|
||||
|
||||
onPressed: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
startX = mouse.x
|
||||
startY = mouse.y
|
||||
}
|
||||
}
|
||||
onReleased: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
return;
|
||||
}
|
||||
dragging = false
|
||||
root.dragReleased(_dragDiffX, _dragDiffY);
|
||||
if (root.automaticallyReset) {
|
||||
root.resetDrag();
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
return;
|
||||
}
|
||||
if (mouse.buttons & Qt.LeftButton) {
|
||||
root._dragDiffX = mouse.x - startX
|
||||
root._dragDiffY = mouse.y - startY
|
||||
const dist = Math.sqrt(root._dragDiffX * root._dragDiffX + root._dragDiffY * root._dragDiffY);
|
||||
root.dragPressed(_dragDiffX, _dragDiffY);
|
||||
root.dragging = true;
|
||||
}
|
||||
}
|
||||
onCanceled: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
return;
|
||||
}
|
||||
released(mouse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
property bool shown: true
|
||||
opacity: shown ? 1 : 0
|
||||
visible: opacity > 0
|
||||
active: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
IconImage {
|
||||
id: root
|
||||
property string url
|
||||
property string displayText
|
||||
|
||||
property real size: 32
|
||||
property string downloadUserAgent: Config.options?.networking.userAgent ?? ""
|
||||
property string faviconDownloadPath: Directories.favicons
|
||||
property string domainName: url.includes("vertexaisearch") ? displayText : StringUtils.getDomain(url)
|
||||
property string faviconUrl: `https://www.google.com/s2/favicons?domain=${domainName}&sz=32`
|
||||
property string fileName: `${domainName}.ico`
|
||||
property string faviconFilePath: `${faviconDownloadPath}/${fileName}`
|
||||
property string urlToLoad
|
||||
|
||||
Process {
|
||||
id: faviconDownloadProcess
|
||||
running: false
|
||||
command: ["bash", "-c", `[ -f ${faviconFilePath} ] || curl -s '${root.faviconUrl}' -o '${faviconFilePath}' -L -H 'User-Agent: ${downloadUserAgent}'`]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.urlToLoad = root.faviconFilePath
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
faviconDownloadProcess.running = true
|
||||
}
|
||||
|
||||
source: Qt.resolvedUrl(root.urlToLoad)
|
||||
implicitSize: root.size
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: root.implicitSize
|
||||
height: root.implicitSize
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
/**
|
||||
* Material 3 FAB.
|
||||
*/
|
||||
RippleButton {
|
||||
id: root
|
||||
property string iconText: "add"
|
||||
property bool expanded: false
|
||||
property real baseSize: 56
|
||||
property real elementSpacing: 5
|
||||
implicitWidth: Math.max(contentRowLayout.implicitWidth + 10 * 2, baseSize)
|
||||
implicitHeight: baseSize
|
||||
buttonRadius: Appearance.rounding.small
|
||||
colBackground: Appearance.colors.colPrimaryContainer
|
||||
colBackgroundHover: Appearance.colors.colPrimaryContainerHover
|
||||
colRipple: Appearance.colors.colPrimaryContainerActive
|
||||
contentItem: RowLayout {
|
||||
id: contentRowLayout
|
||||
property real horizontalMargins: (root.baseSize - icon.width) / 2
|
||||
anchors {
|
||||
verticalCenter: parent?.verticalCenter
|
||||
left: parent?.left
|
||||
leftMargin: contentRowLayout.horizontalMargins
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
MaterialSymbol {
|
||||
id: icon
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
iconSize: 24
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
text: root.iconText
|
||||
}
|
||||
Loader {
|
||||
active: true
|
||||
sourceComponent: Revealer {
|
||||
visible: root.expanded || implicitWidth > 0
|
||||
reveal: root.expanded
|
||||
implicitWidth: reveal ? (buttonText.implicitWidth + root.elementSpacing + contentRowLayout.horizontalMargins) : 0
|
||||
StyledText {
|
||||
id: buttonText
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: root.elementSpacing
|
||||
}
|
||||
text: root.buttonText
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
font.pixelSize: 14
|
||||
font.weight: 450
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* This is just to make sure `RippleButton`s can be used in a Flow layout.
|
||||
*/
|
||||
Flow {
|
||||
property int clickIndex: -1
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
|
||||
MouseArea { // Right side | scroll to change volume
|
||||
id: root
|
||||
|
||||
signal scrollUp(delta: int)
|
||||
signal scrollDown(delta: int)
|
||||
signal movedAway()
|
||||
|
||||
property bool hovered: false
|
||||
property real lastScrollX: 0
|
||||
property real lastScrollY: 0
|
||||
property bool trackingScroll: false
|
||||
property real moveThreshold: 20
|
||||
|
||||
acceptedButtons: Qt.LeftButton
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
root.hovered = true;
|
||||
}
|
||||
|
||||
onExited: {
|
||||
root.hovered = false;
|
||||
root.trackingScroll = false;
|
||||
}
|
||||
|
||||
onWheel: event => {
|
||||
if (event.angleDelta.y < 0)
|
||||
root.scrollDown(event.angleDelta.y);
|
||||
else if (event.angleDelta.y > 0)
|
||||
root.scrollUp(event.angleDelta.y);
|
||||
// Store the mouse position and start tracking
|
||||
root.lastScrollX = event.x;
|
||||
root.lastScrollY = event.y;
|
||||
root.trackingScroll = true;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (root.trackingScroll) {
|
||||
const dx = mouse.x - root.lastScrollX;
|
||||
const dy = mouse.y - root.lastScrollY;
|
||||
if (Math.sqrt(dx * dx + dy * dy) > root.moveThreshold) {
|
||||
root.movedAway();
|
||||
root.trackingScroll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onContainsMouseChanged: {
|
||||
if (!root.containsMouse && root.trackingScroll) {
|
||||
root.movedAway();
|
||||
root.trackingScroll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
/**
|
||||
* Material 3 button with expressive bounciness.
|
||||
* See https://m3.material.io/components/button-groups/overview
|
||||
*/
|
||||
Button {
|
||||
id: root
|
||||
property bool toggled
|
||||
property string buttonText
|
||||
property real buttonRadius: Appearance?.rounding?.small ?? 8
|
||||
property real buttonRadiusPressed: Appearance?.rounding?.small ?? 6
|
||||
property var downAction // When left clicking (down)
|
||||
property var releaseAction // When left clicking (release)
|
||||
property var altAction // When right clicking
|
||||
property var middleClickAction // When middle clicking
|
||||
property bool bounce: true
|
||||
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
property real baseHeight: contentItem.implicitHeight + verticalPadding * 2
|
||||
property real clickedWidth: baseWidth + 20
|
||||
property real clickedHeight: baseHeight
|
||||
property var parentGroup: root.parent
|
||||
property int clickIndex: parentGroup?.clickIndex ?? -1
|
||||
|
||||
Layout.fillWidth: (clickIndex - 1 <= parentGroup?.children.indexOf(root) && parentGroup?.children.indexOf(root) <= clickIndex + 1)
|
||||
Layout.fillHeight: (clickIndex - 1 <= parentGroup?.children.indexOf(root) && parentGroup?.children.indexOf(root) <= clickIndex + 1)
|
||||
implicitWidth: (root.down && bounce) ? clickedWidth : baseWidth
|
||||
implicitHeight: (root.down && bounce) ? clickedHeight : baseHeight
|
||||
|
||||
property color colBackground: ColorUtils.transparentize(colBackgroundHover, 1) || "transparent"
|
||||
property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
|
||||
property color colBackgroundActive: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
|
||||
property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C"
|
||||
property color colBackgroundToggledActive: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
|
||||
|
||||
property real radius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
property real leftRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
property real rightRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
property color color: root.enabled ? (root.toggled ?
|
||||
(root.down ? colBackgroundToggledActive :
|
||||
root.hovered ? colBackgroundToggledHover :
|
||||
colBackgroundToggled) :
|
||||
(root.down ? colBackgroundActive :
|
||||
root.hovered ? colBackgroundHover :
|
||||
colBackground)) : colBackground
|
||||
|
||||
onDownChanged: {
|
||||
if (root.down) {
|
||||
if (root.parent.clickIndex !== undefined) {
|
||||
root.parent.clickIndex = parent.children.indexOf(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on leftRadius {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on rightRadius {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onPressed: (event) => {
|
||||
if(event.button === Qt.RightButton) {
|
||||
if (root.altAction) root.altAction();
|
||||
return;
|
||||
}
|
||||
if(event.button === Qt.MiddleButton) {
|
||||
if (root.middleClickAction) root.middleClickAction();
|
||||
return;
|
||||
}
|
||||
root.down = true
|
||||
if (root.downAction) root.downAction();
|
||||
}
|
||||
onReleased: (event) => {
|
||||
root.down = false
|
||||
if (event.button != Qt.LeftButton) return;
|
||||
if (root.releaseAction) root.releaseAction();
|
||||
}
|
||||
onClicked: (event) => {
|
||||
if (event.button != Qt.LeftButton) return;
|
||||
root.click()
|
||||
}
|
||||
onCanceled: (event) => {
|
||||
root.down = false
|
||||
}
|
||||
|
||||
onPressAndHold: () => {
|
||||
altAction();
|
||||
root.down = false;
|
||||
root.clicked = false;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
background: Rectangle {
|
||||
id: buttonBackground
|
||||
topLeftRadius: root.leftRadius
|
||||
topRightRadius: root.rightRadius
|
||||
bottomLeftRadius: root.leftRadius
|
||||
bottomRightRadius: root.rightRadius
|
||||
implicitHeight: 50
|
||||
|
||||
color: root.color
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: StyledText {
|
||||
text: root.buttonText
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property string key
|
||||
|
||||
property real horizontalPadding: 6
|
||||
property real verticalPadding: 1
|
||||
property real borderWidth: 1
|
||||
property real extraBottomBorderWidth: 2
|
||||
property color borderColor: Appearance.colors.colOnLayer0
|
||||
property real borderRadius: 5
|
||||
property color keyColor: Appearance.m3colors.m3surfaceContainerLow
|
||||
implicitWidth: keyFace.implicitWidth + borderWidth * 2
|
||||
implicitHeight: keyFace.implicitHeight + borderWidth * 2 + extraBottomBorderWidth
|
||||
radius: borderRadius
|
||||
color: borderColor
|
||||
|
||||
Rectangle {
|
||||
id: keyFace
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: borderWidth
|
||||
leftMargin: borderWidth
|
||||
rightMargin: borderWidth
|
||||
bottomMargin: extraBottomBorderWidth + borderWidth
|
||||
}
|
||||
implicitWidth: keyText.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: keyText.implicitHeight + verticalPadding * 2
|
||||
color: keyColor
|
||||
radius: borderRadius - borderWidth
|
||||
|
||||
StyledText {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
text: key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
RippleButton {
|
||||
id: lightDarkButtonRoot
|
||||
required property bool dark
|
||||
property color previewBg: dark ? ColorUtils.colorWithHueOf("#3f3838", Appearance.m3colors.m3primary) :
|
||||
ColorUtils.colorWithHueOf("#F7F9FF", Appearance.m3colors.m3primary)
|
||||
property color previewFg: dark ? Qt.lighter(previewBg, 2.2) : ColorUtils.mix(previewBg, "#292929", 0.85)
|
||||
padding: 5
|
||||
Layout.fillWidth: true
|
||||
colBackground: Appearance.colors.colLayer2
|
||||
toggled: Appearance.m3colors.darkmode === dark
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --mode ${dark ? "dark" : "light"} --noswitch`])
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: buttonContentLayout.implicitWidth
|
||||
implicitHeight: buttonContentLayout.implicitHeight
|
||||
ColumnLayout {
|
||||
id: buttonContentLayout
|
||||
anchors.centerIn: parent
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: 250
|
||||
implicitHeight: skeletonColumnLayout.implicitHeight + 10 * 2
|
||||
radius: lightDarkButtonRoot.buttonRadius - lightDarkButtonRoot.padding
|
||||
color: lightDarkButtonRoot.previewBg
|
||||
border {
|
||||
width: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
}
|
||||
|
||||
// Some skeleton items
|
||||
ColumnLayout {
|
||||
id: skeletonColumnLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.full
|
||||
color: lightDarkButtonRoot.previewFg
|
||||
implicitWidth: 50
|
||||
implicitHeight: 50
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.unsharpenmore
|
||||
color: lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 22
|
||||
}
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.unsharpenmore
|
||||
color: lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 45
|
||||
implicitHeight: 18
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledProgressBar {
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
Layout.fillWidth: true
|
||||
value: 0.7
|
||||
wavy: true
|
||||
animateWave: lightDarkButtonRoot.toggled
|
||||
highlightColor: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg
|
||||
trackColor: ColorUtils.mix(lightDarkButtonRoot.previewBg, lightDarkButtonRoot.previewFg, 0.5)
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 2
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.full
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
MaterialSymbol {
|
||||
visible: lightDarkButtonRoot.toggled
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "check"
|
||||
iconSize: 20
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : lightDarkButtonRoot.previewBg
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.unsharpenmore
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
}
|
||||
Rectangle {
|
||||
topLeftRadius: Appearance.rounding.unsharpenmore
|
||||
bottomLeftRadius: Appearance.rounding.unsharpenmore
|
||||
topRightRadius: Appearance.rounding.full
|
||||
bottomRightRadius: Appearance.rounding.full
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: dark ? Translation.tr("Dark") : Translation.tr("Light")
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real sides: 12
|
||||
property int implicitSize: 100
|
||||
property real amplitude: implicitSize / 50
|
||||
property int renderPoints: 360
|
||||
property color color: "#605790"
|
||||
property alias strokeWidth: shapePath.strokeWidth
|
||||
property bool constantlyRotate: false
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
property real shapeRotation: 0
|
||||
|
||||
Loader {
|
||||
active: constantlyRotate
|
||||
sourceComponent: FrameAnimation {
|
||||
running: true
|
||||
onTriggered: {
|
||||
shapeRotation += 0.05
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on sides {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: shape
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: shapePath
|
||||
strokeWidth: 0
|
||||
fillColor: root.color
|
||||
pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting
|
||||
|
||||
PathPolyline {
|
||||
property var pointsList: {
|
||||
var points = []
|
||||
var cx = shape.width / 2 // center x
|
||||
var cy = shape.height / 2 // center y
|
||||
var steps = root.renderPoints
|
||||
var radius = root.implicitSize / 2 - root.amplitude
|
||||
for (var i = 0; i <= steps; i++) {
|
||||
var angle = (i / steps) * 2 * Math.PI
|
||||
var rotatedAngle = angle * root.sides + Math.PI/2 + (root.shapeRotation * root.constantlyRotate)
|
||||
var wave = Math.sin(rotatedAngle) * root.amplitude
|
||||
var x = Math.cos(angle) * (radius + wave) + cx
|
||||
var y = Math.sin(angle) * (radius + wave) + cy
|
||||
points.push(Qt.point(x, y))
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
path: pointsList
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
StyledText {
|
||||
id: root
|
||||
property real iconSize: Appearance?.font.pixelSize.small ?? 16
|
||||
property real fill: 0
|
||||
property real truncatedFill: fill.toFixed(1) // Reduce memory consumption spikes from constant font remapping
|
||||
renderType: fill !== 0 ? Text.CurveRendering : Text.NativeRendering
|
||||
font {
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
family: Appearance?.font.family.iconMaterial ?? "Material Symbols Rounded"
|
||||
pixelSize: iconSize
|
||||
weight: Font.Normal + (Font.DemiBold - Font.Normal) * truncatedFill
|
||||
variableAxes: {
|
||||
"FILL": truncatedFill,
|
||||
// "wght": font.weight,
|
||||
// "GRAD": 0,
|
||||
"opsz": iconSize,
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on fill { // Leaky leaky, no good
|
||||
NumberAnimation {
|
||||
duration: Appearance?.animation.elementMoveFast.duration ?? 200
|
||||
easing.type: Appearance?.animation.elementMoveFast.type ?? Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance?.animation.elementMoveFast.bezierCurve ?? [0.34, 0.80, 0.34, 1.00, 1, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Material 3 styled TextArea (filled style)
|
||||
* https://m3.material.io/components/text-fields/overview
|
||||
* Note: We don't use NativeRendering because it makes the small placeholder text look weird
|
||||
*/
|
||||
TextArea {
|
||||
id: root
|
||||
Material.theme: Material.System
|
||||
Material.accent: Appearance.m3colors.m3primary
|
||||
Material.primary: Appearance.m3colors.m3primary
|
||||
Material.background: Appearance.m3colors.m3surface
|
||||
Material.foreground: Appearance.m3colors.m3onSurface
|
||||
Material.containerStyle: Material.Filled
|
||||
renderType: Text.QtRendering
|
||||
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
|
||||
background: Rectangle {
|
||||
implicitHeight: 56
|
||||
color: Appearance.m3colors.m3surface
|
||||
topLeftRadius: 4
|
||||
topRightRadius: 4
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: 1
|
||||
color: root.focus ? Appearance.m3colors.m3primary :
|
||||
root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
wrapMode: TextEdit.Wrap
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Material 3 styled TextField (filled style)
|
||||
* https://m3.material.io/components/text-fields/overview
|
||||
* Note: We don't use NativeRendering because it makes the small placeholder text look weird
|
||||
*/
|
||||
TextField {
|
||||
id: root
|
||||
Material.theme: Material.System
|
||||
Material.accent: Appearance.m3colors.m3primary
|
||||
Material.primary: Appearance.m3colors.m3primary
|
||||
Material.background: Appearance.m3colors.m3surface
|
||||
Material.foreground: Appearance.m3colors.m3onSurface
|
||||
Material.containerStyle: Material.Outlined
|
||||
renderType: Text.QtRendering
|
||||
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
clip: true
|
||||
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
|
||||
buttonRadius: 0
|
||||
implicitHeight: 36
|
||||
implicitWidth: buttonTextWidget.implicitWidth + 14 * 2
|
||||
|
||||
contentItem: StyledText {
|
||||
id: buttonTextWidget
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
text: root.buttonText
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: root.enabled ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3outline
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
ColumnLayout { // Window content with navigation rail and content pane
|
||||
id: root
|
||||
property bool expanded: true
|
||||
property int currentIndex: 0
|
||||
spacing: 5
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
TabButton {
|
||||
id: root
|
||||
|
||||
property bool toggled: TabBar.tabBar.currentIndex === TabBar.index
|
||||
property string buttonIcon
|
||||
property real buttonIconRotation: 0
|
||||
property string buttonText
|
||||
property bool expanded: false
|
||||
property bool showToggledHighlight: true
|
||||
readonly property real visualWidth: root.expanded ? root.baseSize + 20 + itemText.implicitWidth : root.baseSize
|
||||
|
||||
property real baseSize: 56
|
||||
property real baseHighlightHeight: 32
|
||||
property real highlightCollapsedTopMargin: 8
|
||||
padding: 0
|
||||
|
||||
// The navigation item’s target area always spans the full width of the
|
||||
// nav rail, even if the item container hugs its contents.
|
||||
Layout.fillWidth: true
|
||||
// implicitWidth: contentItem.implicitWidth
|
||||
implicitHeight: baseSize
|
||||
|
||||
background: null
|
||||
PointingHandInteraction {}
|
||||
|
||||
// Real stuff
|
||||
contentItem: Item {
|
||||
id: buttonContent
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: undefined
|
||||
}
|
||||
|
||||
implicitWidth: root.visualWidth
|
||||
implicitHeight: root.expanded ? itemIconBackground.implicitHeight : itemIconBackground.implicitHeight + itemText.implicitHeight
|
||||
|
||||
Rectangle {
|
||||
id: itemBackground
|
||||
anchors.top: itemIconBackground.top
|
||||
anchors.left: itemIconBackground.left
|
||||
anchors.bottom: itemIconBackground.bottom
|
||||
implicitWidth: root.visualWidth
|
||||
radius: Appearance.rounding.full
|
||||
color: toggled ?
|
||||
root.showToggledHighlight ?
|
||||
(root.down ? Appearance.colors.colSecondaryContainerActive : root.hovered ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer)
|
||||
: ColorUtils.transparentize(Appearance.colors.colSecondaryContainer) :
|
||||
(root.down ? Appearance.colors.colLayer1Active : root.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1))
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
AnchorChanges {
|
||||
target: itemBackground
|
||||
anchors.top: buttonContent.top
|
||||
anchors.left: buttonContent.left
|
||||
anchors.bottom: buttonContent.bottom
|
||||
}
|
||||
PropertyChanges {
|
||||
target: itemBackground
|
||||
implicitWidth: root.visualWidth
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
PropertyAnimation {
|
||||
target: itemBackground
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: itemIconBackground
|
||||
implicitWidth: root.baseSize
|
||||
implicitHeight: root.baseHighlightHeight
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
MaterialSymbol {
|
||||
id: navRailButtonIcon
|
||||
rotation: root.buttonIconRotation
|
||||
anchors.centerIn: parent
|
||||
iconSize: 24
|
||||
fill: toggled ? 1 : 0
|
||||
font.weight: (toggled || root.hovered) ? Font.DemiBold : Font.Normal
|
||||
text: buttonIcon
|
||||
color: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer1
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: itemText
|
||||
anchors {
|
||||
top: itemIconBackground.bottom
|
||||
topMargin: 2
|
||||
horizontalCenter: itemIconBackground.horizontalCenter
|
||||
}
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
AnchorChanges {
|
||||
target: itemText
|
||||
anchors {
|
||||
top: undefined
|
||||
horizontalCenter: undefined
|
||||
left: itemIconBackground.right
|
||||
verticalCenter: itemIconBackground.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
text: buttonText
|
||||
font.pixelSize: 14
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
Layout.leftMargin: 8
|
||||
downAction: () => {
|
||||
parent.expanded = !parent.expanded;
|
||||
}
|
||||
buttonRadius: Appearance.rounding.full
|
||||
|
||||
rotation: root.parent.expanded ? 0 : -180
|
||||
Behavior on rotation {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
id: icon
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
iconSize: 24
|
||||
color: Appearance.colors.colOnLayer1
|
||||
text: root.parent.expanded ? "menu_open" : "menu"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int currentIndex: 0
|
||||
property bool expanded: false
|
||||
default property alias data: tabBarColumn.data
|
||||
implicitHeight: tabBarColumn.implicitHeight
|
||||
implicitWidth: tabBarColumn.implicitWidth
|
||||
Layout.topMargin: 25
|
||||
|
||||
Rectangle {
|
||||
property real itemHeight: tabBarColumn.children[0]?.baseSize ?? 56
|
||||
property real baseHighlightHeight: tabBarColumn.children[0]?.baseHighlightHeight ?? 56
|
||||
anchors {
|
||||
top: tabBarColumn.top
|
||||
left: tabBarColumn.left
|
||||
topMargin: itemHeight * root.currentIndex + (root.expanded ? 0 : ((itemHeight - baseHighlightHeight) / 2))
|
||||
}
|
||||
radius: Appearance.rounding.full
|
||||
color: Appearance.colors.colSecondaryContainer
|
||||
implicitHeight: root.expanded ? itemHeight : baseHighlightHeight
|
||||
implicitWidth: tabBarColumn?.children[root.currentIndex]?.visualWidth ?? 100
|
||||
|
||||
Behavior on anchors.topMargin {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animationCurves.expressiveFastSpatialDuration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: tabBarColumn
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property alias materialIcon: icon.text
|
||||
property alias text: noticeText.text
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Appearance.colors.colPrimaryContainer
|
||||
implicitWidth: mainRowLayout.implicitWidth + mainRowLayout.anchors.margins * 2
|
||||
implicitHeight: mainRowLayout.implicitHeight + mainRowLayout.anchors.margins * 2
|
||||
|
||||
RowLayout {
|
||||
id: mainRowLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
MaterialSymbol {
|
||||
id: icon
|
||||
Layout.fillWidth: false
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: "info"
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: noticeText
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: "Notice message"
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import qs.modules.common
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
RippleButton {
|
||||
id: button
|
||||
property string buttonText
|
||||
property string urgency
|
||||
|
||||
implicitHeight: 30
|
||||
leftPadding: 15
|
||||
rightPadding: 15
|
||||
buttonRadius: Appearance.rounding.small
|
||||
colBackground: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainer : Appearance.colors.colLayer4
|
||||
colBackgroundHover: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colLayer4Hover
|
||||
colRipple: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainerActive : Appearance.colors.colLayer4Active
|
||||
|
||||
contentItem: StyledText {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: buttonText
|
||||
color: (urgency == NotificationUrgency.Critical) ? Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import qs.modules.common
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
MaterialCookie { // App icon
|
||||
id: root
|
||||
property var appIcon: ""
|
||||
property var summary: ""
|
||||
property var urgency: NotificationUrgency.Normal
|
||||
property bool isUrgent: urgency === NotificationUrgency.Critical
|
||||
property var image: ""
|
||||
property real materialIconScale: 0.57
|
||||
property real appIconScale: 0.8
|
||||
property real smallAppIconScale: 0.49
|
||||
property real materialIconSize: implicitSize * materialIconScale
|
||||
property real appIconSize: implicitSize * appIconScale
|
||||
property real smallAppIconSize: implicitSize * smallAppIconScale
|
||||
|
||||
implicitSize: 38 * scale
|
||||
sides: isUrgent ? 10 : 0
|
||||
amplitude: implicitSize / 24
|
||||
|
||||
color: isUrgent ? Appearance.colors.colPrimary : Appearance.colors.colSecondaryContainer
|
||||
Loader {
|
||||
id: materialSymbolLoader
|
||||
active: root.appIcon == ""
|
||||
anchors.fill: parent
|
||||
sourceComponent: MaterialSymbol {
|
||||
text: {
|
||||
const defaultIcon = NotificationUtils.findSuitableMaterialSymbol("")
|
||||
const guessedIcon = NotificationUtils.findSuitableMaterialSymbol(root.summary)
|
||||
return (root.urgency == NotificationUrgency.Critical && guessedIcon === defaultIcon) ?
|
||||
"priority_high" : guessedIcon
|
||||
}
|
||||
anchors.fill: parent
|
||||
color: isUrgent ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSecondaryContainer
|
||||
iconSize: root.materialIconSize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: appIconLoader
|
||||
active: root.image == "" && root.appIcon != ""
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: IconImage {
|
||||
id: appIconImage
|
||||
implicitSize: root.appIconSize
|
||||
asynchronous: true
|
||||
source: Quickshell.iconPath(root.appIcon, "image-missing")
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: notifImageLoader
|
||||
active: root.image != ""
|
||||
anchors.fill: parent
|
||||
sourceComponent: Item {
|
||||
anchors.fill: parent
|
||||
Image {
|
||||
id: notifImage
|
||||
anchors.fill: parent
|
||||
readonly property int size: parent.width
|
||||
|
||||
source: root.image
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
|
||||
width: size
|
||||
height: size
|
||||
sourceSize.width: size
|
||||
sourceSize.height: size
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: notifImage.size
|
||||
height: notifImage.size
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: notifImageAppIconLoader
|
||||
active: root.appIcon != ""
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
sourceComponent: IconImage {
|
||||
implicitSize: root.smallAppIconSize
|
||||
asynchronous: true
|
||||
source: Quickshell.iconPath(root.appIcon, "image-missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
/**
|
||||
* A group of notifications from the same app.
|
||||
* Similar to Android's notifications
|
||||
*/
|
||||
MouseArea { // Notification group area
|
||||
id: root
|
||||
property var notificationGroup
|
||||
property var notifications: notificationGroup?.notifications ?? []
|
||||
property int notificationCount: notifications.length
|
||||
property bool multipleNotifications: notificationCount > 1
|
||||
property bool expanded: false
|
||||
property bool popup: false
|
||||
property real padding: 10
|
||||
implicitHeight: background.implicitHeight
|
||||
|
||||
property real dragConfirmThreshold: 70 // Drag further to discard notification
|
||||
property real dismissOvershoot: 20 // Account for gaps and bouncy animations
|
||||
property var qmlParent: root.parent.parent // There's something between this and the parent ListView
|
||||
property var parentDragIndex: qmlParent.dragIndex
|
||||
property var parentDragDistance: qmlParent.dragDistance
|
||||
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
||||
property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
|
||||
parentDragDistance > dragConfirmThreshold ? 0 :
|
||||
dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
|
||||
dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
|
||||
|
||||
function destroyWithAnimation() {
|
||||
root.qmlParent.resetDrag()
|
||||
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
||||
destroyAnimation.running = true;
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
onContainsMouseChanged: {
|
||||
if (!root.popup) return;
|
||||
if (root.containsMouse) root.notifications.forEach(notif => {
|
||||
Notifications.cancelTimeout(notif.notificationId);
|
||||
});
|
||||
else root.notifications.forEach(notif => {
|
||||
Notifications.timeoutNotification(notif.notificationId);
|
||||
});
|
||||
}
|
||||
|
||||
SequentialAnimation { // Drag finish animation
|
||||
id: destroyAnimation
|
||||
running: false
|
||||
|
||||
NumberAnimation {
|
||||
target: background.anchors
|
||||
property: "leftMargin"
|
||||
to: root.width + root.dismissOvershoot
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
onFinished: () => {
|
||||
root.notifications.forEach((notif) => {
|
||||
Qt.callLater(() => {
|
||||
Notifications.discardNotification(notif.notificationId);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpanded() {
|
||||
if (expanded) implicitHeightAnim.enabled = true;
|
||||
else implicitHeightAnim.enabled = false;
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
|
||||
DragManager { // Drag manager
|
||||
id: dragManager
|
||||
anchors.fill: parent
|
||||
interactive: !expanded
|
||||
automaticallyReset: false
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton)
|
||||
root.toggleExpanded();
|
||||
else if (mouse.button === Qt.MiddleButton)
|
||||
root.destroyWithAnimation();
|
||||
}
|
||||
|
||||
onDraggingChanged: () => {
|
||||
if (dragging) {
|
||||
root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root);
|
||||
}
|
||||
}
|
||||
|
||||
onDragDiffXChanged: () => {
|
||||
root.qmlParent.dragDistance = dragDiffX;
|
||||
}
|
||||
|
||||
onDragReleased: (diffX, diffY) => {
|
||||
if (diffX > root.dragConfirmThreshold)
|
||||
root.destroyWithAnimation();
|
||||
else
|
||||
dragManager.resetDrag();
|
||||
}
|
||||
}
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: background
|
||||
visible: popup
|
||||
}
|
||||
Rectangle { // Background of the notification
|
||||
id: background
|
||||
anchors.left: parent.left
|
||||
width: parent.width
|
||||
color: popup ? ColorUtils.applyAlpha(Appearance.colors.colLayer2, 1 - Appearance.backgroundTransparency) : Appearance.colors.colLayer2
|
||||
radius: Appearance.rounding.normal
|
||||
anchors.leftMargin: root.xOffset
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
enabled: !dragManager.dragging
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
implicitHeight: expanded ?
|
||||
row.implicitHeight + padding * 2 :
|
||||
Math.min(80, row.implicitHeight + padding * 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
id: implicitHeightAnim
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RowLayout { // Left column for icon, right column for content
|
||||
id: row
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: root.padding
|
||||
spacing: 10
|
||||
|
||||
NotificationAppIcon { // Icons
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: false
|
||||
image: root?.multipleNotifications ? "" : notificationGroup?.notifications[0]?.image ?? ""
|
||||
appIcon: notificationGroup?.appIcon
|
||||
summary: notificationGroup?.notifications[root.notificationCount - 1]?.summary
|
||||
urgency: root.notifications.some(n => n.urgency === NotificationUrgency.Critical.toString()) ?
|
||||
NotificationUrgency.Critical : NotificationUrgency.Normal
|
||||
}
|
||||
|
||||
ColumnLayout { // Content
|
||||
Layout.fillWidth: true
|
||||
spacing: expanded ? (root.multipleNotifications ?
|
||||
(notificationGroup?.notifications[root.notificationCount - 1].image != "") ? 35 :
|
||||
5 : 0) : 0
|
||||
// spacing: 00
|
||||
Behavior on spacing {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Item { // App name (or summary when there's only 1 notif) and time
|
||||
id: topRow
|
||||
// spacing: 0
|
||||
Layout.fillWidth: true
|
||||
property real fontSize: Appearance.font.pixelSize.smaller
|
||||
property bool showAppName: root.multipleNotifications
|
||||
implicitHeight: Math.max(topTextRow.implicitHeight, expandButton.implicitHeight)
|
||||
|
||||
RowLayout {
|
||||
id: topTextRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: expandButton.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 5
|
||||
StyledText {
|
||||
id: appName
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
text: (topRow.showAppName ?
|
||||
notificationGroup?.appName :
|
||||
notificationGroup?.notifications[0]?.summary) || ""
|
||||
font.pixelSize: topRow.showAppName ?
|
||||
topRow.fontSize :
|
||||
Appearance.font.pixelSize.small
|
||||
color: topRow.showAppName ?
|
||||
Appearance.colors.colSubtext :
|
||||
Appearance.colors.colOnLayer2
|
||||
}
|
||||
StyledText {
|
||||
id: timeText
|
||||
// Layout.fillWidth: true
|
||||
Layout.rightMargin: 10
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: NotificationUtils.getFriendlyNotifTimeString(notificationGroup?.time)
|
||||
font.pixelSize: topRow.fontSize
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
NotificationGroupExpandButton {
|
||||
id: expandButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
count: root.notificationCount
|
||||
expanded: root.expanded
|
||||
fontSize: topRow.fontSize
|
||||
onClicked: { root.toggleExpanded() }
|
||||
altAction: () => { root.toggleExpanded() }
|
||||
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Tip: right-clicking a group\nalso expands it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledListView { // Notification body (expanded)
|
||||
id: notificationsColumn
|
||||
implicitHeight: contentHeight
|
||||
Layout.fillWidth: true
|
||||
spacing: expanded ? 5 : 3
|
||||
// clip: true
|
||||
interactive: false
|
||||
Behavior on spacing {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: root.expanded ? root.notifications.slice().reverse() :
|
||||
root.notifications.slice().reverse().slice(0, 2)
|
||||
}
|
||||
delegate: NotificationItem {
|
||||
required property int index
|
||||
required property var modelData
|
||||
notificationObject: modelData
|
||||
expanded: root.expanded
|
||||
onlyNotification: (root.notificationCount === 1)
|
||||
opacity: (!root.expanded && index == 1 && root.notificationCount > 2) ? 0.5 : 1
|
||||
visible: root.expanded || (index < 2)
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RippleButton { // Expand button
|
||||
id: root
|
||||
required property int count
|
||||
required property bool expanded
|
||||
property real fontSize: Appearance?.font.pixelSize.small ?? 12
|
||||
property real iconSize: Appearance?.font.pixelSize.normal ?? 16
|
||||
implicitHeight: fontSize + 4 * 2
|
||||
implicitWidth: Math.max(contentItem.implicitWidth + 5 * 2, 30)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillHeight: false
|
||||
|
||||
buttonRadius: Appearance.rounding.full
|
||||
colBackground: ColorUtils.mix(Appearance?.colors.colLayer2, Appearance?.colors.colLayer2Hover, 0.5)
|
||||
colBackgroundHover: Appearance?.colors.colLayer2Hover ?? "#E5DFED"
|
||||
colRipple: Appearance?.colors.colLayer2Active ?? "#D6CEE2"
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: contentRow.implicitWidth
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 3
|
||||
StyledText {
|
||||
Layout.leftMargin: 4
|
||||
visible: root.count > 1
|
||||
text: root.count
|
||||
font.pixelSize: root.fontSize
|
||||
}
|
||||
MaterialSymbol {
|
||||
text: "keyboard_arrow_down"
|
||||
iconSize: root.iconSize
|
||||
color: Appearance.colors.colOnLayer2
|
||||
rotation: expanded ? 180 : 0
|
||||
Behavior on rotation {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
Item { // Notification item area
|
||||
id: root
|
||||
property var notificationObject
|
||||
property bool expanded: false
|
||||
property bool onlyNotification: false
|
||||
property real fontSize: Appearance.font.pixelSize.small
|
||||
property real padding: onlyNotification ? 0 : 8
|
||||
property real summaryElideRatio: 0.85
|
||||
|
||||
property real dragConfirmThreshold: 70 // Drag further to discard notification
|
||||
property real dismissOvershoot: notificationIcon.implicitWidth + 20 // Account for gaps and bouncy animations
|
||||
property var qmlParent: root?.parent?.parent // There's something between this and the parent ListView
|
||||
property var parentDragIndex: qmlParent?.dragIndex ?? -1
|
||||
property var parentDragDistance: qmlParent?.dragDistance ?? 0
|
||||
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
||||
property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
|
||||
parentDragDistance > dragConfirmThreshold ? 0 :
|
||||
dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
|
||||
dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
|
||||
|
||||
implicitHeight: background.implicitHeight
|
||||
|
||||
function processNotificationBody(body, appName) {
|
||||
let processedBody = body
|
||||
|
||||
// Clean Chromium-based browsers notifications - remove first line
|
||||
if (appName) {
|
||||
const lowerApp = appName.toLowerCase()
|
||||
const chromiumBrowsers = [
|
||||
"brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge"
|
||||
]
|
||||
|
||||
if (chromiumBrowsers.some(name => lowerApp.includes(name))) {
|
||||
const lines = body.split('\n\n')
|
||||
|
||||
if (lines.length > 1 && lines[0].startsWith('<a')) {
|
||||
processedBody = lines.slice(1).join('\n\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processedBody
|
||||
}
|
||||
|
||||
function destroyWithAnimation() {
|
||||
root.qmlParent.resetDrag()
|
||||
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
||||
destroyAnimation.running = true;
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: summaryTextMetrics
|
||||
font.pixelSize: root.fontSize
|
||||
text: root.notificationObject.summary || ""
|
||||
}
|
||||
|
||||
SequentialAnimation { // Drag finish animation
|
||||
id: destroyAnimation
|
||||
running: false
|
||||
|
||||
NumberAnimation {
|
||||
target: background.anchors
|
||||
property: "leftMargin"
|
||||
to: root.width + root.dismissOvershoot
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
onFinished: () => {
|
||||
Notifications.discardNotification(notificationObject.notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
DragManager { // Drag manager
|
||||
id: dragManager
|
||||
anchors.fill: root
|
||||
anchors.leftMargin: root.expanded ? -notificationIcon.implicitWidth : 0
|
||||
interactive: expanded
|
||||
automaticallyReset: false
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
root.destroyWithAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
onDraggingChanged: () => {
|
||||
if (dragging) {
|
||||
root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root);
|
||||
}
|
||||
}
|
||||
|
||||
onDragDiffXChanged: () => {
|
||||
root.qmlParent.dragDistance = dragDiffX;
|
||||
}
|
||||
|
||||
onDragReleased: (diffX, diffY) => {
|
||||
if (diffX > root.dragConfirmThreshold)
|
||||
root.destroyWithAnimation();
|
||||
else
|
||||
dragManager.resetDrag();
|
||||
}
|
||||
}
|
||||
|
||||
NotificationAppIcon { // App icon
|
||||
id: notificationIcon
|
||||
opacity: (!onlyNotification && notificationObject.image != "" && expanded) ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
image: notificationObject.image
|
||||
anchors.right: background.left
|
||||
anchors.top: background.top
|
||||
anchors.rightMargin: 10
|
||||
}
|
||||
|
||||
Rectangle { // Background of notification item
|
||||
id: background
|
||||
width: parent.width
|
||||
anchors.left: parent.left
|
||||
radius: Appearance.rounding.small
|
||||
anchors.leftMargin: root.xOffset
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
enabled: !dragManager.dragging
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
color: (expanded && !onlyNotification) ?
|
||||
(notificationObject.urgency == NotificationUrgency.Critical) ?
|
||||
ColorUtils.mix(Appearance.colors.colSecondaryContainer, Appearance.colors.colLayer2, 0.35) :
|
||||
(Appearance.colors.colLayer3) :
|
||||
ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
|
||||
implicitHeight: expanded ? (contentColumn.implicitHeight + padding * 2) : summaryRow.implicitHeight
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
ColumnLayout { // Content column
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: expanded ? root.padding : 0
|
||||
spacing: 3
|
||||
|
||||
Behavior on anchors.margins {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RowLayout { // Summary row
|
||||
id: summaryRow
|
||||
visible: !root.onlyNotification || !root.expanded
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: summaryText.implicitHeight
|
||||
StyledText {
|
||||
id: summaryText
|
||||
Layout.fillWidth: summaryTextMetrics.width >= summaryRow.implicitWidth * root.summaryElideRatio
|
||||
visible: !root.onlyNotification
|
||||
font.pixelSize: root.fontSize
|
||||
color: Appearance.colors.colOnLayer3
|
||||
elide: Text.ElideRight
|
||||
text: root.notificationObject.summary || ""
|
||||
}
|
||||
StyledText {
|
||||
opacity: !root.expanded ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Layout.fillWidth: true
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
font.pixelSize: root.fontSize
|
||||
color: Appearance.colors.colSubtext
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap // Needed for proper eliding????
|
||||
maximumLineCount: 1
|
||||
textFormat: Text.StyledText
|
||||
text: {
|
||||
return processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "<br/>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout { // Expanded content
|
||||
Layout.fillWidth: true
|
||||
opacity: root.expanded ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
StyledText { // Notification body (expanded)
|
||||
id: notificationBodyText
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: root.fontSize
|
||||
color: Appearance.colors.colSubtext
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
return `<style>img{max-width:${300 /* binding to notificationBodyText.width would cause a binding loop */}px;}</style>` +
|
||||
`${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "<br/>")}`
|
||||
}
|
||||
|
||||
onLinkActivated: (link) => {
|
||||
Qt.openUrlExternally(link)
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
}
|
||||
|
||||
PointingHandLinkHover {}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: actionsFlickable.implicitWidth
|
||||
implicitHeight: actionsFlickable.implicitHeight
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: actionsFlickable.width
|
||||
height: actionsFlickable.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
|
||||
ScrollEdgeFade {
|
||||
target: actionsFlickable
|
||||
vertical: false
|
||||
}
|
||||
|
||||
StyledFlickable { // Notification actions
|
||||
id: actionsFlickable
|
||||
anchors.fill: parent
|
||||
implicitHeight: actionRowLayout.implicitHeight
|
||||
contentWidth: actionRowLayout.implicitWidth
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: actionRowLayout
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
NotificationActionButton {
|
||||
Layout.fillWidth: true
|
||||
buttonText: Translation.tr("Close")
|
||||
urgency: notificationObject.urgency
|
||||
implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) :
|
||||
(contentItem.implicitWidth + leftPadding + rightPadding)
|
||||
|
||||
onClicked: {
|
||||
root.destroyWithAnimation()
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
||||
Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
||||
text: "close"
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: actionRepeater
|
||||
model: notificationObject.actions
|
||||
NotificationActionButton {
|
||||
Layout.fillWidth: true
|
||||
buttonText: modelData.text
|
||||
urgency: notificationObject.urgency
|
||||
onClicked: {
|
||||
Notifications.attemptInvokeAction(notificationObject.notificationId, modelData.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotificationActionButton {
|
||||
Layout.fillWidth: true
|
||||
urgency: notificationObject.urgency
|
||||
implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) :
|
||||
(contentItem.implicitWidth + leftPadding + rightPadding)
|
||||
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = notificationObject.body
|
||||
copyIcon.text = "inventory"
|
||||
copyIconTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyIconTimer
|
||||
interval: 1500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
copyIcon.text = "content_copy"
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
id: copyIcon
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
||||
Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
||||
text: "content_copy"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
StyledListView { // Scrollable window
|
||||
id: root
|
||||
property bool popup: false
|
||||
|
||||
spacing: 3
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.popup ? Notifications.popupAppNameList : Notifications.appNameList
|
||||
}
|
||||
delegate: NotificationGroup {
|
||||
required property int index
|
||||
required property var modelData
|
||||
popup: root.popup
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
notificationGroup: popup ?
|
||||
Notifications.popupGroupsByAppName[modelData] :
|
||||
Notifications.groupsByAppName[modelData]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
required property string icon
|
||||
property real iconSize: Appearance.font.pixelSize.larger
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
active: root.icon && root.icon.length > 0
|
||||
visible: active
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitWidth: materialSymbol.implicitWidth
|
||||
|
||||
MaterialSymbol {
|
||||
id: materialSymbol
|
||||
anchors.centerIn: parent
|
||||
|
||||
iconSize: root.iconSize
|
||||
color: root.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSecondaryContainer
|
||||
text: root.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: (mouse) => mouse.accepted = false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton // Only for hover
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property string text: ""
|
||||
property bool extraVisibleCondition: true
|
||||
property bool alternativeVisibleCondition: false
|
||||
property real horizontalPadding: 10
|
||||
property real verticalPadding: 5
|
||||
|
||||
property var anchorEdges: Edges.Top
|
||||
property var anchorGravity: anchorEdges
|
||||
|
||||
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
anchors.fill: parent
|
||||
active: internalVisibleCondition
|
||||
sourceComponent: PopupWindow {
|
||||
visible: true
|
||||
anchor {
|
||||
window: root.QsWindow.window
|
||||
item: root.parent
|
||||
edges: root.anchorEdges
|
||||
gravity: root.anchorGravity
|
||||
}
|
||||
mask: Region {
|
||||
item: null
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
implicitWidth: contentItem.implicitWidth + root.horizontalPadding * 2
|
||||
implicitHeight: contentItem.implicitHeight + root.verticalPadding * 2
|
||||
|
||||
StyledToolTipContent {
|
||||
id: contentItem
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
shown: false
|
||||
Component.onCompleted: shown = true
|
||||
horizontalPadding: root.horizontalPadding
|
||||
verticalPadding: root.verticalPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import qs.modules.common
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
required property var tabButtonList // Something like [{"icon": "notifications", "name": Translation.tr("Notifications")}, {"icon": "volume_up", "name": Translation.tr("Volume mixer")}]
|
||||
required property var externalTrackedTab
|
||||
property bool enableIndicatorAnimation: false
|
||||
property color colIndicator: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colBorder: Appearance?.m3colors.m3outlineVariant ?? "#C6C6D0"
|
||||
signal currentIndexChanged(int index)
|
||||
|
||||
property bool centerTabBar: parent.width > 500
|
||||
Layout.fillWidth: !centerTabBar
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: Math.max(tabBar.implicitWidth, 600)
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
currentIndex: root.externalTrackedTab
|
||||
onCurrentIndexChanged: {
|
||||
root.onCurrentIndexChanged(currentIndex)
|
||||
}
|
||||
|
||||
background: Item {
|
||||
WheelHandler {
|
||||
onWheel: (event) => {
|
||||
if (event.angleDelta.y < 0)
|
||||
tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
|
||||
else if (event.angleDelta.y > 0)
|
||||
tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
|
||||
}
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.tabButtonList
|
||||
delegate: PrimaryTabButton {
|
||||
selected: (index == root.externalTrackedTab)
|
||||
buttonText: modelData.name
|
||||
buttonIcon: modelData.icon
|
||||
minimumWidth: 160
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { // Tab indicator
|
||||
id: tabIndicator
|
||||
Layout.fillWidth: true
|
||||
height: 3
|
||||
Connections {
|
||||
target: root
|
||||
function onExternalTrackedTabChanged() {
|
||||
root.enableIndicatorAnimation = true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
property int tabCount: root.tabButtonList.length
|
||||
property real fullTabSize: root.width / tabCount;
|
||||
property real targetWidth: tabBar.contentItem?.children[0]?.children[tabBar.currentIndex]?.tabContentWidth ?? 0
|
||||
|
||||
implicitWidth: targetWidth
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
|
||||
|
||||
color: root.colIndicator
|
||||
radius: Appearance?.rounding.full ?? 9999
|
||||
|
||||
Behavior on x {
|
||||
animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Tabbar bottom border
|
||||
id: tabBarBottomBorder
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 1
|
||||
color: root.colBorder
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
TabButton {
|
||||
id: button
|
||||
property string buttonText
|
||||
property string buttonIcon
|
||||
property real minimumWidth: 110
|
||||
property bool selected: false
|
||||
property int tabContentWidth: contentItem.children[0].implicitWidth
|
||||
property int rippleDuration: 1200
|
||||
height: buttonBackground.height
|
||||
implicitWidth: Math.max(tabContentWidth, buttonBackground.implicitWidth, minimumWidth)
|
||||
|
||||
property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent"
|
||||
property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
|
||||
property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
|
||||
property color colActive: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colInactive: Appearance?.colors.colOnLayer1 ?? "#45464F"
|
||||
|
||||
component RippleAnim: NumberAnimation {
|
||||
duration: rippleDuration
|
||||
easing.type: Appearance?.animation.elementMoveEnter.type
|
||||
easing.bezierCurve: Appearance?.animationCurves.standardDecel
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: (event) => {
|
||||
button.click() // Because the MouseArea already consumed the event
|
||||
const {x,y} = event
|
||||
const stateY = buttonBackground.y;
|
||||
rippleAnim.x = x;
|
||||
rippleAnim.y = y - stateY;
|
||||
|
||||
const dist = (ox,oy) => ox*ox + oy*oy
|
||||
const stateEndY = stateY + buttonBackground.height
|
||||
rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)))
|
||||
|
||||
rippleFadeAnim.complete();
|
||||
rippleAnim.restart();
|
||||
}
|
||||
onReleased: (event) => {
|
||||
rippleFadeAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
RippleAnim {
|
||||
id: rippleFadeAnim
|
||||
duration: rippleDuration * 2
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnim
|
||||
|
||||
property real x
|
||||
property real y
|
||||
property real radius
|
||||
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "x"
|
||||
value: rippleAnim.x
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "y"
|
||||
value: rippleAnim.y
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
value: 1
|
||||
}
|
||||
ParallelAnimation {
|
||||
RippleAnim {
|
||||
target: ripple
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
from: 0
|
||||
to: rippleAnim.radius * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: buttonBackground
|
||||
radius: Appearance?.rounding.small
|
||||
implicitHeight: 50
|
||||
color: (button.hovered ? button.colBackgroundHover : button.colBackground)
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: buttonBackground.width
|
||||
height: buttonBackground.height
|
||||
radius: buttonBackground.radius
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: ripple
|
||||
width: ripple.implicitWidth
|
||||
height: ripple.implicitHeight
|
||||
opacity: 0
|
||||
|
||||
property real implicitWidth: 0
|
||||
property real implicitHeight: 0
|
||||
visible: width > 0 && height > 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RadialGradient {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: button.colRipple }
|
||||
GradientStop { position: 0.3; color: button.colRipple }
|
||||
GradientStop { position: 0.5 ; color: Qt.rgba(button.colRipple.r, button.colRipple.g, button.colRipple.b, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: -ripple.width / 2
|
||||
y: -ripple.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: buttonBackground
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
MaterialSymbol {
|
||||
visible: buttonIcon?.length > 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: buttonIcon
|
||||
iconSize: Appearance?.font.pixelSize.hugeass ?? 25
|
||||
fill: selected ? 1 : 0
|
||||
color: selected ? button.colActive : button.colInactive
|
||||
Behavior on color {
|
||||
animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: buttonTextWidget
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance?.font.pixelSize.small
|
||||
color: selected ? button.colActive : button.colInactive
|
||||
text: buttonText
|
||||
Behavior on color {
|
||||
animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* Recreation of GTK revealer. Expects one single child.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
property bool reveal
|
||||
property bool vertical: false
|
||||
clip: true
|
||||
|
||||
implicitWidth: (reveal || vertical) ? childrenRect.width : 0
|
||||
implicitHeight: (reveal || !vertical) ? childrenRect.height : 0
|
||||
visible: reveal || (width > 0 && height > 0)
|
||||
|
||||
Behavior on implicitWidth {
|
||||
enabled: !vertical
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
enabled: vertical
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* A button with ripple effect similar to in Material Design.
|
||||
*/
|
||||
Button {
|
||||
id: root
|
||||
property bool toggled
|
||||
property string buttonText
|
||||
property bool pointingHandCursor: true
|
||||
property real buttonRadius: Appearance?.rounding?.small ?? 4
|
||||
property real buttonRadiusPressed: buttonRadius
|
||||
property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
property int rippleDuration: 1200
|
||||
property bool rippleEnabled: true
|
||||
property var downAction // When left clicking (down)
|
||||
property var releaseAction // When left clicking (release)
|
||||
property var altAction // When right clicking
|
||||
property var middleClickAction // When middle clicking
|
||||
|
||||
property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent"
|
||||
property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
|
||||
property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C"
|
||||
property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
|
||||
property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
|
||||
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
property color buttonColor: ColorUtils.transparentize(root.toggled ?
|
||||
(root.hovered ? colBackgroundToggledHover :
|
||||
colBackgroundToggled) :
|
||||
(root.hovered ? colBackgroundHover :
|
||||
colBackground), root.enabled ? 0 : 1)
|
||||
property color rippleColor: root.toggled ? colRippleToggled : colRipple
|
||||
|
||||
function startRipple(x, y) {
|
||||
const stateY = buttonBackground.y;
|
||||
rippleAnim.x = x;
|
||||
rippleAnim.y = y - stateY;
|
||||
|
||||
const dist = (ox,oy) => ox*ox + oy*oy
|
||||
const stateEndY = stateY + buttonBackground.height
|
||||
rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)))
|
||||
|
||||
rippleFadeAnim.complete();
|
||||
rippleAnim.restart();
|
||||
}
|
||||
|
||||
component RippleAnim: NumberAnimation {
|
||||
duration: rippleDuration
|
||||
easing.type: Appearance?.animation.elementMoveEnter.type
|
||||
easing.bezierCurve: Appearance?.animationCurves.standardDecel
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: root.pointingHandCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onPressed: (event) => {
|
||||
if(event.button === Qt.RightButton) {
|
||||
if (root.altAction) root.altAction(event);
|
||||
return;
|
||||
}
|
||||
if(event.button === Qt.MiddleButton) {
|
||||
if (root.middleClickAction) root.middleClickAction();
|
||||
return;
|
||||
}
|
||||
root.down = true
|
||||
if (root.downAction) root.downAction();
|
||||
if (!root.rippleEnabled) return;
|
||||
const {x,y} = event
|
||||
startRipple(x, y)
|
||||
}
|
||||
onReleased: (event) => {
|
||||
root.down = false
|
||||
if (event.button != Qt.LeftButton) return;
|
||||
if (root.releaseAction) root.releaseAction();
|
||||
root.click() // Because the MouseArea already consumed the event
|
||||
if (!root.rippleEnabled) return;
|
||||
rippleFadeAnim.restart();
|
||||
}
|
||||
onCanceled: (event) => {
|
||||
root.down = false
|
||||
if (!root.rippleEnabled) return;
|
||||
rippleFadeAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
RippleAnim {
|
||||
id: rippleFadeAnim
|
||||
duration: rippleDuration * 2
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnim
|
||||
|
||||
property real x
|
||||
property real y
|
||||
property real radius
|
||||
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "x"
|
||||
value: rippleAnim.x
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "y"
|
||||
value: rippleAnim.y
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
value: 1
|
||||
}
|
||||
ParallelAnimation {
|
||||
RippleAnim {
|
||||
target: ripple
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
from: 0
|
||||
to: rippleAnim.radius * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: buttonBackground
|
||||
radius: root.buttonEffectiveRadius
|
||||
implicitHeight: 30
|
||||
|
||||
color: root.buttonColor
|
||||
Behavior on color {
|
||||
animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: buttonBackground.width
|
||||
height: buttonBackground.height
|
||||
radius: root.buttonEffectiveRadius
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: ripple
|
||||
width: ripple.implicitWidth
|
||||
height: ripple.implicitHeight
|
||||
opacity: 0
|
||||
visible: width > 0 && height > 0
|
||||
|
||||
property real implicitWidth: 0
|
||||
property real implicitHeight: 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RadialGradient {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: root.rippleColor }
|
||||
GradientStop { position: 0.3; color: root.rippleColor }
|
||||
GradientStop { position: 0.5; color: Qt.rgba(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: -ripple.width / 2
|
||||
y: -ripple.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: StyledText {
|
||||
text: root.buttonText
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RippleButton {
|
||||
id: buttonWithIconRoot
|
||||
property string nerdIcon
|
||||
property string materialIcon
|
||||
property bool materialIconFill: true
|
||||
property string mainText: "Button text"
|
||||
property Component mainContentComponent: Component {
|
||||
StyledText {
|
||||
text: buttonWithIconRoot.mainText
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
}
|
||||
implicitHeight: 35
|
||||
horizontalPadding: 10
|
||||
buttonRadius: Appearance.rounding.small
|
||||
colBackground: Appearance.colors.colLayer2
|
||||
|
||||
contentItem: RowLayout {
|
||||
Item {
|
||||
Layout.fillWidth: false
|
||||
implicitWidth: Math.max(materialIconLoader.implicitWidth, nerdIconLoader.implicitWidth)
|
||||
Loader {
|
||||
id: materialIconLoader
|
||||
anchors.centerIn: parent
|
||||
active: !nerdIcon
|
||||
sourceComponent: MaterialSymbol {
|
||||
text: buttonWithIconRoot.materialIcon
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
fill: buttonWithIconRoot.materialIconFill ? 1 : 0
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: nerdIconLoader
|
||||
anchors.centerIn: parent
|
||||
active: nerdIcon
|
||||
sourceComponent: StyledText {
|
||||
text: buttonWithIconRoot.nerdIcon
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
font.family: Appearance.font.family.iconNerd
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: buttonWithIconRoot.mainContentComponent
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight }
|
||||
property var corner: RoundCorner.CornerEnum.TopLeft
|
||||
property alias leftVisualMargin: shape.anchors.leftMargin
|
||||
property alias topVisualMargin: shape.anchors.topMargin
|
||||
property alias rightVisualMargin: shape.anchors.rightMargin
|
||||
property alias bottomVisualMargin: shape.anchors.bottomMargin
|
||||
|
||||
property int implicitSize: 25
|
||||
property color color: "#000000"
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
property bool isTopLeft: corner === RoundCorner.CornerEnum.TopLeft
|
||||
property bool isBottomLeft: corner === RoundCorner.CornerEnum.BottomLeft
|
||||
property bool isTopRight: corner === RoundCorner.CornerEnum.TopRight
|
||||
property bool isBottomRight: corner === RoundCorner.CornerEnum.BottomRight
|
||||
property bool isTop: isTopLeft || isTopRight
|
||||
property bool isBottom: isBottomLeft || isBottomRight
|
||||
property bool isLeft: isTopLeft || isBottomLeft
|
||||
property bool isRight: isTopRight || isBottomRight
|
||||
|
||||
Shape {
|
||||
id: shape
|
||||
anchors {
|
||||
top: root.isTop ? parent.top : undefined
|
||||
bottom: root.isBottom ? parent.bottom : undefined
|
||||
left: root.isLeft ? parent.left : undefined
|
||||
right: root.isRight ? parent.right : undefined
|
||||
}
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: shapePath
|
||||
strokeWidth: 0
|
||||
fillColor: root.color
|
||||
pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting
|
||||
|
||||
startX: switch (root.corner) {
|
||||
case RoundCorner.CornerEnum.TopLeft: return 0;
|
||||
case RoundCorner.CornerEnum.TopRight: return root.implicitSize;
|
||||
case RoundCorner.CornerEnum.BottomLeft: return 0;
|
||||
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
|
||||
}
|
||||
startY: switch (root.corner) {
|
||||
case RoundCorner.CornerEnum.TopLeft: return 0;
|
||||
case RoundCorner.CornerEnum.TopRight: return 0;
|
||||
case RoundCorner.CornerEnum.BottomLeft: return root.implicitSize;
|
||||
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
|
||||
}
|
||||
PathAngleArc {
|
||||
moveToStart: false
|
||||
centerX: root.implicitSize - shapePath.startX
|
||||
centerY: root.implicitSize - shapePath.startY
|
||||
radiusX: root.implicitSize
|
||||
radiusY: root.implicitSize
|
||||
startAngle: switch (root.corner) {
|
||||
case RoundCorner.CornerEnum.TopLeft: return 180;
|
||||
case RoundCorner.CornerEnum.TopRight: return -90;
|
||||
case RoundCorner.CornerEnum.BottomLeft: return 90;
|
||||
case RoundCorner.CornerEnum.BottomRight: return 0;
|
||||
}
|
||||
sweepAngle: 90
|
||||
}
|
||||
PathLine {
|
||||
x: shapePath.startX
|
||||
y: shapePath.startY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
Item {
|
||||
id: root
|
||||
z: 99
|
||||
required property Item target
|
||||
property real fadeSize: Appearance.m3colors.darkmode ? 40 : 20
|
||||
property color color: ColorUtils.transparentize(Appearance.colors.colShadow, Appearance.m3colors.darkmode ? 0 : 0.7)
|
||||
property bool vertical: true
|
||||
|
||||
anchors.fill: target
|
||||
|
||||
EndGradient {
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: vertical ? parent.right : undefined
|
||||
bottom: vertical ? undefined : parent.bottom
|
||||
}
|
||||
shown: !(root.vertical ? root.target.atYBeginning : root.target.atXBeginning)
|
||||
}
|
||||
|
||||
EndGradient {
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
left: vertical ? parent.left : undefined
|
||||
top: vertical ? undefined : parent.top
|
||||
}
|
||||
shown: !(root.vertical ? root.target.atYEnd : root.target.atXEnd)
|
||||
rotation: 180
|
||||
}
|
||||
|
||||
component EndGradient: Rectangle {
|
||||
required property bool shown
|
||||
height: vertical ? root.fadeSize : parent.height
|
||||
width: vertical ? parent.width : root.fadeSize
|
||||
|
||||
opacity: shown ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: root.vertical ? Gradient.Vertical : Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: root.color
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: ColorUtils.transparentize(root.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
TabButton {
|
||||
id: root
|
||||
property string buttonText
|
||||
property string buttonIcon
|
||||
property bool selected: false
|
||||
property int rippleDuration: 1200
|
||||
height: buttonBackground.height
|
||||
property int tabContentWidth: buttonBackground.width - buttonBackground.radius*2
|
||||
|
||||
property color colBackground: ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
|
||||
property color colBackgroundHover: Appearance.colors.colLayer1Hover
|
||||
property color colRipple: Appearance.colors.colLayer1Active
|
||||
|
||||
PointingHandInteraction {}
|
||||
|
||||
component RippleAnim: NumberAnimation {
|
||||
duration: rippleDuration
|
||||
easing.type: Appearance.animation.elementMoveEnter.type
|
||||
easing.bezierCurve: Appearance.animationCurves.standardDecel
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: (event) => {
|
||||
root.click() // Because the MouseArea already consumed the event
|
||||
const {x,y} = event
|
||||
const stateY = buttonBackground.y;
|
||||
rippleAnim.x = x;
|
||||
rippleAnim.y = y - stateY;
|
||||
|
||||
const dist = (ox,oy) => ox*ox + oy*oy
|
||||
const stateEndY = stateY + buttonBackground.height
|
||||
rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)))
|
||||
|
||||
rippleFadeAnim.complete();
|
||||
rippleAnim.restart();
|
||||
}
|
||||
onReleased: (event) => {
|
||||
rippleFadeAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
RippleAnim {
|
||||
id: rippleFadeAnim
|
||||
duration: rippleDuration * 2
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnim
|
||||
|
||||
property real x
|
||||
property real y
|
||||
property real radius
|
||||
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "x"
|
||||
value: rippleAnim.x
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "y"
|
||||
value: rippleAnim.y
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
value: 1
|
||||
}
|
||||
ParallelAnimation {
|
||||
RippleAnim {
|
||||
target: ripple
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
from: 0
|
||||
to: rippleAnim.radius * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: buttonBackground
|
||||
radius: Appearance?.rounding.small ?? 7
|
||||
implicitHeight: 37
|
||||
color: (root.hovered ? root.colBackgroundHover : root.colBackground)
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: buttonBackground.width
|
||||
height: buttonBackground.height
|
||||
radius: buttonBackground.radius
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: ripple
|
||||
width: ripple.implicitWidth
|
||||
height: ripple.implicitHeight
|
||||
opacity: 0
|
||||
|
||||
property real implicitWidth: 0
|
||||
property real implicitHeight: 0
|
||||
visible: width > 0 && height > 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RadialGradient {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: root.colRipple }
|
||||
GradientStop { position: 0.3; color: root.colRipple }
|
||||
GradientStop { position: 0.5 ; color: Qt.rgba(root.colRipple.r, root.colRipple.g, root.colRipple.b, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: -ripple.width / 2
|
||||
y: -ripple.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: buttonBackground
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
id: iconLoader
|
||||
active: buttonIcon?.length > 0
|
||||
sourceComponent: buttonIcon?.length > 0 ? materialSymbolComponent : null
|
||||
Layout.rightMargin: 5
|
||||
}
|
||||
|
||||
Component {
|
||||
id: materialSymbolComponent
|
||||
MaterialSymbol {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: buttonIcon
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
fill: selected ? 1 : 0
|
||||
color: selected ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: buttonTextWidget
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: selected ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
|
||||
text: buttonText
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real dialogPadding: 15
|
||||
property real dialogMargin: 30
|
||||
property string titleText: "Selection Dialog"
|
||||
property alias items: choiceModel.values
|
||||
property int selectedId: choiceListView.currentIndex
|
||||
property var defaultChoice
|
||||
|
||||
signal canceled();
|
||||
signal selected(var result);
|
||||
|
||||
Rectangle { // Scrim
|
||||
id: scrimOverlay
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.small
|
||||
color: Appearance.colors.colScrim
|
||||
MouseArea {
|
||||
hoverEnabled: true
|
||||
anchors.fill: parent
|
||||
preventStealing: true
|
||||
propagateComposedEvents: false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // The dialog
|
||||
id: dialog
|
||||
color: Appearance.colors.colSurfaceContainerHigh
|
||||
radius: Appearance.rounding.normal
|
||||
anchors.fill: parent
|
||||
anchors.margins: dialogMargin
|
||||
implicitHeight: dialogColumnLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: dialogColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 16
|
||||
|
||||
StyledText {
|
||||
id: dialogTitle
|
||||
Layout.topMargin: dialogPadding
|
||||
Layout.leftMargin: dialogPadding
|
||||
Layout.rightMargin: dialogPadding
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
text: root.titleText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Appearance.m3colors.m3outline
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: dialogPadding
|
||||
Layout.rightMargin: dialogPadding
|
||||
}
|
||||
|
||||
StyledListView {
|
||||
id: choiceListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1
|
||||
spacing: 6
|
||||
|
||||
model: ScriptModel {
|
||||
id: choiceModel
|
||||
}
|
||||
|
||||
delegate: StyledRadioButton {
|
||||
id: radioButton
|
||||
required property var modelData
|
||||
required property int index
|
||||
anchors {
|
||||
left: parent?.left
|
||||
right: parent?.right
|
||||
leftMargin: root.dialogPadding
|
||||
rightMargin: root.dialogPadding
|
||||
}
|
||||
|
||||
description: modelData.toString()
|
||||
checked: index === choiceListView.currentIndex
|
||||
|
||||
onCheckedChanged: {
|
||||
if (checked) {
|
||||
choiceListView.currentIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Appearance.m3colors.m3outline
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: dialogPadding
|
||||
Layout.rightMargin: dialogPadding
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: dialogButtonsRowLayout
|
||||
Layout.bottomMargin: dialogPadding
|
||||
Layout.leftMargin: dialogPadding
|
||||
Layout.rightMargin: dialogPadding
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Cancel")
|
||||
onClicked: root.canceled()
|
||||
}
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("OK")
|
||||
onClicked: root.selected(
|
||||
root.selectedId === -1 ? null :
|
||||
root.items[root.selectedId]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
GroupButton {
|
||||
id: root
|
||||
horizontalPadding: 12
|
||||
verticalPadding: 8
|
||||
bounce: false
|
||||
property string buttonIcon
|
||||
property bool leftmost: false
|
||||
property bool rightmost: false
|
||||
leftRadius: (toggled || leftmost) ? (height / 2) : Appearance.rounding.unsharpenmore
|
||||
rightRadius: (toggled || rightmost) ? (height / 2) : Appearance.rounding.unsharpenmore
|
||||
colBackground: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundHover: Appearance.colors.colSecondaryContainerHover
|
||||
colBackgroundActive: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 4 * (root.buttonText?.length > 0)
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
active: root.buttonIcon && root.buttonIcon.length > 0
|
||||
visible: active
|
||||
sourceComponent: Item {
|
||||
implicitWidth: materialSymbol.implicitWidth
|
||||
MaterialSymbol {
|
||||
id: materialSymbol
|
||||
anchors.centerIn: parent
|
||||
text: root.buttonIcon
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: root.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: root.buttonText?.length > 0 ? textItem.implicitWidth : 0
|
||||
implicitHeight: textMetrics.height // Force height to that of regular text
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
font.family: Appearance.font.family.main
|
||||
text: "Abc"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
color: root.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSecondaryContainer
|
||||
text: root.buttonText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
MultiEffect {
|
||||
id: root
|
||||
source: wallpaper
|
||||
anchors.fill: source
|
||||
saturation: 0.2
|
||||
blurEnabled: true
|
||||
blurMax: 100
|
||||
blur: 1
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.modules.common
|
||||
|
||||
Flickable {
|
||||
id: root
|
||||
maximumFlickVelocity: 3500
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
property real touchpadScrollFactor: Config?.options.interactions.scrolling.touchpadScrollFactor ?? 100
|
||||
property real mouseScrollFactor: Config?.options.interactions.scrolling.mouseScrollFactor ?? 50
|
||||
property real mouseScrollDeltaThreshold: Config?.options.interactions.scrolling.mouseScrollDeltaThreshold ?? 120
|
||||
// Accumulated scroll destination so wheel deltas stack while animating
|
||||
property real scrollTargetY: 0
|
||||
|
||||
ScrollBar.vertical: StyledScrollBar {}
|
||||
|
||||
MouseArea {
|
||||
visible: Config?.options.interactions.scrolling.fasterTouchpadScroll
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheelEvent) {
|
||||
const delta = wheelEvent.angleDelta.y / root.mouseScrollDeltaThreshold;
|
||||
// The angleDelta.y of a touchpad is usually small and continuous,
|
||||
// while that of a mouse wheel is typically in multiples of ±120.
|
||||
var scrollFactor = Math.abs(wheelEvent.angleDelta.y) >= root.mouseScrollDeltaThreshold ? root.mouseScrollFactor : root.touchpadScrollFactor;
|
||||
|
||||
const maxY = Math.max(0, root.contentHeight - root.height);
|
||||
const base = scrollAnim.running ? root.scrollTargetY : root.contentY;
|
||||
var targetY = Math.max(0, Math.min(base - delta * scrollFactor, maxY));
|
||||
|
||||
root.scrollTargetY = targetY;
|
||||
root.contentY = targetY;
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on contentY {
|
||||
NumberAnimation {
|
||||
id: scrollAnim
|
||||
duration: Appearance.animation.scroll.duration
|
||||
easing.type: Appearance.animation.scroll.type
|
||||
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
// Keep target synced when not animating (e.g., drag/flick or programmatic changes)
|
||||
onContentYChanged: {
|
||||
if (!scrollAnim.running) {
|
||||
root.scrollTargetY = root.contentY;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
visible: opacity > 0
|
||||
opacity: (status === Image.Ready) ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
ProgressBar {
|
||||
indeterminate: true
|
||||
Material.accent: Appearance.colors.colPrimary
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Label {
|
||||
renderType: Text.NativeRendering
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font {
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
}
|
||||
color: Appearance?.m3colors.m3onBackground ?? "black"
|
||||
linkColor: Appearance?.m3colors.m3primary
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* A ListView with animations.
|
||||
*/
|
||||
ListView {
|
||||
id: root
|
||||
spacing: 5
|
||||
property real removeOvershoot: 20 // Account for gaps and bouncy animations
|
||||
property int dragIndex: -1
|
||||
property real dragDistance: 0
|
||||
property bool popin: true
|
||||
property bool animateAppearance: true
|
||||
property bool animateMovement: false
|
||||
// Accumulated scroll destination so wheel deltas stack while animating
|
||||
property real scrollTargetY: 0
|
||||
|
||||
property real touchpadScrollFactor: Config?.options.interactions.scrolling.touchpadScrollFactor ?? 100
|
||||
property real mouseScrollFactor: Config?.options.interactions.scrolling.mouseScrollFactor ?? 50
|
||||
property real mouseScrollDeltaThreshold: Config?.options.interactions.scrolling.mouseScrollDeltaThreshold ?? 120
|
||||
|
||||
function resetDrag() {
|
||||
root.dragIndex = -1
|
||||
root.dragDistance = 0
|
||||
}
|
||||
|
||||
maximumFlickVelocity: 3500
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
ScrollBar.vertical: StyledScrollBar {}
|
||||
|
||||
MouseArea {
|
||||
visible: Config?.options.interactions.scrolling.fasterTouchpadScroll
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheelEvent) {
|
||||
const delta = wheelEvent.angleDelta.y / root.mouseScrollDeltaThreshold;
|
||||
// The angleDelta.y of a touchpad is usually small and continuous,
|
||||
// while that of a mouse wheel is typically in multiples of ±120.
|
||||
var scrollFactor = Math.abs(wheelEvent.angleDelta.y) >= root.mouseScrollDeltaThreshold ? root.mouseScrollFactor : root.touchpadScrollFactor;
|
||||
|
||||
const maxY = Math.max(0, root.contentHeight - root.height);
|
||||
const base = scrollAnim.running ? root.scrollTargetY : root.contentY;
|
||||
var targetY = Math.max(0, Math.min(base - delta * scrollFactor, maxY));
|
||||
|
||||
root.scrollTargetY = targetY;
|
||||
root.contentY = targetY;
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on contentY {
|
||||
NumberAnimation {
|
||||
id: scrollAnim
|
||||
duration: Appearance.animation.scroll.duration
|
||||
easing.type: Appearance.animation.scroll.type
|
||||
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
// Keep target synced when not animating (e.g., drag/flick or programmatic changes)
|
||||
onContentYChanged: {
|
||||
if (!scrollAnim.running) {
|
||||
root.scrollTargetY = root.contentY;
|
||||
}
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
from: 0,
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
addDisplaced: Transition {
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
moveDisplaced: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "x",
|
||||
to: root.width + root.removeOvershoot,
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "opacity",
|
||||
to: 0,
|
||||
})
|
||||
] : []
|
||||
}
|
||||
|
||||
// This is movement when something is removed, not removing animation!
|
||||
removeDisplaced: Transition {
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
|
||||
/**
|
||||
* Material 3 progress bar. See https://m3.material.io/components/progress-indicators/overview
|
||||
*/
|
||||
ProgressBar {
|
||||
id: root
|
||||
property real valueBarWidth: 120
|
||||
property real valueBarHeight: 4
|
||||
property real valueBarGap: 4
|
||||
property color highlightColor: Appearance?.colors.colPrimary ?? "#685496"
|
||||
property color trackColor: Appearance?.m3colors.m3secondaryContainer ?? "#F1D3F9"
|
||||
property bool wavy: false // If true, the progress bar will have a wavy fill effect
|
||||
property bool animateWave: true
|
||||
property real waveAmplitudeMultiplier: wavy ? 0.5 : 0
|
||||
property real waveFrequency: 6
|
||||
property real waveFps: 60
|
||||
|
||||
Behavior on waveAmplitudeMultiplier {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on value {
|
||||
animation: Appearance?.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
background: Item {
|
||||
implicitHeight: valueBarHeight
|
||||
implicitWidth: valueBarWidth
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
|
||||
Loader {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
active: root.wavy
|
||||
sourceComponent: WavyLine {
|
||||
id: wavyFill
|
||||
frequency: root.waveFrequency
|
||||
color: root.highlightColor
|
||||
amplitudeMultiplier: root.wavy ? 0.5 : 0
|
||||
height: contentItem.height * 6
|
||||
width: contentItem.width * root.visualPosition
|
||||
lineWidth: contentItem.height
|
||||
fullLength: root.width
|
||||
Connections {
|
||||
target: root
|
||||
function onValueChanged() { wavyFill.requestPaint(); }
|
||||
function onHighlightColorChanged() { wavyFill.requestPaint(); }
|
||||
}
|
||||
FrameAnimation {
|
||||
running: root.animateWave
|
||||
onTriggered: {
|
||||
wavyFill.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: !root.wavy
|
||||
sourceComponent: Rectangle {
|
||||
anchors.left: parent.left
|
||||
width: contentItem.width * root.visualPosition
|
||||
height: contentItem.height
|
||||
radius: height / 2
|
||||
color: root.highlightColor
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Right remaining part fill
|
||||
anchors.right: parent.right
|
||||
width: (1 - root.visualPosition) * parent.width - valueBarGap
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: root.trackColor
|
||||
}
|
||||
|
||||
Rectangle { // Stop point
|
||||
anchors.right: parent.right
|
||||
width: valueBarGap
|
||||
height: valueBarGap
|
||||
radius: height / 2
|
||||
color: root.highlightColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
RadioButton {
|
||||
id: root
|
||||
padding: 4
|
||||
implicitHeight: contentItem.implicitHeight + padding * 2
|
||||
property string description
|
||||
property color activeColor: Appearance?.colors.colPrimary ?? "#685496"
|
||||
property color inactiveColor: Appearance?.m3colors.m3onSurfaceVariant ?? "#45464F"
|
||||
|
||||
PointingHandInteraction {}
|
||||
|
||||
indicator: Item{}
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: contentItem
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
Rectangle {
|
||||
id: radio
|
||||
Layout.fillWidth: false
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
width: 20
|
||||
height: 20
|
||||
radius: Appearance?.rounding.full
|
||||
border.color: checked ? root.activeColor : root.inactiveColor
|
||||
border.width: 2
|
||||
color: "transparent"
|
||||
|
||||
// Checked indicator
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: checked ? 10 : 4
|
||||
height: checked ? 10 : 4
|
||||
radius: Appearance?.rounding.full
|
||||
color: Appearance?.colors.colPrimary
|
||||
opacity: checked ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on width {
|
||||
animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Hover
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: root.hovered ? 40 : 20
|
||||
height: root.hovered ? 40 : 20
|
||||
radius: Appearance?.rounding.full
|
||||
color: Appearance?.m3colors.m3onSurface
|
||||
opacity: root.hovered ? 0.1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on width {
|
||||
animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
color: Appearance?.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.modules.common
|
||||
|
||||
RectangularShadow {
|
||||
required property var target
|
||||
anchors.fill: target
|
||||
radius: target.radius
|
||||
blur: 0.9 * Appearance.sizes.elevationMargin
|
||||
offset: Qt.vector2d(0.0, 1.0)
|
||||
spread: 1
|
||||
color: Appearance.colors.colShadow
|
||||
cached: true
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
ScrollBar {
|
||||
id: root
|
||||
|
||||
policy: ScrollBar.AsNeeded
|
||||
topPadding: Appearance.rounding.normal
|
||||
bottomPadding: Appearance.rounding.normal
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 4
|
||||
implicitHeight: root.visualSize
|
||||
radius: width / 2
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
|
||||
opacity: root.policy === ScrollBar.AlwaysOn || (root.active && root.size < 1.0) ? 0.5 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
|
||||
/**
|
||||
* Material 3 slider. See https://m3.material.io/components/sliders/overview
|
||||
* It doesn't exactly match the spec because it does not make sense to have stuff on a computer that fucking huge.
|
||||
* Should be at 3/4 scale...
|
||||
*/
|
||||
|
||||
Slider {
|
||||
id: root
|
||||
|
||||
property list<real> stopIndicatorValues: [1]
|
||||
enum Configuration {
|
||||
Wavy = 4,
|
||||
XS = 12,
|
||||
S = 18,
|
||||
M = 30,
|
||||
L = 42,
|
||||
XL = 72
|
||||
}
|
||||
|
||||
property var configuration: StyledSlider.Configuration.S
|
||||
|
||||
property real handleDefaultWidth: 3
|
||||
property real handlePressedWidth: 1.5
|
||||
property color highlightColor: Appearance.colors.colPrimary
|
||||
property color trackColor: Appearance.colors.colSecondaryContainer
|
||||
property color handleColor: Appearance.colors.colPrimary
|
||||
property color dotColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
property color dotColorHighlighted: Appearance.m3colors.m3onPrimary
|
||||
property real unsharpenRadius: Appearance.rounding.unsharpen
|
||||
property real trackWidth: configuration
|
||||
property real trackRadius: trackWidth >= StyledSlider.Configuration.XL ? 21
|
||||
: trackWidth >= StyledSlider.Configuration.L ? 12
|
||||
: trackWidth >= StyledSlider.Configuration.M ? 9
|
||||
: trackWidth >= StyledSlider.Configuration.S ? 6
|
||||
: height / 2
|
||||
property real handleHeight: (configuration === StyledSlider.Configuration.Wavy) ? 24 : Math.max(33, trackWidth + 9)
|
||||
property real handleWidth: root.pressed ? handlePressedWidth : handleDefaultWidth
|
||||
property real handleMargins: 4
|
||||
property real trackDotSize: 3
|
||||
property string tooltipContent: `${Math.round(value * 100)}%`
|
||||
property bool wavy: configuration === StyledSlider.Configuration.Wavy // If true, the progress bar will have a wavy fill effect
|
||||
property bool animateWave: true
|
||||
property real waveAmplitudeMultiplier: wavy ? 0.5 : 0
|
||||
property real waveFrequency: 6
|
||||
property real waveFps: 60
|
||||
|
||||
leftPadding: handleMargins
|
||||
rightPadding: handleMargins
|
||||
property real effectiveDraggingWidth: width - leftPadding - rightPadding
|
||||
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1
|
||||
|
||||
Behavior on value { // This makes the adjusted value (like volume) shift smoothly
|
||||
SmoothedAnimation {
|
||||
velocity: Appearance.animation.elementMoveFast.velocity
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on handleMargins {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
component TrackDot: Rectangle {
|
||||
required property real value
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: root.handleMargins + (value * root.effectiveDraggingWidth) - (root.trackDotSize / 2)
|
||||
width: root.trackDotSize
|
||||
height: root.trackDotSize
|
||||
radius: Appearance.rounding.full
|
||||
color: value > root.visualPosition ? root.dotColor : root.dotColorHighlighted
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: (mouse) => mouse.accepted = false
|
||||
cursorShape: root.pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
background: Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
implicitHeight: trackWidth
|
||||
|
||||
// Fill left
|
||||
Loader {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
}
|
||||
width: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins)
|
||||
height: root.trackWidth
|
||||
active: !root.wavy
|
||||
sourceComponent: Rectangle {
|
||||
color: root.highlightColor
|
||||
topLeftRadius: root.trackRadius
|
||||
bottomLeftRadius: root.trackRadius
|
||||
topRightRadius: root.unsharpenRadius
|
||||
bottomRightRadius: root.unsharpenRadius
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
}
|
||||
width: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins)
|
||||
height: root.height
|
||||
active: root.wavy
|
||||
sourceComponent: WavyLine {
|
||||
id: wavyFill
|
||||
frequency: root.waveFrequency
|
||||
fullLength: root.width
|
||||
color: root.highlightColor
|
||||
amplitudeMultiplier: root.wavy ? 0.5 : 0
|
||||
width: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins)
|
||||
height: root.trackWidth
|
||||
Connections {
|
||||
target: root
|
||||
function onValueChanged() { wavyFill.requestPaint(); }
|
||||
function onHighlightColorChanged() { wavyFill.requestPaint(); }
|
||||
}
|
||||
FrameAnimation {
|
||||
running: root.animateWave
|
||||
onTriggered: {
|
||||
wavyFill.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill right
|
||||
Rectangle {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
right: parent.right
|
||||
}
|
||||
width: root.handleMargins + ((1 - root.visualPosition) * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins)
|
||||
height: trackWidth
|
||||
color: root.trackColor
|
||||
topRightRadius: root.trackRadius
|
||||
bottomRightRadius: root.trackRadius
|
||||
topLeftRadius: root.unsharpenRadius
|
||||
bottomLeftRadius: root.unsharpenRadius
|
||||
}
|
||||
|
||||
// Stop indicators
|
||||
Repeater {
|
||||
model: root.stopIndicatorValues
|
||||
TrackDot {
|
||||
required property real modelData
|
||||
value: modelData
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle: Rectangle {
|
||||
id: handle
|
||||
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: root.handleHeight
|
||||
x: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Appearance.rounding.full
|
||||
color: root.handleColor
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
extraVisibleCondition: root.pressed
|
||||
text: root.tooltipContent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Material 3 styled SpinBox component.
|
||||
*/
|
||||
SpinBox {
|
||||
id: root
|
||||
|
||||
property real baseHeight: 35
|
||||
property real radius: Appearance.rounding.small
|
||||
property real innerButtonRadius: Appearance.rounding.unsharpen
|
||||
editable: true
|
||||
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
|
||||
background: Rectangle {
|
||||
color: Appearance.colors.colLayer2
|
||||
radius: root.radius
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: root.baseHeight
|
||||
implicitWidth: Math.max(labelText.implicitWidth, 40)
|
||||
|
||||
StyledTextInput {
|
||||
id: labelText
|
||||
anchors.centerIn: parent
|
||||
text: root.value // displayText would make the numbers weird like 1,000 instead of 1000
|
||||
color: Appearance.colors.colOnLayer2
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
validator: root.validator
|
||||
onTextChanged: {
|
||||
root.value = parseFloat(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
down.indicator: Rectangle {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
}
|
||||
implicitHeight: root.baseHeight
|
||||
implicitWidth: root.baseHeight
|
||||
topLeftRadius: root.radius
|
||||
bottomLeftRadius: root.radius
|
||||
topRightRadius: root.innerButtonRadius
|
||||
bottomRightRadius: root.innerButtonRadius
|
||||
|
||||
color: root.down.pressed ? Appearance.colors.colLayer2Active :
|
||||
root.down.hovered ? Appearance.colors.colLayer2Hover :
|
||||
ColorUtils.transparentize(Appearance.colors.colLayer2)
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
text: "remove"
|
||||
iconSize: 20
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
|
||||
up.indicator: Rectangle {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
right: parent.right
|
||||
}
|
||||
implicitHeight: root.baseHeight
|
||||
implicitWidth: root.baseHeight
|
||||
topRightRadius: root.radius
|
||||
bottomRightRadius: root.radius
|
||||
topLeftRadius: root.innerButtonRadius
|
||||
bottomLeftRadius: root.innerButtonRadius
|
||||
|
||||
color: root.up.pressed ? Appearance.colors.colLayer2Active :
|
||||
root.up.hovered ? Appearance.colors.colLayer2Hover :
|
||||
ColorUtils.transparentize(Appearance.colors.colLayer2)
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
text: "add"
|
||||
iconSize: 20
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
/**
|
||||
* Material 3 switch. See https://m3.material.io/components/switch/overview
|
||||
*/
|
||||
Switch {
|
||||
id: root
|
||||
property real scale: 0.6 // Default in m3 spec is huge af
|
||||
implicitHeight: 32 * root.scale
|
||||
implicitWidth: 52 * root.scale
|
||||
property color activeColor: Appearance?.colors.colPrimary ?? "#685496"
|
||||
property color inactiveColor: Appearance?.colors.colSurfaceContainerHighest ?? "#45464F"
|
||||
|
||||
PointingHandInteraction {}
|
||||
|
||||
// Custom track styling
|
||||
background: Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: Appearance?.rounding.full ?? 9999
|
||||
color: root.checked ? root.activeColor : root.inactiveColor
|
||||
border.width: 2 * root.scale
|
||||
border.color: root.checked ? root.activeColor : Appearance.m3colors.m3outline
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
Behavior on border.color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom thumb styling
|
||||
indicator: Rectangle {
|
||||
width: (root.pressed || root.down) ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale)
|
||||
height: (root.pressed || root.down) ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale)
|
||||
radius: Appearance.rounding.full
|
||||
color: root.checked ? Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3outline
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.checked ? ((root.pressed || root.down) ? (22 * root.scale) : 24 * root.scale) : ((root.pressed || root.down) ? (2 * root.scale) : 8 * root.scale)
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on width {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
id: root
|
||||
property bool animateChange: false
|
||||
property real animationDistanceX: 0
|
||||
property real animationDistanceY: 6
|
||||
|
||||
renderType: Text.NativeRendering
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font {
|
||||
hintingPreference: Font.PreferDefaultHinting
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
}
|
||||
color: Appearance?.m3colors.m3onBackground ?? "black"
|
||||
linkColor: Appearance?.m3colors.m3primary
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
target: root
|
||||
duration: 300 / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
textAnimationBehavior.originalX = root.x;
|
||||
textAnimationBehavior.originalY = root.y;
|
||||
}
|
||||
|
||||
Behavior on text {
|
||||
id: textAnimationBehavior
|
||||
property real originalX: root.x
|
||||
property real originalY: root.y
|
||||
enabled: root.animateChange
|
||||
|
||||
SequentialAnimation {
|
||||
alwaysRunToEnd: true
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "x"
|
||||
to: textAnimationBehavior.originalX - root.animationDistanceX
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
Anim {
|
||||
property: "y"
|
||||
to: textAnimationBehavior.originalY - root.animationDistanceY
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
}
|
||||
PropertyAction {} // Tie the text update to this point (we don't want it to happen during the first slide+fade)
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "x"
|
||||
value: textAnimationBehavior.originalX + root.animationDistanceX
|
||||
}
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "y"
|
||||
value: textAnimationBehavior.originalY + root.animationDistanceY
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "x"
|
||||
to: textAnimationBehavior.originalX
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
Anim {
|
||||
property: "y"
|
||||
to: textAnimationBehavior.originalY
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 1
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Does not include visual layout, but includes the easily neglected colors.
|
||||
*/
|
||||
TextArea {
|
||||
renderType: Text.NativeRendering
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Does not include visual layout, but includes the easily neglected colors.
|
||||
*/
|
||||
TextInput {
|
||||
color: Appearance.colors.colOnLayer1
|
||||
renderType: Text.NativeRendering
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ToolTip {
|
||||
id: root
|
||||
property bool extraVisibleCondition: true
|
||||
property bool alternativeVisibleCondition: false
|
||||
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
|
||||
verticalPadding: 5
|
||||
horizontalPadding: 10
|
||||
background: null
|
||||
|
||||
visible: internalVisibleCondition
|
||||
|
||||
contentItem: StyledToolTipContent {
|
||||
id: contentItem
|
||||
text: root.text
|
||||
shown: root.internalVisibleCondition
|
||||
horizontalPadding: root.horizontalPadding
|
||||
verticalPadding: root.verticalPadding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property string text
|
||||
property bool shown: false
|
||||
property real horizontalPadding: 10
|
||||
property real verticalPadding: 5
|
||||
implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding
|
||||
implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding
|
||||
|
||||
property bool isVisible: backgroundRectangle.implicitHeight > 0
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRectangle
|
||||
anchors {
|
||||
bottom: root.bottom
|
||||
horizontalCenter: root.horizontalCenter
|
||||
}
|
||||
color: Appearance?.colors.colTooltip ?? "#3C4043"
|
||||
radius: Appearance?.rounding.verysmall ?? 7
|
||||
opacity: shown ? 1 : 0
|
||||
implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0
|
||||
implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: tooltipTextObject
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
font.pixelSize: Appearance?.font.pixelSize.smaller ?? 14
|
||||
font.hintingPreference: Font.PreferNoHinting // Prevent shaky text
|
||||
color: Appearance?.colors.colOnTooltip ?? "#FFFFFF"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
/**
|
||||
* Thumbnail image. It currently generates to the right place at the right size, but does not handle metadata/maintenance on modification.
|
||||
* See Freedesktop's spec: https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
|
||||
*/
|
||||
StyledImage {
|
||||
id: root
|
||||
|
||||
property bool generateThumbnail: true
|
||||
required property string sourcePath
|
||||
property string thumbnailSizeName: Images.thumbnailSizeNameForDimensions(sourceSize.width, sourceSize.height)
|
||||
property string thumbnailPath: {
|
||||
if (sourcePath.length == 0) return;
|
||||
const resolvedUrlWithoutFileProtocol = FileUtils.trimFileProtocol(`${Qt.resolvedUrl(sourcePath)}`);
|
||||
const encodedUrlWithoutFileProtocol = resolvedUrlWithoutFileProtocol.split("/").map(part => encodeURIComponent(part)).join("/");
|
||||
const md5Hash = Qt.md5(`file://${encodedUrlWithoutFileProtocol}`);
|
||||
return `${Directories.genericCache}/thumbnails/${thumbnailSizeName}/${md5Hash}.png`;
|
||||
}
|
||||
source: thumbnailPath
|
||||
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: false
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
onSourceSizeChanged: {
|
||||
if (!root.generateThumbnail) return;
|
||||
thumbnailGeneration.running = false;
|
||||
thumbnailGeneration.running = true;
|
||||
}
|
||||
Process {
|
||||
id: thumbnailGeneration
|
||||
command: {
|
||||
const maxSize = Images.thumbnailSizes[root.thumbnailSizeName];
|
||||
return ["bash", "-c",
|
||||
`[ -f '${FileUtils.trimFileProtocol(root.thumbnailPath)}' ] && exit 0 || { magick '${root.sourcePath}' -resize ${maxSize}x${maxSize} '${FileUtils.trimFileProtocol(root.thumbnailPath)}' && exit 1; }`
|
||||
]
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 1) { // Force reload if thumbnail had to be generated
|
||||
root.source = "";
|
||||
root.source = root.thumbnailPath; // Force reload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
/**
|
||||
* Material 3 expressive style toolbar.
|
||||
* https://m3.material.io/components/toolbars
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real padding: 8
|
||||
property alias colBackground: background.color
|
||||
property alias spacing: toolbarLayout.spacing
|
||||
default property alias data: toolbarLayout.data
|
||||
implicitWidth: background.implicitWidth
|
||||
implicitHeight: background.implicitHeight
|
||||
property alias radius: background.radius
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: background
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3surfaceContainer // Needs to be opaque
|
||||
implicitHeight: Math.max(toolbarLayout.implicitHeight + root.padding * 2, 56)
|
||||
implicitWidth: toolbarLayout.implicitWidth + root.padding * 2
|
||||
radius: height / 2
|
||||
|
||||
RowLayout {
|
||||
id: toolbarLayout
|
||||
spacing: 4
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.padding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
|
||||
RippleButton {
|
||||
Layout.fillHeight: true
|
||||
buttonRadius: Appearance.rounding.full
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
TextField {
|
||||
id: filterField
|
||||
|
||||
property alias colBackground: background.color
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 200
|
||||
padding: 10
|
||||
|
||||
placeholderTextColor: Appearance.colors.colSubtext
|
||||
color: Appearance.colors.colOnLayer1
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
renderType: Text.NativeRendering
|
||||
selectedTextColor: Appearance.colors.colOnSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
|
||||
background: Rectangle {
|
||||
id: background
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
/**
|
||||
* A container that supports GroupButton children for bounciness.
|
||||
* See https://m3.material.io/components/button-groups/overview
|
||||
*/
|
||||
Rectangle {
|
||||
id: root
|
||||
default property alias content: columnLayout.data
|
||||
property real spacing: 5
|
||||
property real padding: 0
|
||||
property int clickIndex: columnLayout.clickIndex
|
||||
|
||||
property real contentHeight: {
|
||||
let total = 0;
|
||||
for (let i = 0; i < columnLayout.children.length; ++i) {
|
||||
const child = columnLayout.children[i];
|
||||
total += child.baseHeight ?? child.implicitHeight ?? child.height;
|
||||
}
|
||||
return total + columnLayout.spacing * (columnLayout.children.length - 1);
|
||||
}
|
||||
|
||||
topLeftRadius: columnLayout.children.length > 0 ? (columnLayout.children[0].radius + padding) :
|
||||
Appearance?.rounding?.small
|
||||
topRightRadius: topLeftRadius
|
||||
bottomLeftRadius: columnLayout.children.length > 0 ? (columnLayout.children[columnLayout.children.length - 1].radius + padding) :
|
||||
Appearance?.rounding?.small
|
||||
bottomRightRadius: bottomLeftRadius
|
||||
|
||||
color: "transparent"
|
||||
height: root.contentHeight + padding * 2
|
||||
implicitWidth: columnLayout.implicitWidth + padding * 2
|
||||
implicitHeight: root.contentHeight + padding * 2
|
||||
|
||||
children: [ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
spacing: root.spacing
|
||||
property int clickIndex: -1
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
ToolbarButton {
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colPrimaryContainer)
|
||||
colBackgroundHover: Appearance.colors.colPrimaryContainerHover
|
||||
colRipple: Appearance.colors.colPrimaryContainerActive
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
Canvas { // Visualizer
|
||||
id: root
|
||||
property list<var> points
|
||||
property list<var> smoothPoints
|
||||
property real maxVisualizerValue: 1000
|
||||
property int smoothing: 2
|
||||
property bool live: true
|
||||
property color color: Appearance.m3colors.m3primary
|
||||
|
||||
onPointsChanged: () => {
|
||||
root.requestPaint()
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
var points = root.points;
|
||||
var maxVal = root.maxVisualizerValue || 1;
|
||||
var h = height;
|
||||
var w = width;
|
||||
var n = points.length;
|
||||
if (n < 2) return;
|
||||
|
||||
// Smoothing: simple moving average (optional)
|
||||
var smoothWindow = root.smoothing; // adjust for more/less smoothing
|
||||
root.smoothPoints = [];
|
||||
for (var i = 0; i < n; ++i) {
|
||||
var sum = 0, count = 0;
|
||||
for (var j = -smoothWindow; j <= smoothWindow; ++j) {
|
||||
var idx = Math.max(0, Math.min(n - 1, i + j));
|
||||
sum += points[idx];
|
||||
count++;
|
||||
}
|
||||
root.smoothPoints.push(sum / count);
|
||||
}
|
||||
if (!root.live) root.smoothPoints.fill(0); // If not playing, show no points
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h);
|
||||
for (var i = 0; i < n; ++i) {
|
||||
var x = i * w / (n - 1);
|
||||
var y = h - (root.smoothPoints[i] / maxVal) * h;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo(w, h);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = Qt.rgba(
|
||||
root.color.r,
|
||||
root.color.g,
|
||||
root.color.b,
|
||||
0.15
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect { // Blur a bit to obscure away the points
|
||||
source: root
|
||||
saturation: 0.2
|
||||
blurEnabled: true
|
||||
blurMax: 7
|
||||
blur: 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
property real amplitudeMultiplier: 0.5
|
||||
property real frequency: 6
|
||||
property color color: Appearance?.colors.colPrimary ?? "#685496"
|
||||
property real lineWidth: 4
|
||||
property real fullLength: width
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
var amplitude = root.lineWidth * root.amplitudeMultiplier;
|
||||
var frequency = root.frequency;
|
||||
var phase = Date.now() / 400.0;
|
||||
var centerY = height / 2;
|
||||
|
||||
ctx.strokeStyle = root.color;
|
||||
ctx.lineWidth = root.lineWidth;
|
||||
ctx.lineCap = "round";
|
||||
ctx.beginPath();
|
||||
for (var x = ctx.lineWidth / 2; x <= root.width - ctx.lineWidth / 2; x += 1) {
|
||||
var waveY = centerY + amplitude * Math.sin(frequency * 2 * Math.PI * x / root.fullLength + phase);
|
||||
if (x === 0)
|
||||
ctx.moveTo(x, waveY);
|
||||
else
|
||||
ctx.lineTo(x, waveY);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool show: false
|
||||
default property alias data: contentColumn.data
|
||||
property real backgroundHeight: 600
|
||||
property real backgroundAnimationMovementDistance: 60
|
||||
|
||||
signal dismiss()
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dismiss();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
color: root.show ? Appearance.colors.colScrim : ColorUtils.transparentize(Appearance.colors.colScrim)
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
visible: dialogBackground.implicitHeight > 0
|
||||
|
||||
onShowChanged: {
|
||||
dialogBackgroundHeightAnimation.easing.bezierCurve = (show ? Appearance.animationCurves.emphasizedDecel : Appearance.animationCurves.emphasizedAccel)
|
||||
dialogBackground.implicitHeight = show ? backgroundHeight : 0
|
||||
}
|
||||
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
MouseArea { // Clicking outside the dialog should dismiss
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
hoverEnabled: true
|
||||
onPressed: root.dismiss()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dialogBackground
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
radius: Appearance.rounding.large
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh // Use opaque version of layer3
|
||||
|
||||
property real targetY: root.height / 2 - root.backgroundHeight / 2
|
||||
y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance)
|
||||
implicitWidth: 350
|
||||
implicitHeight: 0
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
id: dialogBackgroundHeightAnimation
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: dialogBackgroundHeightAnimation.duration
|
||||
easing.type: dialogBackgroundHeightAnimation.easing.type
|
||||
easing.bezierCurve: dialogBackgroundHeightAnimation.easing.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea { // So clicking inside the dialog won't dismiss
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: dialogBackground.radius
|
||||
}
|
||||
spacing: 16
|
||||
opacity: root.show ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
spacing: 4
|
||||
|
||||
// These shouldn't be needed but it would be a terrible waste of space to follow the spec
|
||||
Layout.margins: -8
|
||||
Layout.topMargin: 0
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
implicitHeight: 1
|
||||
color: Appearance.colors.colOutline
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
Layout.topMargin: -8
|
||||
Layout.bottomMargin: -8
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
StyledText {
|
||||
text: "Dialog Title"
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.title
|
||||
family: Appearance.font.family.title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
/**
|
||||
* @param { string } summary
|
||||
* @returns { string }
|
||||
*/
|
||||
function findSuitableMaterialSymbol(summary = "") {
|
||||
const defaultType = 'chat';
|
||||
if(summary.length === 0) return defaultType;
|
||||
|
||||
const keywordsToTypes = {
|
||||
'reboot': 'restart_alt',
|
||||
'record': 'screen_record',
|
||||
'battery': 'power',
|
||||
'power': 'power',
|
||||
'screenshot': 'screenshot_monitor',
|
||||
'welcome': 'waving_hand',
|
||||
'time': 'scheduleb',
|
||||
'installed': 'download',
|
||||
'configuration reloaded': 'reset_wrench',
|
||||
'config': 'reset_wrench',
|
||||
'update': 'update',
|
||||
'ai response': 'neurology',
|
||||
'control': 'settings',
|
||||
'upsca': 'compare',
|
||||
'install': 'deployed_code_update',
|
||||
'startswith:file': 'folder_copy', // Declarative startsWith check
|
||||
};
|
||||
|
||||
const lowerSummary = summary.toLowerCase();
|
||||
|
||||
for (const [keyword, type] of Object.entries(keywordsToTypes)) {
|
||||
if (keyword.startsWith('startswith:')) {
|
||||
const startsWithKeyword = keyword.replace('startswith:', '');
|
||||
if (lowerSummary.startsWith(startsWithKeyword)) {
|
||||
return type;
|
||||
}
|
||||
} else if (lowerSummary.includes(keyword)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { number | string | Date } timestamp
|
||||
* @returns { string }
|
||||
*/
|
||||
const getFriendlyNotifTimeString = (timestamp) => {
|
||||
if (!timestamp) return '';
|
||||
const messageTime = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - messageTime.getTime();
|
||||
|
||||
// Less than 1 minute
|
||||
if (diffMs < 60000)
|
||||
return 'Now';
|
||||
|
||||
// Same day - show relative time
|
||||
if (messageTime.toDateString() === now.toDateString()) {
|
||||
const diffMinutes = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
|
||||
if (diffHours > 0) {
|
||||
return `${diffHours}h`;
|
||||
} else {
|
||||
return `${diffMinutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
// Yesterday
|
||||
if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString())
|
||||
return 'Yesterday';
|
||||
|
||||
// Older dates
|
||||
return Qt.formatDateTime(messageTime, "MMMM dd");
|
||||
};
|
||||
Reference in New Issue
Block a user