chore: move sidebar directory to modules

This commit is contained in:
2026-10-23 00:35:20 +02:00
parent 36cddc96c3
commit cbd4e6caa7
40 changed files with 12 additions and 12 deletions
+61
View File
@@ -0,0 +1,61 @@
import qs.Components
import ZShell.Config
import QtQuick
import QtQuick.Shapes
import qs.Services
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Config.bar.rounding
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property var sidebar
required property Wrapper wrapper
fillColor: Colors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathLine {
relativeX: -(root.wrapper.width + root.rounding)
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.sidebar.utilsRoundingX
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.sidebar.utilsRoundingX
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: root.rounding
relativeX: root.rounding
relativeY: -root.rounding
}
}
+289
View File
@@ -0,0 +1,289 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Components
import ZShell.Config
import qs.Helpers
import qs.Services
CustomRect {
id: root
required property ScreenState screenState
Layout.fillWidth: true
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Tokens.rounding.small
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.medium
RowLayout {
spacing: Tokens.spacing.medium
z: 1
CustomRect {
color: Recorder.running ? Colors.palette.m3secondary : Colors.palette.m3secondaryContainer
implicitHeight: {
const h = icon.implicitHeight + Tokens.padding.small * 2;
return h - (h % 2);
}
implicitWidth: implicitHeight
radius: Tokens.rounding.full
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -0.5
anchors.verticalCenterOffset: 1.5
color: Recorder.running ? Colors.palette.m3onSecondary : Colors.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.large
text: "screen_record"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.medium
text: qsTr("Screen Recorder")
}
CustomText {
Layout.fillWidth: true
color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font: Tokens.font.body.small
text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off")
}
}
SplitButton {
active: menuItems.find(m => root.screenState.recordingMode === m.icon + m.text) ?? menuItems[0]
enabled: !Recorder.running
menuOnTop: true
menuItems: [
MenuItem {
activeText: qsTr("Fullscreen")
icon: "fullscreen"
text: qsTr("Record fullscreen")
onClicked: Recorder.start()
},
MenuItem {
activeText: qsTr("Region")
icon: "screenshot_region"
text: qsTr("Record region")
onClicked: Recorder.start(["-r"])
},
MenuItem {
activeText: qsTr("Fullscreen")
icon: "select_to_speak"
text: qsTr("Record fullscreen with sound")
onClicked: Recorder.start(["-s"])
},
MenuItem {
activeText: qsTr("Region")
icon: "volume_up"
text: qsTr("Record region with sound")
onClicked: Recorder.start(["-s", "-r"])
}
]
menu.onItemSelected: item => root.screenState.recordingMode = item.icon + item.text
}
}
Loader {
id: listOrControls
property bool running: Recorder.running
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
asynchronous: true
sourceComponent: running ? recordingControls : recordingList
Behavior on Layout.preferredHeight {
id: locHeightAnim
enabled: false
Anim {}
}
Behavior on running {
SequentialAnimation {
ParallelAnimation {
Anim {
duration: Tokens.anim.durations.small
easing: Tokens.anim.standardAccel
property: "scale"
target: listOrControls
to: 0.7
}
Anim {
duration: Tokens.anim.durations.small
easing: Tokens.anim.standardAccel
property: "opacity"
target: listOrControls
to: 0
}
}
PropertyAction {
property: "enabled"
target: locHeightAnim
value: true
}
PropertyAction {}
PropertyAction {
property: "enabled"
target: locHeightAnim
value: false
}
ParallelAnimation {
Anim {
duration: Tokens.anim.durations.small
easing: Tokens.anim.standardDecel
property: "scale"
target: listOrControls
to: 1
}
Anim {
duration: Tokens.anim.durations.small
easing: Tokens.anim.standardDecel
property: "opacity"
target: listOrControls
to: 1
}
}
}
}
}
}
Component {
id: recordingList
RecordingList {
screenState: root.screenState
}
}
Component {
id: recordingControls
RowLayout {
spacing: Tokens.spacing.medium
CustomRect {
color: Recorder.paused ? Colors.palette.m3tertiary : Colors.palette.m3error
implicitHeight: recText.implicitHeight + Tokens.padding.small * 2
implicitWidth: recText.implicitWidth + Tokens.padding.small * 2
radius: Tokens.rounding.full
Behavior on implicitWidth {
Anim {}
}
SequentialAnimation on opacity {
alwaysRunToEnd: true
loops: Animation.Infinite
running: !Recorder.paused
Anim {
duration: Tokens.anim.durations.large
easing: Tokens.anim.emphasizedAccel
from: 1
to: 0
}
Anim {
duration: Tokens.anim.durations.extraLarge
easing: Tokens.anim.emphasizedDecel
from: 0
to: 1
}
}
CustomText {
id: recText
anchors.centerIn: parent
animate: true
color: Recorder.paused ? Colors.palette.m3onTertiary : Colors.palette.m3onError
font: Tokens.font.mono.small
text: Recorder.paused ? "PAUSED" : "REC"
}
}
CustomText {
font: Tokens.font.body.medium
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);
}
}
Item {
Layout.fillWidth: true
}
IconButton {
checked: Recorder.paused
font: Tokens.font.icon.medium
icon: Recorder.paused ? "play_arrow" : "pause"
isRound: true
isToggle: true
label.animate: true
type: IconButton.Tonal
onClicked: {
Recorder.togglePause();
internalChecked = Recorder.paused;
}
}
IconButton {
font: Tokens.font.icon.medium
icon: "stop"
isRound: true
inactiveColor: Colors.palette.m3error
inactiveOnColor: Colors.palette.m3onError
onClicked: Recorder.stop()
}
}
}
}
@@ -0,0 +1,224 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ZShell.Models
import qs.Components
import qs.Helpers
import qs.Paths
import ZShell.Config
import qs.Services
ColumnLayout {
id: root
required property ScreenState screenState
spacing: 0
WrapperMouseArea {
Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor
onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
RowLayout {
spacing: Tokens.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
fontStyle: Tokens.font.icon.large
text: "list"
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
font: Tokens.font.body.medium
text: qsTr("Recordings")
}
IconButton {
icon: root.screenState.recordingListExpanded ? "unfold_less" : "unfold_more"
label.animate: true
type: IconButton.Text
onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
}
}
}
CustomListView {
id: list
Layout.fillWidth: true
Layout.rightMargin: -Tokens.spacing.small
clip: true
implicitHeight: (Tokens.font.body.large.pointSize + Tokens.padding.extraSmall) * (root.screenState.recordingListExpanded ? 10 : 3)
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
Anim {
from: 0.5
property: "scale"
to: 1
}
}
delegate: RowLayout {
id: recording
property string baseName
required property FileSystemEntry modelData
anchors.left: list.contentItem.left
anchors.right: list.contentItem.right
anchors.rightMargin: Tokens.spacing.small
spacing: Tokens.spacing.small / 2
Component.onCompleted: baseName = modelData.baseName
CustomText {
Layout.fillWidth: true
Layout.rightMargin: Tokens.spacing.small / 2
color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
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);
return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
}
}
IconButton {
icon: "play_arrow"
type: IconButton.Text
onClicked: {
root.screenState.sidebar = false;
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
else
Quickshell.execDetached([...Config.general.apps.playback, recording.modelData.path]);
}
}
IconButton {
icon: "folder"
type: IconButton.Text
onClicked: {
root.screenState.sidebar = false;
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
else
Quickshell.execDetached([...Config.general.apps.explorer, recording.modelData.path]);
}
}
}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim {
property: "y"
}
}
Behavior on implicitHeight {
Anim {}
}
model: FileSystemModel {
nameFilters: ["recording_*.mp4"]
path: Paths.recsdir
sortReverse: true
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim {
property: "scale"
to: 0.5
}
}
Loader {
active: opacity > 0
anchors.centerIn: parent
asynchronous: true
opacity: list.count === 0 ? 1 : 0
Behavior on opacity {
Anim {}
}
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: root.screenState.recordingListExpanded ? implicitHeight : 0
color: Colors.palette.m3outline
fontStyle: Tokens.font.icon.extraLarge
opacity: root.screenState.recordingListExpanded ? 1 : 0
scale: root.screenState.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredHeight {
Anim {}
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
}
RowLayout {
spacing: Tokens.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: !root.screenState.recordingListExpanded ? implicitWidth : 0
color: Colors.palette.m3outline
opacity: !root.screenState.recordingListExpanded ? 1 : 0
scale: !root.screenState.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredWidth {
Anim {}
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
}
CustomText {
color: Colors.palette.m3outline
text: qsTr("No recordings found")
}
}
}
}
}
}
+184
View File
@@ -0,0 +1,184 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Networking as QSNetwork
import ZShell.Components
import ZShell.Config
import qs.Components
import qs.Services
import qs.Helpers
import qs.Daemons
import qs.Modules.Settings
CustomRect {
id: root
readonly property bool needExtraRow: quickToggles.length > 6
readonly property var quickToggles: {
const seenIds = new Set();
return Config.utilities.quickToggles.values.filter(item => {
if (!item.enabled)
return false;
if (seenIds.has(item.id)) {
return false;
}
if (item.id === "bluetooth" && !Bluetooth.defaultAdapter)
return false;
if (item.id === "wifi" && !QSNetwork.Networking.devices.values.some(n => n.type === QSNetwork.DeviceType.Wifi))
return false;
seenIds.add(item.id);
return true;
});
}
required property ScreenState screenState
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
radius: Tokens.rounding.small
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.medium
CustomText {
font: Tokens.font.body.medium
text: qsTr("Quick Toggles")
}
QuickToggleRow {
model: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles
}
QuickToggleRow {
model: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : []
visible: root.needExtraRow
}
}
component QuickToggleRow: ButtonRow {
property alias model: repeater.model
Layout.fillWidth: true
spacing: Tokens.spacing.small
Repeater {
id: repeater
delegate: DelegateChooser {
role: "id"
DelegateChoice {
roleValue: "wifi"
delegate: Toggle {
checked: Network.wifiEnabled
icon: checked ? "wifi" : "wifi_off"
onClicked: Network.toggleWifi()
}
}
DelegateChoice {
roleValue: "bluetooth"
delegate: Toggle {
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
icon: "bluetooth"
onClicked: {
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
if (adapter)
adapter.enabled = !adapter.enabled;
}
}
}
DelegateChoice {
roleValue: "mic"
delegate: Toggle {
checked: !Audio.sourceMuted
icon: checked ? "mic" : "mic_off"
onClicked: {
const audio = Audio.source?.audio;
if (audio)
audio.muted = !audio.muted;
}
}
}
DelegateChoice {
roleValue: "audio"
delegate: Toggle {
checked: !Audio.muted
icon: checked ? "volume_up" : "volume_off"
onClicked: {
const audio = Audio.sink?.audio;
if (audio)
audio.muted = !audio.muted;
}
}
}
DelegateChoice {
roleValue: "gameMode"
delegate: Toggle {
checked: GameMode.enabled
icon: "gamepad"
onClicked: GameMode.enabled = !GameMode.enabled
}
}
DelegateChoice {
roleValue: "dnd"
delegate: Toggle {
checked: NotifServer.dnd
icon: "notifications_off"
onClicked: NotifServer.dnd = !NotifServer.dnd
}
}
DelegateChoice {
roleValue: "settings"
delegate: Toggle {
icon: "settings"
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isToggle: false
onClicked: {
root.screenState.sidebar = false;
root.screenState.settings = true;
}
}
}
}
}
}
component Toggle: IconButton {
fillWidth: true
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
isRound: true
isToggle: true
shapeMorph: true
}
}
+34
View File
@@ -0,0 +1,34 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Modules.Sidebar.Utils.Cards
Item {
id: root
required property ScreenState screenState
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 8
IdleInhibit {}
Record {
screenState: root.screenState
z: 1
}
Toggles {
Layout.fillWidth: true
screenState: root.screenState
}
}
}
+111
View File
@@ -0,0 +1,111 @@
import qs.Components
import ZShell.Config
import qs.Helpers
import QtQuick
import QtQuick.Layouts
import qs.Services
CustomRect {
id: root
Layout.fillWidth: true
clip: true
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + 18 * 2
radius: Tokens.rounding.small
Behavior on implicitHeight {
Anim {}
}
RowLayout {
id: layout
anchors.left: parent.left
anchors.margins: 18
anchors.right: parent.right
anchors.top: parent.top
spacing: 10
CustomRect {
color: IdleInhibitor.enabled ? Colors.palette.m3secondary : Colors.palette.m3secondaryContainer
implicitHeight: icon.implicitHeight + 7 * 2
implicitWidth: implicitHeight
radius: Tokens.rounding.full
MaterialIcon {
id: icon
anchors.centerIn: parent
color: IdleInhibitor.enabled ? Colors.palette.m3onSecondary : Colors.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.large
text: "coffee"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.medium
text: qsTr("Keep Awake")
}
CustomText {
Layout.fillWidth: true
color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font: Tokens.font.body.small
text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management")
}
}
CustomSwitch {
checked: IdleInhibitor.enabled
onToggled: IdleInhibitor.enabled = checked
}
}
Loader {
id: activeChip
anchors.bottom: parent.bottom
anchors.bottomMargin: IdleInhibitor.enabled ? 18 : -implicitHeight
anchors.left: parent.left
anchors.leftMargin: 18
anchors.topMargin: 20
opacity: IdleInhibitor.enabled ? 1 : 0
scale: IdleInhibitor.enabled ? 1 : 0.5
Behavior on anchors.bottomMargin {
Anim {}
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
sourceComponent: CustomRect {
color: Colors.palette.m3primary
implicitHeight: activeText.implicitHeight + 10 * 2
implicitWidth: activeText.implicitWidth + 10 * 2
radius: Tokens.rounding.full
CustomText {
id: activeText
anchors.centerIn: parent
color: Colors.palette.m3onPrimary
font: Tokens.font.body.builders.small.size(Math.round(Tokens.font.body.small.pointSize * 0.9)).build()
text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm"))
}
}
Component.onCompleted: active = Qt.binding(() => opacity > 0)
}
}
+42
View File
@@ -0,0 +1,42 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import ZShell.Config
import qs.Components
Item {
id: root
property real offsetScale: shouldBeActive ? 0 : 1
required property ScreenState screenState
readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive
required property Item sidebar
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
opacity: 1 - offsetScale
visible: offsetScale < 1
Behavior on offsetScale {
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
}
}
Loader {
id: content
active: root.shouldBeActive || root.visible
anchors.left: parent.left
anchors.margins: 8
anchors.top: parent.top
sourceComponent: Content {
implicitWidth: root.implicitWidth - 8 * 2
screenState: root.screenState
}
}
}