quickshell: pin shared ii base and phone overlay
This commit is contained in:
parent
cd2150542e
commit
b0a1304ba1
970 changed files with 88310 additions and 1 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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
|
||||
} }
|
||||
}
|
||||
|
|
@ -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}` : "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue