forked from Shinonome/dots-hyprland
Rearrange for tidier structure (#2212)
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import "./calendar"
|
||||
import "./todo"
|
||||
import "./pomodoro"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
radius: Appearance.rounding.normal
|
||||
color: Appearance.colors.colLayer1
|
||||
clip: true
|
||||
implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : bottomWidgetGroupRow.implicitHeight
|
||||
property int selectedTab: Persistent.states.sidebar.bottomGroup.tab
|
||||
property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed
|
||||
property var tabs: [
|
||||
{"type": "calendar", "name": Translation.tr("Calendar"), "icon": "calendar_month", "widget": calendarWidget},
|
||||
{"type": "todo", "name": Translation.tr("To Do"), "icon": "done_outline", "widget": todoWidget},
|
||||
{"type": "timer", "name": Translation.tr("Timer"), "icon": "schedule", "widget": pomodoroWidget},
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
height: tabStack.height
|
||||
spacing: 10
|
||||
|
||||
// Navigation rail
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: false
|
||||
Layout.leftMargin: 10
|
||||
Layout.topMargin: 10
|
||||
width: tabBar.width
|
||||
// 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 {
|
||||
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
|
||||
StackLayout {
|
||||
id: tabStack
|
||||
Layout.fillWidth: true
|
||||
// Take the highest one, because the TODO list has no implicit height. This way the heigth of the calendar is used when it's initially loaded with the TODO list
|
||||
height: Math.max(...tabStack.children.map(child => child.tabLoader?.implicitHeight || 0)) // TODO: make this less stupid
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
property int realIndex: root.selectedTab
|
||||
property int animationDuration: Appearance.animation.elementMoveFast.duration * 1.5
|
||||
currentIndex: root.selectedTab
|
||||
|
||||
// Switch the tab on halfway of the anim duration
|
||||
Connections {
|
||||
target: root
|
||||
function onSelectedTabChanged() {
|
||||
delayedStackSwitch.start()
|
||||
tabStack.realIndex = root.selectedTab
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: delayedStackSwitch
|
||||
interval: tabStack.animationDuration / 2
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
tabStack.currentIndex = root.selectedTab
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: tabs
|
||||
Item { // TODO: make behavior on y also act for the item that's switched to
|
||||
id: tabItem
|
||||
property int tabIndex: index
|
||||
property string tabType: modelData.type
|
||||
property int animDistance: 5
|
||||
property var tabLoader: tabLoader
|
||||
// Opacity: show up only when being animated to
|
||||
opacity: (tabStack.currentIndex === tabItem.tabIndex && tabStack.realIndex === tabItem.tabIndex) ? 1 : 0
|
||||
// Y: starts animating when user selects a different tab
|
||||
y: (tabStack.realIndex === tabItem.tabIndex) ? 0 : (tabStack.realIndex < tabItem.tabIndex) ? animDistance : -animDistance
|
||||
Behavior on opacity { NumberAnimation { duration: tabStack.animationDuration / 2; easing.type: Easing.OutCubic } }
|
||||
Behavior on y { NumberAnimation { duration: tabStack.animationDuration; easing.type: Easing.OutExpo } }
|
||||
Loader {
|
||||
id: tabLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: modelData.widget
|
||||
focus: root.selectedTab === tabItem.tabIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar component
|
||||
Component {
|
||||
id: calendarWidget
|
||||
|
||||
CalendarWidget {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
// To Do component
|
||||
Component {
|
||||
id: todoWidget
|
||||
TodoWidget {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
}
|
||||
}
|
||||
|
||||
// Pomodoro component
|
||||
Component {
|
||||
id: pomodoroWidget
|
||||
PomodoroWidget {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import "./notifications"
|
||||
import "./volumeMixer"
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
radius: Appearance.rounding.normal
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
property int selectedTab: 0
|
||||
property var tabButtonList: [
|
||||
{"icon": "notifications", "name": Translation.tr("Notifications")},
|
||||
{"icon": "volume_up", "name": Translation.tr("Audio")}
|
||||
]
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) {
|
||||
if (event.key === Qt.Key_PageDown) {
|
||||
root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1)
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
root.selectedTab = Math.max(root.selectedTab - 1, 0)
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
if (event.modifiers === Qt.ControlModifier) {
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length
|
||||
} else if (event.key === Qt.Key_Backtab) {
|
||||
root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.margins: 5
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
PrimaryTabBar {
|
||||
id: tabBar
|
||||
tabButtonList: root.tabButtonList
|
||||
externalTrackedTab: root.selectedTab
|
||||
|
||||
function onCurrentIndexChanged(currentIndex) {
|
||||
root.selectedTab = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
Layout.topMargin: 5
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 10
|
||||
currentIndex: root.selectedTab
|
||||
onCurrentIndexChanged: {
|
||||
tabBar.enableIndicatorAnimation = true
|
||||
root.selectedTab = currentIndex
|
||||
}
|
||||
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
|
||||
NotificationList {}
|
||||
VolumeMixer {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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: sidebarRoot
|
||||
visible: GlobalStates.sidebarRightOpen
|
||||
|
||||
function hide() {
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
}
|
||||
|
||||
exclusiveZone: 0
|
||||
implicitWidth: sidebarWidth
|
||||
WlrLayershell.namespace: "quickshell:sidebarRight"
|
||||
// Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
|
||||
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [ sidebarRoot ]
|
||||
active: GlobalStates.sidebarRightOpen
|
||||
onCleared: () => {
|
||||
if (!active) sidebarRoot.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) {
|
||||
sidebarRoot.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,210 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import "./quickToggles/"
|
||||
import "./wifiNetworks/"
|
||||
import "./bluetoothDevices/"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||
property int sidebarPadding: 12
|
||||
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
||||
property bool showWifiDialog: false
|
||||
property bool showBluetoothDialog: false
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onSidebarRightOpenChanged() {
|
||||
if (!GlobalStates.sidebarRightOpen) {
|
||||
root.showWifiDialog = false;
|
||||
root.showBluetoothDialog = 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
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: false
|
||||
spacing: 10
|
||||
Layout.margins: 10
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 0
|
||||
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
width: 25
|
||||
height: 25
|
||||
source: SystemInfo.distroIcon
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||
textFormat: Text.MarkdownText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "restart_alt"
|
||||
onClicked: {
|
||||
Hyprland.dispatch("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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 5
|
||||
padding: 5
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
NetworkToggle {
|
||||
altAction: () => {
|
||||
Network.enableWifi();
|
||||
Network.rescanWifi();
|
||||
root.showWifiDialog = true;
|
||||
}
|
||||
}
|
||||
BluetoothToggle {
|
||||
altAction: () => {
|
||||
Bluetooth.defaultAdapter.enabled = true;
|
||||
Bluetooth.defaultAdapter.discovering = true;
|
||||
root.showBluetoothDialog = true;
|
||||
}
|
||||
}
|
||||
NightLight {}
|
||||
GameMode {}
|
||||
IdleInhibitor {}
|
||||
EasyEffectsToggle {}
|
||||
CloudflareWarp {}
|
||||
}
|
||||
|
||||
CenterWidgetGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
BottomWidgetGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onShowWifiDialogChanged: if (showWifiDialog) wifiDialogLoader.active = true;
|
||||
Loader {
|
||||
id: wifiDialogLoader
|
||||
anchors.fill: parent
|
||||
|
||||
active: root.showWifiDialog || item.visible
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
item.show = true;
|
||||
item.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: WifiDialog {
|
||||
onDismiss: {
|
||||
show = false
|
||||
root.showWifiDialog = false
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible && !root.showWifiDialog) wifiDialogLoader.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onShowBluetoothDialogChanged: {
|
||||
if (showBluetoothDialog) bluetoothDialogLoader.active = true;
|
||||
else Bluetooth.defaultAdapter.discovering = false;
|
||||
}
|
||||
Loader {
|
||||
id: bluetoothDialogLoader
|
||||
anchors.fill: parent
|
||||
|
||||
active: root.showBluetoothDialog || item.visible
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
item.show = true;
|
||||
item.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: BluetoothDialog {
|
||||
onDismiss: {
|
||||
show = false
|
||||
root.showBluetoothDialog = false
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
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
|
||||
pointingHandCursor: !expanded
|
||||
|
||||
onClicked: expanded = !expanded
|
||||
altAction: () => expanded = !expanded
|
||||
|
||||
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")
|
||||
}
|
||||
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
|
||||
}
|
||||
ActionButton {
|
||||
buttonText: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect")
|
||||
|
||||
onClicked: {
|
||||
if (root.device?.connected) {
|
||||
root.device.disconnect();
|
||||
} else {
|
||||
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,89 @@
|
||||
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
|
||||
|
||||
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: [...Bluetooth.devices.values].sort((a, b) => {
|
||||
// Connected -> paired -> others
|
||||
let conn = (b.connected - a.connected) || (b.paired - a.paired);
|
||||
if (conn !== 0) return conn;
|
||||
|
||||
// Ones with meaningful names before MAC addresses
|
||||
const macRegex = /^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$/;
|
||||
const aIsMac = macRegex.test(a.name);
|
||||
const bIsMac = macRegex.test(b.name);
|
||||
if (aIsMac !== bIsMac) return aIsMac ? 1 : -1;
|
||||
|
||||
// Alphabetical by name
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
}
|
||||
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,115 @@
|
||||
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,117 @@
|
||||
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
|
||||
Item {
|
||||
anchors.fill: listview
|
||||
|
||||
visible: opacity > 0
|
||||
opacity: (Notifications.list.length === 0) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.menuDecel.duration
|
||||
easing.type: Appearance.animation.menuDecel.type
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
iconSize: 55
|
||||
color: Appearance.m3colors.m3outline
|
||||
text: "notifications_active"
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3outline
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Translation.tr("No notifications")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: statusRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.max(
|
||||
controls.implicitHeight,
|
||||
statusText.implicitHeight
|
||||
)
|
||||
|
||||
StyledText {
|
||||
id: statusText
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Translation.tr("%1 notifications").arg(Notifications.list.length)
|
||||
|
||||
opacity: Notifications.list.length > 0 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: controls
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 5
|
||||
|
||||
NotificationStatusButton {
|
||||
buttonIcon: "notifications_paused"
|
||||
buttonText: Translation.tr("Silent")
|
||||
toggled: Notifications.silent
|
||||
onClicked: () => {
|
||||
Notifications.silent = !Notifications.silent;
|
||||
}
|
||||
}
|
||||
NotificationStatusButton {
|
||||
buttonIcon: "clear_all"
|
||||
buttonText: Translation.tr("Clear")
|
||||
onClicked: () => {
|
||||
Notifications.discardAllNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
GroupButton {
|
||||
id: button
|
||||
property string buttonText: ""
|
||||
property string buttonIcon: ""
|
||||
|
||||
baseWidth: content.implicitWidth + 10 * 2
|
||||
baseHeight: 30
|
||||
|
||||
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 {
|
||||
text: buttonIcon
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: button.colText
|
||||
}
|
||||
StyledText {
|
||||
text: buttonText
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: button.colText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// The Pomodoro timer circle
|
||||
CircularProgress {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
lineWidth: 8
|
||||
value: {
|
||||
return TimerService.pomodoroSecondsLeft / TimerService.pomodoroLapDuration;
|
||||
}
|
||||
implicitSize: 200
|
||||
enableAnimation: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.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.pixelSize: 40
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: TimerService.pomodoroLongBreak ? Translation.tr("Long break") : TimerService.pomodoroBreak ? Translation.tr("Break") : Translation.tr("Focus")
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.full
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
implicitWidth: 36
|
||||
implicitHeight: implicitWidth
|
||||
|
||||
StyledText {
|
||||
id: cycleText
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colOnLayer2
|
||||
text: TimerService.pomodoroCycle + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Start/Stop and Reset buttons
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 10
|
||||
|
||||
RippleButton {
|
||||
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: 35
|
||||
implicitWidth: 90
|
||||
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 {
|
||||
implicitHeight: 35
|
||||
implicitWidth: 90
|
||||
|
||||
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,143 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int currentTab: 0
|
||||
property var tabButtonList: [
|
||||
{"name": Translation.tr("Pomodoro"), "icon": "search_activity"},
|
||||
{"name": Translation.tr("Stopwatch"), "icon": "timer"}
|
||||
]
|
||||
|
||||
// These are keybinds for stopwatch and pomodoro
|
||||
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) {
|
||||
currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
currentTab = Math.max(currentTab - 1, 0)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Space || event.key === Qt.Key_S) { // Pause/resume with Space or S
|
||||
if (currentTab === 0) {
|
||||
TimerService.togglePomodoro()
|
||||
} else {
|
||||
TimerService.toggleStopwatch()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_R) { // Reset with R
|
||||
if (currentTab === 0) {
|
||||
TimerService.resetPomodoro()
|
||||
} else {
|
||||
TimerService.stopwatchReset()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_L) { // Record lap with L
|
||||
TimerService.stopwatchRecordLap()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
currentIndex: currentTab
|
||||
onCurrentIndexChanged: currentTab = currentIndex
|
||||
|
||||
background: Item {
|
||||
WheelHandler {
|
||||
onWheel: (event) => {
|
||||
if (event.angleDelta.y < 0)
|
||||
tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
|
||||
else if (event.angleDelta.y > 0)
|
||||
tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
|
||||
}
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.tabButtonList
|
||||
delegate: SecondaryTabButton {
|
||||
selected: (index == currentTab)
|
||||
buttonText: modelData.name
|
||||
buttonIcon: modelData.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { // Tab indicator
|
||||
id: tabIndicator
|
||||
Layout.fillWidth: true
|
||||
height: 3
|
||||
property bool enableIndicatorAnimation: false
|
||||
Connections {
|
||||
target: root
|
||||
function onCurrentTabChanged() {
|
||||
tabIndicator.enableIndicatorAnimation = true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
property int tabCount: root.tabButtonList.length
|
||||
property real fullTabSize: root.width / tabCount;
|
||||
property real targetWidth: tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth
|
||||
|
||||
implicitWidth: targetWidth
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
|
||||
|
||||
color: Appearance.colors.colPrimary
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
Behavior on x {
|
||||
enabled: tabIndicator.enableIndicatorAnimation
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
enabled: tabIndicator.enableIndicatorAnimation
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Tabbar bottom border
|
||||
id: tabBarBottomBorder
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
Layout.topMargin: 10
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 10
|
||||
clip: true
|
||||
currentIndex: currentTab
|
||||
onCurrentIndexChanged: {
|
||||
tabIndicator.enableIndicatorAnimation = true
|
||||
currentTab = currentIndex
|
||||
}
|
||||
|
||||
// Tabs
|
||||
PomodoroTimer {}
|
||||
Stopwatch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
toggled: EasyEffects.active
|
||||
visible: EasyEffects.available
|
||||
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 "../"
|
||||
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,27 @@
|
||||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import Quickshell.Io
|
||||
|
||||
QuickToggleButton {
|
||||
id: nightLightButton
|
||||
property bool enabled: Hyprsunset.active
|
||||
toggled: enabled
|
||||
buttonIcon: Config.options.light.night.automatic ? "night_sight_auto" : "bedtime"
|
||||
onClicked: {
|
||||
Hyprsunset.toggle()
|
||||
}
|
||||
|
||||
altAction: () => {
|
||||
Config.options.light.night.automatic = !Config.options.light.night.automatic
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Hyprsunset.fetchState()
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Night Light | Right-click to toggle Auto mode")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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: 20
|
||||
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,180 @@
|
||||
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
|
||||
|
||||
StyledFlickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
contentHeight: columnLayout.height
|
||||
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: flickable.width
|
||||
height: flickable.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: taskList
|
||||
}
|
||||
delegate: Item {
|
||||
id: todoItem
|
||||
property bool pendingDoneToggle: false
|
||||
property bool pendingDelete: false
|
||||
property bool enableHeightAnimation: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: todoItemRectangle.implicitHeight + todoListItemSpacing
|
||||
height: implicitHeight
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function startAction() {
|
||||
enableHeightAnimation = true
|
||||
todoItem.implicitHeight = 0
|
||||
actionTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: actionTimer
|
||||
interval: Appearance.animation.elementMoveFast.duration
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (todoItem.pendingDelete) {
|
||||
Todo.deleteItem(modelData.originalIndex)
|
||||
} else if (todoItem.pendingDoneToggle) {
|
||||
if (!modelData.done) Todo.markDone(modelData.originalIndex)
|
||||
else Todo.markUnfinished(modelData.originalIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Layout.fillWidth: true // Needed for wrapping
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
Layout.topMargin: todoListItemPadding
|
||||
id: todoContentText
|
||||
text: modelData.content
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
RowLayout {
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
Layout.bottomMargin: todoListItemPadding
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
TodoItemActionButton {
|
||||
Layout.fillWidth: false
|
||||
onClicked: {
|
||||
todoItem.pendingDoneToggle = true
|
||||
todoItem.startAction()
|
||||
}
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: modelData.done ? "remove_done" : "check"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
TodoItemActionButton {
|
||||
Layout.fillWidth: false
|
||||
onClicked: {
|
||||
todoItem.pendingDelete = true
|
||||
todoItem.startAction()
|
||||
}
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "delete_forever"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Bottom padding
|
||||
Item {
|
||||
implicitHeight: listBottomPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,293 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int currentTab: 0
|
||||
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) {
|
||||
currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
currentTab = Math.max(currentTab - 1, 0)
|
||||
}
|
||||
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
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
currentIndex: currentTab
|
||||
onCurrentIndexChanged: currentTab = currentIndex
|
||||
|
||||
background: Item {
|
||||
WheelHandler {
|
||||
onWheel: (event) => {
|
||||
if (event.angleDelta.y < 0)
|
||||
tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
|
||||
else if (event.angleDelta.y > 0)
|
||||
tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
|
||||
}
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.tabButtonList
|
||||
delegate: SecondaryTabButton {
|
||||
selected: (index == currentTab)
|
||||
buttonText: modelData.name
|
||||
buttonIcon: modelData.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { // Tab indicator
|
||||
id: tabIndicator
|
||||
Layout.fillWidth: true
|
||||
height: 3
|
||||
property bool enableIndicatorAnimation: false
|
||||
Connections {
|
||||
target: root
|
||||
function onCurrentTabChanged() {
|
||||
tabIndicator.enableIndicatorAnimation = true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
property int tabCount: root.tabButtonList.length
|
||||
property real fullTabSize: root.width / tabCount;
|
||||
property real targetWidth: tabBar?.contentItem?.children[0]?.children[tabBar.currentIndex]?.tabContentWidth ?? 0
|
||||
|
||||
implicitWidth: targetWidth
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
|
||||
|
||||
color: Appearance.colors.colPrimary
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
Behavior on x {
|
||||
enabled: tabIndicator.enableIndicatorAnimation
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
enabled: tabIndicator.enableIndicatorAnimation
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Tabbar bottom border
|
||||
id: tabBarBottomBorder
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
Layout.topMargin: 10
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 10
|
||||
clip: true
|
||||
currentIndex: currentTab
|
||||
onCurrentIndexChanged: {
|
||||
tabIndicator.enableIndicatorAnimation = true
|
||||
currentTab = 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
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
text: "add"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
color: Appearance.m3colors.m3onPrimaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
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.colors.colSurfaceContainerHigh
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
function addTask() {
|
||||
if (todoInput.text.length > 0) {
|
||||
Todo.addTask(todoInput.text)
|
||||
todoInput.text = ""
|
||||
root.showAddDialog = false
|
||||
root.currentTab = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -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,275 @@
|
||||
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
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool showDeviceSelector: false
|
||||
property bool deviceSelectorInput
|
||||
property int dialogMargins: 16
|
||||
property PwNode selectedDevice
|
||||
readonly property list<PwNode> appPwNodes: Pipewire.nodes.values.filter((node) => {
|
||||
// return node.type == "21" // Alternative, not as clean
|
||||
return node.isSink && node.isStream
|
||||
})
|
||||
|
||||
function showDeviceSelectorDialog(input: bool) {
|
||||
root.selectedDevice = null
|
||||
root.showDeviceSelector = true
|
||||
root.deviceSelectorInput = input
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
// Close dialog on pressing Esc if open
|
||||
if (event.key === Qt.Key_Escape && root.showDeviceSelector) {
|
||||
root.showDeviceSelector = false
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
StyledListView {
|
||||
id: listView
|
||||
model: root.appPwNodes
|
||||
clip: true
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: 10
|
||||
bottomMargin: 10
|
||||
}
|
||||
spacing: 6
|
||||
|
||||
delegate: VolumeMixerEntry {
|
||||
// Layout.fillWidth: true
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: 10
|
||||
rightMargin: 10
|
||||
}
|
||||
required property var modelData
|
||||
node: modelData
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder when list is empty
|
||||
Item {
|
||||
anchors.fill: listView
|
||||
|
||||
visible: opacity > 0
|
||||
opacity: (root.appPwNodes.length === 0) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.menuDecel.duration
|
||||
easing.type: Appearance.animation.menuDecel.type
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
iconSize: 55
|
||||
color: Appearance.m3colors.m3outline
|
||||
text: "brand_awareness"
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3outline
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Translation.tr("No audio source")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device selector
|
||||
RowLayout {
|
||||
id: deviceSelectorRowLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: false
|
||||
uniformCellSizes: true
|
||||
|
||||
AudioDeviceSelectorButton {
|
||||
Layout.fillWidth: true
|
||||
input: false
|
||||
downAction: () => root.showDeviceSelectorDialog(input)
|
||||
}
|
||||
AudioDeviceSelectorButton {
|
||||
Layout.fillWidth: true
|
||||
input: true
|
||||
downAction: () => root.showDeviceSelectorDialog(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device selector dialog
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
z: 9999
|
||||
|
||||
visible: opacity > 0
|
||||
opacity: root.showDeviceSelector ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Scrim
|
||||
id: scrimOverlay
|
||||
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
|
||||
color: Appearance.colors.colSurfaceContainerHigh
|
||||
radius: Appearance.rounding.normal
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: 30
|
||||
implicitHeight: dialogColumnLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: dialogColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 16
|
||||
|
||||
StyledText {
|
||||
id: dialogTitle
|
||||
Layout.topMargin: dialogMargins
|
||||
Layout.leftMargin: dialogMargins
|
||||
Layout.rightMargin: dialogMargins
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
text: root.deviceSelectorInput ? Translation.tr("Select input device") : Translation.tr("Select output device")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Appearance.m3colors.m3outline
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: dialogMargins
|
||||
Layout.rightMargin: dialogMargins
|
||||
}
|
||||
|
||||
StyledFlickable {
|
||||
id: dialogFlickable
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
implicitHeight: Math.min(scrimOverlay.height - dialogMargins * 8 - dialogTitle.height - dialogButtonsRowLayout.height, devicesColumnLayout.implicitHeight)
|
||||
|
||||
contentHeight: devicesColumnLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: devicesColumnLayout
|
||||
anchors.fill: parent
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Pipewire.nodes.values.filter(node => {
|
||||
return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
|
||||
})
|
||||
}
|
||||
|
||||
// This could and should be refractored, but all data becomes null when passed wtf
|
||||
delegate: StyledRadioButton {
|
||||
id: radioButton
|
||||
required property var modelData
|
||||
Layout.leftMargin: root.dialogMargins
|
||||
Layout.rightMargin: root.dialogMargins
|
||||
Layout.fillWidth: true
|
||||
|
||||
description: modelData.description
|
||||
checked: modelData.id === Pipewire.defaultAudioSink?.id
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShowDeviceSelectorChanged() {
|
||||
if(!root.showDeviceSelector) return;
|
||||
radioButton.checked = (modelData.id === Pipewire.defaultAudioSink?.id)
|
||||
}
|
||||
}
|
||||
|
||||
onCheckedChanged: {
|
||||
if (checked) {
|
||||
root.selectedDevice = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
implicitHeight: dialogMargins
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Appearance.m3colors.m3outline
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: dialogMargins
|
||||
Layout.rightMargin: dialogMargins
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: dialogButtonsRowLayout
|
||||
Layout.bottomMargin: dialogMargins
|
||||
Layout.leftMargin: dialogMargins
|
||||
Layout.rightMargin: dialogMargins
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Cancel")
|
||||
onClicked: {
|
||||
root.showDeviceSelector = false
|
||||
}
|
||||
}
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("OK")
|
||||
onClicked: {
|
||||
root.showDeviceSelector = false
|
||||
if (root.selectedDevice) {
|
||||
if (root.deviceSelectorInput) {
|
||||
Pipewire.preferredDefaultAudioSource = root.selectedDevice
|
||||
} else {
|
||||
Pipewire.preferredDefaultAudioSink = root.selectedDevice
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property PwNode node
|
||||
PwObjectTracker {
|
||||
objects: [node]
|
||||
}
|
||||
|
||||
implicitHeight: rowLayout.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.fill: parent
|
||||
spacing: 6
|
||||
|
||||
Image {
|
||||
property real size: slider.height * 0.9
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
visible: source != ""
|
||||
sourceSize.width: size
|
||||
sourceSize.height: size
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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 = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name);
|
||||
const media = root.node.properties["media.name"];
|
||||
return media != undefined ? `${app} • ${media}` : app;
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: slider
|
||||
value: root.node.audio.volume
|
||||
onMoved: root.node.audio.volume = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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
|
||||
|
||||
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.wifiNetworks].sort((a, b) => {
|
||||
if (a.active && !b.active)
|
||||
return -1;
|
||||
if (!a.active && b.active)
|
||||
return 1;
|
||||
return b.strength - a.strength;
|
||||
})
|
||||
}
|
||||
delegate: WifiNetworkItem {
|
||||
required property WifiAccessPoint modelData
|
||||
wifiNetwork: modelData
|
||||
anchors {
|
||||
left: parent?.left
|
||||
right: parent?.right
|
||||
}
|
||||
}
|
||||
}
|
||||
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,118 @@
|
||||
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
|
||||
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")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user