sidebar: android-style quick toggles & sliders (#2217)

Co-Authored-By: Vague Syntax <173799252+vaguesyntax@users.noreply.github.com>
This commit is contained in:
end-4
2025-10-18 19:07:10 +02:00
parent 77ae119d32
commit 5d1a9b1e9c
35 changed files with 1895 additions and 90 deletions
@@ -0,0 +1,18 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Audio")
statusText: toggled ? Translation.tr("Unmuted") : Translation.tr("Muted")
toggled: !Audio.sink?.audio?.muted
buttonIcon: Audio.sink?.audio?.muted ? "volume_off" : "volume_up"
onClicked: {
Audio.sink.audio.muted = !Audio.sink.audio.muted
}
}
@@ -0,0 +1,27 @@
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
import Quickshell
import Quickshell.Bluetooth
AndroidQuickToggleButton {
id: root
name: Translation.tr("Bluetooth")
statusText: BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("No device")
toggled: BluetoothStatus.enabled
buttonIcon: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
onClicked: {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
}
StyledToolTip {
text: Translation.tr("%1 | Right-click to configure").arg(
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
)
}
}
@@ -0,0 +1,80 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Io
AndroidQuickToggleButton {
id: root
name: Translation.tr("Cloudflare WARP")
toggled: false
buttonIcon: "cloud_lock"
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)")
}
}
@@ -0,0 +1,26 @@
import qs
import qs.modules.common
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Color Picker")
toggled: false
buttonIcon: "colorize"
onClicked: {
GlobalStates.sidebarRightOpen = false;
delayedActionTimer.start()
}
Timer {
id: delayedActionTimer
interval: 300
repeat: false
onTriggered: {
Quickshell.execDetached(["hyprpicker", "-a"])
}
}
}
@@ -0,0 +1,23 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Dark Mode")
statusText: Appearance.m3colors.darkmode ? Translation.tr("Dark") : Translation.tr("Light")
toggled: Appearance.m3colors.darkmode
buttonIcon: Appearance.m3colors.darkmode ? "contrast" : "light_mode"
onClicked: event => {
if (Appearance.m3colors.darkmode) {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]);
} else {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]);
}
}
}
@@ -0,0 +1,32 @@
import qs
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("EasyEffects")
toggled: EasyEffects.active
buttonIcon: "graphic_eq"
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")
}
}
@@ -0,0 +1,34 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Io
AndroidQuickToggleButton {
id: root
name: Translation.tr("Game mode")
toggled: toggled
buttonIcon: "gamepad"
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")
}
}
@@ -0,0 +1,21 @@
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
AndroidQuickToggleButton {
id: root
name: Translation.tr("Idle Inhibitor")
toggled: Idle.inhibit
buttonIcon: "coffee"
onClicked: {
Idle.toggleInhibit()
}
StyledToolTip {
text: Translation.tr("Keep system awake")
}
}
@@ -0,0 +1,18 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Microphone")
statusText: toggled ? Translation.tr("Enabled") : Translation.tr("Muted")
toggled: !Audio.source?.audio?.muted
buttonIcon: Audio.source?.audio?.muted ? "mic_off" : "mic"
onClicked: {
Audio.source.audio.muted = !Audio.source.audio.muted
}
}
@@ -0,0 +1,23 @@
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
AndroidQuickToggleButton {
id: root
name: Translation.tr("Internet")
statusText: Network.networkName
toggled: Network.wifiStatus !== "disabled"
buttonIcon: Network.materialSymbol
onClicked: Network.toggleWifi()
altAction: () => {
root.openMenu()
}
StyledToolTip {
text: Translation.tr("%1 | Right-click to configure").arg(Network.networkName)
}
}
@@ -0,0 +1,33 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
property bool auto: Config.options.light.night.automatic
name: Translation.tr("Night Light")
statusText: (auto ? Translation.tr("Auto, ") : "") + (toggled ? Translation.tr("Active") : Translation.tr("Inactive"))
toggled: Hyprsunset.active
buttonIcon: auto ? "night_sight_auto" : "bedtime"
onClicked: {
Hyprsunset.toggle()
}
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")
}
}
@@ -0,0 +1,18 @@
import qs
import qs.modules.common
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Notifications")
statusText: toggled ? Translation.tr("Show") : Translation.tr("Silent")
toggled: !Notifications.silent
buttonIcon: toggled ? "notifications_active" : "notifications_paused"
onClicked: {
Notifications.silent = !Notifications.silent;
}
}
@@ -0,0 +1,17 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Virtual Keyboard")
toggled: GlobalStates.oskOpen
buttonIcon: toggled ? "keyboard_hide" : "keyboard"
onClicked: {
GlobalStates.oskOpen = !GlobalStates.oskOpen
}
}
@@ -0,0 +1,42 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Services.UPower
AndroidQuickToggleButton {
id: root
name: Translation.tr("Power Profile")
toggled: PowerProfiles.profile !== PowerProfile.Balanced
buttonIcon: switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "energy_savings_leaf"
case PowerProfile.Balanced: return "settings_slow_motion"
case PowerProfile.Performance: return "local_fire_department"
}
statusText: switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "Power Saver"
case PowerProfile.Balanced: return "Balanced"
case PowerProfile.Performance: return "Performance"
}
onClicked: (event) => {
if (PowerProfiles.hasPerformanceProfile) {
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: PowerProfiles.profile = PowerProfile.Balanced
break;
case PowerProfile.Balanced: PowerProfiles.profile = PowerProfile.Performance
break;
case PowerProfile.Performance: PowerProfiles.profile = PowerProfile.PowerSaver
break;
}
} else {
PowerProfiles.profile = PowerProfiles.profile == PowerProfile.Balanced ? PowerProfile.PowerSaver : PowerProfile.Balanced
}
}
StyledToolTip {
text: Translation.tr("Click to cycle through power profiles")
}
}
@@ -0,0 +1,173 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
GroupButton {
id: root
required property int buttonIndex
required property var buttonData
required property bool expandedSize
required property string buttonIcon
required property string name
property string statusText: toggled ? Translation.tr("Active") : Translation.tr("Inactive")
required property real baseCellWidth
required property real baseCellHeight
required property real cellSpacing
required property int cellSize
baseWidth: root.baseCellWidth * cellSize + cellSpacing * (cellSize - 1)
baseHeight: root.baseCellHeight
Behavior on baseWidth {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
property bool editMode: false
signal openMenu()
padding: 6
horizontalPadding: padding
verticalPadding: padding
colBackground: 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)) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2
property color colIcon: expandedSize ? (root.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer3) : colText
contentItem: RowLayout {
id: contentItem
spacing: 4
anchors {
centerIn: root.expandedSize ? undefined : parent
fill: root.expandedSize ? parent : undefined
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.topMargin: root.verticalPadding
Layout.bottomMargin: root.verticalPadding
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)
}
MaterialSymbol {
anchors.centerIn: parent
fill: root.toggled ? 1 : 0
iconSize: Appearance.font.pixelSize.huge
color: root.colIcon
text: root.buttonIcon
}
}
Loader {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
visible: root.expandedSize
active: visible
sourceComponent: Column {
StyledText {
anchors {
left: parent.left
right: parent.right
}
font.pixelSize: Appearance.font.pixelSize.smallie
color: root.colText
elide: Text.ElideRight
text: root.name
}
StyledText {
visible: root.statusText
anchors {
left: parent.left
right: parent.right
}
font {
pixelSize: Appearance.font.pixelSize.smaller
weight: Font.Light
}
color: root.colText
elide: Text.ElideRight
text: root.statusText
}
}
}
}
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 (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;
}
}
}
@@ -0,0 +1,27 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Screen snip")
toggled: false
buttonIcon: "screenshot_region"
onClicked: {
GlobalStates.sidebarRightOpen = false;
delayedActionTimer.start()
}
Timer {
id: delayedActionTimer
interval: 300
repeat: false
onTriggered: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
}
}
}
@@ -0,0 +1,225 @@
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
import "./androidStyle/"
DelegateChooser {
id: root
property bool editMode: false
required property real baseCellWidth
required property real baseCellHeight
required property real spacing
required property int startingIndex
signal openWifiDialog()
signal openBluetoothDialog()
role: "type"
DelegateChoice { roleValue: "network"; AndroidNetworkToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
altAction: () => {
root.openWifiDialog()
}
} }
DelegateChoice { roleValue: "bluetooth"; AndroidBluetoothToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
altAction: () => {
root.openBluetoothDialog()
}
} }
DelegateChoice { roleValue: "idleInhibitor"; AndroidIdleInhibitorToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
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
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "nightLight"; AndroidNightLightToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
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
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "cloudflareWarp"; AndroidCloudflareWarpToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
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
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
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
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
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
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "audio"; AndroidAudioToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
DelegateChoice { roleValue: "notifications"; AndroidNotificationToggle {
required property int index
required property var modelData
buttonIndex: root.startingIndex + index
buttonData: modelData
editMode: root.editMode
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
expandedSize: modelData.size > 1
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
} }
}