Watch
1
0
Fork
You've already forked souveraine
0

quickshell: pin shared ii base and phone overlay

This commit is contained in:
Fimeg 2026-07-21 18:55:02 -04:00
commit b0a1304ba1
970 changed files with 88310 additions and 1 deletions

View file

@ -0,0 +1,297 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.ii.sidebarRight.calendar
import qs.modules.ii.sidebarRight.todo
import qs.modules.ii.sidebarRight.pomodoro
import QtQuick
import QtQuick.Layouts
Rectangle {
id: root
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
clip: true
implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : 430
property int selectedTab: Persistent.states.sidebar.bottomGroup.tab
property int previousIndex: -1
property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed
readonly property int collapsedHeight: Math.max(
Appearance.font.pixelSize.larger + 24,
Appearance.font.pixelSize.large + 24
)
property var tabs: [
{
"type": "calendar",
"name": Translation.tr("Calendar"),
"icon": "calendar_month",
"widget": "calendar/CalendarWidget.qml"
},
{
"type": "todo",
"name": Translation.tr("To Do"),
"icon": "done_outline",
"widget": "todo/TodoWidget.qml"
},
{
"type": "pomodoro",
"name": Translation.tr("Pomodoro"),
"icon": "search_activity",
"widget": "pomodoro/PomodoroWidget.qml"
},
{
"type": "stopwatch",
"name": Translation.tr("Stopwatch"),
"icon": "timer",
"widget": "pomodoro/StopwatchWidget.qml"
},
]
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
function setCollapsed(state) {
Persistent.states.sidebar.bottomGroup.collapsed = state;
if (collapsed) {
bottomWidgetGroupRow.opacity = 0;
} else {
collapsedBottomWidgetGroupRow.opacity = 0;
}
collapseCleanFadeTimer.start();
}
Connections {
target: Persistent.states.sidebar.bottomGroup
function onTabChanged() {
root.selectedTab = Persistent.states.sidebar.bottomGroup.tab;
}
}
Timer {
id: collapseCleanFadeTimer
interval: Appearance.animation.elementMove.duration / 2
repeat: false
onTriggered: {
if (collapsed)
collapsedBottomWidgetGroupRow.opacity = 1;
else
bottomWidgetGroupRow.opacity = 1;
}
}
Keys.onPressed: event => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_PageDown) {
root.selectedTab = Math.min(root.selectedTab + 1, root.tabs.length - 1);
} else if (event.key === Qt.Key_PageUp) {
root.selectedTab = Math.max(root.selectedTab - 1, 0);
}
event.accepted = true;
}
}
// The thing when collapsed
RowLayout {
id: collapsedBottomWidgetGroupRow
opacity: collapsed ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
id: collapsedBottomWidgetGroupRowFade
duration: Appearance.animation.elementMove.duration / 2
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
spacing: 15
CalendarHeaderButton {
Layout.margins: 10
Layout.rightMargin: 0
forceCircle: true
downAction: () => {
root.setCollapsed(false);
}
contentItem: MaterialSymbol {
text: "keyboard_arrow_up"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
StyledText {
property int remainingTasks: Todo.list.filter(task => !task.done).length
Layout.margins: 10
Layout.leftMargin: 0
// text: `${DateTime.collapsedCalendarFormat} ${remainingTasks} task${remainingTasks > 1 ? "s" : ""}`
text: Translation.tr("%1 • %2 tasks").arg(DateTime.collapsedCalendarFormat).arg(remainingTasks)
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
}
}
// The thing when expanded
RowLayout {
id: bottomWidgetGroupRow
opacity: collapsed ? 0 : 1
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
id: bottomWidgetGroupRowFade
duration: Appearance.animation.elementMove.duration / 2
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
anchors.fill: parent
// implicitHeight: tabStack.implicitHeight
spacing: 20
// Navigation rail
Item {
Layout.fillHeight: true
Layout.fillWidth: false
Layout.leftMargin: 10
Layout.topMargin: 10
implicitWidth: tabBar.implicitWidth
// Navigation rail buttons
NavigationRailTabArray {
id: tabBar
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
currentIndex: root.selectedTab
expanded: false
Repeater {
model: root.tabs
NavigationRailButton {
required property int index
required property var modelData
showToggledHighlight: false
toggled: root.selectedTab == index
buttonText: modelData.name
buttonIcon: modelData.icon
onPressed: {
root.selectedTab = index;
Persistent.states.sidebar.bottomGroup.tab = index;
}
}
}
}
// Collapse button
CalendarHeaderButton {
anchors.left: parent.left
anchors.top: parent.top
forceCircle: true
downAction: () => {
root.setCollapsed(true);
}
contentItem: MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
}
// Content area
Item {
Layout.fillWidth: true
Layout.fillHeight: true
// implicitHeight: tabStack.implicitHeight
Loader {
id: tabStack
anchors.fill: parent
anchors.bottomMargin: -anchors.topMargin
Component.onCompleted: {
tabStack.source = root.tabs[root.selectedTab].widget;
}
Connections {
target: root
function onSelectedTabChanged() {
if (root.currentTab > root.previousIndex)
tabSwitchBehavior.animation.down = true;
else if (root.currentTab < root.previousIndex)
tabSwitchBehavior.animation.down = false;
tabStack.source = root.tabs[root.selectedTab].widget;
}
}
Behavior on source {
id: tabSwitchBehavior
animation: TabSwitchAnim {
id: upAnim
down: true
}
}
}
}
}
component TabSwitchAnim: SequentialAnimation {
id: switchAnim
property bool down: false
ParallelAnimation {
PropertyAnimation {
target: tabStack
properties: "opacity"
to: 0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
PropertyAnimation {
target: tabStack.anchors
properties: "topMargin"
to: 10 * (switchAnim.down ? -1 : 1)
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
PropertyAction {
target: tabStack
property: "source"
value: root.tabs[root.selectedTab].widget
} // The source change happens here
ParallelAnimation {
PropertyAnimation {
target: tabStack.anchors
properties: "topMargin"
from: 10 * -(switchAnim.down ? -1 : 1)
to: 0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve
}
PropertyAnimation {
target: tabStack
properties: "opacity"
to: 1
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve
}
}
ScriptAction {
script: {
root.previousIndex = root.selectedTab;
}
}
}
}

View file

@ -0,0 +1,20 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.ii.sidebarRight.notifications
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Rectangle {
id: root
clip: true
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
NotificationList {
anchors.fill: parent
anchors.margins: 5
}
}

View file

@ -0,0 +1,146 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.UPower
Rectangle {
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
implicitWidth: contentItem.implicitWidth + root.horizontalPadding * 2
implicitHeight: contentItem.implicitHeight + root.verticalPadding * 2
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
property real verticalPadding: 4
property real horizontalPadding: 12
Column {
id: contentItem
anchors {
fill: parent
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
topMargin: root.verticalPadding
bottomMargin: root.verticalPadding
}
Loader {
anchors {
left: parent.left
right: parent.right
}
visible: active
active: Config.options.sidebar.quickSliders.showBrightness
sourceComponent: QuickSlider {
materialSymbol: "light_mode"
secondaryMaterialSymbol: "wb_twilight"
stopIndicatorValues: Hyprsunset.gamma !== 100 && root.brightnessMonitor?.brightness !== 0 ? [0.3 + root.brightnessMonitor?.brightness * 0.7] : []
value: Hyprsunset.gamma === 100? 0.3 + root.brightnessMonitor?.brightness * 0.7 : (Hyprsunset.gamma - Hyprsunset.gammaLowerLimit) / (100 - Hyprsunset.gammaLowerLimit) * 0.3
tooltipContent: Hyprsunset.gamma === 100 ? `${Math.round(root.brightnessMonitor?.brightness * 100)}%` : `${Translation.tr("Gamma")} ${Hyprsunset.gamma}%`
onMoved: {
if (value >= 0.3) {
// 0.3 - 1.0 brightness
root.brightnessMonitor.setBrightness((value - 0.3) / 0.7);
if (Hyprsunset.gamma !== 100) {
Hyprsunset.setGamma(100);
}
} else {
// 0 - 0.3 gamma
if (root.brightnessMonitor.brightness !== 0) {
root.brightnessMonitor.setBrightness(0);
}
Hyprsunset.setGamma((value / 0.3 * (100 - Hyprsunset.gammaLowerLimit) + Hyprsunset.gammaLowerLimit));
}
}
}
}
Loader {
anchors {
left: parent.left
right: parent.right
}
visible: active
active: Config.options.sidebar.quickSliders.showVolume
sourceComponent: QuickSlider {
materialSymbol: "volume_up"
value: Audio.sink.audio.volume
onMoved: {
Audio.sink.audio.volume = value
}
}
}
Loader {
anchors {
left: parent.left
right: parent.right
}
visible: active
active: Config.options.sidebar.quickSliders.showMic
sourceComponent: QuickSlider {
materialSymbol: "mic"
value: Audio.source.audio.volume
onMoved: {
Audio.source.audio.volume = value
}
}
}
}
component QuickSlider: StyledSlider {
id: quickSlider
required property string materialSymbol
property string secondaryMaterialSymbol
configuration: StyledSlider.Configuration.M
stopIndicatorValues: []
dividerValues: secondaryMaterialSymbol.length > 0 ? [secondaryIcon.iconLocation] : []
MaterialSymbol {
id: icon
property bool nearFull: quickSlider.value >= 0.9
anchors {
verticalCenter: quickSlider.verticalCenter
right: nearFull ? quickSlider.handle.right : quickSlider.right
rightMargin: nearFull ? 14 : 8
}
iconSize: 20
color: nearFull ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSecondaryContainer
text: quickSlider.materialSymbol
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
Behavior on anchors.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
MaterialSymbol {
id: secondaryIcon
visible: secondaryMaterialSymbol.length > 0
property real iconLocation: 0.3
property bool nearIcon: iconLocation - quickSlider.value <= 0.1 && iconLocation - quickSlider.value > (quickSlider.handleWidth + 8 - 14) / quickSlider.effectiveDraggingWidth
anchors {
verticalCenter: quickSlider.verticalCenter
right: nearIcon ? quickSlider.handle.right : quickSlider.right
rightMargin: nearIcon ? 14 : (1 - iconLocation) * quickSlider.effectiveDraggingWidth + quickSlider.rightPadding + 8
}
iconSize: 20
color: quickSlider.value >= iconLocation - 0.1 ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSecondaryContainer
text: secondaryMaterialSymbol
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}

View file

@ -0,0 +1,110 @@
import qs
import qs.services
import qs.modules.common
import QtQuick
import Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth
PanelWindow {
id: panelWindow
visible: GlobalStates.sidebarRightOpen
function hide() {
GlobalStates.sidebarRightOpen = false;
}
exclusiveZone: 0
implicitWidth: sidebarWidth
WlrLayershell.namespace: "quickshell:sidebarRight"
WlrLayershell.keyboardFocus: GlobalStates.sidebarRightOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
right: true
bottom: true
}
onVisibleChanged: {
if (visible) {
GlobalFocusGrab.addDismissable(panelWindow);
} else {
GlobalFocusGrab.removeDismissable(panelWindow);
}
}
Connections {
target: GlobalFocusGrab
function onDismissed() {
panelWindow.hide();
}
}
Loader {
id: sidebarContentLoader
active: GlobalStates.sidebarRightOpen || Config?.options.sidebar.keepRightSidebarLoaded
anchors {
fill: parent
margins: Appearance.sizes.hyprlandGapsOut
leftMargin: Appearance.sizes.elevationMargin
}
width: sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin
height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
focus: GlobalStates.sidebarRightOpen
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
panelWindow.hide();
}
}
sourceComponent: SidebarRightContent {}
}
}
IpcHandler {
target: "sidebarRight"
function toggle(): void {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
function close(): void {
GlobalStates.sidebarRightOpen = false;
}
function open(): void {
GlobalStates.sidebarRightOpen = true;
}
}
GlobalShortcut {
name: "sidebarRightToggle"
description: "Toggles right sidebar on press"
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
}
GlobalShortcut {
name: "sidebarRightOpen"
description: "Opens right sidebar on press"
onPressed: {
GlobalStates.sidebarRightOpen = true;
}
}
GlobalShortcut {
name: "sidebarRightClose"
description: "Closes right sidebar on press"
onPressed: {
GlobalStates.sidebarRightOpen = false;
}
}
}

View file

@ -0,0 +1,330 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Hyprland
import qs.modules.ii.sidebarRight.quickToggles
import qs.modules.ii.sidebarRight.quickToggles.classicStyle
import qs.modules.ii.sidebarRight.bluetoothDevices
import qs.modules.ii.sidebarRight.nightLight
import qs.modules.ii.sidebarRight.volumeMixer
import qs.modules.ii.sidebarRight.wifiNetworks
Item {
id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 10
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
property bool showAudioOutputDialog: false
property bool showAudioInputDialog: false
property bool showBluetoothDialog: false
property bool showNightLightDialog: false
property bool showWifiDialog: false
property bool editMode: false
Connections {
target: GlobalStates
function onSidebarRightOpenChanged() {
if (!GlobalStates.sidebarRightOpen) {
root.showWifiDialog = false;
root.showBluetoothDialog = false;
root.showAudioOutputDialog = false;
root.showAudioInputDialog = false;
}
}
}
implicitHeight: sidebarRightBackground.implicitHeight
implicitWidth: sidebarRightBackground.implicitWidth
StyledRectangularShadow {
target: sidebarRightBackground
}
Rectangle {
id: sidebarRightBackground
anchors.fill: parent
implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
ColumnLayout {
anchors.fill: parent
anchors.margins: sidebarPadding
spacing: sidebarPadding
SystemButtonRow {
Layout.fillHeight: false
Layout.fillWidth: true
// Layout.margins: 10
Layout.topMargin: 5
Layout.bottomMargin: 0
}
Loader {
id: slidersLoader
Layout.fillWidth: true
visible: active
active: {
const configQuickSliders = Config.options.sidebar.quickSliders
if (!configQuickSliders.enable) return false
if (!configQuickSliders.showMic && !configQuickSliders.showVolume && !configQuickSliders.showBrightness) return false;
return true;
}
sourceComponent: QuickSliders {}
}
LoaderedQuickPanelImplementation {
styleName: "classic"
sourceComponent: ClassicQuickPanel {}
}
LoaderedQuickPanelImplementation {
styleName: "android"
sourceComponent: AndroidQuickPanel {
editMode: root.editMode
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
CenterWidgetGroup {
id: centerGroup
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: bottomGroup.top
anchors.bottomMargin: root.sidebarPadding
opacity: bottomGroup.collapsed ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
}
BottomWidgetGroup {
id: bottomGroup
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: bottomGroup.collapsed ? bottomGroup.collapsedHeight : parent.height
Behavior on height {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
}
}
}
}
ToggleDialog {
shownPropertyString: "showAudioOutputDialog"
dialog: VolumeDialog {
isSink: true
}
}
ToggleDialog {
shownPropertyString: "showAudioInputDialog"
dialog: VolumeDialog {
isSink: false
}
}
ToggleDialog {
shownPropertyString: "showBluetoothDialog"
dialog: BluetoothDialog {}
onShownChanged: {
if (!shown) {
Bluetooth.defaultAdapter.discovering = false;
} else {
Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true;
}
}
}
ToggleDialog {
shownPropertyString: "showNightLightDialog"
dialog: NightLightDialog {}
}
ToggleDialog {
shownPropertyString: "showWifiDialog"
dialog: WifiDialog {}
onShownChanged: {
if (!shown) return;
Network.enableWifi();
Network.rescanWifi();
}
}
component ToggleDialog: Loader {
id: toggleDialogLoader
required property string shownPropertyString
property alias dialog: toggleDialogLoader.sourceComponent
readonly property bool shown: root[shownPropertyString]
anchors.fill: parent
onShownChanged: if (shown) toggleDialogLoader.active = true;
active: shown
onActiveChanged: {
if (active) {
item.show = true;
item.forceActiveFocus();
}
}
Connections {
target: toggleDialogLoader.item
function onDismiss() {
toggleDialogLoader.item.show = false
root[toggleDialogLoader.shownPropertyString] = false;
}
function onVisibleChanged() {
if (!toggleDialogLoader.item.visible && !root[toggleDialogLoader.shownPropertyString]) toggleDialogLoader.active = false;
}
}
}
component LoaderedQuickPanelImplementation: Loader {
id: quickPanelImplLoader
required property string styleName
Layout.alignment: item?.Layout.alignment ?? Qt.AlignHCenter
Layout.fillWidth: item?.Layout.fillWidth ?? false
visible: active
active: Config.options.sidebar.quickToggles.style === styleName
Connections {
target: quickPanelImplLoader.item
function onOpenAudioOutputDialog() {
root.showAudioOutputDialog = true;
}
function onOpenAudioInputDialog() {
root.showAudioInputDialog = true;
}
function onOpenBluetoothDialog() {
root.showBluetoothDialog = true;
}
function onOpenNightLightDialog() {
root.showNightLightDialog = true;
}
function onOpenWifiDialog() {
root.showWifiDialog = true;
}
}
}
component SystemButtonRow: Item {
implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight)
Rectangle {
id: uptimeContainer
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
color: Appearance.colors.colLayer1
radius: height / 2
implicitWidth: uptimeRow.implicitWidth + 24
implicitHeight: uptimeRow.implicitHeight + 8
Row {
id: uptimeRow
anchors.centerIn: parent
spacing: 8
CustomIcon {
id: distroIcon
anchors.verticalCenter: parent.verticalCenter
width: 25
height: 25
source: SystemInfo.distroIcon
colorize: true
color: Appearance.colors.colOnLayer0
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0
text: Translation.tr("Up %1").arg(DateTime.uptime)
textFormat: Text.MarkdownText
}
}
}
ButtonGroup {
id: systemButtonsRow
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
color: Appearance.colors.colLayer1
padding: 4
QuickToggleButton {
toggled: root.editMode
visible: Config.options.sidebar.quickToggles.style === "android"
buttonIcon: "edit"
onClicked: root.editMode = !root.editMode
StyledToolTip {
text: Translation.tr("Edit quick toggles") + (root.editMode ? Translation.tr("\nLMB to enable/disable\nRMB to toggle size\nScroll to swap position") : "")
}
}
QuickToggleButton {
toggled: false
buttonIcon: "restart_alt"
onClicked: {
Quickshell.execDetached(["hyprctl", "reload"])
Quickshell.reload(true);
}
StyledToolTip {
text: Translation.tr("Reload Hyprland & Quickshell")
}
}
QuickToggleButton {
toggled: false
buttonIcon: "settings"
onClicked: {
GlobalStates.sidebarRightOpen = false;
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]);
}
StyledToolTip {
text: Translation.tr("Settings")
}
}
QuickToggleButton {
toggled: false
buttonIcon: "power_settings_new"
onClicked: {
GlobalStates.sessionOpen = true;
}
StyledToolTip {
text: Translation.tr("Session")
}
}
}
}
}

View file

@ -0,0 +1,192 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
DialogListItem {
id: root
required property var device
property bool expanded: false
property bool actionInProgress: false
property bool actionWasConnect: false
pointingHandCursor: !expanded
onClicked: expanded = !expanded
altAction: () => expanded = !expanded
onDeviceChanged: clearActionFeedback()
function clearActionFeedback() {
actionInProgress = false;
actionFeedbackTimeout.stop();
}
Connections {
target: root.device
ignoreUnknownSignals: true
function onConnectedChanged() {
root.clearActionFeedback();
}
function onPairedChanged() {
root.clearActionFeedback();
}
}
Timer {
id: actionFeedbackTimeout
interval: 10000
onTriggered: root.actionInProgress = false
}
component ActionButton: DialogButton {
colBackground: Appearance.colors.colPrimary
colBackgroundHover: Appearance.colors.colPrimaryHover
colRipple: Appearance.colors.colPrimaryActive
colText: Appearance.colors.colOnPrimary
}
contentItem: ColumnLayout {
anchors {
fill: parent
topMargin: root.verticalPadding
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
spacing: 0
RowLayout {
// Name
spacing: 10
MaterialSymbol {
iconSize: Appearance.font.pixelSize.larger
text: Icons.getBluetoothDeviceMaterialSymbol(root.device?.icon || "")
color: Appearance.colors.colOnSurfaceVariant
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
color: Appearance.colors.colOnSurfaceVariant
elide: Text.ElideRight
text: root.device?.name || Translation.tr("Unknown device")
textFormat: Text.PlainText
}
StyledText {
visible: (root.device?.connected || root.device?.paired) ?? false
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
elide: Text.ElideRight
text: {
if (!root.device?.paired) return "";
let statusText = root.device?.connected ? Translation.tr("Connected") : Translation.tr("Paired");
if (!root.device?.batteryAvailable) return statusText;
statusText += ` ${Math.round(root.device?.battery * 100)}%`;
return statusText;
}
}
}
MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer3
rotation: root.expanded ? 180 : 0
Behavior on rotation {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
}
RowLayout {
visible: root.expanded
Layout.topMargin: 8
Item {
Layout.fillWidth: true
}
Item {
Layout.preferredWidth: 18
Layout.preferredHeight: 18
MaterialSymbol {
anchors.centerIn: parent
text: "bluetooth_connected"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colPrimary
opacity: root.actionInProgress ? 1 : 0
scale: root.actionInProgress ? pulseAnim.pulseScale : 0.8
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on scale {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
SequentialAnimation {
id: pulseAnim
property real pulseScale: 1
running: root.actionInProgress
loops: Animation.Infinite
NumberAnimation {
target: pulseAnim
property: "pulseScale"
from: 0.9
to: 1.08
duration: 450
easing.type: Easing.InOutSine
}
NumberAnimation {
target: pulseAnim
property: "pulseScale"
from: 1.08
to: 0.9
duration: 450
easing.type: Easing.InOutSine
}
}
}
}
ActionButton {
enabled: !root.actionInProgress
buttonText: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect")
onClicked: {
if (root.device?.connected) {
root.actionWasConnect = false;
root.actionInProgress = true;
actionFeedbackTimeout.restart();
root.device.disconnect();
} else {
root.actionWasConnect = true;
root.actionInProgress = true;
actionFeedbackTimeout.restart();
root.device.connect();
}
}
}
ActionButton {
visible: root.device?.paired ?? false
colBackground: Appearance.colors.colError
colBackgroundHover: Appearance.colors.colErrorHover
colRipple: Appearance.colors.colErrorActive
colText: Appearance.colors.colOnError
buttonText: Translation.tr("Forget")
onClicked: {
root.device?.forget();
}
}
}
Item {
Layout.fillHeight: true
}
}
}

View file

@ -0,0 +1,77 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell.Bluetooth
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog {
id: root
backgroundHeight: 600
WindowDialogTitle {
text: Translation.tr("Bluetooth devices")
}
WindowDialogSeparator {
visible: !(Bluetooth.defaultAdapter?.discovering ?? false)
}
StyledIndeterminateProgressBar {
visible: Bluetooth.defaultAdapter?.discovering ?? false
Layout.fillWidth: true
Layout.topMargin: -8
Layout.bottomMargin: -8
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
}
StyledListView {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: -15
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
clip: true
spacing: 0
animateAppearance: false
model: ScriptModel {
values: BluetoothStatus.friendlyDeviceList
}
delegate: BluetoothDeviceItem {
required property BluetoothDevice modelData
device: modelData
anchors {
left: parent?.left
right: parent?.right
}
}
}
WindowDialogSeparator {}
WindowDialogButtonRow {
DialogButton {
buttonText: Translation.tr("Details")
onClicked: {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]);
GlobalStates.sidebarRightOpen = false;
}
}
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}

View file

@ -0,0 +1,34 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
RippleButton {
id: button
property string day
property int isToday
property bool bold
Layout.fillWidth: false
Layout.fillHeight: false
implicitWidth: 38;
implicitHeight: 38;
toggled: (isToday == 1)
buttonRadius: Appearance.rounding.small
contentItem: StyledText {
anchors.fill: parent
text: day
horizontalAlignment: Text.AlignHCenter
font.weight: bold ? Font.DemiBold : Font.Normal
color: (isToday == 1) ? Appearance.m3colors.m3onPrimary :
(isToday == 0) ? Appearance.colors.colOnLayer1 :
Appearance.colors.colOutlineVariant
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}

View file

@ -0,0 +1,36 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: button
property string buttonText: ""
property string tooltipText: ""
property bool forceCircle: false
implicitHeight: 30
implicitWidth: forceCircle ? implicitHeight : (contentItem.implicitWidth + 10 * 2)
Behavior on implicitWidth {
SmoothedAnimation {
velocity: Appearance.animation.elementMove.velocity
}
}
background.anchors.fill: button
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
contentItem: StyledText {
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
StyledToolTip {
text: tooltipText
extraVisibleCondition: tooltipText.length > 0
}
}

View file

@ -0,0 +1,122 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import "calendar_layout.js" as CalendarLayout
import QtQuick
import QtQuick.Layouts
Item {
// Layout.topMargin: 10
anchors.topMargin: 10
property int monthShift: 0
property var viewingDate: CalendarLayout.getDateInXMonthsTime(monthShift)
property var calendarLayout: CalendarLayout.getCalendarLayout(viewingDate, monthShift === 0)
width: calendarColumn.width
implicitHeight: calendarColumn.height + 10 * 2
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp)
&& event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageDown) {
monthShift++;
} else if (event.key === Qt.Key_PageUp) {
monthShift--;
}
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
onWheel: (event) => {
if (event.angleDelta.y > 0) {
monthShift--;
} else if (event.angleDelta.y < 0) {
monthShift++;
}
}
}
ColumnLayout {
id: calendarColumn
anchors.centerIn: parent
spacing: 5
// Calendar header
RowLayout {
Layout.fillWidth: true
spacing: 5
CalendarHeaderButton {
clip: true
buttonText: `${monthShift != 0 ? "• " : ""}${viewingDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")}`
tooltipText: (monthShift === 0) ? "" : Translation.tr("Jump to current month")
downAction: () => {
monthShift = 0;
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: false
}
CalendarHeaderButton {
forceCircle: true
downAction: () => {
monthShift--;
}
contentItem: MaterialSymbol {
text: "chevron_left"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
CalendarHeaderButton {
forceCircle: true
downAction: () => {
monthShift++;
}
contentItem: MaterialSymbol {
text: "chevron_right"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
}
// Week days row
RowLayout {
id: weekDaysRow
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
spacing: 5
Repeater {
model: CalendarLayout.weekDays
delegate: CalendarDayButton {
day: Translation.tr(modelData.day)
isToday: modelData.today
bold: true
enabled: false
}
}
}
// Real week rows
Repeater {
id: calendarRows
// model: calendarLayout
model: 6
delegate: RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
spacing: 5
Repeater {
model: Array(7).fill(modelData)
delegate: CalendarDayButton {
day: calendarLayout[modelData][index].day
isToday: calendarLayout[modelData][index].today
}
}
}
}
}
}

View file

@ -0,0 +1,113 @@
const weekDays = [ // MONDAY IS THE FIRST DAY OF THE WEEK :HESRIGHTYOUKNOW:
{ day: 'Mo', today: 0 },
{ day: 'Tu', today: 0 },
{ day: 'We', today: 0 },
{ day: 'Th', today: 0 },
{ day: 'Fr', today: 0 },
{ day: 'Sa', today: 0 },
{ day: 'Su', today: 0 },
]
function checkLeapYear(year) {
return (
year % 400 == 0 ||
(year % 4 == 0 && year % 100 != 0));
}
function getMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 31;
if (month == 2 && leapYear) return 29;
if (month == 2 && !leapYear) return 28;
return 30;
}
function getNextMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 1 && leapYear) return 29;
if (month == 1 && !leapYear) return 28;
if (month == 12) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getPrevMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 3 && leapYear) return 29;
if (month == 3 && !leapYear) return 28;
if (month == 1) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getDateInXMonthsTime(x) {
var currentDate = new Date(); // Get the current date
if (x == 0) return currentDate; // If x is 0, return the current date
var targetMonth = currentDate.getMonth() + x; // Calculate the target month
var targetYear = currentDate.getFullYear(); // Get the current year
// Adjust the year and month if necessary
targetYear += Math.floor(targetMonth / 12);
targetMonth = (targetMonth % 12 + 12) % 12;
// Create a new date object with the target year and month
var targetDate = new Date(targetYear, targetMonth, 1);
// Set the day to the last day of the month to get the desired date
// targetDate.setDate(0);
return targetDate;
}
function getCalendarLayout(dateObject, highlight) {
if (!dateObject) dateObject = new Date();
const weekday = (dateObject.getDay() + 6) % 7; // MONDAY IS THE FIRST DAY OF THE WEEK
const day = dateObject.getDate();
const month = dateObject.getMonth() + 1;
const year = dateObject.getFullYear();
const weekdayOfMonthFirst = (weekday + 35 - (day - 1)) % 7;
const daysInMonth = getMonthDays(month, year);
const daysInNextMonth = getNextMonthDays(month, year);
const daysInPrevMonth = getPrevMonthDays(month, year);
// Fill
var monthDiff = (weekdayOfMonthFirst == 0 ? 0 : -1);
var toFill, dim;
if (weekdayOfMonthFirst == 0) {
toFill = 1;
dim = daysInMonth;
}
else {
toFill = (daysInPrevMonth - (weekdayOfMonthFirst - 1));
dim = daysInPrevMonth;
}
var calendar = [...Array(6)].map(() => Array(7));
var i = 0, j = 0;
while (i < 6 && j < 7) {
calendar[i][j] = {
"day": toFill,
"today": ((toFill == day && monthDiff == 0 && highlight) ? 1 : (
monthDiff == 0 ? 0 : -1
))
};
// Increment
toFill++;
if (toFill > dim) { // Next month?
monthDiff++;
if (monthDiff == 0)
dim = daysInMonth;
else if (monthDiff == 1)
dim = daysInNextMonth;
toFill = 1;
}
// Next tile
j++;
if (j == 7) {
j = 0;
i++;
}
}
return calendar;
}

View file

@ -0,0 +1,204 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog {
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
backgroundHeight: 700
WindowDialogTitle {
text: Translation.tr("Eye protection")
}
WindowDialogSectionHeader {
text: Translation.tr("Night Light")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: nightLightColumn
Layout.topMargin: -16
Layout.fillWidth: true
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "check"
text: Translation.tr("Enable now")
checked: Hyprsunset.temperatureActive
onCheckedChanged: {
Hyprsunset.toggleTemperature(checked)
}
}
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "night_sight_auto"
text: Translation.tr("Automatic")
checked: Config.options.light.night.automatic
onCheckedChanged: {
Config.options.light.night.automatic = checked;
}
}
WindowDialogSlider {
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: 4
}
text: Translation.tr("Intensity")
from: 6500
to: 1200
stopIndicatorValues: [5000, to]
value: Config.options.light.night.colorTemperature
onMoved: Config.options.light.night.colorTemperature = value
tooltipContent: `${Math.round(value)}K`
}
}
WindowDialogSectionHeader {
text: Translation.tr("Anti-flashbang (experimental)")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: antiFlashbangColumn
Layout.topMargin: -16
Layout.fillWidth: true
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "filter"
text: Translation.tr("Content adjustment")
checked: HyprlandAntiFlashbangShader.enabled
onCheckedChanged: {
if (checked) HyprlandAntiFlashbangShader.enable()
else HyprlandAntiFlashbangShader.disable()
}
StyledToolTip {
text: Translation.tr("<b>Dims screen content</b> as needed.<br><br>Pros: Immediately responsive<br>Cons: Expensive and can hurt color accuracy<br><br><i>Uses a Hyprland screen shader</i>")
}
}
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "light_mode"
text: Translation.tr("Brightness adjustment")
checked: Config.options.light.antiFlashbang.enable
onCheckedChanged: {
Config.options.light.antiFlashbang.enable = checked;
}
StyledToolTip {
text: Translation.tr("Adapts the <b>display (physical screen) brightness</b><br><br>Pros: Less expensive, retains colors<br>Cons: Not immediately responsive<br><br><i>Adjusts display brightness after each Hyprland IPC event</i>")
}
}
}
WindowDialogSectionHeader {
text: Translation.tr("Brightness")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: brightnessColumn
Layout.topMargin: -16
Layout.fillWidth: true
WindowDialogSlider {
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: 4
}
value: root.brightnessMonitor.brightness
onMoved: root.brightnessMonitor.setBrightness(value)
}
}
WindowDialogSectionHeader {
text: Translation.tr("Gamma")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: gammaColumn
Layout.topMargin: -16
Layout.fillWidth: true
Layout.fillHeight: true
WindowDialogSlider {
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: 4
}
from: Hyprsunset.gammaLowerLimit / 100
value: Hyprsunset.gamma / 100
onMoved: Hyprsunset.setGamma(value * 100)
tooltipContent: `${Math.round(value * 100)}%`
}
}
WindowDialogButtonRow {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}

View file

@ -0,0 +1,71 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
NotificationListView { // Scrollable window
id: listview
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: statusRow.top
anchors.bottomMargin: 5
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: listview.width
height: listview.height
radius: Appearance.rounding.normal
}
}
popup: false
}
// Placeholder when list is empty
PagePlaceholder {
shown: Notifications.list.length === 0
icon: "notifications_active"
description: Translation.tr("Nothing")
shape: MaterialShape.Shape.Ghostish
descriptionHorizontalAlignment: Text.AlignHCenter
}
ButtonGroup {
id: statusRow
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
NotificationStatusButton {
Layout.fillWidth: false
buttonIcon: "notifications_paused"
toggled: Notifications.silent
onClicked: () => {
Notifications.silent = !Notifications.silent;
}
}
NotificationStatusButton {
enabled: false
Layout.fillWidth: true
buttonText: Translation.tr("%1 notifications").arg(Notifications.list.length)
}
NotificationStatusButton {
Layout.fillWidth: false
buttonIcon: "delete_sweep"
onClicked: () => {
Notifications.discardAllNotifications()
}
}
}
}

View file

@ -0,0 +1,46 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
GroupButton {
id: button
property string buttonIcon: ""
property string buttonText: ""
baseHeight: 36
baseWidth: content.implicitWidth + 46
clickedWidth: baseWidth + 6
buttonRadius: baseHeight / 2
buttonRadiusPressed: Appearance.rounding.small
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colBackgroundActive: Appearance.colors.colLayer2Active
property color colText: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
contentItem: Item {
id: content
anchors.fill: parent
implicitWidth: contentRowLayout.implicitWidth
implicitHeight: contentRowLayout.implicitHeight
RowLayout {
id: contentRowLayout
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
visible: buttonIcon !== ""
text: buttonIcon
iconSize: Appearance.font.pixelSize.huge
color: button.colText
}
StyledText {
visible: buttonText !== ""
text: buttonText
font.pixelSize: Appearance.font.pixelSize.small
color: button.colText
}
}
}
}

View file

@ -0,0 +1,368 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
StyledFlickable {
id: root
clip: true
contentWidth: width
contentHeight: settingsColumn.implicitHeight
function setMinutes(optionName, minutes) {
Config.options.time.pomodoro[optionName] = Math.max(1, minutes) * 60;
if (!TimerService.pomodoroRunning) {
TimerService.resetPomodoro();
}
}
function minutes(optionName) {
return Math.round(Config.options.time.pomodoro[optionName] / 60);
}
function applyPreset(focus, shortBreak, longBreak, cycles) {
Config.options.time.pomodoro.focus = focus * 60;
Config.options.time.pomodoro.breakTime = shortBreak * 60;
Config.options.time.pomodoro.longBreak = longBreak * 60;
Config.options.time.pomodoro.cyclesBeforeLongBreak = cycles;
if (!TimerService.pomodoroRunning) {
TimerService.resetPomodoro();
}
}
function setCycles(cycles) {
Config.options.time.pomodoro.cyclesBeforeLongBreak = cycles;
if (!TimerService.pomodoroRunning) {
TimerService.resetPomodoro();
}
}
ColumnLayout {
id: settingsColumn
width: root.width
spacing: 10
Rectangle {
Layout.fillWidth: true
Layout.leftMargin: 4
Layout.rightMargin: 12
implicitHeight: 108
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer2
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
Rectangle {
Layout.alignment: Qt.AlignVCenter
implicitWidth: 42
implicitHeight: 42
radius: Appearance.rounding.full
color: Appearance.colors.colSecondaryContainer
MaterialSymbol {
anchors.centerIn: parent
text: "search_activity"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.colors.colOnSecondaryContainer
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
StyledText {
text: Translation.tr("Focus profile")
font.pixelSize: Appearance.font.pixelSize.normal
font.weight: Font.Medium
color: Appearance.colors.colOnLayer2
}
StyledText {
text: `${root.minutes("focus")} / ${root.minutes("breakTime")} / ${root.minutes("longBreak")} min ${Config.options.time.pomodoro.cyclesBeforeLongBreak} cycles`
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
ProfileChip {
iconName: "search_activity"
label: `${root.minutes("focus")}m`
color: Appearance.colors.colSecondaryContainer
textColor: Appearance.colors.colOnSecondaryContainer
}
ProfileChip {
iconName: "coffee"
label: `${root.minutes("breakTime")}m`
color: Appearance.colors.colTertiaryContainer
textColor: Appearance.colors.colOnTertiaryContainer
}
ProfileChip {
iconName: "spa"
label: `${root.minutes("longBreak")}m`
color: Appearance.colors.colLayer1
textColor: Appearance.colors.colOnLayer1
}
Item { Layout.fillWidth: true }
RowLayout {
spacing: 3
Repeater {
model: Config.options.time.pomodoro.cyclesBeforeLongBreak
Rectangle {
implicitWidth: 7
implicitHeight: 7
radius: Appearance.rounding.full
color: Appearance.colors.colOnLayer2
opacity: 0.45
}
}
}
}
}
}
ContentSection {
icon: "timer"
title: Translation.tr("Pomodoro")
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 6
uniformCellSizes: true
PresetButton {
label: "25/5"
onClicked: root.applyPreset(25, 5, 15, 4)
}
PresetButton {
label: "50/10"
onClicked: root.applyPreset(50, 10, 25, 4)
}
PresetButton {
label: "15/5"
onClicked: root.applyPreset(15, 5, 15, 4)
}
}
PomodoroSpinRow {
iconName: "search_activity"
label: Translation.tr("Focus")
suffix: Translation.tr("min")
from: 1
to: 180
stepSize: 5
value: root.minutes("focus")
onValueModified: value => root.setMinutes("focus", value)
}
PomodoroSpinRow {
iconName: "coffee"
label: Translation.tr("Break")
suffix: Translation.tr("min")
from: 1
to: 60
stepSize: 1
value: root.minutes("breakTime")
onValueModified: value => root.setMinutes("breakTime", value)
}
PomodoroSpinRow {
iconName: "spa"
label: Translation.tr("Long break")
suffix: Translation.tr("min")
from: 1
to: 120
stepSize: 5
value: root.minutes("longBreak")
onValueModified: value => root.setMinutes("longBreak", value)
}
PomodoroSpinRow {
iconName: "repeat"
label: Translation.tr("Cycles")
suffix: ""
from: 1
to: 12
stepSize: 1
value: Config.options.time.pomodoro.cyclesBeforeLongBreak
onValueModified: value => root.setCycles(value)
}
}
ContentSection {
icon: "notifications"
title: Translation.tr("Alerts")
ConfigRow {
uniform: true
ConfigSwitch {
buttonIcon: "notifications"
text: Translation.tr("Notifications")
checked: Config.options.time.pomodoro.notifications
onCheckedChanged: Config.options.time.pomodoro.notifications = checked
}
ConfigSwitch {
buttonIcon: "notification_sound"
text: Translation.tr("Sound")
checked: Config.options.sounds.pomodoro
onCheckedChanged: Config.options.sounds.pomodoro = checked
}
}
RippleButton {
Layout.fillWidth: true
implicitHeight: 40
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
onClicked: Audio.playSystemSound("alarm-clock-elapsed")
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 8
MaterialSymbol {
text: "play_circle"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer2
}
StyledText {
Layout.fillWidth: true
text: Translation.tr("Test sound")
color: Appearance.colors.colOnLayer2
}
}
}
}
Item {
Layout.fillWidth: true
implicitHeight: 4
}
}
component PresetButton: RippleButton {
id: preset
property string label
Layout.fillWidth: true
implicitHeight: 34
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
contentItem: StyledText {
anchors.centerIn: parent
text: preset.label
horizontalAlignment: Text.AlignHCenter
font.family: Appearance.font.family.monospace
font.weight: Font.DemiBold
color: Appearance.colors.colOnLayer2
}
}
component ProfileChip: Rectangle {
property string iconName
property string label
property color textColor
Layout.fillWidth: true
implicitHeight: 28
radius: Appearance.rounding.full
RowLayout {
anchors.centerIn: parent
spacing: 4
MaterialSymbol {
text: parent.parent.iconName
iconSize: 14
color: parent.parent.textColor
}
StyledText {
text: parent.parent.label
font.family: Appearance.font.family.monospace
font.pixelSize: Appearance.font.pixelSize.smaller
font.weight: Font.DemiBold
color: parent.parent.textColor
}
}
}
component PomodoroSpinRow: RowLayout {
id: row
property string iconName
property string label
property string suffix
property alias value: spinBox.value
property alias from: spinBox.from
property alias to: spinBox.to
property alias stepSize: spinBox.stepSize
signal valueModified(int value)
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 10
MaterialSymbol {
text: row.iconName
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnSecondaryContainer
}
StyledText {
Layout.fillWidth: true
text: row.label
color: Appearance.colors.colOnSecondaryContainer
}
StyledSpinBox {
id: spinBox
Layout.preferredWidth: 96
stepSize: 1
onValueModified: row.valueModified(value)
}
StyledText {
Layout.preferredWidth: 24
text: row.suffix
color: Appearance.colors.colSubtext
}
}
}

View file

@ -0,0 +1,187 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Item {
id: root
implicitHeight: contentColumn.implicitHeight
implicitWidth: contentColumn.implicitWidth
readonly property color stateColor: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainer : Appearance.colors.colSecondaryContainer
readonly property color stateTextColor: TimerService.pomodoroBreak ? Appearance.colors.colOnTertiaryContainer : Appearance.colors.colOnSecondaryContainer
readonly property string stateLabel: TimerService.pomodoroLongBreak ? Translation.tr("Long break") : TimerService.pomodoroBreak ? Translation.tr("Break") : Translation.tr("Focus")
readonly property int cyclePosition: TimerService.pomodoroCycle + 1
ColumnLayout {
id: contentColumn
anchors.fill: parent
spacing: 10
// The Pomodoro timer circle
CircularProgress {
Layout.alignment: Qt.AlignHCenter
lineWidth: 10
value: {
return TimerService.pomodoroSecondsLeft / TimerService.pomodoroLapDuration;
}
implicitSize: 200
colPrimary: root.stateTextColor
colSecondary: root.stateColor
enableAnimation: true
ColumnLayout {
anchors.centerIn: parent
spacing: 2
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 150
horizontalAlignment: Text.AlignHCenter
text: {
let minutes = Math.floor(TimerService.pomodoroSecondsLeft / 60).toString().padStart(2, '0');
let seconds = Math.floor(TimerService.pomodoroSecondsLeft % 60).toString().padStart(2, '0');
return `${minutes}:${seconds}`;
}
font.family: Appearance.font.family.monospace
font.pixelSize: 40
font.weight: Font.DemiBold
color: Appearance.m3colors.m3onSurface
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: root.stateLabel
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colSubtext
}
}
Rectangle {
radius: Appearance.rounding.full
color: root.stateColor
anchors {
right: parent.right
bottom: parent.bottom
}
implicitWidth: 58
implicitHeight: 36
RowLayout {
anchors.centerIn: parent
spacing: 3
MaterialSymbol {
text: "repeat"
iconSize: 14
color: root.stateTextColor
}
StyledText {
id: cycleText
font.family: Appearance.font.family.monospace
font.weight: Font.DemiBold
color: root.stateTextColor
text: `${root.cyclePosition}/${TimerService.cyclesBeforeLongBreak}`
}
}
}
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 210
Layout.preferredHeight: 30
radius: Appearance.rounding.full
color: Appearance.colors.colLayer2
RowLayout {
anchors.centerIn: parent
spacing: 6
MaterialSymbol {
text: TimerService.pomodoroBreak ? "coffee" : "search_activity"
iconSize: Appearance.font.pixelSize.larger
color: root.stateTextColor
}
StyledText {
text: root.stateLabel
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer2
}
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 5
Repeater {
model: TimerService.cyclesBeforeLongBreak
Rectangle {
required property int index
implicitWidth: index === TimerService.pomodoroCycle ? 18 : 8
implicitHeight: 8
radius: Appearance.rounding.full
color: index <= TimerService.pomodoroCycle ? root.stateTextColor : Appearance.colors.colLayer2
Behavior on implicitWidth {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}
// The Start/Stop and Reset buttons
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 10
RippleButton {
buttonRadius: Appearance.rounding.full
contentItem: StyledText {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: TimerService.pomodoroRunning ? Translation.tr("Pause") : (TimerService.pomodoroSecondsLeft === TimerService.focusTime) ? Translation.tr("Start") : Translation.tr("Resume")
color: TimerService.pomodoroRunning ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnPrimary
}
implicitHeight: 38
implicitWidth: 96
font.pixelSize: Appearance.font.pixelSize.larger
onClicked: TimerService.togglePomodoro()
colBackground: TimerService.pomodoroRunning ? Appearance.colors.colSecondaryContainer : Appearance.colors.colPrimary
colBackgroundHover: TimerService.pomodoroRunning ? Appearance.colors.colSecondaryContainer : Appearance.colors.colPrimary
}
RippleButton {
buttonRadius: Appearance.rounding.full
implicitHeight: 38
implicitWidth: 96
onClicked: TimerService.resetPomodoro()
enabled: (TimerService.pomodoroSecondsLeft < TimerService.pomodoroLapDuration) || TimerService.pomodoroCycle > 0 || TimerService.pomodoroBreak
font.pixelSize: Appearance.font.pixelSize.larger
colBackground: Appearance.colors.colErrorContainer
colBackgroundHover: Appearance.colors.colErrorContainerHover
colRipple: Appearance.colors.colErrorContainerActive
contentItem: StyledText {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: Translation.tr("Reset")
color: Appearance.colors.colOnErrorContainer
}
}
}
}
}

View file

@ -0,0 +1,68 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var tabButtonList: [
{"name": Translation.tr("Timer"), "icon": "search_activity"},
{"name": Translation.tr("Customize"), "icon": "tune"}
]
// Pomodoro keybinds
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) { // Switch tabs
if (event.key === Qt.Key_PageDown) {
tabBar.incrementCurrentIndex();
} else if (event.key === Qt.Key_PageUp) {
tabBar.decrementCurrentIndex();
}
event.accepted = true
} else if (event.key === Qt.Key_Space || event.key === Qt.Key_S) { // Pause/resume with Space or S
if (tabBar.currentIndex === 0) {
TimerService.togglePomodoro()
}
event.accepted = true
} else if (event.key === Qt.Key_R) { // Reset with R
if (tabBar.currentIndex === 0) {
TimerService.resetPomodoro()
}
event.accepted = true
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
SecondaryTabBar {
id: tabBar
currentIndex: swipeView.currentIndex
Repeater {
model: root.tabButtonList
delegate: SecondaryTabButton {
buttonText: modelData.name
buttonIcon: modelData.icon
}
}
}
SwipeView {
id: swipeView
Layout.topMargin: 10
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
clip: true
currentIndex: tabBar.currentIndex
// Tabs
PomodoroTimer {}
PomodoroSettings {}
}
}
}

View file

@ -0,0 +1,207 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Item {
id: stopwatchTab
Layout.fillWidth: true
Layout.fillHeight: true
Item {
anchors {
fill: parent
topMargin: 8
leftMargin: 16
rightMargin: 16
}
RowLayout { // Elapsed
id: elapsedIndicator
anchors {
top: undefined
verticalCenter: parent.verticalCenter
left: controlButtons.left
leftMargin: 6
}
states: State {
name: "hasLaps"
when: TimerService.stopwatchLaps.length > 0
AnchorChanges {
target: elapsedIndicator
anchors.top: parent.top
anchors.verticalCenter: undefined
anchors.left: controlButtons.left
}
}
transitions: Transition {
AnchorAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
spacing: 0
StyledText {
// Layout.preferredWidth: elapsedIndicator.width * 0.6 // Prevent shakiness
font.pixelSize: 40
color: Appearance.m3colors.m3onSurface
text: {
let totalSeconds = Math.floor(TimerService.stopwatchTime) / 100
let minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0')
let seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0')
return `${minutes}:${seconds}`
}
}
StyledText {
Layout.fillWidth: true
font.pixelSize: 40
color: Appearance.colors.colSubtext
text: {
return `:<sub>${(Math.floor(TimerService.stopwatchTime) % 100).toString().padStart(2, '0')}</sub>`
}
}
}
// Laps
StyledListView {
id: lapsList
anchors {
top: elapsedIndicator.bottom
bottom: controlButtons.top
left: parent.left
right: parent.right
topMargin: 16
bottomMargin: 16
}
spacing: 4
clip: true
popin: true
model: ScriptModel {
values: TimerService.stopwatchLaps.map((v, i, arr) => arr[arr.length - 1 - i])
}
delegate: Rectangle {
id: lapItem
required property int index
required property var modelData
property var horizontalPadding: 10
property var verticalPadding: 6
width: lapsList.width
implicitHeight: lapRow.implicitHeight + verticalPadding * 2
implicitWidth: lapRow.implicitWidth + horizontalPadding * 2
color: Appearance.colors.colLayer2
radius: Appearance.rounding.small
RowLayout {
id: lapRow
anchors {
fill: parent
leftMargin: lapItem.horizontalPadding
rightMargin: lapItem.horizontalPadding
topMargin: lapItem.verticalPadding
bottomMargin: lapItem.verticalPadding
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colSubtext
text: `${TimerService.stopwatchLaps.length - lapItem.index}.`
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
text: {
const lapTime = lapItem.modelData
const _10ms = (Math.floor(lapTime) % 100).toString().padStart(2, '0')
const totalSeconds = Math.floor(lapTime) / 100
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0')
const seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0')
return `${minutes}:${seconds}.${_10ms}`
}
}
Item { Layout.fillWidth: true }
StyledText {
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colPrimary
text: {
const originalIndex = TimerService.stopwatchLaps.length - lapItem.index - 1
const lastTime = originalIndex > 0 ? TimerService.stopwatchLaps[originalIndex - 1] : 0
const lapTime = lapItem.modelData - lastTime
const _10ms = (Math.floor(lapTime) % 100).toString().padStart(2, '0')
const totalSeconds = Math.floor(lapTime) / 100
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0')
const seconds = Math.floor(totalSeconds % 60).toString().padStart(2, '0')
return `+${minutes == "00" ? "" : minutes + ":"}${seconds}.${_10ms}`
}
}
}
}
}
RowLayout {
id: controlButtons
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 6
}
spacing: 4
RippleButton {
Layout.preferredHeight: 35
Layout.preferredWidth: 90
font.pixelSize: Appearance.font.pixelSize.larger
onClicked: {
TimerService.toggleStopwatch()
}
colBackground: TimerService.stopwatchRunning ? Appearance.colors.colSecondaryContainer : Appearance.colors.colPrimary
colBackgroundHover: TimerService.stopwatchRunning ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colPrimaryHover
colRipple: TimerService.stopwatchRunning ? Appearance.colors.colSecondaryContainerActive : Appearance.colors.colPrimaryActive
contentItem: StyledText {
horizontalAlignment: Text.AlignHCenter
color: TimerService.stopwatchRunning ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnPrimary
text: TimerService.stopwatchRunning ? Translation.tr("Pause") : TimerService.stopwatchTime === 0 ? Translation.tr("Start") : Translation.tr("Resume")
}
}
RippleButton {
implicitHeight: 35
implicitWidth: 90
font.pixelSize: Appearance.font.pixelSize.larger
onClicked: {
if (TimerService.stopwatchRunning)
TimerService.stopwatchRecordLap()
else
TimerService.stopwatchReset()
}
enabled: TimerService.stopwatchTime > 0 || Persistent.states.timer.stopwatch.laps.length > 0
colBackground: TimerService.stopwatchRunning ? Appearance.colors.colLayer2 : Appearance.colors.colErrorContainer
colBackgroundHover: TimerService.stopwatchRunning ? Appearance.colors.colLayer2Hover : Appearance.colors.colErrorContainerHover
colRipple: TimerService.stopwatchRunning ? Appearance.colors.colLayer2Active : Appearance.colors.colErrorContainerActive
contentItem: StyledText {
horizontalAlignment: Text.AlignHCenter
text: TimerService.stopwatchRunning ? Translation.tr("Lap") : Translation.tr("Reset")
color: TimerService.stopwatchRunning ? Appearance.colors.colOnLayer2 : Appearance.colors.colOnErrorContainer
}
}
}
}
}

View file

@ -0,0 +1,63 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Space || event.key === Qt.Key_S) {
TimerService.toggleStopwatch();
event.accepted = true;
} else if (event.key === Qt.Key_R) {
TimerService.stopwatchReset();
event.accepted = true;
} else if (event.key === Qt.Key_L) {
TimerService.stopwatchRecordLap();
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
spacing: 8
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 4
Layout.rightMargin: 14
spacing: 8
MaterialSymbol {
text: "timer"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.colors.colOnSecondaryContainer
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: Translation.tr("Stopwatch")
font.pixelSize: Appearance.font.pixelSize.larger
font.weight: Font.Medium
color: Appearance.colors.colOnLayer1
}
StyledText {
text: TimerService.stopwatchRunning ? Translation.tr("Running") : Translation.tr("Ready")
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
}
}
}
Stopwatch {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}

View file

@ -0,0 +1,15 @@
import QtQuick
import qs.modules.common
Rectangle {
id: root
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
signal openAudioOutputDialog()
signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
}

View file

@ -0,0 +1,308 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.modules.ii.sidebarRight.quickToggles.androidStyle
AbstractQuickPanel {
id: root
property bool editMode: false
Layout.fillWidth: true
// Sizes
implicitHeight: (editMode ? contentItem.implicitHeight : usedRows.implicitHeight) + root.padding * 2
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
property real spacing: 6
property real padding: 6
readonly property real baseCellWidth: {
// This is the wrong calculation, but it looks correct in reality???
// (theoretically spacing should be multiplied by 1 column less)
const availableWidth = root.width - (root.padding * 2) - (root.spacing * (root.columns))
return availableWidth / root.columns
}
readonly property real baseCellHeight: 56
// Toggles
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile","musicRecognition", "antiFlashbang"]
readonly property int columns: Config.options.sidebar.quickToggles.android.columns
readonly property list<var> toggles: Config.ready ? Config.options.sidebar.quickToggles.android.toggles : []
// Filter out ghost items (config entries with types that have no matching delegate).
// Each entry carries its original config-array index so edit operations can target
// the right slot even when ghosts are interspersed.
readonly property list<var> validToggles: {
const result = []
const seen = new Set()
for (let i = 0; i < toggles.length; i++) {
const t = toggles[i]
if (t && availableToggleTypes.includes(t.type) && !seen.has(t.type)) {
result.push({ type: t.type, size: t.size, _configIndex: i })
seen.add(t.type)
}
}
return result
}
readonly property list<var> toggleRows: toggleRowsForList(validToggles)
readonly property list<var> unusedToggles: {
const types = availableToggleTypes.filter(type => !validToggles.some(toggle => toggle.type === type))
return types.map(type => { return { type: type, size: 1 } })
}
readonly property list<var> unusedToggleRows: toggleRowsForList(unusedToggles)
property int dragIndex: -1 // flat config index of item being dragged (-1 = none)
// Map (x, y) in usedRows coordinates flat config index.
// Uses the same stride math as the RowLayout so no item references are needed.
function toggleIndexAt(x, y) {
const rowH = root.baseCellHeight + root.spacing
const rowIdx = Math.max(0, Math.min(root.toggleRows.length - 1, Math.floor(y / rowH)))
if (root.toggleRows.length === 0) return -1
let flatStart = 0
for (let r = 0; r < rowIdx; r++) flatStart += root.toggleRows[r].length
const row = root.toggleRows[rowIdx]
if (!row || row.length === 0) return -1
// Each column slot is (baseCellWidth + spacing) wide; a size-2 button takes 2 slots.
const stride = root.baseCellWidth + root.spacing
let accumulated = 0
for (let c = 0; c < row.length; c++) {
accumulated += row[c].size * stride
// Drop target switches at the midpoint of the gap between buttons
if (x < accumulated - root.spacing / 2) return flatStart + c
}
return -1 // Click is in empty space past all buttons in this row
}
// Map a visual flat index (into validToggles) to the real config-array index.
function configIndexAt(visualIndex) {
if (visualIndex < 0 || visualIndex >= validToggles.length) return -1
return validToggles[visualIndex]._configIndex
}
function swapToggles(fromIdx, toIdx) {
const fromConfig = configIndexAt(fromIdx)
const toConfig = configIndexAt(toIdx)
if (fromConfig < 0 || toConfig < 0) return
const list = Config.options.sidebar.quickToggles.android.toggles
const temp = list[fromConfig]
list[fromConfig] = list[toConfig]
list[toConfig] = temp
}
function removeToggleAt(index) {
const configIdx = configIndexAt(index)
if (configIdx < 0) return
Config.options.sidebar.quickToggles.android.toggles.splice(configIdx, 1)
}
function resizeToggleAt(index) {
const configIdx = configIndexAt(index)
if (configIdx < 0) return
const list = Config.options.sidebar.quickToggles.android.toggles
list[configIdx] = { type: list[configIdx].type, size: 3 - list[configIdx].size }
}
function toggleRowsForList(togglesList) {
var rows = [];
var row = [];
var totalSize = 0; // Total cols taken in current row
for (var i = 0; i < togglesList.length; i++) {
if (!togglesList[i]) continue;
if (totalSize + togglesList[i].size > columns) {
rows.push(row);
row = [];
totalSize = 0;
}
row.push(togglesList[i]);
totalSize += togglesList[i].size;
}
if (row.length > 0) {
rows.push(row);
}
return rows;
}
Column {
id: contentItem
anchors {
fill: parent
margins: root.padding
}
spacing: 12
Column {
id: usedRows
spacing: root.spacing
Repeater {
id: usedRowsRepeater
model: ScriptModel {
values: Array(root.toggleRows.length)
}
delegate: ButtonGroup {
id: toggleRow
required property int index
property var modelData: root.toggleRows[index]
property int startingIndex: {
const rows = root.toggleRows;
let sum = 0;
for (let i = 0; i < index; i++) {
sum += rows[i].length;
}
return sum;
}
spacing: root.spacing
Repeater {
model: ScriptModel {
values: toggleRow?.modelData ?? []
objectProp: "type"
}
delegate: AndroidToggleDelegateChooser {
startingIndex: toggleRow.startingIndex
editMode: root.editMode
dragIndex: root.dragIndex
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
spacing: root.spacing
onOpenAudioOutputDialog: root.openAudioOutputDialog()
onOpenAudioInputDialog: root.openAudioInputDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenNightLightDialog: root.openNightLightDialog()
onOpenWifiDialog: root.openWifiDialog()
}
}
}
}
}
FadeLoader {
shown: root.editMode
anchors {
left: parent.left
right: parent.right
leftMargin: root.baseCellHeight / 2
rightMargin: root.baseCellHeight / 2
}
sourceComponent: Rectangle {
implicitHeight: 1
color: Appearance.colors.colOutlineVariant
}
}
FadeLoader {
shown: root.editMode
sourceComponent: Column {
id: unusedRows
spacing: root.spacing
Repeater {
model: ScriptModel {
values: Array(root.unusedToggleRows.length)
}
delegate: ButtonGroup {
id: unusedToggleRow
required property int index
property var modelData: root.unusedToggleRows[index]
spacing: root.spacing
Repeater {
model: ScriptModel {
values: unusedToggleRow?.modelData ?? []
objectProp: "type"
}
delegate: AndroidToggleDelegateChooser {
startingIndex: -1
editMode: root.editMode
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
spacing: root.spacing
}
}
}
}
}
}
}
// Edit-mode drag overlay
// Direct child of root so it floats above contentItem (z:100).
// Positioned to exactly cover usedRows in root's coordinate space:
// contentItem has margins=root.padding, usedRows is contentItem's first child.
// Intercepts all pointer events on the used-section buttons so drag, click,
// and resize are all handled here. Unused-section buttons sit below this
// overlay's height, so their own editModeInteraction MouseArea still fires.
MouseArea {
id: editDragOverlay
z: 100
x: root.padding
y: root.padding
width: usedRows.width
height: usedRows.height
visible: root.editMode
enabled: root.editMode
acceptedButtons: Qt.AllButtons
hoverEnabled: true
cursorShape: root.dragIndex >= 0 ? Qt.ClosedHandCursor : Qt.OpenHandCursor
property int sourceIndex: -1
property bool dragActive: false
property real pressX: 0
property real pressY: 0
property int pressedButton: Qt.NoButton
readonly property real dragThreshold: 6
onPressed: (mouse) => {
pressX = mouse.x
pressY = mouse.y
dragActive = false
pressedButton = mouse.button
sourceIndex = root.toggleIndexAt(mouse.x, mouse.y)
if (mouse.button === Qt.RightButton && sourceIndex >= 0) {
root.resizeToggleAt(sourceIndex)
sourceIndex = -1
}
}
onPositionChanged: (mouse) => {
if (!dragActive && pressedButton === Qt.LeftButton) {
const dx = mouse.x - pressX
const dy = mouse.y - pressY
if (Math.sqrt(dx * dx + dy * dy) > dragThreshold) {
dragActive = true
root.dragIndex = sourceIndex
}
}
if (dragActive && root.dragIndex >= 0) {
const targetIdx = root.toggleIndexAt(mouse.x, mouse.y)
if (targetIdx >= 0 && targetIdx !== root.dragIndex) {
root.swapToggles(root.dragIndex, targetIdx)
root.dragIndex = targetIdx // follow the dragged item
}
}
}
onPressAndHold: {
if (sourceIndex >= 0 && !dragActive) {
root.resizeToggleAt(sourceIndex)
sourceIndex = -1 // suppress the upcoming release click
}
}
onReleased: (mouse) => {
if (!dragActive && mouse.button === Qt.LeftButton && sourceIndex >= 0)
root.removeToggleAt(sourceIndex)
root.dragIndex = -1
sourceIndex = -1
dragActive = false
}
// Consume wheel events scroll-to-reorder is replaced by drag
onWheel: (wheel) => wheel.accepted = true
}
//
}

View file

@ -0,0 +1,39 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import qs.modules.ii.sidebarRight.quickToggles.classicStyle
AbstractQuickPanel {
id: root
Layout.alignment: Qt.AlignHCenter
implicitWidth: buttonGroup.implicitWidth
implicitHeight: buttonGroup.implicitHeight
color: "transparent"
ButtonGroup {
id: buttonGroup
spacing: 5
padding: 5
color: Appearance.colors.colLayer1
NetworkToggle {
altAction: () => {
root.openWifiDialog();
}
}
BluetoothToggle {
altAction: () => {
root.openBluetoothDialog();
}
}
NightLight {}
GameMode {}
IdleInhibitor {}
EasyEffectsToggle {}
CloudflareWarp {}
}
}

View file

@ -0,0 +1,13 @@
import qs.modules.common.models.quickToggles
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
toggleModel: AntiFlashbangToggle {}
}

View file

@ -0,0 +1,13 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
toggleModel: AudioToggle {}
}

View file

@ -0,0 +1,21 @@
import qs.services
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
import Quickshell
import Quickshell.Bluetooth
AndroidQuickToggleButton {
id: root
toggleModel: BluetoothToggle {}
mainAction: () => {
Quickshell.execDetached([
"bash", "-lc",
"rfkill list bluetooth | grep -q 'Soft blocked: yes' && rfkill unblock bluetooth || rfkill block bluetooth"
])
}
}

View file

@ -0,0 +1,13 @@
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Io
AndroidQuickToggleButton {
id: root
toggleModel: CloudflareWarpToggle {}
}

View file

@ -0,0 +1,13 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
toggleModel: ColorPickerToggle {}
}

View file

@ -0,0 +1,10 @@
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
toggleModel: DarkModeToggle {}
}

View file

@ -0,0 +1,11 @@
import qs
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
toggleModel: EasyEffectsToggle {}
}

View file

@ -0,0 +1,11 @@
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Io
AndroidQuickToggleButton {
toggleModel: GameModeToggle {}
}

View file

@ -0,0 +1,11 @@
import qs.services
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
AndroidQuickToggleButton {
toggleModel: IdleInhibitorToggle {}
}

View file

@ -0,0 +1,11 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
toggleModel: MicToggle {}
}

View file

@ -0,0 +1,13 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import QtQuick
import Quickshell
import Quickshell.Io
import qs.services
AndroidQuickToggleButton {
toggleModel: MusicRecognitionToggle {}
}

View file

@ -0,0 +1,13 @@
import qs.services
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
AndroidQuickToggleButton {
id: root
toggleModel: NetworkToggle {}
}

View file

@ -0,0 +1,11 @@
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
toggleModel: NightLightToggle {}
}

View file

@ -0,0 +1,11 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
toggleModel: NotificationToggle {}
}

View file

@ -0,0 +1,11 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
toggleModel: OnScreenKeyboardToggle {}
}

View file

@ -0,0 +1,12 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Services.UPower
AndroidQuickToggleButton {
toggleModel: PowerProfilesToggle {}
}

View file

@ -0,0 +1,288 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.functions
import qs.modules.common.widgets
GroupButton {
id: root
// Info to be passed to by repeater
required property int buttonIndex
required property var buttonData
required property bool expandedSize
required property real baseCellWidth
required property real baseCellHeight
required property real cellSpacing
required property int cellSize
// Signals
signal openMenu()
// Declared in specific toggles
property QuickToggleModel toggleModel
property string name: toggleModel?.name ?? ""
property string statusText: (toggleModel?.hasStatusText) ? (toggleModel?.statusText || (toggled ? Translation.tr("On") : Translation.tr("Off"))) : ""
property string tooltipText: toggleModel?.tooltipText ?? ""
property string buttonIcon: toggleModel?.icon ?? "close"
property bool available: toggleModel?.available ?? true
toggled: toggleModel?.toggled ?? false
property var mainAction: toggleModel?.mainAction ?? null
altAction: toggleModel?.hasMenu ? (() => root.openMenu()) : (toggleModel?.altAction ?? null)
// Edit mode state
property bool editMode: false
property int dragIndex: -1
readonly property bool isBeingDragged: editMode && dragIndex >= 0 && dragIndex === buttonIndex
// Sizing shenanigans
baseWidth: root.baseCellWidth * cellSize + cellSpacing * (cellSize - 1)
baseHeight: root.baseCellHeight
enableImplicitWidthAnimation: !editMode && root.mouseArea.containsMouse
enableImplicitHeightAnimation: !editMode && root.mouseArea.containsMouse
Behavior on baseWidth {
enabled: !root.editMode
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on baseHeight {
enabled: !root.editMode
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
opacity: editMode ? 1 : 0
Component.onCompleted: {
opacity = 1
}
Behavior on opacity {
enabled: !root.editMode
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
scale: isBeingDragged ? 1.06 : 1.0
Behavior on scale {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
enabled: available || editMode
padding: 6
horizontalPadding: padding
verticalPadding: padding
colBackground: isBeingDragged ? Appearance.colors.colLayer3 : Appearance.colors.colLayer2
colBackgroundToggled: (altAction && expandedSize) ? Appearance.colors.colLayer2 : Appearance.colors.colPrimary
colBackgroundToggledHover: (altAction && expandedSize) ? Appearance.colors.colLayer2Hover : Appearance.colors.colPrimaryHover
colBackgroundToggledActive: (altAction && expandedSize) ? Appearance.colors.colLayer2Active : Appearance.colors.colPrimaryActive
buttonRadius: toggled ? Appearance.rounding.large : height / 2
buttonRadiusPressed: Appearance.rounding.normal
property color colText: (toggled && !(altAction && expandedSize) && enabled) ? Appearance.colors.colOnPrimary : ColorUtils.transparentize(Appearance.colors.colOnLayer2, enabled ? 0 : 0.7)
property color colIcon: expandedSize ? ((root.toggled) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer3) : colText
onClicked: {
if (root.expandedSize && root.altAction) root.altAction();
else root.mainAction();
}
contentItem: RowLayout {
id: contentItem
spacing: 4
anchors {
centerIn: root.expandedSize ? undefined : parent
fill: root.expandedSize ? parent : undefined
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
// Icon
MouseArea {
id: iconMouseArea
hoverEnabled: true
acceptedButtons: (root.expandedSize && root.altAction) ? Qt.LeftButton : Qt.NoButton
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.topMargin: root.verticalPadding
Layout.bottomMargin: root.verticalPadding
implicitHeight: iconBackground.implicitHeight
implicitWidth: iconBackground.implicitWidth
cursorShape: Qt.PointingHandCursor
onClicked: root.mainAction()
Rectangle {
id: iconBackground
anchors.fill: parent
implicitWidth: height
radius: root.radius - root.verticalPadding
color: {
const baseColor = root.toggled ? Appearance.colors.colPrimary : Appearance.colors.colLayer3
const transparentizeAmount = (root.altAction && root.expandedSize) ? 0 : 1
return ColorUtils.transparentize(baseColor, transparentizeAmount)
}
Behavior on radius {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
MaterialSymbol {
anchors.centerIn: parent
fill: root.toggled ? 1 : 0
iconSize: root.expandedSize ? 22 : 24
color: root.colIcon
text: root.buttonIcon
}
// State layer
Loader {
anchors.fill: parent
active: (root.expandedSize && root.altAction)
sourceComponent: Rectangle {
radius: iconBackground.radius
color: ColorUtils.transparentize(root.colIcon, iconMouseArea.containsPress ? 0.88 : iconMouseArea.containsMouse ? 0.95 : 1)
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}
}
// Text column for expanded size
Loader {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
visible: root.expandedSize
active: visible
sourceComponent: Column {
spacing: -2
StyledText {
anchors {
left: parent.left
right: parent.right
}
font.pixelSize: Appearance.font.pixelSize.smallie
font.weight: 600
color: root.colText
elide: Text.ElideRight
text: root.name
}
Rectangle {
id: statusTextContainer
visible: root.statusText
anchors {
left: parent.left
right: parent.right
}
height: statusTextObject.height
color: "transparent"
clip: true
StyledText {
id: statusTextObject
font {
pixelSize: Appearance.font.pixelSize.smaller
weight: 100
}
color: root.colText
text: root.statusText
readonly property bool needsScrolling: implicitWidth > statusTextContainer.width
readonly property real scrollDistance: implicitWidth - statusTextContainer.width
SequentialAnimation on x {
loops: Animation.Infinite
running: statusTextObject.needsScrolling && statusTextContainer.width > 0
PropertyAction { value: 0 }
PauseAnimation { duration: 2000 }
NumberAnimation {
from: 0
to: -statusTextObject.scrollDistance
duration: 3000
easing.type: Easing.InOutQuad
}
PauseAnimation { duration: 1000 }
NumberAnimation {
from: -statusTextObject.scrollDistance
to: 0
duration: 3000
easing.type: Easing.InOutQuad
}
}
}
}
}
}
}
MouseArea { // Blocking MouseArea for edit interactions
id: editModeInteraction
visible: root.editMode
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.AllButtons
function toggleEnabled() {
const index = root.buttonIndex;
const toggleList = Config.options.sidebar.quickToggles.android.toggles;
const buttonType = root.buttonData.type;
if (!toggleList.find(toggle => toggle.type === buttonType)) {
toggleList.push({ type: buttonType, size: 1 });
} else {
toggleList.splice(index, 1);
}
}
function toggleSize() {
const index = root.buttonIndex;
const toggleList = Config.options.sidebar.quickToggles.android.toggles;
const buttonType = root.buttonData.type;
if (!toggleList.find(toggle => toggle.type === buttonType)) return;
toggleList[index].size = 3 - toggleList[index].size; // Alternate between 1 and 2
}
function movePositionBy(offset) {
const index = root.buttonIndex;
const toggleList = Config.options.sidebar.quickToggles.android.toggles;
const buttonType = root.buttonData.type;
const targetIndex = index + offset;
if (!toggleList.find(toggle => toggle.type === buttonType)) return;
if (targetIndex < 0 || targetIndex >= toggleList.length) return;
const temp = toggleList[index];
toggleList[index] = toggleList[targetIndex];
toggleList[targetIndex] = temp;
}
onReleased: (event) => {
if (event.button === Qt.LeftButton)
toggleEnabled();
}
onPressed: (event) => {
if (event.button === Qt.RightButton) toggleSize();
}
onPressAndHold: (event) => { // Also toggle size
toggleSize();
}
onWheel: (event) => {
const index = root.buttonIndex;
const toggleList = Config.options.sidebar.quickToggles.android.toggles;
const buttonType = root.buttonData.type;
if (event.angleDelta.y < 0) { // Move to right
movePositionBy(1);
} else if (event.angleDelta.y > 0) { // Move to left
movePositionBy(-1);
}
event.accepted = true;
}
}
StyledToolTip {
extraVisibleCondition: root.tooltipText !== ""
text: root.tooltipText
}
}

View file

@ -0,0 +1,12 @@
import qs
import qs.modules.common
import qs.modules.common.models.quickToggles
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Hyprland
AndroidQuickToggleButton {
toggleModel: ScreenSnipToggle {}
}

View file

@ -0,0 +1,281 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
DelegateChooser {
id: root
property bool editMode: false
required property real baseCellWidth
required property real baseCellHeight
required property real spacing
required property int startingIndex
property int dragIndex: -1
signal openAudioOutputDialog()
signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
role: "type"
DelegateChoice { roleValue: "antiFlashbang"; AndroidAntiFlashbangToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openNightLightDialog()
}
} }
DelegateChoice { roleValue: "audio"; AndroidAudioToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openAudioOutputDialog()
}
} }
DelegateChoice { roleValue: "bluetooth"; AndroidBluetoothToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openBluetoothDialog()
}
} }
DelegateChoice { roleValue: "cloudflareWarp"; AndroidCloudflareWarpToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "colorPicker"; AndroidColorPickerToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "darkMode"; AndroidDarkModeToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "easyEffects"; AndroidEasyEffectsToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "gameMode"; AndroidGameModeToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "idleInhibitor"; AndroidIdleInhibitorToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "mic"; AndroidMicToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openAudioInputDialog()
}
} }
DelegateChoice { roleValue: "musicRecognition"; AndroidMusicRecognition {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "network"; AndroidNetworkToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openWifiDialog()
}
} }
DelegateChoice { roleValue: "nightLight"; AndroidNightLightToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openNightLightDialog()
}
} }
DelegateChoice { roleValue: "notifications"; AndroidNotificationToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "onScreenKeyboard"; AndroidOnScreenKeyboardToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "powerProfile"; AndroidPowerProfileToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "screenSnip"; AndroidScreenSnipToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
dragIndex: root.dragIndex
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
}

View file

@ -0,0 +1,30 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Hyprland
QuickToggleButton {
id: root
visible: BluetoothStatus.available
toggled: BluetoothStatus.enabled
buttonIcon: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
onClicked: {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
}
altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`])
GlobalStates.sidebarRightOpen = false
}
StyledToolTip {
text: Translation.tr("%1 | Right-click to configure").arg(
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
)
}
}

View file

@ -0,0 +1,92 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell.Io
import Quickshell
QuickToggleButton {
id: root
toggled: false
visible: false
contentItem: CustomIcon {
id: distroIcon
source: 'cloudflare-dns-symbolic'
anchors.centerIn: parent
width: 16
height: 16
colorize: true
color: root.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
onClicked: {
if (toggled) {
root.toggled = false
Quickshell.execDetached(["warp-cli", "disconnect"])
} else {
root.toggled = true
Quickshell.execDetached(["warp-cli", "connect"])
}
}
Process {
id: connectProc
command: ["warp-cli", "connect"]
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
Quickshell.execDetached(["notify-send",
Translation.tr("Cloudflare WARP"),
Translation.tr("Connection failed. Please inspect manually with the <tt>warp-cli</tt> command")
, "-a", "Shell"
])
}
}
}
Process {
id: registrationProc
command: ["warp-cli", "registration", "new"]
onExited: (exitCode, exitStatus) => {
console.log("Warp registration exited with code and status:", exitCode, exitStatus)
if (exitCode === 0) {
connectProc.running = true
} else {
Quickshell.execDetached(["notify-send",
Translation.tr("Cloudflare WARP"),
Translation.tr("Registration failed. Please inspect manually with the <tt>warp-cli</tt> command"),
"-a", "Shell"
])
}
}
}
Process {
id: fetchActiveState
running: true
command: ["bash", "-c", "warp-cli status"]
stdout: StdioCollector {
id: warpStatusCollector
onStreamFinished: {
if (warpStatusCollector.text.length > 0) {
root.visible = true
}
if (warpStatusCollector.text.includes("Unable")) {
registrationProc.running = true
} else if (warpStatusCollector.text.includes("Connected")) {
root.toggled = true
} else if (warpStatusCollector.text.includes("Disconnected")) {
root.toggled = false
}
}
}
}
StyledToolTip {
text: Translation.tr("Cloudflare WARP (1.1.1.1)")
}
}

View file

@ -0,0 +1,31 @@
import qs.modules.common.widgets
import qs
import qs.services
import QtQuick
import Quickshell.Io
import Quickshell
import Quickshell.Hyprland
QuickToggleButton {
id: root
visible: EasyEffects.available
toggled: EasyEffects.active
buttonIcon: "instant_mix"
Component.onCompleted: {
EasyEffects.fetchActiveState()
}
onClicked: {
EasyEffects.toggle()
}
altAction: () => {
Quickshell.execDetached(["bash", "-c", "flatpak run com.github.wwmm.easyeffects || easyeffects"])
GlobalStates.sidebarRightOpen = false
}
StyledToolTip {
text: Translation.tr("EasyEffects | Right-click to configure")
}
}

View file

@ -0,0 +1,31 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Quickshell
import Quickshell.Io
QuickToggleButton {
id: root
buttonIcon: "gamepad"
toggled: toggled
onClicked: {
root.toggled = !root.toggled
if (root.toggled) {
Quickshell.execDetached(["bash", "-c", `hyprctl --batch "keyword animations:enabled 0; keyword decoration:shadow:enabled 0; keyword decoration:blur:enabled 0; keyword general:gaps_in 0; keyword general:gaps_out 0; keyword general:border_size 1; keyword decoration:rounding 0; keyword general:allow_tearing 1"`])
} else {
Quickshell.execDetached(["hyprctl", "reload"])
}
}
Process {
id: fetchActiveState
running: true
command: ["bash", "-c", `test "$(hyprctl getoption animations:enabled -j | jq ".int")" -ne 0`]
onExited: (exitCode, exitStatus) => {
root.toggled = exitCode !== 0 // Inverted because enabled = nonzero exit
}
}
StyledToolTip {
text: Translation.tr("Game mode")
}
}

View file

@ -0,0 +1,15 @@
import qs.modules.common.widgets
import qs.services
QuickToggleButton {
id: root
toggled: Idle.inhibit
buttonIcon: "coffee"
onClicked: {
Idle.toggleInhibit()
}
StyledToolTip {
text: Translation.tr("Keep system awake")
}
}

View file

@ -0,0 +1,23 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.ii.sidebarRight.quickToggles
import qs
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
QuickToggleButton {
toggled: Network.wifiStatus !== "disabled"
buttonIcon: Network.materialSymbol
onClicked: Network.toggleWifi()
altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`])
GlobalStates.sidebarRightOpen = false
}
StyledToolTip {
text: Translation.tr("%1 | Right-click to configure").arg(Network.networkName)
}
}

View file

@ -0,0 +1,26 @@
import QtQuick
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Quickshell.Io
QuickToggleButton {
id: nightLightButton
toggled: Hyprsunset.temperatureActive
buttonIcon: Config.options.light.night.automatic ? "night_sight_auto" : "bedtime"
onClicked: {
Hyprsunset.toggleTemperature()
}
altAction: () => {
Config.options.light.night.automatic = !Config.options.light.night.automatic
}
Component.onCompleted: {
Hyprsunset.fetchState()
}
StyledToolTip {
text: Translation.tr("Night Light | Right-click to toggle Auto mode")
}
}

View file

@ -0,0 +1,29 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
GroupButton {
id: button
property string buttonIcon
baseWidth: 40
baseHeight: 40
clickedWidth: baseWidth + 20
toggled: false
buttonRadius: (altAction && toggled) ? Appearance?.rounding.normal : Math.min(baseHeight, baseWidth) / 2
buttonRadiusPressed: Appearance?.rounding?.small
contentItem: MaterialSymbol {
anchors.centerIn: parent
iconSize: 22
fill: toggled ? 1 : 0
color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: buttonIcon
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}

View file

@ -0,0 +1,141 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Item {
id: root
required property var taskList
property string emptyPlaceholderIcon
property string emptyPlaceholderText
property int todoListItemSpacing: 5
property int todoListItemPadding: 8
property int listBottomPadding: 80
StyledListView {
id: listView
anchors.fill: parent
spacing: root.todoListItemSpacing
animateAppearance: false
model: ScriptModel {
values: root.taskList
}
delegate: Item {
id: todoItem
required property var modelData
property bool pendingDoneToggle: false
property bool pendingDelete: false
property bool enableHeightAnimation: false
implicitHeight: todoItemRectangle.implicitHeight
width: ListView.view.width
clip: true
Behavior on implicitHeight {
enabled: enableHeightAnimation
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
Rectangle {
id: todoItemRectangle
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: todoContentRowLayout.implicitHeight
color: Appearance.colors.colLayer2
radius: Appearance.rounding.small
ColumnLayout {
id: todoContentRowLayout
anchors.left: parent.left
anchors.right: parent.right
StyledText {
id: todoContentText
Layout.fillWidth: true // Needed for wrapping
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.topMargin: todoListItemPadding
text: todoItem.modelData.content
wrapMode: Text.Wrap
}
RowLayout {
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: todoListItemPadding
Item {
Layout.fillWidth: true
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
if (!todoItem.modelData.done)
Todo.markDone(todoItem.modelData.originalIndex);
else
Todo.markUnfinished(todoItem.modelData.originalIndex);
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: todoItem.modelData.done ? "remove_done" : "check"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
Todo.deleteItem(todoItem.modelData.originalIndex);
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "delete_forever"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
}
}
}
}
}
Item {
// Placeholder when list is empty
visible: opacity > 0
opacity: taskList.length === 0 ? 1 : 0
anchors.fill: parent
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: emptyPlaceholderIcon
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: emptyPlaceholderText
}
}
}
}

View file

@ -0,0 +1,32 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: button
property string buttonText: ""
property string tooltipText: ""
implicitHeight: 30
implicitWidth: implicitHeight
Behavior on implicitWidth {
SmoothedAnimation {
velocity: Appearance.animation.elementMove.velocity
}
}
buttonRadius: Appearance.rounding.small
contentItem: StyledText {
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
StyledToolTip {
text: tooltipText
extraVisibleCondition: tooltipText.length > 0
}
}

View file

@ -0,0 +1,219 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var tabButtonList: [{"icon": "checklist", "name": Translation.tr("Unfinished")}, {"name": Translation.tr("Done"), "icon": "check_circle"}]
property bool showAddDialog: false
property int dialogMargins: 20
property int fabSize: 48
property int fabMargins: 14
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageDown) {
tabBar.incrementCurrentIndex();
} else if (event.key === Qt.Key_PageUp) {
tabBar.decrementCurrentIndex();
}
event.accepted = true;
}
// Open add dialog on "N" (any modifiers)
else if (event.key === Qt.Key_N) {
root.showAddDialog = true
event.accepted = true;
}
// Close dialog on Esc if open
else if (event.key === Qt.Key_Escape && root.showAddDialog) {
root.showAddDialog = false
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
SecondaryTabBar {
id: tabBar
currentIndex: swipeView.currentIndex
Repeater {
model: root.tabButtonList
delegate: SecondaryTabButton {
buttonText: modelData.name
buttonIcon: modelData.icon
}
}
}
SwipeView {
id: swipeView
Layout.topMargin: 10
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
clip: true
currentIndex: tabBar.currentIndex
// To Do tab
TaskList {
listBottomPadding: root.fabSize + root.fabMargins * 2
emptyPlaceholderIcon: "check_circle"
emptyPlaceholderText: Translation.tr("Nothing here!")
taskList: Todo.list
.map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
.filter(function(item) { return !item.done; })
}
TaskList {
listBottomPadding: root.fabSize + root.fabMargins * 2
emptyPlaceholderIcon: "checklist"
emptyPlaceholderText: Translation.tr("Finished tasks will go here")
taskList: Todo.list
.map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
.filter(function(item) { return item.done; })
}
}
}
// + FAB
StyledRectangularShadow {
target: fabButton
radius: fabButton.buttonRadius
blur: 0.6 * Appearance.sizes.elevationMargin
}
FloatingActionButton {
id: fabButton
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: root.fabMargins
anchors.bottomMargin: root.fabMargins
onClicked: root.showAddDialog = true
iconText: "add"
}
Item {
anchors.fill: parent
z: 9999
visible: opacity > 0
opacity: root.showAddDialog ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
onVisibleChanged: {
if (!visible) {
todoInput.text = ""
fabButton.focus = true
}
}
Rectangle { // Scrim
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
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: root.dialogMargins
implicitHeight: dialogColumnLayout.implicitHeight
color: Appearance.m3colors.m3surfaceContainerHigh
radius: Appearance.rounding.normal
function addTask() {
if (todoInput.text.length > 0) {
Todo.addTask(todoInput.text)
todoInput.text = ""
root.showAddDialog = false
tabBar.setCurrentIndex(0) // Show unfinished tasks
}
}
ColumnLayout {
id: dialogColumnLayout
anchors.fill: parent
spacing: 16
StyledText {
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignLeft
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.larger
text: Translation.tr("Add task")
}
TextField {
id: todoInput
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
padding: 10
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
renderType: Text.NativeRendering
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer
placeholderText: Translation.tr("Task description")
placeholderTextColor: Appearance.m3colors.m3outline
focus: root.showAddDialog
onAccepted: dialog.addTask()
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.verysmall
border.width: 2
border.color: todoInput.activeFocus ? Appearance.colors.colPrimary : Appearance.m3colors.m3outline
color: "transparent"
}
cursorDelegate: Rectangle {
width: 1
color: todoInput.activeFocus ? Appearance.colors.colPrimary : "transparent"
radius: 1
}
}
RowLayout {
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignRight
spacing: 5
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: root.showAddDialog = false
}
DialogButton {
buttonText: Translation.tr("Add")
enabled: todoInput.text.length > 0
onClicked: dialog.addTask()
}
}
}
}
}
}

View file

@ -0,0 +1,55 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
RippleButton {
id: button
required property bool input
buttonRadius: Appearance.rounding.small
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
implicitHeight: contentItem.implicitHeight + 6 * 2
implicitWidth: contentItem.implicitWidth + 6 * 2
contentItem: RowLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: false
Layout.leftMargin: 5
color: Appearance.colors.colOnLayer2
iconSize: Appearance.font.pixelSize.hugeass
text: input ? "mic_external_on" : "media_output"
}
ColumnLayout {
Layout.fillWidth: true
Layout.rightMargin: 5
spacing: 0
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Appearance.font.pixelSize.normal
text: input ? Translation.tr("Input") : Translation.tr("Output")
color: Appearance.colors.colOnLayer2
}
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Appearance.font.pixelSize.smaller
text: (input ? Pipewire.defaultAudioSource?.description : Pipewire.defaultAudioSink?.description) ?? Translation.tr("Unknown")
color: Appearance.m3colors.m3outline
animateChange: true
}
}
}
}

View file

@ -0,0 +1,48 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
WindowDialog {
id: root
property bool isSink: true
backgroundHeight: 600
WindowDialogTitle {
text: root.isSink ? Translation.tr("Audio output") : Translation.tr("Audio input")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
VolumeDialogContent {
isSink: root.isSink
}
WindowDialogButtonRow {
DialogButton {
buttonText: Translation.tr("Details")
onClicked: {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.volumeMixer}`]);
GlobalStates.sidebarRightOpen = false;
}
}
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}

View file

@ -0,0 +1,85 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
ColumnLayout {
id: root
required property bool isSink
readonly property list<var> appPwNodes: isSink ? Audio.outputAppNodes : Audio.inputAppNodes
readonly property list<var> devices: isSink ? Audio.outputDevices : Audio.inputDevices
readonly property bool hasApps: appPwNodes.length > 0
spacing: 16
DialogSectionListView {
Layout.fillHeight: true
topMargin: 14
model: ScriptModel {
values: root.appPwNodes
}
delegate: VolumeMixerEntry {
anchors {
left: parent?.left
right: parent?.right
}
required property var modelData
node: modelData
}
PagePlaceholder {
icon: "widgets"
title: Translation.tr("No applications")
shown: !root.hasApps
shape: MaterialShape.Shape.Cookie7Sided
}
}
StyledComboBox {
id: deviceSelector
Layout.fillHeight: false
Layout.fillWidth: true
Layout.bottomMargin: 6
model: root.devices.map(node => Audio.friendlyDeviceName(node))
currentIndex: root.devices.findIndex(item => {
if (root.isSink) {
return item.id === Pipewire.defaultAudioSink?.id
} else {
return item.id === Pipewire.defaultAudioSource?.id
}
})
onActivated: (index) => {
print(index)
const item = root.devices[index]
if (root.isSink) {
Audio.setDefaultSink(item)
} else {
Audio.setDefaultSource(item)
}
}
}
component DialogSectionListView: StyledListView {
Layout.fillWidth: true
Layout.topMargin: -22
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
topMargin: 12
bottomMargin: 12
leftMargin: 20
rightMargin: 20
clip: true
spacing: 4
animateAppearance: false
}
Component {
id: listElementComp
ListElement {}
}
}

View file

@ -0,0 +1,106 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import Qt5Compat.GraphicalEffects
Item {
id: root
required property PwNode node
PwObjectTracker {
objects: [root.node]
}
implicitHeight: rowLayout.implicitHeight
RowLayout {
id: rowLayout
anchors.fill: parent
spacing: 6
MouseArea {
property real size: 36
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.preferredWidth: size
Layout.preferredHeight: size
cursorShape: Qt.PointingHandCursor
onClicked: root.node.audio.muted = !root.node.audio.muted
hoverEnabled: true
property bool hovered: containsMouse
StyledToolTip {
text: root.node?.audio.muted ? Translation.tr("Click to unmute") : Translation.tr("Click to mute")
}
StyledImage {
id: iconImg
anchors.fill: parent
visible: false
source: {
let icon;
icon = AppSearch.guessIcon(root.node?.properties["application.icon-name"] ?? "");
if (AppSearch.iconExists(icon))
return Quickshell.iconPath(icon, "image-missing");
icon = AppSearch.guessIcon(root.node?.properties["node.name"] ?? "");
return Quickshell.iconPath(icon, "image-missing");
}
}
Desaturate {
anchors.fill: iconImg
source: iconImg
desaturation: root.node?.audio.muted ? 1.0 : 0.0
visible: iconImg.source !== ""
opacity: root.node?.audio.muted ? 0.4 : 1.0
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
Behavior on desaturation {
NumberAnimation {
duration: 150
}
}
}
MaterialSymbol {
anchors.centerIn: parent
visible: root.node?.audio.muted ?? false
text: root.node?.isSink ? "volume_off" : "mic_off"
iconSize: 22
color: Appearance.colors.colOnLayer1
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: -4
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colSubtext
elide: Text.ElideRight
text: {
// application.name -> description -> name
const app = Audio.appNodeDisplayName(root.node);
const media = root.node.properties["media.name"];
return media != undefined ? `${app} ${media}` : app;
}
}
StyledSlider {
id: slider
value: root.node?.audio.volume ?? 0
onMoved: root.node.audio.volume = value
configuration: StyledSlider.Configuration.S
}
}
}
}

View file

@ -0,0 +1,67 @@
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
WindowDialog {
id: root
backgroundHeight: 600
WindowDialogTitle {
text: Translation.tr("Connect to Wi-Fi")
}
WindowDialogSeparator {
visible: !Network.wifiScanning
}
StyledIndeterminateProgressBar {
visible: Network.wifiScanning
Layout.fillWidth: true
Layout.topMargin: -8
Layout.bottomMargin: -8
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
}
ListView {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: -15
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
clip: true
spacing: 0
model: ScriptModel {
values: Network.friendlyWifiNetworks
}
delegate: WifiNetworkItem {
required property WifiAccessPoint modelData
wifiNetwork: modelData
width: ListView.view.width
}
}
WindowDialogSeparator {}
WindowDialogButtonRow {
DialogButton {
buttonText: Translation.tr("Details")
onClicked: {
Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`]);
GlobalStates.sidebarRightOpen = false;
}
}
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}

View file

@ -0,0 +1,120 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.services.network
import QtQuick
import QtQuick.Layouts
DialogListItem {
id: root
required property WifiAccessPoint wifiNetwork
enabled: !(Network.wifiConnectTarget === root.wifiNetwork && !wifiNetwork?.active)
active: (wifiNetwork?.askingPassword || wifiNetwork?.active) ?? false
buttonRadius: Appearance.rounding.normal
onClicked: {
Network.connectToWifiNetwork(wifiNetwork);
}
contentItem: ColumnLayout {
anchors {
fill: parent
topMargin: root.verticalPadding
bottomMargin: root.verticalPadding
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
spacing: 0
RowLayout {
// Name
spacing: 10
MaterialSymbol {
iconSize: Appearance.font.pixelSize.larger
property int strength: root.wifiNetwork?.strength ?? 0
text: strength > 80 ? "signal_wifi_4_bar" : strength > 60 ? "network_wifi_3_bar" : strength > 40 ? "network_wifi_2_bar" : strength > 20 ? "network_wifi_1_bar" : "signal_wifi_0_bar"
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
color: Appearance.colors.colOnSurfaceVariant
elide: Text.ElideRight
text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown")
textFormat: Text.PlainText
}
MaterialSymbol {
visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false
text: root.wifiNetwork?.active ? "check" : Network.wifiConnectTarget === root.wifiNetwork ? "settings_ethernet" : "lock"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnSurfaceVariant
}
}
ColumnLayout { // Password
id: passwordPrompt
Layout.topMargin: 8
visible: root.wifiNetwork?.askingPassword ?? false
MaterialTextField {
id: passwordField
Layout.fillWidth: true
placeholderText: Translation.tr("Password")
// Password
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
onAccepted: {
Network.changePassword(root.wifiNetwork, passwordField.text);
}
}
RowLayout {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: {
root.wifiNetwork.askingPassword = false;
}
}
DialogButton {
buttonText: Translation.tr("Connect")
onClicked: {
Network.changePassword(root.wifiNetwork, passwordField.text);
}
}
}
}
ColumnLayout { // Public wifi login page
id: publicWifiPortal
Layout.topMargin: 8
visible: (root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0) ?? false
RowLayout {
DialogButton {
Layout.fillWidth: true
buttonText: Translation.tr("Open network portal")
colBackground: Appearance.colors.colLayer4
colBackgroundHover: Appearance.colors.colLayer4Hover
colRipple: Appearance.colors.colLayer4Active
onClicked: {
Network.openPublicWifiPortal()
GlobalStates.sidebarRightOpen = false
}
}
}
}
Item {
Layout.fillHeight: true
}
}
}