forked from Shinonome/dots-hyprland
Merge remote-tracking branch 'origin/main' into addon-i18n
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
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 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,95 @@
|
||||
// From https://github.com/rafzby/circular-progressbar with modifications
|
||||
// License: LGPL-3.0 - A copy can be found in `licenses` folder of repo
|
||||
|
||||
import QtQuick
|
||||
import "root:/modules/common"
|
||||
|
||||
/**
|
||||
* Material 3 circular progress. See https://m3.material.io/components/progress-indicators/specs
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int size: 30
|
||||
property int lineWidth: 2
|
||||
property real value: 0
|
||||
property color primaryColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
property color secondaryColor: Appearance.colors.colSecondaryContainer
|
||||
property real gapAngle: Math.PI / 9
|
||||
property bool fill: false
|
||||
property int fillOverflow: 2
|
||||
property int animationDuration: 1000
|
||||
property var easingType: Easing.OutCubic
|
||||
|
||||
width: size
|
||||
height: size
|
||||
|
||||
signal animationFinished();
|
||||
|
||||
onValueChanged: {
|
||||
canvas.degree = value * 360;
|
||||
}
|
||||
onPrimaryColorChanged: {
|
||||
canvas.requestPaint();
|
||||
}
|
||||
onSecondaryColorChanged: {
|
||||
canvas.requestPaint();
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: canvas
|
||||
|
||||
property real degree: 0
|
||||
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
|
||||
onDegreeChanged: {
|
||||
requestPaint();
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
var x = root.width / 2;
|
||||
var y = root.height / 2;
|
||||
var radius = root.size / 2 - root.lineWidth;
|
||||
var startAngle = (Math.PI / 180) * 270;
|
||||
var fullAngle = (Math.PI / 180) * (270 + 360);
|
||||
var progressAngle = (Math.PI / 180) * (270 + degree);
|
||||
var epsilon = 0.01; // Small angle in radians
|
||||
|
||||
ctx.reset();
|
||||
if (root.fill) {
|
||||
ctx.fillStyle = root.secondaryColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius + fillOverflow, startAngle, fullAngle);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = root.lineWidth;
|
||||
|
||||
// Secondary
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, progressAngle + gapAngle, fullAngle - gapAngle);
|
||||
ctx.strokeStyle = root.secondaryColor;
|
||||
ctx.stroke();
|
||||
|
||||
// Primary (value indication)
|
||||
var endAngle = progressAngle + (value > 0 ? 0 : epsilon);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, startAngle, endAngle);
|
||||
ctx.strokeStyle = root.primaryColor;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Behavior on degree {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.easingType
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import "root:/modules/common/functions/file_utils.js" as FileUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Qt.labs.platform
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property string entry
|
||||
property real maxWidth
|
||||
property real maxHeight
|
||||
|
||||
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 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}'`])
|
||||
}
|
||||
|
||||
Image {
|
||||
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
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: image.width
|
||||
height: image.height
|
||||
radius: root.radius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
property bool uniform: false
|
||||
spacing: 10
|
||||
uniformCellSizes: uniform
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import "root:/services/"
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets/"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import "root:/modules/common/functions/file_utils.js" as FileUtils
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
property list<var> options: []
|
||||
property string configOptionName: ""
|
||||
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
|
||||
buttonText: modelData.displayName;
|
||||
toggled: root.currentValue === modelData.value
|
||||
onClicked: {
|
||||
root.selected(modelData.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import "root:/modules/common/widgets/"
|
||||
import "root:/modules/common/"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
property string text: ""
|
||||
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
|
||||
|
||||
StyledText {
|
||||
id: labelWidget
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
|
||||
StyledSpinBox {
|
||||
id: spinBoxWidget
|
||||
Layout.fillWidth: false
|
||||
value: root.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import "root:/modules/common/widgets/"
|
||||
import "root:/modules/common/"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentItem.implicitHeight + 8 * 2
|
||||
onClicked: checked = !checked
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 10
|
||||
StyledText {
|
||||
id: labelWidget
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
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.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets/"
|
||||
|
||||
Flickable {
|
||||
id: root
|
||||
property real baseWidth: 550
|
||||
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: 10
|
||||
}
|
||||
spacing: 20
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets/"
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property string title
|
||||
default property alias data: sectionContent.data
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
ColumnLayout {
|
||||
id: sectionContent
|
||||
spacing: 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/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
|
||||
content: root.tooltip
|
||||
}
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
ColumnLayout {
|
||||
id: sectionContent
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets/"
|
||||
|
||||
StyledText {
|
||||
text: "Subsection"
|
||||
color: Appearance.colors.colSubtext
|
||||
Layout.leftMargin: 4
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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: "root:/assets/icons" // The folder to check first
|
||||
width: 30
|
||||
height: 30
|
||||
|
||||
IconImage {
|
||||
id: iconImage
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
if (iconFolder && iconFolder + "/" + root.source) {
|
||||
return iconFolder + "/" + root.source
|
||||
}
|
||||
return root.source
|
||||
}
|
||||
implicitSize: root.height
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.colorize
|
||||
anchors.fill: iconImage
|
||||
sourceComponent: ColorOverlay {
|
||||
source: iconImage
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
/**
|
||||
* Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
|
||||
*/
|
||||
RippleButton {
|
||||
id: button
|
||||
|
||||
property string buttonText
|
||||
implicitHeight: 30
|
||||
implicitWidth: buttonTextWidget.implicitWidth + 15 * 2
|
||||
buttonRadius: Appearance?.rounding.full ?? 9999
|
||||
|
||||
property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96"
|
||||
|
||||
contentItem: StyledText {
|
||||
id: buttonTextWidget
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 15
|
||||
anchors.rightMargin: 15
|
||||
text: buttonText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance?.font.pixelSize.small ?? 12
|
||||
color: button.enabled ? button.colEnabled : button.colDisabled
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/services"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
/**
|
||||
* 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,53 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import "root:/modules/common/functions/file_utils.js" as FileUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Qt.labs.platform
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
|
||||
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,60 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/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,123 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
/**
|
||||
* 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(Appearance?.colors.colLayer1Hover, 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();
|
||||
root.click() // Because the MouseArea already consumed the event
|
||||
}
|
||||
onCanceled: (event) => {
|
||||
root.down = 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,46 @@
|
||||
import "root:/modules/common"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
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,124 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
GroupButton {
|
||||
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
|
||||
sperm: true
|
||||
animateSperm: 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 ? "Dark" : "Light"
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import "root:/modules/common/"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Text {
|
||||
id: root
|
||||
property real iconSize: Appearance?.font.pixelSize.small ?? 16
|
||||
property real fill: 0
|
||||
property real truncatedFill: Math.round(fill * 100) / 100 // Reduce memory consumption spikes from constant font remapping
|
||||
renderType: Text.NativeRendering
|
||||
font {
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
family: Appearance?.font.family.iconMaterial ?? "Material Symbols Rounded"
|
||||
pixelSize: iconSize
|
||||
weight: Font.Normal + (Font.DemiBold - Font.Normal) * fill
|
||||
variableAxes: {
|
||||
"FILL": truncatedFill,
|
||||
// "wght": font.weight,
|
||||
// "GRAD": 0,
|
||||
"opsz": iconSize,
|
||||
}
|
||||
}
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Appearance.m3colors.m3onBackground
|
||||
|
||||
// Behavior on fill {
|
||||
// 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,53 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
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,31 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
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,12 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/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,149 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
|
||||
TabButton {
|
||||
id: root
|
||||
|
||||
property bool toggled: TabBar.tabBar.currentIndex === TabBar.index
|
||||
property string buttonIcon
|
||||
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
|
||||
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,31 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets/"
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
Layout.leftMargin: 8
|
||||
onClicked: {
|
||||
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,44 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
|
||||
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
|
||||
property real baseHighlightHeight: tabBarColumn.children[0].baseHighlightHeight
|
||||
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
|
||||
|
||||
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,27 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/services"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
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.colSurfaceContainerHighest
|
||||
colBackgroundHover: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSurfaceContainerHighestHover
|
||||
colRipple: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainerActive : Appearance.colors.colSurfaceContainerHighestActive
|
||||
|
||||
contentItem: StyledText {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: buttonText
|
||||
color: (urgency == NotificationUrgency.Critical) ? Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import "root:/modules/common"
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
Rectangle { // App icon
|
||||
id: root
|
||||
property var appIcon: ""
|
||||
property var summary: ""
|
||||
property var urgency: NotificationUrgency.Normal
|
||||
property var image: ""
|
||||
property real scale: 1
|
||||
property real size: 45 * scale
|
||||
property real materialIconScale: 0.57
|
||||
property real appIconScale: 0.7
|
||||
property real smallAppIconScale: 0.49
|
||||
property real materialIconSize: size * materialIconScale
|
||||
property real appIconSize: size * appIconScale
|
||||
property real smallAppIconSize: size * smallAppIconScale
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
radius: Appearance.rounding.full
|
||||
color: 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) ?
|
||||
"release_alert" : guessedIcon
|
||||
}
|
||||
anchors.fill: parent
|
||||
color: (root.urgency == NotificationUrgency.Critical) ?
|
||||
ColorUtils.mix(Appearance.m3colors.m3onSecondary, Appearance.m3colors.m3onSecondaryContainer, 0.1) :
|
||||
Appearance.m3colors.m3onSecondaryContainer
|
||||
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,244 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/services"
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
/**
|
||||
* A group of notifications from the same app.
|
||||
* Similar to Android's notifications
|
||||
*/
|
||||
Item { // 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;
|
||||
}
|
||||
|
||||
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.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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: Appearance.colors.colSurfaceContainer
|
||||
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
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
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,52 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/services"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
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,318 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/services"
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
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 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;
|
||||
}
|
||||
|
||||
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.id);
|
||||
}
|
||||
}
|
||||
|
||||
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.colSurfaceContainerHigh) :
|
||||
ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHighest)
|
||||
|
||||
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
|
||||
// Layout.fillWidth: true
|
||||
StyledText {
|
||||
id: summaryText
|
||||
visible: !root.onlyNotification
|
||||
font.pixelSize: root.fontSize
|
||||
color: Appearance.colors.colOnLayer2
|
||||
elide: Text.ElideRight
|
||||
text: root.notificationObject.summary || ""
|
||||
}
|
||||
StyledText {
|
||||
opacity: !root.expanded ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: root.fontSize
|
||||
color: Appearance.colors.colSubtext
|
||||
elide: Text.ElideRight
|
||||
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:${notificationBodyText.width}px;}</style>` +
|
||||
`${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "<br/>")}`
|
||||
}
|
||||
|
||||
onLinkActivated: (link) => {
|
||||
Qt.openUrlExternally(link)
|
||||
Hyprland.dispatch("global quickshell:sidebarRightClose")
|
||||
}
|
||||
|
||||
PointingHandLinkHover {}
|
||||
}
|
||||
|
||||
Flickable { // Notification actions
|
||||
id: actionsFlickable
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: actionRowLayout.implicitHeight
|
||||
contentWidth: actionRowLayout.implicitWidth
|
||||
clip: !onlyNotification
|
||||
|
||||
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.id, 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,31 @@
|
||||
import "root:/"
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
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,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,98 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/services/"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
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
|
||||
|
||||
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,173 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
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) => {
|
||||
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) => {
|
||||
button.click() // Because the MouseArea already consumed the event
|
||||
rippleFadeAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
RippleAnim {
|
||||
id: rippleFadeAnim
|
||||
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,26 @@
|
||||
import "root:/modules/common"
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
/**
|
||||
* 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 "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
/**
|
||||
* A button with ripple effect similar to in Material Design.
|
||||
*/
|
||||
Button {
|
||||
id: root
|
||||
property bool toggled
|
||||
property string buttonText
|
||||
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"
|
||||
|
||||
property color buttonColor: root.enabled ? (root.toggled ?
|
||||
(root.hovered ? colBackgroundToggledHover :
|
||||
colBackgroundToggled) :
|
||||
(root.hovered ? colBackgroundHover :
|
||||
colBackground)) : colBackground
|
||||
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: 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();
|
||||
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
|
||||
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: 50
|
||||
|
||||
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,56 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/modules/common/"
|
||||
import "root:/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: 15
|
||||
buttonRadius: Appearance.rounding.small
|
||||
colBackground: Appearance.colors.colLayer2
|
||||
|
||||
contentItem: RowLayout {
|
||||
Item {
|
||||
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 {
|
||||
sourceComponent: buttonWithIconRoot.mainContentComponent
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import QtQuick 2.9
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight }
|
||||
property var corner: RoundCorner.CornerEnum.TopLeft // Default to TopLeft
|
||||
|
||||
property int size: 25
|
||||
property color color: "#000000"
|
||||
|
||||
onColorChanged: {
|
||||
canvas.requestPaint();
|
||||
}
|
||||
onCornerChanged: {
|
||||
canvas.requestPaint();
|
||||
}
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
|
||||
Canvas {
|
||||
id: canvas
|
||||
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
var r = root.size;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.beginPath();
|
||||
switch (root.corner) {
|
||||
case RoundCorner.CornerEnum.TopLeft:
|
||||
ctx.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
|
||||
ctx.lineTo(0, 0);
|
||||
break;
|
||||
case RoundCorner.CornerEnum.TopRight:
|
||||
ctx.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
|
||||
ctx.lineTo(r, 0);
|
||||
break;
|
||||
case RoundCorner.CornerEnum.BottomLeft:
|
||||
ctx.arc(r, 0, r, Math.PI / 2, Math.PI);
|
||||
ctx.lineTo(0, r);
|
||||
break;
|
||||
case RoundCorner.CornerEnum.BottomRight:
|
||||
ctx.arc(0, 0, r, 0, Math.PI / 2);
|
||||
ctx.lineTo(r, r);
|
||||
break;
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = root.color;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on size {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
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) => {
|
||||
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) => {
|
||||
root.click() // Because the MouseArea already consumed the event
|
||||
rippleFadeAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
RippleAnim {
|
||||
id: rippleFadeAnim
|
||||
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)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: ripple
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: root.colRipple
|
||||
opacity: 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,130 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import "root:/services/"
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
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
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: choiceListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1
|
||||
|
||||
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,24 @@
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import "root:/services/"
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets/"
|
||||
|
||||
GroupButton {
|
||||
id: root
|
||||
horizontalPadding: 12
|
||||
verticalPadding: 8
|
||||
bounce: false
|
||||
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
|
||||
contentItem: StyledText {
|
||||
color: parent.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSecondaryContainer
|
||||
text: root.buttonText
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import "root:/modules/common"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
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,110 @@
|
||||
import "root:/"
|
||||
import "root:/modules/common/"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
function resetDrag() {
|
||||
root.dragIndex = -1
|
||||
root.dragDistance = 0
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
animations: [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
from: 0,
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
addDisplaced: Transition {
|
||||
animations: [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
// displaced: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
|
||||
// move: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
// moveDisplaced: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
|
||||
remove: Transition {
|
||||
animations: [
|
||||
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: [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import "root:/services"
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
/**
|
||||
* 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 sperm: false // If true, the progress bar will have a wavy fill effect
|
||||
property bool animateSperm: true
|
||||
property real spermAmplitudeMultiplier: sperm ? 0.5 : 0
|
||||
property real spermFrequency: 6
|
||||
property real spermFps: 60
|
||||
|
||||
Behavior on spermAmplitudeMultiplier {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on value {
|
||||
animation: Appearance?.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
background: Item {
|
||||
anchors.fill: parent
|
||||
implicitHeight: valueBarHeight
|
||||
implicitWidth: valueBarWidth
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Canvas {
|
||||
id: wavyFill
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
height: parent.height * 6
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
var progress = root.visualPosition;
|
||||
var fillWidth = progress * width;
|
||||
var amplitude = parent.height * root.spermAmplitudeMultiplier;
|
||||
var frequency = root.spermFrequency;
|
||||
var phase = Date.now() / 400.0;
|
||||
var centerY = height / 2;
|
||||
|
||||
ctx.strokeStyle = root.highlightColor;
|
||||
ctx.lineWidth = parent.height;
|
||||
ctx.lineCap = "round";
|
||||
ctx.beginPath();
|
||||
for (var x = ctx.lineWidth / 2; x <= fillWidth; x += 1) {
|
||||
var waveY = centerY + amplitude * Math.sin(frequency * 2 * Math.PI * x / width + phase);
|
||||
if (x === 0)
|
||||
ctx.moveTo(x, waveY);
|
||||
else
|
||||
ctx.lineTo(x, waveY);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
Connections {
|
||||
target: root
|
||||
function onValueChanged() { wavyFill.requestPaint(); }
|
||||
function onHighlightColorChanged() { wavyFill.requestPaint(); }
|
||||
}
|
||||
Timer {
|
||||
interval: 1000 / root.spermFps
|
||||
running: root.animateSperm
|
||||
repeat: root.sperm
|
||||
onTriggered: wavyFill.requestPaint()
|
||||
}
|
||||
}
|
||||
Rectangle { // Right remaining part fill
|
||||
anchors.right: parent.right
|
||||
width: (1 - root.visualPosition) * parent.width - valueBarGap
|
||||
height: parent.height
|
||||
radius: Appearance?.rounding.full ?? 9999
|
||||
color: root.trackColor
|
||||
}
|
||||
Rectangle { // Stop point
|
||||
anchors.right: parent.right
|
||||
width: valueBarGap
|
||||
height: valueBarGap
|
||||
radius: Appearance?.rounding.full ?? 9999
|
||||
color: root.highlightColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
RadioButton {
|
||||
id: root
|
||||
implicitHeight: 40
|
||||
property string description
|
||||
property color activeColor: Appearance?.colors.colPrimary ?? "#685496"
|
||||
property color inactiveColor: Appearance?.m3colors.m3onSurfaceVariant ?? "#45464F"
|
||||
|
||||
PointingHandInteraction {}
|
||||
|
||||
indicator: Item{}
|
||||
|
||||
contentItem: RowLayout {
|
||||
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 "root:/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,113 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
|
||||
// Material 3 slider. See https://m3.material.io/components/sliders/overview
|
||||
Slider {
|
||||
id: root
|
||||
property real scale: 0.85
|
||||
property real backgroundDotSize: 4 * scale
|
||||
property real backgroundDotMargins: 4 * scale
|
||||
// property real handleMargins: 0 * scale
|
||||
property real handleMargins: (root.pressed ? 0 : 2) * scale
|
||||
property real handleWidth: (root.pressed ? 3 : 5) * scale
|
||||
property real handleHeight: 44 * scale
|
||||
property real handleLimit: root.backgroundDotMargins
|
||||
property real trackHeight: 30 * scale
|
||||
property color highlightColor: Appearance.colors.colPrimary
|
||||
property color trackColor: Appearance.colors.colSecondaryContainer
|
||||
property color handleColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
property real trackRadius: Appearance.rounding.verysmall * scale
|
||||
property real unsharpenRadius: Appearance.rounding.unsharpen
|
||||
|
||||
property real limitedHandleRangeWidth: (root.availableWidth - handleWidth - root.handleLimit * 2)
|
||||
property string tooltipContent: `${Math.round(value * 100)}%`
|
||||
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 {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: (mouse) => mouse.accepted = false
|
||||
cursorShape: root.pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
background: Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitHeight: trackHeight
|
||||
|
||||
// Fill left
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: root.handleLimit * 2 + root.visualPosition * root.limitedHandleRangeWidth - (root.handleMargins + root.handleWidth / 2)
|
||||
height: trackHeight
|
||||
color: root.highlightColor
|
||||
topLeftRadius: root.trackRadius
|
||||
bottomLeftRadius: root.trackRadius
|
||||
topRightRadius: root.unsharpenRadius
|
||||
bottomRightRadius: root.unsharpenRadius
|
||||
}
|
||||
|
||||
// Fill right
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
width: root.handleLimit * 2 + (1 - root.visualPosition) * root.limitedHandleRangeWidth - (root.handleMargins + root.handleWidth / 2)
|
||||
height: trackHeight
|
||||
color: root.trackColor
|
||||
topLeftRadius: root.unsharpenRadius
|
||||
bottomLeftRadius: root.unsharpenRadius
|
||||
topRightRadius: root.trackRadius
|
||||
bottomRightRadius: root.trackRadius
|
||||
}
|
||||
|
||||
// Dot at the end
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.backgroundDotMargins
|
||||
width: root.backgroundDotSize
|
||||
height: root.backgroundDotSize
|
||||
radius: Appearance.rounding.full
|
||||
color: root.handleColor
|
||||
}
|
||||
}
|
||||
|
||||
handle: Rectangle {
|
||||
id: handle
|
||||
x: root.leftPadding + root.handleLimit + root.visualPosition * root.limitedHandleRangeWidth
|
||||
y: root.topPadding + root.availableHeight / 2 - height / 2
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: root.handleHeight
|
||||
radius: Appearance.rounding.full
|
||||
color: root.handleColor
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
extraVisibleCondition: root.pressed
|
||||
content: root.tooltipContent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
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
|
||||
|
||||
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 "root:/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,15 @@
|
||||
import "root:/modules/common"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Text {
|
||||
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,18 @@
|
||||
import "root:/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,17 @@
|
||||
import "root:/modules/common"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Does not include visual layout, but includes the easily neglected colors.
|
||||
*/
|
||||
TextInput {
|
||||
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,60 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ToolTip {
|
||||
id: root
|
||||
property string content
|
||||
property bool extraVisibleCondition: true
|
||||
property bool alternativeVisibleCondition: false
|
||||
property bool internalVisibleCondition: {
|
||||
const ans = (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
|
||||
return ans
|
||||
}
|
||||
verticalPadding: 5
|
||||
horizontalPadding: 10
|
||||
opacity: internalVisibleCondition ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
background: null
|
||||
|
||||
contentItem: Item {
|
||||
id: contentItemBackground
|
||||
implicitWidth: tooltipTextObject.width + 2 * root.horizontalPadding
|
||||
implicitHeight: tooltipTextObject.height + 2 * root.verticalPadding
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRectangle
|
||||
anchors.bottom: contentItemBackground.bottom
|
||||
anchors.horizontalCenter: contentItemBackground.horizontalCenter
|
||||
color: Appearance?.colors.colTooltip ?? "#3C4043"
|
||||
radius: Appearance?.rounding.verysmall ?? 7
|
||||
width: internalVisibleCondition ? (tooltipTextObject.width + 2 * padding) : 0
|
||||
height: internalVisibleCondition ? (tooltipTextObject.height + 2 * padding) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on width {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: tooltipTextObject
|
||||
anchors.centerIn: parent
|
||||
text: content
|
||||
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,47 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
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,78 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "root:/services"
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
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,77 @@
|
||||
|
||||
/**
|
||||
* @param { string } summary
|
||||
* @returns { string }
|
||||
*/
|
||||
function findSuitableMaterialSymbol(summary = "") {
|
||||
const defaultType = 'chat';
|
||||
if(summary.length === 0) return defaultType;
|
||||
|
||||
const keywordsToTypes = {
|
||||
'reboot': 'restart_alt',
|
||||
'recording': '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',
|
||||
'upscale': '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