quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,55 @@
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
required property var sidebar
readonly property real rounding: Config.border.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: Colours.palette.m3surface
PathLine {
relativeX: -(root.wrapper.width + root.rounding)
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
relativeX: root.sidebar.utilsRoundingX
relativeY: -root.roundingY
radiusX: root.sidebar.utilsRoundingX
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.rounding
radiusX: root.rounding
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
Behavior on fillColor {
CAnim {}
}
}

View File

@@ -0,0 +1,39 @@
import "cards"
import qs.config
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property var props
required property var visibilities
required property Item popouts
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
anchors.fill: parent
spacing: Appearance.spacing.normal
IdleInhibit {}
Record {
props: root.props
visibilities: root.visibilities
z: 1
}
Toggles {
visibilities: root.visibilities
popouts: root.popouts
}
}
RecordingDeleteModal {
props: root.props
}
}

View File

@@ -0,0 +1,207 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.components.effects
import qs.services
import qs.config
import Caelestia
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
Loader {
id: root
required property var props
anchors.fill: parent
opacity: root.props.recordingConfirmDelete ? 1 : 0
active: opacity > 0
sourceComponent: MouseArea {
id: deleteConfirmation
property string path
Component.onCompleted: path = root.props.recordingConfirmDelete
hoverEnabled: true
onClicked: root.props.recordingConfirmDelete = ""
Item {
anchors.fill: parent
anchors.margins: -Appearance.padding.large
anchors.rightMargin: -Appearance.padding.large - Config.border.thickness
anchors.bottomMargin: -Appearance.padding.large - Config.border.thickness
opacity: 0.5
StyledRect {
anchors.fill: parent
topLeftRadius: Config.border.rounding
color: Colours.palette.m3scrim
}
Shape {
id: shape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
ShapePath {
startX: -Config.border.rounding * 2
startY: shape.height - Config.border.thickness
strokeWidth: 0
fillGradient: LinearGradient {
orientation: LinearGradient.Horizontal
x1: -Config.border.rounding * 2
GradientStop {
position: 0
color: Qt.alpha(Colours.palette.m3scrim, 0)
}
GradientStop {
position: 1
color: Colours.palette.m3scrim
}
}
PathLine {
relativeX: Config.border.rounding
relativeY: 0
}
PathArc {
relativeY: -Config.border.rounding
radiusX: Config.border.rounding
radiusY: Config.border.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: Config.border.rounding + Config.border.thickness
}
PathLine {
relativeX: -Config.border.rounding * 2
relativeY: 0
}
}
ShapePath {
startX: shape.width - Config.border.rounding - Config.border.thickness
strokeWidth: 0
fillGradient: LinearGradient {
orientation: LinearGradient.Vertical
y1: -Config.border.rounding * 2
GradientStop {
position: 0
color: Qt.alpha(Colours.palette.m3scrim, 0)
}
GradientStop {
position: 1
color: Colours.palette.m3scrim
}
}
PathArc {
relativeX: Config.border.rounding
relativeY: -Config.border.rounding
radiusX: Config.border.rounding
radiusY: Config.border.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -Config.border.rounding
}
PathLine {
relativeX: Config.border.thickness
relativeY: 0
}
PathLine {
relativeX: 0
}
}
}
}
StyledRect {
anchors.centerIn: parent
radius: Appearance.rounding.large
color: Colours.palette.m3surfaceContainerHigh
scale: 0
Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0)
width: Math.min(parent.width - Appearance.padding.large * 2, implicitWidth)
implicitWidth: deleteConfirmationLayout.implicitWidth + Appearance.padding.large * 3
implicitHeight: deleteConfirmationLayout.implicitHeight + Appearance.padding.large * 3
MouseArea {
anchors.fill: parent
}
Elevation {
anchors.fill: parent
radius: parent.radius
z: -1
level: 3
}
ColumnLayout {
id: deleteConfirmationLayout
anchors.fill: parent
anchors.margins: Appearance.padding.large * 1.5
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Delete recording?")
font.pointSize: Appearance.font.size.large
}
StyledText {
Layout.fillWidth: true
text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
RowLayout {
Layout.topMargin: Appearance.spacing.normal
Layout.alignment: Qt.AlignRight
spacing: Appearance.spacing.normal
TextButton {
text: qsTr("Cancel")
type: TextButton.Text
onClicked: root.props.recordingConfirmDelete = ""
}
TextButton {
text: qsTr("Delete")
type: TextButton.Text
onClicked: {
CUtils.deleteFile(Qt.resolvedUrl(root.props.recordingConfirmDelete));
root.props.recordingConfirmDelete = "";
}
}
}
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}
Behavior on opacity {
Anim {}
}
}

View File

@@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.config
import Quickshell
import QtQuick
Item {
id: root
required property var visibilities
required property Item sidebar
required property Item popouts
readonly property PersistentProperties props: PersistentProperties {
property bool recordingListExpanded: false
property string recordingConfirmDelete
property string recordingMode
reloadableId: "utilities"
}
readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled))
visible: height > 0
implicitHeight: 0
implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width
onStateChanged: {
if (state === "visible" && timer.running) {
timer.triggered();
timer.stop();
}
}
states: State {
name: "visible"
when: root.shouldBeActive
PropertyChanges {
root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
Anim {
target: root
property: "implicitHeight"
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Timer {
id: timer
running: true
interval: Appearance.anim.durations.extraLarge
onTriggered: {
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
content.visible = true;
}
}
Loader {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Appearance.padding.large
visible: false
active: true
sourceComponent: Content {
implicitWidth: root.implicitWidth - Appearance.padding.large * 2
props: root.props
visibilities: root.visibilities
popouts: root.popouts
}
}
}

View File

@@ -0,0 +1,125 @@
import qs.components
import qs.components.controls
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
clip: true
RowLayout {
id: layout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
StyledRect {
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "coffee"
color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
font.pointSize: Appearance.font.size.large
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
text: qsTr("Keep Awake")
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management")
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
elide: Text.ElideRight
}
}
StyledSwitch {
checked: IdleInhibitor.enabled
onToggled: IdleInhibitor.enabled = checked
}
}
Loader {
id: activeChip
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: Appearance.spacing.larger
anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight
anchors.leftMargin: Appearance.padding.large
opacity: IdleInhibitor.enabled ? 1 : 0
scale: IdleInhibitor.enabled ? 1 : 0.5
Component.onCompleted: active = Qt.binding(() => opacity > 0)
sourceComponent: StyledRect {
implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2
implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.full
color: Colours.palette.m3primary
StyledText {
id: activeText
anchors.centerIn: parent
text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm"))
color: Colours.palette.m3onPrimary
font.pointSize: Math.round(Appearance.font.size.small * 0.9)
}
}
Behavior on anchors.bottomMargin {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.small
}
}
Behavior on scale {
Anim {}
}
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}

View File

@@ -0,0 +1,277 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
required property var props
required property var visibilities
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
RowLayout {
spacing: Appearance.spacing.normal
z: 1
StyledRect {
implicitWidth: implicitHeight
implicitHeight: {
const h = icon.implicitHeight + Appearance.padding.smaller * 2;
return h - (h % 2);
}
radius: Appearance.rounding.full
color: Recorder.running ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -0.5
anchors.verticalCenterOffset: 1.5
text: "screen_record"
color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
font.pointSize: Appearance.font.size.large
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
text: qsTr("Screen Recorder")
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off")
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
elide: Text.ElideRight
}
}
SplitButton {
disabled: Recorder.running
active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0]
menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text
menuItems: [
MenuItem {
icon: "fullscreen"
text: qsTr("Record fullscreen")
activeText: qsTr("Fullscreen")
onClicked: Recorder.start()
},
MenuItem {
icon: "screenshot_region"
text: qsTr("Record region")
activeText: qsTr("Region")
onClicked: Recorder.start(["-r"])
},
MenuItem {
icon: "select_to_speak"
text: qsTr("Record fullscreen with sound")
activeText: qsTr("Fullscreen")
onClicked: Recorder.start(["-s"])
},
MenuItem {
icon: "volume_up"
text: qsTr("Record region with sound")
activeText: qsTr("Region")
onClicked: Recorder.start(["-sr"])
}
]
}
}
Loader {
id: listOrControls
property bool running: Recorder.running
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
sourceComponent: running ? recordingControls : recordingList
Behavior on Layout.preferredHeight {
id: locHeightAnim
enabled: false
Anim {}
}
Behavior on running {
SequentialAnimation {
ParallelAnimation {
Anim {
target: listOrControls
property: "scale"
to: 0.7
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
target: listOrControls
property: "opacity"
to: 0
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
}
PropertyAction {
target: locHeightAnim
property: "enabled"
value: true
}
PropertyAction {}
PropertyAction {
target: locHeightAnim
property: "enabled"
value: false
}
ParallelAnimation {
Anim {
target: listOrControls
property: "scale"
to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
target: listOrControls
property: "opacity"
to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
}
}
}
Component {
id: recordingList
RecordingList {
props: root.props
visibilities: root.visibilities
}
}
Component {
id: recordingControls
RowLayout {
spacing: Appearance.spacing.normal
StyledRect {
radius: Appearance.rounding.full
color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error
implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2
implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2
StyledText {
id: recText
anchors.centerIn: parent
animate: true
text: Recorder.paused ? "PAUSED" : "REC"
color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError
font.family: Appearance.font.family.mono
}
Behavior on implicitWidth {
Anim {}
}
SequentialAnimation on opacity {
running: !Recorder.paused
alwaysRunToEnd: true
loops: Animation.Infinite
Anim {
from: 1
to: 0
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.emphasizedAccel
}
Anim {
from: 0
to: 1
duration: Appearance.anim.durations.extraLarge
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
}
StyledText {
text: {
const elapsed = Recorder.elapsed;
const hours = Math.floor(elapsed / 3600);
const mins = Math.floor((elapsed % 3600) / 60);
const secs = Math.floor(elapsed % 60).toString().padStart(2, "0");
let time;
if (hours > 0)
time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
else
time = `${mins}:${secs}`;
return qsTr("Recording for %1").arg(time);
}
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
IconButton {
label.animate: true
icon: Recorder.paused ? "play_arrow" : "pause"
toggle: true
checked: Recorder.paused
type: IconButton.Tonal
font.pointSize: Appearance.font.size.large
onClicked: {
Recorder.togglePause();
internalChecked = Recorder.paused;
}
}
IconButton {
icon: "stop"
inactiveColour: Colours.palette.m3error
inactiveOnColour: Colours.palette.m3onError
font.pointSize: Appearance.font.size.large
onClicked: Recorder.stop()
}
}
}
}

View File

@@ -0,0 +1,241 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.components.containers
import qs.services
import qs.config
import qs.utils
import Caelestia
import Caelestia.Models
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property var props
required property var visibilities
spacing: 0
WrapperMouseArea {
Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
RowLayout {
spacing: Appearance.spacing.smaller
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
text: "list"
font.pointSize: Appearance.font.size.large
}
StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: qsTr("Recordings")
font.pointSize: Appearance.font.size.normal
}
IconButton {
icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more"
type: IconButton.Text
label.animate: true
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
}
}
}
StyledListView {
id: list
model: FileSystemModel {
path: Paths.recsdir
nameFilters: ["recording_*.mp4"]
sortReverse: true
}
Layout.fillWidth: true
Layout.rightMargin: -Appearance.spacing.small
implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)
clip: true
StyledScrollBar.vertical: StyledScrollBar {
flickable: list
}
delegate: RowLayout {
id: recording
required property FileSystemEntry modelData
property string baseName
anchors.left: list.contentItem.left
anchors.right: list.contentItem.right
anchors.rightMargin: Appearance.spacing.small
spacing: Appearance.spacing.small / 2
Component.onCompleted: baseName = modelData.baseName
StyledText {
Layout.fillWidth: true
Layout.rightMargin: Appearance.spacing.small / 2
text: {
const time = recording.baseName;
const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/);
if (!matches)
return time;
const date = new Date(...matches.slice(1));
date.setMonth(date.getMonth() - 1); // Woe (months start from 0)
return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
}
color: Colours.palette.m3onSurfaceVariant
elide: Text.ElideRight
}
IconButton {
icon: "play_arrow"
type: IconButton.Text
onClicked: {
root.visibilities.utilities = false;
root.visibilities.sidebar = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
}
}
IconButton {
icon: "folder"
type: IconButton.Text
onClicked: {
root.visibilities.utilities = false;
root.visibilities.sidebar = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
}
}
IconButton {
icon: "delete_forever"
type: IconButton.Text
label.color: Colours.palette.m3error
stateLayer.color: Colours.palette.m3error
onClicked: root.props.recordingConfirmDelete = recording.modelData.path
}
}
add: Transition {
Anim {
property: "opacity"
from: 0
to: 1
}
Anim {
property: "scale"
from: 0.5
to: 1
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim {
property: "scale"
to: 0.5
}
}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim {
property: "y"
}
}
Loader {
anchors.centerIn: parent
opacity: list.count === 0 ? 1 : 0
active: opacity > 0
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "scan_delete"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.extraLarge
opacity: root.props.recordingListExpanded ? 1 : 0
scale: root.props.recordingListExpanded ? 1 : 0
Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on Layout.preferredHeight {
Anim {}
}
}
RowLayout {
spacing: Appearance.spacing.smaller
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "scan_delete"
color: Colours.palette.m3outline
opacity: !root.props.recordingListExpanded ? 1 : 0
scale: !root.props.recordingListExpanded ? 1 : 0
Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on Layout.preferredWidth {
Anim {}
}
}
StyledText {
text: qsTr("No recordings found")
color: Colours.palette.m3outline
}
}
}
Behavior on opacity {
Anim {}
}
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}

View File

@@ -0,0 +1,113 @@
import qs.components
import qs.components.controls
import qs.services
import qs.config
import qs.modules.controlcenter
import Quickshell
import Quickshell.Bluetooth
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
required property var visibilities
required property Item popouts
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Quick Toggles")
font.pointSize: Appearance.font.size.normal
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Appearance.spacing.small
Toggle {
icon: "wifi"
checked: Nmcli.wifiEnabled
onClicked: Nmcli.toggleWifi()
}
Toggle {
icon: "bluetooth"
checked: Bluetooth.defaultAdapter?.enabled ?? false
onClicked: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.enabled = !adapter.enabled;
}
}
Toggle {
icon: "mic"
checked: !Audio.sourceMuted
onClicked: {
const audio = Audio.source?.audio;
if (audio)
audio.muted = !audio.muted;
}
}
Toggle {
icon: "settings"
inactiveOnColour: Colours.palette.m3onSurfaceVariant
toggle: false
onClicked: {
root.visibilities.utilities = false;
root.popouts.detach("network");
}
}
Toggle {
icon: "gamepad"
checked: GameMode.enabled
onClicked: GameMode.enabled = !GameMode.enabled
}
Toggle {
icon: "notifications_off"
checked: Notifs.dnd
onClicked: Notifs.dnd = !Notifs.dnd
}
Toggle {
icon: "vpn_key"
checked: VPN.connected
enabled: !VPN.connecting
visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false)
onClicked: VPN.toggle()
}
}
}
component Toggle: IconButton {
Layout.fillWidth: true
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)
radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal
inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
toggle: true
radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial
radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
Behavior on Layout.preferredWidth {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
}
}

View File

@@ -0,0 +1,135 @@
import qs.components
import qs.components.effects
import qs.services
import qs.config
import Caelestia
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
required property Toast modelData
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.normal
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3successContainer;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3secondary;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3errorContainer;
return Colours.palette.m3surface;
}
border.width: 1
border.color: {
let colour = Colours.palette.m3outlineVariant;
if (root.modelData.type === Toast.Success)
colour = Colours.palette.m3success;
if (root.modelData.type === Toast.Warning)
colour = Colours.palette.m3secondaryContainer;
if (root.modelData.type === Toast.Error)
colour = Colours.palette.m3error;
return Qt.alpha(colour, 0.3);
}
Elevation {
anchors.fill: parent
radius: parent.radius
opacity: parent.opacity
z: -1
level: 3
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledRect {
radius: Appearance.rounding.normal
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3success;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3secondaryContainer;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3error;
return Colours.palette.m3surfaceContainerHigh;
}
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2
MaterialIcon {
id: icon
anchors.centerIn: parent
text: root.modelData.icon
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3onSuccess;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3onSecondaryContainer;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3onError;
return Colours.palette.m3onSurfaceVariant;
}
font.pointSize: Math.round(Appearance.font.size.large * 1.2)
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
id: title
Layout.fillWidth: true
text: root.modelData.title
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3onSuccessContainer;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3onSecondary;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3onErrorContainer;
return Colours.palette.m3onSurface;
}
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
textFormat: Text.StyledText
text: root.modelData.message
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3onSuccessContainer;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3onSecondary;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3onErrorContainer;
return Colours.palette.m3onSurface;
}
opacity: 0.8
elide: Text.ElideRight
}
}
}
Behavior on border.color {
CAnim {}
}
}

View File

@@ -0,0 +1,143 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.config
import Caelestia
import Quickshell
import QtQuick
Item {
id: root
readonly property int spacing: Appearance.spacing.small
property bool flag
implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2
implicitHeight: {
let h = -spacing;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i) as ToastWrapper;
if (!item.modelData.closed && !item.previewHidden)
h += item.implicitHeight + spacing;
}
return h;
}
Repeater {
id: repeater
model: ScriptModel {
values: {
const toasts = [];
let count = 0;
for (const toast of Toaster.toasts) {
toasts.push(toast);
if (!toast.closed) {
count++;
if (count > Config.utilities.maxToasts)
break;
}
}
return toasts;
}
onValuesChanged: root.flagChanged()
}
ToastWrapper {}
}
component ToastWrapper: MouseArea {
id: toast
required property int index
required property Toast modelData
readonly property bool previewHidden: {
let extraHidden = 0;
for (let i = 0; i < index; i++)
if (Toaster.toasts[i].closed)
extraHidden++;
return index >= Config.utilities.maxToasts + extraHidden;
}
onPreviewHiddenChanged: {
if (initAnim.running && previewHidden)
initAnim.stop();
}
opacity: modelData.closed || previewHidden ? 0 : 1
scale: modelData.closed || previewHidden ? 0.7 : 1
anchors.bottomMargin: {
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i) as ToastWrapper;
if (item && !item.modelData.closed && !item.previewHidden)
y += item.implicitHeight + root.spacing;
}
return y;
}
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: toastInner.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: modelData.close()
Component.onCompleted: modelData.lock(this)
Anim {
id: initAnim
Component.onCompleted: running = !toast.previewHidden
target: toast
properties: "opacity,scale"
from: 0
to: 1
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
ParallelAnimation {
running: toast.modelData.closed
onStarted: toast.anchors.bottomMargin = toast.anchors.bottomMargin
onFinished: toast.modelData.unlock(toast)
Anim {
target: toast
property: "opacity"
to: 0
}
Anim {
target: toast
property: "scale"
to: 0.7
}
}
ToastItem {
id: toastInner
modelData: toast.modelData
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on anchors.bottomMargin {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}