Merge branch 'dev'
This commit is contained in:
commit
d2d993d621
70 changed files with 8102 additions and 2264 deletions
264
Bar/Bar.qml
264
Bar/Bar.qml
|
|
@ -1,26 +1,30 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Bar.Modules
|
import qs.Bar.Modules
|
||||||
import qs.Settings
|
|
||||||
import qs.Services
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Services
|
||||||
|
import qs.Settings
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Widgets.Sidebar
|
|
||||||
import qs.Widgets.Sidebar.Panel
|
|
||||||
import qs.Widgets.Notification
|
import qs.Widgets.Notification
|
||||||
|
import qs.Widgets.SidePanel
|
||||||
|
|
||||||
|
|
||||||
|
// Main bar component - creates panels on selected monitors with widgets and corners
|
||||||
Scope {
|
Scope {
|
||||||
id: rootScope
|
id: rootScope
|
||||||
|
|
||||||
property var shell
|
property var shell
|
||||||
|
property alias visible: barRootItem.visible
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: barRootItem
|
id: barRootItem
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
|
|
@ -31,19 +35,20 @@ Scope {
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: panel
|
id: panel
|
||||||
|
|
||||||
screen: modelData
|
screen: modelData
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
implicitHeight: barBackground.height
|
implicitHeight: barBackground.height
|
||||||
anchors.top: true
|
anchors.top: true
|
||||||
anchors.left: true
|
anchors.left: true
|
||||||
anchors.right: true
|
anchors.right: true
|
||||||
|
visible: Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0)
|
||||||
visible: true
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: barBackground
|
id: barBackground
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 36
|
height: 36 * Theme.scale(Screen)
|
||||||
color: Theme.backgroundPrimary
|
color: Theme.backgroundPrimary
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
@ -51,10 +56,11 @@ Scope {
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: leftWidgetsRow
|
id: leftWidgetsRow
|
||||||
|
|
||||||
anchors.verticalCenter: barBackground.verticalCenter
|
anchors.verticalCenter: barBackground.verticalCenter
|
||||||
anchors.left: barBackground.left
|
anchors.left: barBackground.left
|
||||||
anchors.leftMargin: 18
|
anchors.leftMargin: 18 * Theme.scale(Screen)
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
|
|
||||||
SystemInfo {
|
SystemInfo {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -67,6 +73,7 @@ Scope {
|
||||||
Taskbar {
|
Taskbar {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ActiveWindow {
|
ActiveWindow {
|
||||||
|
|
@ -75,6 +82,7 @@ Scope {
|
||||||
|
|
||||||
Workspace {
|
Workspace {
|
||||||
id: workspace
|
id: workspace
|
||||||
|
|
||||||
screen: modelData
|
screen: modelData
|
||||||
anchors.horizontalCenter: barBackground.horizontalCenter
|
anchors.horizontalCenter: barBackground.horizontalCenter
|
||||||
anchors.verticalCenter: barBackground.verticalCenter
|
anchors.verticalCenter: barBackground.verticalCenter
|
||||||
|
|
@ -82,13 +90,15 @@ Scope {
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: rightWidgetsRow
|
id: rightWidgetsRow
|
||||||
|
|
||||||
anchors.verticalCenter: barBackground.verticalCenter
|
anchors.verticalCenter: barBackground.verticalCenter
|
||||||
anchors.right: barBackground.right
|
anchors.right: barBackground.right
|
||||||
anchors.rightMargin: 18
|
anchors.rightMargin: 18 * Theme.scale(Screen)
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
|
|
||||||
SystemTray {
|
SystemTray {
|
||||||
id: systemTrayModule
|
id: systemTrayModule
|
||||||
|
|
||||||
shell: rootScope.shell
|
shell: rootScope.shell
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
bar: panel
|
bar: panel
|
||||||
|
|
@ -100,22 +110,34 @@ Scope {
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationIcon {
|
NotificationIcon {
|
||||||
|
shell: rootScope.shell
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Wifi {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Bluetooth {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Battery {
|
Battery {
|
||||||
id: widgetsBattery
|
id: widgetsBattery
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Brightness {
|
Brightness {
|
||||||
id: widgetsBrightness
|
id: widgetsBrightness
|
||||||
|
|
||||||
screen: modelData
|
screen: modelData
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Volume {
|
Volume {
|
||||||
id: widgetsVolume
|
id: widgetsVolume
|
||||||
|
|
||||||
shell: rootScope.shell
|
shell: rootScope.shell
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +149,8 @@ Scope {
|
||||||
|
|
||||||
PanelPopup {
|
PanelPopup {
|
||||||
id: sidebarPopup
|
id: sidebarPopup
|
||||||
|
|
||||||
|
shell: rootScope.shell
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
|
@ -135,121 +159,129 @@ Scope {
|
||||||
screen: modelData
|
screen: modelData
|
||||||
sidebarPopup: sidebarPopup
|
sidebarPopup: sidebarPopup
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Background {}
|
|
||||||
Overview {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelWindow {
|
Loader {
|
||||||
id: topLeftPanel
|
active: Settings.settings.showCorners && (Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0))
|
||||||
anchors.top: true
|
|
||||||
anchors.left: true
|
|
||||||
|
|
||||||
color: "transparent"
|
sourceComponent: Item {
|
||||||
screen: modelData
|
PanelWindow {
|
||||||
margins.top: 36
|
id: topLeftPanel
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
||||||
visible: true
|
anchors.top: true
|
||||||
WlrLayershell.layer: WlrLayer.Background
|
anchors.left: true
|
||||||
aboveWindows: false
|
color: "transparent"
|
||||||
WlrLayershell.namespace: "swww-daemon"
|
screen: modelData
|
||||||
implicitHeight: 24
|
margins.top: 36
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.namespace: "swww-daemon"
|
||||||
|
aboveWindows: false
|
||||||
|
implicitHeight: 24
|
||||||
|
|
||||||
|
Corner {
|
||||||
|
id: topLeftCorner
|
||||||
|
|
||||||
|
position: "bottomleft"
|
||||||
|
size: 1.3
|
||||||
|
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
||||||
|
offsetX: -39
|
||||||
|
offsetY: 0
|
||||||
|
anchors.top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: topRightPanel
|
||||||
|
|
||||||
|
anchors.top: true
|
||||||
|
anchors.right: true
|
||||||
|
color: "transparent"
|
||||||
|
screen: modelData
|
||||||
|
margins.top: 36
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.namespace: "swww-daemon"
|
||||||
|
aboveWindows: false
|
||||||
|
implicitHeight: 24
|
||||||
|
|
||||||
|
Corner {
|
||||||
|
id: topRightCorner
|
||||||
|
|
||||||
|
position: "bottomright"
|
||||||
|
size: 1.3
|
||||||
|
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
||||||
|
offsetX: 39
|
||||||
|
offsetY: 0
|
||||||
|
anchors.top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: bottomLeftPanel
|
||||||
|
|
||||||
|
anchors.bottom: true
|
||||||
|
anchors.left: true
|
||||||
|
color: "transparent"
|
||||||
|
screen: modelData
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.namespace: "swww-daemon"
|
||||||
|
aboveWindows: false
|
||||||
|
implicitHeight: 24
|
||||||
|
|
||||||
|
Corner {
|
||||||
|
id: bottomLeftCorner
|
||||||
|
|
||||||
|
position: "topleft"
|
||||||
|
size: 1.3
|
||||||
|
fillColor: Theme.backgroundPrimary
|
||||||
|
offsetX: -39
|
||||||
|
offsetY: 0
|
||||||
|
anchors.top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: bottomRightPanel
|
||||||
|
|
||||||
|
anchors.bottom: true
|
||||||
|
anchors.right: true
|
||||||
|
color: "transparent"
|
||||||
|
screen: modelData
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.namespace: "swww-daemon"
|
||||||
|
aboveWindows: false
|
||||||
|
implicitHeight: 24
|
||||||
|
|
||||||
|
Corner{
|
||||||
|
id: bottomRightCorner
|
||||||
|
|
||||||
|
position: "topright"
|
||||||
|
size: 1.3
|
||||||
|
fillColor: Theme.backgroundPrimary
|
||||||
|
offsetX: 39
|
||||||
|
offsetY: 0
|
||||||
|
anchors.top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: topLeftCorner
|
|
||||||
position: "bottomleft"
|
|
||||||
size: 1.3
|
|
||||||
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
|
||||||
offsetX: -39
|
|
||||||
offsetY: 0
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: topRightPanel
|
|
||||||
anchors.top: true
|
|
||||||
anchors.right: true
|
|
||||||
color: "transparent"
|
|
||||||
screen: modelData
|
|
||||||
margins.top: 36
|
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
||||||
visible: true
|
|
||||||
WlrLayershell.layer: WlrLayer.Background
|
|
||||||
aboveWindows: false
|
|
||||||
WlrLayershell.namespace: "swww-daemon"
|
|
||||||
|
|
||||||
implicitHeight: 24
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: topRightCorner
|
|
||||||
position: "bottomright"
|
|
||||||
size: 1.3
|
|
||||||
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
|
||||||
offsetX: 39
|
|
||||||
offsetY: 0
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: bottomLeftPanel
|
|
||||||
anchors.bottom: true
|
|
||||||
anchors.left: true
|
|
||||||
color: "transparent"
|
|
||||||
screen: modelData
|
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
||||||
visible: true
|
|
||||||
WlrLayershell.layer: WlrLayer.Background
|
|
||||||
aboveWindows: false
|
|
||||||
WlrLayershell.namespace: "swww-daemon"
|
|
||||||
|
|
||||||
implicitHeight: 24
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: bottomLeftCorner
|
|
||||||
position: "topleft"
|
|
||||||
size: 1.3
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
offsetX: -39
|
|
||||||
offsetY: 0
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: bottomRightPanel
|
|
||||||
anchors.bottom: true
|
|
||||||
anchors.right: true
|
|
||||||
color: "transparent"
|
|
||||||
screen: modelData
|
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
||||||
visible: true
|
|
||||||
WlrLayershell.layer: WlrLayer.Background
|
|
||||||
aboveWindows: false
|
|
||||||
WlrLayershell.namespace: "swww-daemon"
|
|
||||||
|
|
||||||
implicitHeight: 24
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: bottomRightCorner
|
|
||||||
position: "topright"
|
|
||||||
size: 1.3
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
offsetX: 39
|
|
||||||
offsetY: 0
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This alias exposes the visual bar's visibility to the outside world
|
|
||||||
property alias visible: barRootItem.visible
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ import qs.Settings
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: activeWindowPanel
|
id: activeWindowPanel
|
||||||
|
|
||||||
|
// Lower case "screen" from modelData
|
||||||
|
property int barHeight: 36 * Theme.scale(screen)
|
||||||
|
|
||||||
screen: (typeof modelData !== 'undefined' ? modelData : null)
|
screen: (typeof modelData !== 'undefined' ? modelData : null)
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
anchors.top: true
|
anchors.top: true
|
||||||
|
|
@ -14,26 +18,25 @@ PanelWindow {
|
||||||
anchors.right: true
|
anchors.right: true
|
||||||
focusable: false
|
focusable: false
|
||||||
margins.top: barHeight
|
margins.top: barHeight
|
||||||
visible: !activeWindowWrapper.finallyHidden
|
visible: Settings.settings.showActiveWindow && !activeWindowWrapper.finallyHidden
|
||||||
implicitHeight: activeWindowTitleContainer.height
|
implicitHeight: activeWindowTitleContainer.height
|
||||||
implicitWidth: 0
|
implicitWidth: 0
|
||||||
property int barHeight: 36
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
function getIcon() {
|
function getIcon() {
|
||||||
var icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId.toLowerCase(), true);
|
var icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId.toLowerCase(), true);
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId, true);
|
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.appId, true);
|
||||||
}
|
|
||||||
if (!icon) {
|
|
||||||
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title, true);
|
|
||||||
}
|
|
||||||
if (!icon) {
|
|
||||||
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title.toLowerCase(), "application-x-executable");
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon;
|
|
||||||
}
|
}
|
||||||
|
if (!icon) {
|
||||||
|
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title, true);
|
||||||
|
}
|
||||||
|
if (!icon) {
|
||||||
|
icon = Quickshell.iconPath(ToplevelManager.activeToplevel.title.toLowerCase(), "application-x-executable");
|
||||||
|
}
|
||||||
|
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: activeWindowWrapper
|
id: activeWindowWrapper
|
||||||
|
|
@ -44,7 +47,7 @@ PanelWindow {
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: visibilityTimer
|
id: visibilityTimer
|
||||||
interval: 1200
|
interval: 1500
|
||||||
running: false
|
running: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
activeWindowWrapper.shouldShow = false;
|
activeWindowWrapper.shouldShow = false;
|
||||||
|
|
@ -101,64 +104,62 @@ PanelWindow {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: activeWindowTitleContainer
|
id: activeWindowTitleContainer
|
||||||
color: Theme.backgroundPrimary
|
color: "transparent"
|
||||||
bottomLeftRadius: Math.max(0, width / 2)
|
|
||||||
bottomRightRadius: Math.max(0, width / 2)
|
|
||||||
|
|
||||||
width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + (Settings.settings.showActiveWindowIcon ? 28 : 22))
|
|
||||||
|
width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + (Settings.settings.showActiveWindowIcon ? 28 : 22)) + 16
|
||||||
height: activeWindowTitle.implicitHeight + 12
|
height: activeWindowTitle.implicitHeight + 12
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
IconImage {
|
Rectangle {
|
||||||
id: icon
|
id: innerRect
|
||||||
width: 12
|
|
||||||
height: 12
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 6
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
source: ToplevelManager?.activeToplevel ? getIcon() : ""
|
|
||||||
visible: Settings.settings.showActiveWindowIcon
|
|
||||||
anchors.verticalCenterOffset: -3
|
|
||||||
|
|
||||||
|
bottomLeftRadius: Math.max(0, width / 2)
|
||||||
|
bottomRightRadius: Math.max(0, width / 2)
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
leftMargin: 0
|
||||||
|
rightMargin: 0
|
||||||
|
topMargin: -1
|
||||||
|
bottomMargin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
border.color: Theme.outline || "#444";
|
||||||
|
border.width: 1;
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: icon
|
||||||
|
width: 12
|
||||||
|
height: 12
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 14
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
source: ToplevelManager?.activeToplevel ? getIcon() : ""
|
||||||
|
visible: Settings.settings.showActiveWindowIcon
|
||||||
|
anchors.verticalCenterOffset: -3
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: activeWindowTitle
|
||||||
|
text: ToplevelManager?.activeToplevel?.title && ToplevelManager?.activeToplevel?.title.length > 60 ? ToplevelManager?.activeToplevel?.title.substring(0, 60) + "..." : ToplevelManager?.activeToplevel?.title || ""
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
anchors.left: icon.right
|
||||||
|
anchors.leftMargin: Settings.settings.showActiveWindowIcon ? 4 : 6
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 14
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: -3
|
||||||
|
horizontalAlignment: Settings.settings.showActiveWindowIcon ? Text.AlignRight : Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
|
||||||
id: activeWindowTitle
|
|
||||||
text: ToplevelManager?.activeToplevel?.title && ToplevelManager?.activeToplevel?.title.length > 60 ? ToplevelManager?.activeToplevel?.title.substring(0, 60) + "..." : ToplevelManager?.activeToplevel?.title || ""
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: Theme.textSecondary
|
|
||||||
anchors.left: icon.right
|
|
||||||
anchors.leftMargin: Settings.settings.showActiveWindowIcon ? 4 : 6
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 6
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.verticalCenterOffset: -3
|
|
||||||
horizontalAlignment: Settings.settings.showActiveWindowIcon ? Text.AlignRight : Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: activeCornerRight
|
|
||||||
position: "bottomleft"
|
|
||||||
size: 1.1
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
offsetX: activeWindowTitleContainer.x + activeWindowTitleContainer.width - 34
|
|
||||||
offsetY: -1
|
|
||||||
anchors.top: activeWindowTitleContainer.top
|
|
||||||
}
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: activeCornerLeft
|
|
||||||
position: "bottomright"
|
|
||||||
size: 1.1
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
anchors.top: activeWindowTitleContainer.top
|
|
||||||
x: activeWindowTitleContainer.x + 34 - width
|
|
||||||
offsetY: -1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,151 @@ import qs.Settings
|
||||||
import "../../Helpers/Fuzzysort.js" as Fuzzysort
|
import "../../Helpers/Fuzzysort.js" as Fuzzysort
|
||||||
|
|
||||||
PanelWithOverlay {
|
PanelWithOverlay {
|
||||||
|
Timer {
|
||||||
|
id: clipboardTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: appLauncherPanel.visible && searchField.text.startsWith(">clip")
|
||||||
|
onTriggered: {
|
||||||
|
updateClipboardHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var clipboardHistory: []
|
||||||
|
property bool clipboardInitialized: false
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: clipboardTypeProcess
|
||||||
|
property bool isLoading: false
|
||||||
|
property var currentTypes: []
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
currentTypes = String(stdout.text).trim().split('\n').filter(t => t);
|
||||||
|
|
||||||
|
const imageType = currentTypes.find(t => t.startsWith('image/'));
|
||||||
|
if (imageType) {
|
||||||
|
clipboardImageProcess.mimeType = imageType;
|
||||||
|
clipboardImageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`];
|
||||||
|
clipboardImageProcess.running = true;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
clipboardHistoryProcess.command = ["wl-paste", "-n", "--type", "text/plain"];
|
||||||
|
clipboardHistoryProcess.running = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
clipboardTypeProcess.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: clipboardImageProcess
|
||||||
|
property string mimeType: ""
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
const base64 = stdout.text.trim();
|
||||||
|
if (base64) {
|
||||||
|
const entry = {
|
||||||
|
type: 'image',
|
||||||
|
mimeType: mimeType,
|
||||||
|
data: `data:${mimeType};base64,${base64}`,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const exists = clipboardHistory.find(item =>
|
||||||
|
item.type === 'image' && item.data === entry.data
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
clipboardHistory = [entry, ...clipboardHistory].slice(0, 20);
|
||||||
|
root.updateFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clipboardHistoryProcess.isLoading) {
|
||||||
|
clipboardInitialized = true;
|
||||||
|
}
|
||||||
|
clipboardTypeProcess.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: clipboardHistoryProcess
|
||||||
|
property bool isLoading: false
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
const content = String(stdout.text).trim();
|
||||||
|
if (content && !content.startsWith("vscode-file://")) {
|
||||||
|
const entry = {
|
||||||
|
type: 'text',
|
||||||
|
content: content,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const exists = clipboardHistory.find(item => {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
return item.content === content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item === content;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
|
||||||
|
const newHistory = clipboardHistory.map(item => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
content: item,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
clipboardHistory = [entry, ...newHistory].slice(0, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
clipboardHistoryProcess.isLoading = false;
|
||||||
|
}
|
||||||
|
clipboardInitialized = true;
|
||||||
|
clipboardTypeProcess.isLoading = false;
|
||||||
|
root.updateFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function updateClipboardHistory() {
|
||||||
|
if (!clipboardTypeProcess.isLoading && !clipboardHistoryProcess.isLoading) {
|
||||||
|
clipboardTypeProcess.isLoading = true;
|
||||||
|
clipboardTypeProcess.command = ["wl-paste", "-l"];
|
||||||
|
clipboardTypeProcess.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
id: appLauncherPanel
|
id: appLauncherPanel
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
function isPinned(app) {
|
function isPinned(app) {
|
||||||
return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1;
|
return app && app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePin(app) {
|
function togglePin(app) {
|
||||||
if (!app || !app.execString) return;
|
if (!app || !app.execString) return;
|
||||||
var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : [];
|
var arr = Settings.settings.pinnedExecs ? Settings.settings.pinnedExecs.slice() : [];
|
||||||
|
|
@ -71,6 +210,11 @@ PanelWithOverlay {
|
||||||
root.selectedIndex = 0;
|
root.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
width: 460
|
width: 460
|
||||||
|
|
@ -103,9 +247,11 @@ PanelWithOverlay {
|
||||||
appLauncherPanel.visible = false;
|
appLauncherPanel.visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMathExpression(str) {
|
function isMathExpression(str) {
|
||||||
return /^[-+*/().0-9\s]+$/.test(str);
|
return /^[-+*/().0-9\s]+$/.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeEval(expr) {
|
function safeEval(expr) {
|
||||||
try {
|
try {
|
||||||
return Function('return (' + expr + ')')();
|
return Function('return (' + expr + ')')();
|
||||||
|
|
@ -113,13 +259,114 @@ PanelWithOverlay {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFilter() {
|
function updateFilter() {
|
||||||
var query = searchField.text ? searchField.text.toLowerCase() : "";
|
var query = searchField.text ? searchField.text.toLowerCase() : "";
|
||||||
var apps = root.appModel.slice();
|
var apps = root.appModel.slice();
|
||||||
var results = [];
|
var results = [];
|
||||||
// Calculator mode: starts with '='
|
|
||||||
if (query.startsWith("=")) {
|
|
||||||
var expr = searchField.text.slice(1).trim();
|
if (query === ">") {
|
||||||
|
results.push({
|
||||||
|
isCommand: true,
|
||||||
|
name: ">calc",
|
||||||
|
content: "Calculator - evaluate mathematical expressions",
|
||||||
|
icon: "calculate",
|
||||||
|
execute: function() {
|
||||||
|
searchField.text = ">calc ";
|
||||||
|
searchField.cursorPosition = searchField.text.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
isCommand: true,
|
||||||
|
name: ">clip",
|
||||||
|
content: "Clipboard history - browse and restore clipboard items",
|
||||||
|
icon: "content_paste",
|
||||||
|
execute: function() {
|
||||||
|
searchField.text = ">clip ";
|
||||||
|
searchField.cursorPosition = searchField.text.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
root.filteredApps = results;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (query.startsWith(">clip")) {
|
||||||
|
if (!clipboardInitialized) {
|
||||||
|
updateClipboardHistory();
|
||||||
|
}
|
||||||
|
const searchTerm = query.slice(5).trim();
|
||||||
|
|
||||||
|
clipboardHistory.forEach(function(clip, index) {
|
||||||
|
let searchContent = clip.type === 'image' ?
|
||||||
|
clip.mimeType :
|
||||||
|
clip.content || clip; // Support both new object format and old string format
|
||||||
|
|
||||||
|
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
|
||||||
|
let entry;
|
||||||
|
if (clip.type === 'image') {
|
||||||
|
entry = {
|
||||||
|
isClipboard: true,
|
||||||
|
name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
|
||||||
|
content: "Image: " + clip.mimeType,
|
||||||
|
icon: "image",
|
||||||
|
type: 'image',
|
||||||
|
data: clip.data,
|
||||||
|
execute: function() {
|
||||||
|
// Convert base64 image data back to binary and copy to clipboard
|
||||||
|
const base64Data = clip.data.split(',')[1];
|
||||||
|
clipboardTypeProcess.command = ["sh", "-c", `echo '${base64Data}' | base64 -d | wl-copy -t '${clip.mimeType}'`];
|
||||||
|
clipboardTypeProcess.running = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const textContent = clip.content || clip; // Support both new object format and old string format
|
||||||
|
let displayContent = textContent;
|
||||||
|
let previewContent = "";
|
||||||
|
|
||||||
|
// Clean up whitespace for display
|
||||||
|
displayContent = displayContent.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
// Truncate long content and show preview
|
||||||
|
if (displayContent.length > 50) {
|
||||||
|
previewContent = displayContent;
|
||||||
|
// Show first line or first 50 characters as title
|
||||||
|
displayContent = displayContent.split('\n')[0].substring(0, 50) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
isClipboard: true,
|
||||||
|
name: displayContent,
|
||||||
|
content: previewContent || textContent,
|
||||||
|
icon: "content_paste",
|
||||||
|
execute: function() {
|
||||||
|
Quickshell.execDetached(["sh", "-c", "echo -n '" + textContent.replace(/'/g, "'\\''") + "' | wl-copy"]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
results.push(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
results.push({
|
||||||
|
isClipboard: true,
|
||||||
|
name: "No clipboard history",
|
||||||
|
content: "No matching clipboard entries found",
|
||||||
|
icon: "content_paste_off"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
root.filteredApps = results;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (query.startsWith(">calc")) {
|
||||||
|
var expr = searchField.text.slice(5).trim();
|
||||||
if (expr && isMathExpression(expr)) {
|
if (expr && isMathExpression(expr)) {
|
||||||
var value = safeEval(expr);
|
var value = safeEval(expr);
|
||||||
if (value !== undefined && value !== null && value !== "") {
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
|
@ -132,8 +379,27 @@ PanelWithOverlay {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var pinned = [];
|
||||||
|
var unpinned = [];
|
||||||
|
for (var i = 0; i < results.length; ++i) {
|
||||||
|
var app = results[i];
|
||||||
|
if (app.execString && Settings.settings.pinnedExecs.indexOf(app.execString) !== -1) {
|
||||||
|
pinned.push(app);
|
||||||
|
} else {
|
||||||
|
unpinned.push(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort pinned apps alphabetically for consistent display
|
||||||
|
pinned.sort(function(a, b) {
|
||||||
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
|
});
|
||||||
|
root.filteredApps = pinned.concat(unpinned);
|
||||||
|
root.selectedIndex = 0;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!query || query.startsWith("=")) {
|
if (!query) {
|
||||||
results = results.concat(apps.sort(function (a, b) {
|
results = results.concat(apps.sort(function (a, b) {
|
||||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
}));
|
}));
|
||||||
|
|
@ -145,7 +411,7 @@ PanelWithOverlay {
|
||||||
return r.obj;
|
return r.obj;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// Pinning logic: split into pinned and unpinned
|
|
||||||
var pinned = [];
|
var pinned = [];
|
||||||
var unpinned = [];
|
var unpinned = [];
|
||||||
for (var i = 0; i < results.length; ++i) {
|
for (var i = 0; i < results.length; ++i) {
|
||||||
|
|
@ -163,10 +429,12 @@ PanelWithOverlay {
|
||||||
root.filteredApps = pinned.concat(unpinned);
|
root.filteredApps = pinned.concat(unpinned);
|
||||||
root.selectedIndex = 0;
|
root.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNext() {
|
function selectNext() {
|
||||||
if (filteredApps.length > 0)
|
if (filteredApps.length > 0)
|
||||||
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
|
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPrev() {
|
function selectPrev() {
|
||||||
if (filteredApps.length > 0)
|
if (filteredApps.length > 0)
|
||||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||||
|
|
@ -184,6 +452,10 @@ PanelWithOverlay {
|
||||||
Quickshell.clipboardText = String(modelData.result);
|
Quickshell.clipboardText = String(modelData.result);
|
||||||
Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]);
|
Quickshell.execDetached(["notify-send", "Calculator Result", `${modelData.expr} = ${modelData.result} (copied to clipboard)`]);
|
||||||
});
|
});
|
||||||
|
} else if (modelData.isCommand) {
|
||||||
|
|
||||||
|
modelData.execute();
|
||||||
|
return;
|
||||||
} else if (modelData.runInTerminal && termEmu){
|
} else if (modelData.runInTerminal && termEmu){
|
||||||
Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]);
|
Quickshell.execDetached([termEmu, "-e", modelData.execString.trim()]);
|
||||||
} else if (modelData.execute) {
|
} else if (modelData.execute) {
|
||||||
|
|
@ -202,281 +474,356 @@ PanelWithOverlay {
|
||||||
|
|
||||||
Component.onCompleted: updateFilter()
|
Component.onCompleted: updateFilter()
|
||||||
|
|
||||||
ColumnLayout {
|
RowLayout {
|
||||||
anchors.left: parent.left
|
anchors.fill: parent
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: 32
|
anchors.margins: 32
|
||||||
spacing: 18
|
spacing: 18
|
||||||
|
|
||||||
// Search Bar
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: searchBar
|
id: previewPanel
|
||||||
color: Theme.surfaceVariant
|
Layout.preferredWidth: 200
|
||||||
|
Layout.fillHeight: true
|
||||||
|
color: Theme.surface
|
||||||
radius: 20
|
radius: 20
|
||||||
height: 48
|
visible: false
|
||||||
Layout.fillWidth: true
|
|
||||||
border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
|
|
||||||
border.width: searchField.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
RowLayout {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.fill: parent
|
||||||
anchors.right: parent.right
|
anchors.margins: 16
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
color: "transparent"
|
||||||
anchors.leftMargin: 14
|
clip: true
|
||||||
anchors.rightMargin: 14
|
|
||||||
spacing: 10
|
|
||||||
Text {
|
|
||||||
text: "search"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: Theme.fontSizeHeader
|
|
||||||
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
TextField {
|
|
||||||
id: searchField
|
|
||||||
placeholderText: "Search apps..."
|
|
||||||
color: Theme.textPrimary
|
|
||||||
placeholderTextColor: Theme.textSecondary
|
|
||||||
background: null
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeBody
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
onTextChanged: root.updateFilter()
|
|
||||||
selectedTextColor: Theme.onAccent
|
|
||||||
selectionColor: Theme.accentPrimary
|
|
||||||
padding: 0
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
leftPadding: 0
|
|
||||||
rightPadding: 0
|
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
|
||||||
font.bold: true
|
|
||||||
Component.onCompleted: contentItem.cursorColor = Theme.textPrimary
|
|
||||||
onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary
|
|
||||||
|
|
||||||
Keys.onDownPressed: root.selectNext()
|
Image {
|
||||||
Keys.onUpPressed: root.selectPrev()
|
id: previewImage
|
||||||
Keys.onEnterPressed: root.activateSelected()
|
anchors.fill: parent
|
||||||
Keys.onReturnPressed: root.activateSelected()
|
fillMode: Image.PreserveAspectFit
|
||||||
Keys.onEscapePressed: appLauncherPanel.hidePanel()
|
asynchronous: true
|
||||||
}
|
cache: true
|
||||||
}
|
smooth: true
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: 120
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// App List Card
|
|
||||||
Rectangle {
|
ColumnLayout {
|
||||||
color: Theme.surface
|
|
||||||
radius: 20
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
clip: true
|
spacing: 18
|
||||||
property int innerPadding: 16
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.top: parent.top
|
Rectangle {
|
||||||
anchors.left: parent.left
|
id: searchBar
|
||||||
anchors.right: parent.right
|
color: Theme.surfaceVariant
|
||||||
height: parent.innerPadding
|
radius: 20
|
||||||
visible: false
|
height: 48
|
||||||
|
Layout.fillWidth: true
|
||||||
|
border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: searchField.activeFocus ? 2 : 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: 14
|
||||||
|
anchors.rightMargin: 14
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "search"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: Theme.fontSizeHeader * Theme.scale(Screen)
|
||||||
|
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: searchField
|
||||||
|
placeholderText: "Search apps..."
|
||||||
|
color: Theme.textPrimary
|
||||||
|
placeholderTextColor: Theme.textSecondary
|
||||||
|
background: null
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
onTextChanged: root.updateFilter()
|
||||||
|
selectedTextColor: Theme.onAccent
|
||||||
|
selectionColor: Theme.accentPrimary
|
||||||
|
padding: 0
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
topPadding: 0
|
||||||
|
bottomPadding: 0
|
||||||
|
font.bold: true
|
||||||
|
Component.onCompleted: contentItem.cursorColor = Theme.textPrimary
|
||||||
|
onActiveFocusChanged: contentItem.cursorColor = Theme.textPrimary
|
||||||
|
|
||||||
|
Keys.onDownPressed: root.selectNext()
|
||||||
|
Keys.onUpPressed: root.selectPrev()
|
||||||
|
Keys.onEnterPressed: root.activateSelected()
|
||||||
|
Keys.onReturnPressed: root.activateSelected()
|
||||||
|
Keys.onEscapePressed: appLauncherPanel.hidePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: appList
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: parent.innerPadding
|
|
||||||
spacing: 2
|
|
||||||
model: root.filteredApps
|
|
||||||
currentIndex: root.selectedIndex
|
|
||||||
delegate: Item {
|
|
||||||
id: appDelegate
|
|
||||||
width: appList.width
|
|
||||||
height: 48
|
|
||||||
property bool hovered: mouseArea.containsMouse
|
|
||||||
property bool isSelected: index === root.selectedIndex
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
color: Theme.surface
|
||||||
color: (hovered || isSelected)
|
radius: 20
|
||||||
? Theme.accentPrimary
|
Layout.fillWidth: true
|
||||||
: (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent")
|
Layout.fillHeight: true
|
||||||
radius: 12
|
clip: true
|
||||||
border.color: appLauncherPanel.isPinned(modelData)
|
property int innerPadding: 16
|
||||||
? "transparent"
|
|
||||||
: (hovered || isSelected ? Theme.accentPrimary : "transparent")
|
ListView {
|
||||||
border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0)
|
id: appList
|
||||||
Behavior on color {
|
anchors.fill: parent
|
||||||
ColorAnimation {
|
anchors.margins: parent.innerPadding
|
||||||
duration: 120
|
spacing: 2
|
||||||
}
|
model: root.filteredApps
|
||||||
}
|
currentIndex: root.selectedIndex
|
||||||
Behavior on border.color {
|
delegate: Item {
|
||||||
ColorAnimation {
|
id: appDelegate
|
||||||
duration: 120
|
width: appList.width
|
||||||
}
|
height: (modelData.isClipboard || modelData.isCommand) ? 64 : 48
|
||||||
}
|
property bool hovered: mouseArea.containsMouse
|
||||||
Behavior on border.width {
|
property bool isSelected: index === root.selectedIndex
|
||||||
NumberAnimation {
|
|
||||||
duration: 120
|
Rectangle {
|
||||||
}
|
anchors.fill: parent
|
||||||
}
|
color: (hovered || isSelected)
|
||||||
}
|
? Theme.accentPrimary
|
||||||
|
: (appLauncherPanel.isPinned(modelData) ? Theme.surfaceVariant : "transparent")
|
||||||
|
radius: 12
|
||||||
|
border.color: appLauncherPanel.isPinned(modelData)
|
||||||
|
? "transparent"
|
||||||
|
: (hovered || isSelected ? Theme.accentPrimary : "transparent")
|
||||||
|
border.width: appLauncherPanel.isPinned(modelData) ? 0 : (hovered || isSelected ? 2 : 0)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
property bool iconLoaded: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: clipboardImage
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: modelData.type === 'image'
|
||||||
|
source: modelData.data || ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
propagateComposedEvents: true
|
||||||
|
onContainsMouseChanged: {
|
||||||
|
if (containsMouse && modelData.type === 'image') {
|
||||||
|
previewImage.source = modelData.data;
|
||||||
|
previewPanel.visible = true;
|
||||||
|
} else {
|
||||||
|
previewPanel.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseXChanged: mouse.accepted = false
|
||||||
|
onMouseYChanged: mouse.accepted = false
|
||||||
|
onClicked: mouse.accepted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
anchors.fill: parent
|
||||||
|
asynchronous: true
|
||||||
|
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" :
|
||||||
|
modelData.isClipboard ? "qrc:/icons/" + modelData.icon + ".svg" :
|
||||||
|
modelData.isCommand ? "qrc:/icons/" + modelData.icon + ".svg" :
|
||||||
|
Quickshell.iconPath(modelData.icon, "application-x-executable")
|
||||||
|
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !modelData.isCalculator && !modelData.isClipboard && !modelData.isCommand && !parent.iconLoaded && modelData.type !== 'image'
|
||||||
|
text: "broken_image"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: Theme.fontSizeHeader * Theme.scale(Screen)
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name
|
||||||
|
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary)
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
|
font.bold: hovered || isSelected
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) :
|
||||||
|
modelData.isClipboard ? modelData.content :
|
||||||
|
modelData.isCommand ? modelData.content :
|
||||||
|
(modelData.comment || modelData.genericName || "No description available")
|
||||||
|
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary)
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
|
||||||
|
font.italic: !(modelData.comment || modelData.genericName)
|
||||||
|
opacity: modelData.isClipboard ? 0.8 : modelData.isCommand ? 0.9 : ((modelData.comment || modelData.genericName) ? 1.0 : 0.6)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: (modelData.isClipboard || modelData.isCommand) ? 2 : 1
|
||||||
|
wrapMode: (modelData.isClipboard || modelData.isCommand) ? Text.WordWrap : Text.NoWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: (modelData.isClipboard || modelData.isCommand) ? implicitHeight : contentHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.isCalculator ? "content_copy" : "chevron_right"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
|
||||||
|
color: (hovered || isSelected)
|
||||||
|
? Theme.onAccent
|
||||||
|
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary)
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
Layout.rightMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Item { width: 8; height: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: ripple
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.onAccent
|
||||||
|
opacity: 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
if (pinArea.containsMouse) return;
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
appLauncherPanel.togglePin(modelData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ripple.opacity = 0.18;
|
||||||
|
rippleNumberAnimation.start();
|
||||||
|
root.selectedIndex = index;
|
||||||
|
root.activateSelected();
|
||||||
|
}
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: ripple.opacity = 0.18
|
||||||
|
onReleased: ripple.opacity = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: rippleNumberAnimation
|
||||||
|
target: ripple
|
||||||
|
property: "opacity"
|
||||||
|
to: 0.0
|
||||||
|
duration: 320
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: index === appList.count - 1 ? 0 : 0.10
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
anchors.rightMargin: 10
|
|
||||||
spacing: 10
|
|
||||||
Item {
|
Item {
|
||||||
width: 28
|
id: pinArea
|
||||||
height: 28
|
width: 28; height: 28
|
||||||
property bool iconLoaded: !modelData.isCalculator && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
|
z: 100
|
||||||
IconImage {
|
anchors.right: parent.right
|
||||||
id: iconImg
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
preventStealing: true
|
||||||
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : Quickshell.iconPath(modelData.icon, "application-x-executable")
|
z: 100
|
||||||
visible: modelData.isCalculator || parent.iconLoaded
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
propagateComposedEvents: false
|
||||||
|
onClicked: {
|
||||||
|
appLauncherPanel.togglePin(modelData);
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: !modelData.isCalculator && !parent.iconLoaded
|
text: "star"
|
||||||
text: "broken_image"
|
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeHeader
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
color: Theme.accentPrimary
|
color: (parent.MouseArea.containsMouse || hovered || isSelected)
|
||||||
}
|
? Theme.onAccent
|
||||||
}
|
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled)
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 1
|
|
||||||
Text {
|
|
||||||
text: modelData.name
|
|
||||||
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textPrimary)
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.bold: hovered || isSelected
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
Text {
|
|
||||||
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : (modelData.comment || modelData.genericName || "No description available")
|
|
||||||
color: (hovered || isSelected) ? Theme.onAccent : (appLauncherPanel.isPinned(modelData) ? Theme.textSecondary : Theme.textSecondary)
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
|
||||||
font.italic: !(modelData.comment || modelData.genericName)
|
|
||||||
opacity: (modelData.comment || modelData.genericName) ? 1.0 : 0.6
|
|
||||||
elide: Text.ElideRight
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: modelData.isCalculator ? "content_copy" : "chevron_right"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: Theme.fontSizeBody
|
|
||||||
color: (hovered || isSelected)
|
|
||||||
? Theme.onAccent
|
|
||||||
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textSecondary)
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
Layout.rightMargin: 8 // Add margin to separate from star
|
|
||||||
}
|
|
||||||
// Add a spacing item between chevron and star
|
|
||||||
Item { width: 8; height: 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: ripple
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Theme.onAccent
|
|
||||||
opacity: 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onClicked: {
|
|
||||||
// Prevent app launch if click is inside pinArea
|
|
||||||
if (pinArea.containsMouse) return;
|
|
||||||
if (mouse.button === Qt.RightButton) {
|
|
||||||
appLauncherPanel.togglePin(modelData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ripple.opacity = 0.18;
|
|
||||||
rippleNumberAnimation.start();
|
|
||||||
root.selectedIndex = index;
|
|
||||||
root.activateSelected();
|
|
||||||
}
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: ripple.opacity = 0.18
|
|
||||||
onReleased: ripple.opacity = 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: rippleNumberAnimation
|
|
||||||
target: ripple
|
|
||||||
property: "opacity"
|
|
||||||
to: 0.0
|
|
||||||
duration: 320
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: index === appList.count - 1 ? 0 : 0.10
|
|
||||||
}
|
|
||||||
// Pin/Unpin button (move to last child for stacking)
|
|
||||||
Item {
|
|
||||||
id: pinArea
|
|
||||||
width: 28; height: 28
|
|
||||||
z: 100 // Ensure above everything else
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
preventStealing: true
|
|
||||||
z: 100
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
propagateComposedEvents: false
|
|
||||||
onClicked: {
|
|
||||||
appLauncherPanel.togglePin(modelData);
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "star"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: (parent.MouseArea.containsMouse || hovered || isSelected)
|
|
||||||
? Theme.onAccent
|
|
||||||
: (appLauncherPanel.isPinned(modelData) ? Theme.textPrimary : Theme.textDisabled)
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -484,25 +831,5 @@ PanelWithOverlay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: launcherCornerRight
|
|
||||||
position: "bottomleft"
|
|
||||||
size: 1.1
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
anchors.top: root.top
|
|
||||||
offsetX: 416
|
|
||||||
offsetY: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: launcherCornerLeft
|
|
||||||
position: "bottomright"
|
|
||||||
size: 1.1
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
anchors.top: root.top
|
|
||||||
offsetX: -416
|
|
||||||
offsetY: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,13 +7,67 @@ import qs.Settings
|
||||||
|
|
||||||
PanelWithOverlay {
|
PanelWithOverlay {
|
||||||
id: ioSelector
|
id: ioSelector
|
||||||
signal panelClosed()
|
|
||||||
property int tabIndex: 0
|
property int tabIndex: 0
|
||||||
property Item anchorItem: null
|
property Item anchorItem: null
|
||||||
|
|
||||||
|
signal panelClosed()
|
||||||
|
|
||||||
|
function sinkNodes() {
|
||||||
|
let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {
|
||||||
|
return n.isSink && n.audio && n.isStream === false;
|
||||||
|
}) : [];
|
||||||
|
if (Pipewire.defaultAudioSink)
|
||||||
|
nodes = nodes.slice().sort(function(a, b) {
|
||||||
|
if (a.id === Pipewire.defaultAudioSink.id)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (b.id === Pipewire.defaultAudioSink.id)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceNodes() {
|
||||||
|
let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {
|
||||||
|
return !n.isSink && n.audio && n.isStream === false;
|
||||||
|
}) : [];
|
||||||
|
if (Pipewire.defaultAudioSource)
|
||||||
|
nodes = nodes.slice().sort(function(a, b) {
|
||||||
|
if (a.id === Pipewire.defaultAudioSource.id)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (b.id === Pipewire.defaultAudioSource.id)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (Pipewire.nodes && Pipewire.nodes.values) {
|
||||||
|
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
|
||||||
|
var n = Pipewire.nodes.values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible)
|
||||||
|
panelClosed();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Bind all Pipewire nodes so their properties are valid
|
// Bind all Pipewire nodes so their properties are valid
|
||||||
PwObjectTracker {
|
PwObjectTracker {
|
||||||
id: nodeTracker
|
id: nodeTracker
|
||||||
|
|
||||||
objects: Pipewire.nodes
|
objects: Pipewire.nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,6 +81,11 @@ PanelWithOverlay {
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
anchors.rightMargin: 4
|
anchors.rightMargin: 4
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 16
|
anchors.margins: 16
|
||||||
|
|
@ -40,239 +99,257 @@ PanelWithOverlay {
|
||||||
|
|
||||||
Tabs {
|
Tabs {
|
||||||
id: ioTabs
|
id: ioTabs
|
||||||
tabsModel: [
|
|
||||||
{ label: "Output", icon: "volume_up" },
|
tabsModel: [{
|
||||||
{ label: "Input", icon: "mic" }
|
"label": "Output",
|
||||||
]
|
"icon": "volume_up"
|
||||||
|
}, {
|
||||||
|
"label": "Input",
|
||||||
|
"icon": "mic"
|
||||||
|
}]
|
||||||
currentIndex: tabIndex
|
currentIndex: tabIndex
|
||||||
onTabChanged: {
|
onTabChanged: {
|
||||||
tabIndex = currentIndex;
|
tabIndex = currentIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add vertical space between tabs and entries
|
// Add vertical space between tabs and entries
|
||||||
Item { height: 36; Layout.fillWidth: true }
|
Item {
|
||||||
|
height: 36
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
// Output Devices
|
// Output Devices
|
||||||
Flickable {
|
Flickable {
|
||||||
id: sinkList
|
id: sinkList
|
||||||
|
|
||||||
visible: tabIndex === 0
|
visible: tabIndex === 0
|
||||||
contentHeight: sinkColumn.height
|
contentHeight: sinkColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
interactive: contentHeight > height
|
interactive: contentHeight > height
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 220
|
height: 220
|
||||||
ScrollBar.vertical: ScrollBar {}
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: sinkColumn
|
id: sinkColumn
|
||||||
|
|
||||||
width: sinkList.width
|
width: sinkList.width
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ioSelector.sinkNodes()
|
model: ioSelector.sinkNodes()
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 36
|
height: 36
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: 6
|
radius: 6
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 6
|
anchors.margins: 6
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "volume_up"
|
text: "volume_up"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 1
|
spacing: 1
|
||||||
Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button
|
Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: modelData.nickname || modelData.description || modelData.name
|
text: modelData.nickname || modelData.description || modelData.name
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: modelData.description !== modelData.nickname ? modelData.description : ""
|
text: modelData.description !== modelData.nickname ? modelData.description : ""
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: Pipewire.preferredDefaultAudioSink !== modelData
|
visible: Pipewire.preferredDefaultAudioSink !== modelData
|
||||||
width: 60; height: 20
|
width: 60
|
||||||
|
height: 20
|
||||||
radius: 4
|
radius: 4
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "Set"
|
text: "Set"
|
||||||
color: Theme.onAccent
|
color: Theme.onAccent
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: Pipewire.preferredDefaultAudioSink = modelData
|
onClicked: Pipewire.preferredDefaultAudioSink = modelData
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "(Current)"
|
text: "(Current)"
|
||||||
visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id
|
visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Devices
|
// Input Devices
|
||||||
Flickable {
|
Flickable {
|
||||||
id: sourceList
|
id: sourceList
|
||||||
|
|
||||||
visible: tabIndex === 1
|
visible: tabIndex === 1
|
||||||
contentHeight: sourceColumn.height
|
contentHeight: sourceColumn.height
|
||||||
clip: true
|
clip: true
|
||||||
interactive: contentHeight > height
|
interactive: contentHeight > height
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 220
|
height: 220
|
||||||
ScrollBar.vertical: ScrollBar {}
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: sourceColumn
|
id: sourceColumn
|
||||||
|
|
||||||
width: sourceList.width
|
width: sourceList.width
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ioSelector.sourceNodes()
|
model: ioSelector.sourceNodes()
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 36
|
height: 36
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: 6
|
radius: 6
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 6
|
anchors.margins: 6
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "mic"
|
text: "mic"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 1
|
spacing: 1
|
||||||
Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button
|
Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: modelData.nickname || modelData.description || modelData.name
|
text: modelData.nickname || modelData.description || modelData.name
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: modelData.description !== modelData.nickname ? modelData.description : ""
|
text: modelData.description !== modelData.nickname ? modelData.description : ""
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: Pipewire.preferredDefaultAudioSource !== modelData
|
visible: Pipewire.preferredDefaultAudioSource !== modelData
|
||||||
width: 60; height: 20
|
width: 60
|
||||||
|
height: 20
|
||||||
radius: 4
|
radius: 4
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "Set"
|
text: "Set"
|
||||||
color: Theme.onAccent
|
color: Theme.onAccent
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: Pipewire.preferredDefaultAudioSource = modelData
|
onClicked: Pipewire.preferredDefaultAudioSource = modelData
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "(Current)"
|
text: "(Current)"
|
||||||
visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id
|
visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sinkNodes() {
|
ScrollBar.vertical: ScrollBar {
|
||||||
let nodes = Pipewire.nodes && Pipewire.nodes.values
|
}
|
||||||
? Pipewire.nodes.values.filter(function(n) {
|
|
||||||
return n.isSink && n.audio && n.isStream === false;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
if (Pipewire.defaultAudioSink) {
|
|
||||||
nodes = nodes.slice().sort(function(a, b) {
|
|
||||||
if (a.id === Pipewire.defaultAudioSink.id) return -1;
|
|
||||||
if (b.id === Pipewire.defaultAudioSink.id) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
function sourceNodes() {
|
|
||||||
let nodes = Pipewire.nodes && Pipewire.nodes.values
|
|
||||||
? Pipewire.nodes.values.filter(function(n) {
|
|
||||||
return !n.isSink && n.audio && n.isStream === false;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
if (Pipewire.defaultAudioSource) {
|
|
||||||
nodes = nodes.slice().sort(function(a, b) {
|
|
||||||
if (a.id === Pipewire.defaultAudioSource.id) return -1;
|
|
||||||
if (b.id === Pipewire.defaultAudioSource.id) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (Pipewire.nodes && Pipewire.nodes.values) {
|
|
||||||
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
|
|
||||||
var n = Pipewire.nodes.values[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Pipewire
|
|
||||||
function onReadyChanged() {
|
function onReadyChanged() {
|
||||||
if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) {
|
if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) {
|
||||||
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
|
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
|
||||||
|
|
@ -280,15 +357,14 @@ PanelWithOverlay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDefaultAudioSinkChanged() {
|
function onDefaultAudioSinkChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDefaultAudioSourceChanged() {
|
function onDefaultAudioSourceChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: Pipewire
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible) panelClosed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
282
Bar/Modules/Bluetooth.qml
Normal file
282
Bar/Modules/Bluetooth.qml
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Components
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
width: Settings.settings.bluetoothEnabled ? 22 : 0
|
||||||
|
height: Settings.settings.bluetoothEnabled ? 22 : 0
|
||||||
|
|
||||||
|
property bool menuVisible: false
|
||||||
|
|
||||||
|
// Bluetooth icon/button
|
||||||
|
Item {
|
||||||
|
id: bluetoothIcon
|
||||||
|
width: 22; height: 22
|
||||||
|
visible: Settings.settings.bluetoothEnabled
|
||||||
|
|
||||||
|
// Check if any devices are currently connected
|
||||||
|
property bool hasConnectedDevices: {
|
||||||
|
if (!Bluetooth.defaultAdapter) return false;
|
||||||
|
|
||||||
|
for (let i = 0; i < Bluetooth.defaultAdapter.devices.count; i++) {
|
||||||
|
if (Bluetooth.defaultAdapter.devices.valueAt(i).connected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: bluetoothText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: {
|
||||||
|
if (!Bluetooth.defaultAdapter || !Bluetooth.defaultAdapter.enabled) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
} else if (parent.hasConnectedDevices) {
|
||||||
|
return "bluetooth_connected"
|
||||||
|
} else {
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.family: mouseAreaBluetooth.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: mouseAreaBluetooth.containsMouse ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseAreaBluetooth
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!bluetoothMenuLoader.active) {
|
||||||
|
bluetoothMenuLoader.loading = true;
|
||||||
|
}
|
||||||
|
if (bluetoothMenuLoader.item) {
|
||||||
|
bluetoothMenuLoader.item.visible = !bluetoothMenuLoader.item.visible;
|
||||||
|
// Enable adapter and start discovery when menu opens
|
||||||
|
if (bluetoothMenuLoader.item.visible && Bluetooth.defaultAdapter) {
|
||||||
|
if (!Bluetooth.defaultAdapter.enabled) {
|
||||||
|
Bluetooth.defaultAdapter.enabled = true;
|
||||||
|
}
|
||||||
|
if (!Bluetooth.defaultAdapter.discovering) {
|
||||||
|
Bluetooth.defaultAdapter.discovering = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onEntered: bluetoothTooltip.tooltipVisible = true
|
||||||
|
onExited: bluetoothTooltip.tooltipVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledTooltip {
|
||||||
|
id: bluetoothTooltip
|
||||||
|
text: "Bluetooth Devices"
|
||||||
|
positionAbove: false
|
||||||
|
tooltipVisible: false
|
||||||
|
targetItem: bluetoothIcon
|
||||||
|
delay: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
// LazyLoader for Bluetooth menu
|
||||||
|
LazyLoader {
|
||||||
|
id: bluetoothMenuLoader
|
||||||
|
loading: false
|
||||||
|
component: PanelWindow {
|
||||||
|
id: bluetoothMenu
|
||||||
|
implicitWidth: 320
|
||||||
|
implicitHeight: 480
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
anchors.top: true
|
||||||
|
anchors.right: true
|
||||||
|
margins.right: 0
|
||||||
|
margins.top: 0
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
// Stop discovery when menu closes to save battery
|
||||||
|
if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) {
|
||||||
|
Bluetooth.defaultAdapter.discovering = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
radius: 12
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "bluetooth"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Bluetooth Devices"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "close"
|
||||||
|
onClicked: {
|
||||||
|
bluetoothMenu.visible = false;
|
||||||
|
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) {
|
||||||
|
Bluetooth.defaultAdapter.discovering = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.12
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: deviceList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : []
|
||||||
|
spacing: 8
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 8
|
||||||
|
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (deviceMouseArea.containsMouse ? Theme.highlight : "transparent")
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.connected ? "bluetooth" : "bluetooth_disabled"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
let deviceName = modelData.name || modelData.deviceName || "Unknown Device";
|
||||||
|
// Hide MAC addresses and show "Unknown Device" instead
|
||||||
|
let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||||
|
if (macPattern.test(deviceName)) {
|
||||||
|
return "Unknown Device";
|
||||||
|
}
|
||||||
|
return deviceName;
|
||||||
|
}
|
||||||
|
color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary)
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
let deviceName = modelData.name || modelData.deviceName || "";
|
||||||
|
let macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||||
|
if (macPattern.test(deviceName)) {
|
||||||
|
// Show MAC address in subtitle for unnamed devices
|
||||||
|
return modelData.address + " • " + (modelData.paired ? "Paired" : "Available");
|
||||||
|
} else {
|
||||||
|
// Show only status for named devices
|
||||||
|
return modelData.paired ? "Paired" : "Available";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color: deviceMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
|
||||||
|
font.pixelSize: 11 * Theme.scale(Screen)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: 22
|
||||||
|
Layout.preferredHeight: 22
|
||||||
|
visible: modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting
|
||||||
|
|
||||||
|
Spinner {
|
||||||
|
visible: parent.visible
|
||||||
|
running: parent.visible
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: 22
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
// Handle device actions: disconnect, pair, or connect
|
||||||
|
if (modelData.connected) {
|
||||||
|
modelData.disconnect();
|
||||||
|
} else if (!modelData.paired) {
|
||||||
|
modelData.pair();
|
||||||
|
} else {
|
||||||
|
modelData.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovering indicator
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
visible: Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Scanning for devices..."
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
Spinner {
|
||||||
|
running: true
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
import "../../Helpers/Holidays.js" as Holidays
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
import Quickshell.Wayland
|
|
||||||
import "../../Helpers/Holidays.js" as Holidays
|
|
||||||
|
|
||||||
PanelWithOverlay {
|
PanelWithOverlay {
|
||||||
id: calendarOverlay
|
id: calendarOverlay
|
||||||
|
|
@ -22,6 +22,11 @@ PanelWithOverlay {
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
anchors.rightMargin: 4
|
anchors.rightMargin: 4
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 16
|
anchors.margins: 16
|
||||||
|
|
@ -47,7 +52,7 @@ PanelWithOverlay {
|
||||||
text: calendar.title
|
text: calendar.title
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
@ -60,33 +65,30 @@ PanelWithOverlay {
|
||||||
calendar.month = newDate.getMonth();
|
calendar.month = newDate.getMonth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DayOfWeekRow {
|
DayOfWeekRow {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 0
|
spacing: 0
|
||||||
Layout.leftMargin: 8 // Align with grid
|
Layout.leftMargin: 8 // Align with grid
|
||||||
Layout.rightMargin: 8
|
Layout.rightMargin: 8
|
||||||
|
|
||||||
delegate: Text {
|
delegate: Text {
|
||||||
text: shortName
|
text: shortName
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.bold: true
|
font.bold: true
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
width: 32
|
width: 32
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MonthGrid {
|
MonthGrid {
|
||||||
id: calendar
|
id: calendar
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 8
|
|
||||||
Layout.rightMargin: 8
|
|
||||||
spacing: 0
|
|
||||||
month: Time.date.getMonth()
|
|
||||||
year: Time.date.getFullYear()
|
|
||||||
|
|
||||||
property var holidays: []
|
property var holidays: []
|
||||||
|
|
||||||
|
|
@ -96,12 +98,19 @@ PanelWithOverlay {
|
||||||
calendar.holidays = holidays;
|
calendar.holidays = holidays;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 8
|
||||||
|
Layout.rightMargin: 8
|
||||||
|
spacing: 0
|
||||||
|
month: Time.date.getMonth()
|
||||||
|
year: Time.date.getFullYear()
|
||||||
onMonthChanged: updateHolidays()
|
onMonthChanged: updateHolidays()
|
||||||
onYearChanged: updateHolidays()
|
onYearChanged: updateHolidays()
|
||||||
Component.onCompleted: updateHolidays()
|
Component.onCompleted: updateHolidays()
|
||||||
|
|
||||||
// Optionally, update when the panel becomes visible
|
// Optionally, update when the panel becomes visible
|
||||||
Connections {
|
Connections {
|
||||||
target: calendarOverlay
|
|
||||||
function onVisibleChanged() {
|
function onVisibleChanged() {
|
||||||
if (calendarOverlay.visible) {
|
if (calendarOverlay.visible) {
|
||||||
calendar.month = Time.date.getMonth();
|
calendar.month = Time.date.getMonth();
|
||||||
|
|
@ -109,29 +118,35 @@ PanelWithOverlay {
|
||||||
calendar.updateHolidays();
|
calendar.updateHolidays();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: calendarOverlay
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 8
|
|
||||||
property var holidayInfo: calendar.holidays.filter(function(h) {
|
property var holidayInfo: calendar.holidays.filter(function(h) {
|
||||||
var d = new Date(h.date);
|
var d = new Date(h.date);
|
||||||
return d.getDate() === model.day && d.getMonth() === model.month && d.getFullYear() === model.year;
|
return d.getDate() === model.day && d.getMonth() === model.month && d.getFullYear() === model.year;
|
||||||
})
|
})
|
||||||
property bool isHoliday: holidayInfo.length > 0
|
property bool isHoliday: holidayInfo.length > 0
|
||||||
|
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 8
|
||||||
color: {
|
color: {
|
||||||
if (model.today)
|
if (model.today)
|
||||||
return Theme.accentPrimary;
|
return Theme.accentPrimary;
|
||||||
|
|
||||||
if (mouseArea2.containsMouse)
|
if (mouseArea2.containsMouse)
|
||||||
return Theme.backgroundTertiary;
|
return Theme.backgroundTertiary;
|
||||||
|
|
||||||
return "transparent";
|
return "transparent";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holiday dot indicator
|
// Holiday dot indicator
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: isHoliday
|
visible: isHoliday
|
||||||
width: 4; height: 4
|
width: 4
|
||||||
|
height: 4
|
||||||
radius: 4
|
radius: 4
|
||||||
color: Theme.accentTertiary
|
color: Theme.accentTertiary
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
@ -145,14 +160,15 @@ PanelWithOverlay {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: model.day
|
text: model.day
|
||||||
color: model.today ? Theme.onAccent : Theme.textPrimary
|
color: model.today ? Theme.onAccent : Theme.textPrimary
|
||||||
opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1.0 : 0.7) : 0.3
|
opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.bold: model.today ? true : false
|
font.bold: model.today ? true : false
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea2
|
id: mouseArea2
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: {
|
onEntered: {
|
||||||
|
|
@ -167,21 +183,28 @@ PanelWithOverlay {
|
||||||
onExited: holidayTooltip.tooltipVisible = false
|
onExited: holidayTooltip.tooltipVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: 150
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledTooltip {
|
StyledTooltip {
|
||||||
id: holidayTooltip
|
id: holidayTooltip
|
||||||
|
|
||||||
text: ""
|
text: ""
|
||||||
tooltipVisible: false
|
tooltipVisible: false
|
||||||
targetItem: null
|
targetItem: null
|
||||||
delay: 100
|
delay: 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 150
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ Rectangle {
|
||||||
text: Time.time
|
text: Time.time
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ PopupWindow {
|
||||||
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
|
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
|
||||||
text: modelData?.text ?? "";
|
text: modelData?.text ?? "";
|
||||||
font.family: Theme.fontFamily;
|
font.family: Theme.fontFamily;
|
||||||
font.pixelSize: Theme.fontSizeSmall;
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen);
|
||||||
verticalAlignment: Text.AlignVCenter;
|
verticalAlignment: Text.AlignVCenter;
|
||||||
elide: Text.ElideRight;
|
elide: Text.ElideRight;
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +145,7 @@ PopupWindow {
|
||||||
// Material Symbols Outlined chevron right for submenu
|
// Material Symbols Outlined chevron right for submenu
|
||||||
text: modelData?.hasChildren ? "menu" : "";
|
text: modelData?.hasChildren ? "menu" : "";
|
||||||
font.family: "Material Symbols Outlined";
|
font.family: "Material Symbols Outlined";
|
||||||
font.pixelSize: 18;
|
font.pixelSize: 18 * Theme.scale(Screen);
|
||||||
verticalAlignment: Text.AlignVCenter;
|
verticalAlignment: Text.AlignVCenter;
|
||||||
visible: modelData?.hasChildren ?? false;
|
visible: modelData?.hasChildren ?? false;
|
||||||
color: Theme.textPrimary;
|
color: Theme.textPrimary;
|
||||||
|
|
@ -362,7 +362,7 @@ PopupWindow {
|
||||||
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
|
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
|
||||||
text: modelData?.text ?? "";
|
text: modelData?.text ?? "";
|
||||||
font.family: Theme.fontFamily;
|
font.family: Theme.fontFamily;
|
||||||
font.pixelSize: Theme.fontSizeSmall;
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen);
|
||||||
verticalAlignment: Text.AlignVCenter;
|
verticalAlignment: Text.AlignVCenter;
|
||||||
elide: Text.ElideRight;
|
elide: Text.ElideRight;
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +378,7 @@ PopupWindow {
|
||||||
Text {
|
Text {
|
||||||
text: modelData?.hasChildren ? "\uE5CC" : "";
|
text: modelData?.hasChildren ? "\uE5CC" : "";
|
||||||
font.family: "Material Symbols Outlined";
|
font.family: "Material Symbols Outlined";
|
||||||
font.pixelSize: 18;
|
font.pixelSize: 18 * Theme.scale(Screen);
|
||||||
verticalAlignment: Text.AlignVCenter;
|
verticalAlignment: Text.AlignVCenter;
|
||||||
visible: modelData?.hasChildren ?? false;
|
visible: modelData?.hasChildren ?? false;
|
||||||
color: Theme.textPrimary;
|
color: Theme.textPrimary;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import qs.Components
|
||||||
Item {
|
Item {
|
||||||
id: mediaControl
|
id: mediaControl
|
||||||
width: visible ? mediaRow.width : 0
|
width: visible ? mediaRow.width : 0
|
||||||
height: 36
|
height: 36 * Theme.scale(Screen)
|
||||||
visible: Settings.settings.showMediaInBar && MusicManager.currentPlayer
|
visible: Settings.settings.showMediaInBar && MusicManager.currentPlayer
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
|
@ -20,8 +20,8 @@ Item {
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: albumArtContainer
|
id: albumArtContainer
|
||||||
width: 24
|
width: 24 * Theme.scale(Screen)
|
||||||
height: 24
|
height: 24 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
// Circular spectrum visualizer
|
// Circular spectrum visualizer
|
||||||
|
|
@ -29,8 +29,8 @@ Item {
|
||||||
id: spectrum
|
id: spectrum
|
||||||
values: MusicManager.cavaValues
|
values: MusicManager.cavaValues
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
innerRadius: 10
|
innerRadius: 10 * Theme.scale(Screen)
|
||||||
outerRadius: 18
|
outerRadius: 18 * Theme.scale(Screen)
|
||||||
fillColor: Theme.accentPrimary
|
fillColor: Theme.accentPrimary
|
||||||
strokeColor: Theme.accentPrimary
|
strokeColor: Theme.accentPrimary
|
||||||
strokeWidth: 0
|
strokeWidth: 0
|
||||||
|
|
@ -40,10 +40,10 @@ Item {
|
||||||
// Album art image
|
// Album art image
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: albumArtwork
|
id: albumArtwork
|
||||||
width: 20
|
width: 20 * Theme.scale(Screen)
|
||||||
height: 20
|
height: 20 * Theme.scale(Screen)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: 12 // circle
|
radius: 12 * Theme.scale(Screen) // circle
|
||||||
color: Qt.darker(Theme.surface, 1.1)
|
color: Qt.darker(Theme.surface, 1.1)
|
||||||
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
@ -79,7 +79,7 @@ Item {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "music_note"
|
text: "music_note"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
|
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
|
||||||
visible: !albumArt.visible
|
visible: !albumArt.visible
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ Item {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: MusicManager.isPlaying ? "pause" : "play_arrow"
|
text: MusicManager.isPlaying ? "pause" : "play_arrow"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ Item {
|
||||||
text: MusicManager.trackTitle + " - " + MusicManager.trackArtist
|
text: MusicManager.trackTitle + " - " + MusicManager.trackArtist
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.maximumWidth: 300
|
Layout.maximumWidth: 300
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
|
||||||
71
Bar/Modules/SettingsButton.qml
Normal file
71
Bar/Modules/SettingsButton.qml
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Components
|
||||||
|
import qs.Widgets.SettingsWindow
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
width: 22
|
||||||
|
height: 22
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: button
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
radius: width / 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "settings"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: mouseArea.containsMouse ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (!settingsWindowLoader.active) {
|
||||||
|
// Start loading the settings window
|
||||||
|
settingsWindowLoader.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsWindowLoader.item) {
|
||||||
|
// Toggle visibility
|
||||||
|
if (settingsWindowLoader.item.visible) {
|
||||||
|
settingsWindowLoader.item.visible = false;
|
||||||
|
} else {
|
||||||
|
settingsWindowLoader.item.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledTooltip {
|
||||||
|
text: "Settings"
|
||||||
|
targetItem: mouseArea
|
||||||
|
tooltipVisible: mouseArea.containsMouse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LazyLoader for SettingsWindow
|
||||||
|
LazyLoader {
|
||||||
|
id: settingsWindowLoader
|
||||||
|
loading: false
|
||||||
|
component: SettingsWindow {
|
||||||
|
// Handle window closure - just hide it, don't destroy
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
// Window is hidden, but keep it loaded for reuse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,6 @@ Row {
|
||||||
spacing: 10
|
spacing: 10
|
||||||
visible: Settings.settings.showSystemInfoInBar
|
visible: Settings.settings.showSystemInfoInBar
|
||||||
|
|
||||||
// The width calculation below is required to ensure our row width is an integer.
|
|
||||||
// If omitted the next component to the right might get blurry (Taskbar icons).
|
|
||||||
width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10))
|
width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10))
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|
@ -19,7 +17,7 @@ Row {
|
||||||
Text {
|
Text {
|
||||||
id: cpuUsageIcon
|
id: cpuUsageIcon
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeBody
|
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
|
||||||
text: "speed"
|
text: "speed"
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -29,7 +27,7 @@ Row {
|
||||||
Text {
|
Text {
|
||||||
id: cpuUsageText
|
id: cpuUsageText
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
text: Sysinfo.cpuUsageStr
|
text: Sysinfo.cpuUsageStr
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -43,7 +41,7 @@ Row {
|
||||||
spacing: 3
|
spacing: 3
|
||||||
Text {
|
Text {
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeBody
|
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
|
||||||
text: "thermometer"
|
text: "thermometer"
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -52,7 +50,7 @@ Row {
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
text: Sysinfo.cpuTempStr
|
text: Sysinfo.cpuTempStr
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -66,7 +64,7 @@ Row {
|
||||||
spacing: 3
|
spacing: 3
|
||||||
Text {
|
Text {
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeBody
|
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
|
||||||
text: "memory"
|
text: "memory"
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
@ -75,7 +73,7 @@ Row {
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
text: Sysinfo.memoryUsageStr
|
text: Sysinfo.memoryUsageStr
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
|
||||||
|
|
@ -21,43 +21,27 @@ Row {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: systemTray.items
|
model: systemTray.items
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: 24
|
width: 24 * Theme.scale(Screen)
|
||||||
height: 24
|
height: 24 * Theme.scale(Screen)
|
||||||
// Hide Spotify icon, or adjust to your liking
|
|
||||||
visible: modelData && modelData.id !== "spotify"
|
visible: modelData
|
||||||
property bool isHovered: trayMouseArea.containsMouse
|
property bool isHovered: trayMouseArea.containsMouse
|
||||||
|
|
||||||
// Hover scale animation
|
// No animations - static display
|
||||||
scale: isHovered ? 1.15 : 1.0
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 150
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtle rotation on hover
|
|
||||||
rotation: isHovered ? 5 : 0
|
|
||||||
Behavior on rotation {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 16
|
width: 16 * Theme.scale(Screen)
|
||||||
height: 16
|
height: 16 * Theme.scale(Screen)
|
||||||
radius: 6
|
radius: 6 * Theme.scale(Screen)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: trayIcon
|
id: trayIcon
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 16
|
width: 16 * Theme.scale(Screen)
|
||||||
height: 16
|
height: 16 * Theme.scale(Screen)
|
||||||
smooth: false
|
smooth: false
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
backer.fillMode: Image.PreserveAspectFit
|
backer.fillMode: Image.PreserveAspectFit
|
||||||
|
|
@ -74,13 +58,6 @@ Row {
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 300
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.onCompleted: {}
|
Component.onCompleted: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +99,7 @@ Row {
|
||||||
if (modelData.hasMenu && modelData.menu && trayMenu) {
|
if (modelData.hasMenu && modelData.menu && trayMenu) {
|
||||||
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
||||||
const menuX = (width / 2) - (trayMenu.width / 2);
|
const menuX = (width / 2) - (trayMenu.width / 2);
|
||||||
const menuY = height + 20;
|
const menuY = height + 20 * Theme.scale(Screen);
|
||||||
trayMenu.menu = modelData.menu;
|
trayMenu.menu = modelData.menu;
|
||||||
trayMenu.showAt(parent, menuX, menuY);
|
trayMenu.showAt(parent, menuX, menuY);
|
||||||
} else
|
} else
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ Item {
|
||||||
visible: !appIcon.visible
|
visible: !appIcon.visible
|
||||||
text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?"
|
text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?"
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375)
|
font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375 * Theme.scale(Screen))
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: appButton.isActive ? Theme.onAccent : Theme.textPrimary
|
color: appButton.isActive ? Theme.onAccent : Theme.textPrimary
|
||||||
}
|
}
|
||||||
|
|
|
||||||
380
Bar/Modules/Wifi.qml
Normal file
380
Bar/Modules/Wifi.qml
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
width: Settings.settings.wifiEnabled ? 22 : 0
|
||||||
|
height: Settings.settings.wifiEnabled ? 22 : 0
|
||||||
|
|
||||||
|
property bool menuVisible: false
|
||||||
|
property string passwordPromptSsid: ""
|
||||||
|
property string passwordInput: ""
|
||||||
|
property bool showPasswordPrompt: false
|
||||||
|
|
||||||
|
Network {
|
||||||
|
id: network
|
||||||
|
}
|
||||||
|
|
||||||
|
// WiFi icon/button
|
||||||
|
Item {
|
||||||
|
id: wifiIcon
|
||||||
|
width: 22; height: 22
|
||||||
|
visible: Settings.settings.wifiEnabled
|
||||||
|
|
||||||
|
property int currentSignal: {
|
||||||
|
let maxSignal = 0;
|
||||||
|
for (const net in network.networks) {
|
||||||
|
if (network.networks[net].connected && network.networks[net].signal > maxSignal) {
|
||||||
|
maxSignal = network.networks[net].signal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: wifiText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: {
|
||||||
|
let connected = false;
|
||||||
|
for (const net in network.networks) {
|
||||||
|
if (network.networks[net].connected) {
|
||||||
|
connected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connected ? network.signalIcon(parent.currentSignal) : "wifi_off"
|
||||||
|
}
|
||||||
|
font.family: mouseAreaWifi.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: mouseAreaWifi.containsMouse ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseAreaWifi
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!wifiMenuLoader.active) {
|
||||||
|
wifiMenuLoader.loading = true;
|
||||||
|
}
|
||||||
|
if (wifiMenuLoader.item) {
|
||||||
|
wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible;
|
||||||
|
if (wifiMenuLoader.item.visible) {
|
||||||
|
network.onMenuOpened();
|
||||||
|
} else {
|
||||||
|
network.onMenuClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onEntered: wifiTooltip.tooltipVisible = true
|
||||||
|
onExited: wifiTooltip.tooltipVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledTooltip {
|
||||||
|
id: wifiTooltip
|
||||||
|
text: "WiFi Networks"
|
||||||
|
positionAbove: false
|
||||||
|
tooltipVisible: false
|
||||||
|
targetItem: wifiIcon
|
||||||
|
delay: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
// LazyLoader for WiFi menu
|
||||||
|
LazyLoader {
|
||||||
|
id: wifiMenuLoader
|
||||||
|
loading: false
|
||||||
|
component: PanelWindow {
|
||||||
|
id: wifiMenu
|
||||||
|
implicitWidth: 320
|
||||||
|
implicitHeight: 480
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
anchors.top: true
|
||||||
|
anchors.right: true
|
||||||
|
margins.right: 0
|
||||||
|
margins.top: 0
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
radius: 12
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "wifi"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "WiFi Networks"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "refresh"
|
||||||
|
onClicked: network.refreshNetworks()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "close"
|
||||||
|
onClicked: {
|
||||||
|
wifiMenu.visible = false;
|
||||||
|
network.onMenuClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.12
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: networkList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: Object.values(network.networks)
|
||||||
|
spacing: 8
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
width: parent.width
|
||||||
|
height: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 48
|
||||||
|
radius: 8
|
||||||
|
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.44) : (networkMouseArea.containsMouse ? Theme.highlight : "transparent")
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: network.signalIcon(modelData.signal)
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.ssid || "Unknown Network"
|
||||||
|
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary)
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
|
||||||
|
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
|
||||||
|
font.pixelSize: 11 * Theme.scale(Screen)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" && network.connectError.length > 0
|
||||||
|
text: network.connectError
|
||||||
|
color: Theme.error
|
||||||
|
font.pixelSize: 11 * Theme.scale(Screen)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: 22
|
||||||
|
Layout.preferredHeight: 22
|
||||||
|
visible: network.connectStatusSsid === modelData.ssid && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid)
|
||||||
|
|
||||||
|
Spinner {
|
||||||
|
visible: network.connectingSsid === modelData.ssid
|
||||||
|
running: network.connectingSsid === modelData.ssid
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: 22
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: network.connectStatus === "success" && !network.connectingSsid
|
||||||
|
text: "check_circle"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
color: "#43a047"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: network.connectStatus === "error" && !network.connectingSsid
|
||||||
|
text: "error"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
color: Theme.error
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: modelData.connected
|
||||||
|
text: "connected"
|
||||||
|
color: networkMouseArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||||
|
font.pixelSize: 11 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: networkMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.connected) {
|
||||||
|
network.disconnectNetwork(modelData.ssid);
|
||||||
|
} else if (network.isSecured(modelData.security) && !modelData.existing) {
|
||||||
|
passwordPromptSsid = modelData.ssid;
|
||||||
|
showPasswordPrompt = true;
|
||||||
|
passwordInput = ""; // Clear previous input
|
||||||
|
Qt.callLater(function() {
|
||||||
|
passwordInputField.forceActiveFocus();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
network.connectNetwork(modelData.ssid, modelData.security);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password prompt section
|
||||||
|
Rectangle {
|
||||||
|
id: passwordPromptSection
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0
|
||||||
|
Layout.margins: 8
|
||||||
|
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
radius: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 8
|
||||||
|
color: "transparent"
|
||||||
|
border.color: passwordInputField.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: passwordInputField
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
text: passwordInput
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
clip: true
|
||||||
|
focus: true
|
||||||
|
selectByMouse: true
|
||||||
|
activeFocusOnTab: true
|
||||||
|
inputMethodHints: Qt.ImhNone
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
onTextChanged: passwordInput = text
|
||||||
|
onAccepted: {
|
||||||
|
network.submitPassword(passwordPromptSsid, passwordInput);
|
||||||
|
showPasswordPrompt = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: passwordInputMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: passwordInputField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 80
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
radius: 18
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 0
|
||||||
|
opacity: 1.0
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
network.submitPassword(passwordPromptSsid, passwordInput);
|
||||||
|
showPasswordPrompt = false;
|
||||||
|
}
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
onEntered: parent.color = Qt.darker(Theme.accentPrimary, 1.1)
|
||||||
|
onExited: parent.color = Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Connect"
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ Item {
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
height: 36
|
height: 36 * Theme.scale(Screen)
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
localWorkspaces.clear();
|
localWorkspaces.clear();
|
||||||
|
|
@ -115,14 +115,14 @@ Item {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: workspaceBackground
|
id: workspaceBackground
|
||||||
width: parent.width - 15
|
width: parent.width - 15 * Theme.scale(Screen)
|
||||||
height: 26
|
height: 26 * Theme.scale(Screen)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
radius: 12
|
radius: 12 * Theme.scale(Screen)
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1)
|
border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1)
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
shadowColor: "black"
|
shadowColor: "black"
|
||||||
|
|
@ -145,7 +145,7 @@ Item {
|
||||||
model: localWorkspaces
|
model: localWorkspaces
|
||||||
Item {
|
Item {
|
||||||
id: workspacePillContainer
|
id: workspacePillContainer
|
||||||
height: 12
|
height: 12 * Theme.scale(Screen)
|
||||||
width: {
|
width: {
|
||||||
if (model.isFocused)
|
if (model.isFocused)
|
||||||
return 44;
|
return 44;
|
||||||
|
|
@ -245,12 +245,12 @@ Item {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: pillBurst
|
id: pillBurst
|
||||||
anchors.centerIn: workspacePillContainer
|
anchors.centerIn: workspacePillContainer
|
||||||
width: workspacePillContainer.width + 18 * root.masterProgress
|
width: workspacePillContainer.width + 18 * root.masterProgress * Theme.scale(Screen)
|
||||||
height: workspacePillContainer.height + 18 * root.masterProgress
|
height: workspacePillContainer.height + 18 * root.masterProgress * Theme.scale(Screen)
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
border.color: root.effectColor
|
border.color: root.effectColor
|
||||||
border.width: 2 + 6 * (1.0 - root.masterProgress)
|
border.width: (2 + 6 * (1.0 - root.masterProgress)) * Theme.scale(Screen)
|
||||||
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
|
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
|
||||||
visible: root.effectsActive && model.isFocused
|
visible: root.effectsActive && model.isFocused
|
||||||
z: 1
|
z: 1
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ Item {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "person"
|
text: "person"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
color: Theme.onAccent
|
color: Theme.onAccent
|
||||||
visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === ""
|
visible: Settings.settings.profileImage === undefined || Settings.settings.profileImage === ""
|
||||||
z: 0
|
z: 0
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,20 @@ Rectangle {
|
||||||
id: circularProgressBar
|
id: circularProgressBar
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
// Properties
|
property real progress: 0.0
|
||||||
property real progress: 0.0 // 0.0 to 1.0
|
|
||||||
property int size: 80
|
property int size: 80
|
||||||
property color backgroundColor: Theme.surfaceVariant
|
property color backgroundColor: Theme.surfaceVariant
|
||||||
property color progressColor: Theme.accentPrimary
|
property color progressColor: Theme.accentPrimary
|
||||||
property int strokeWidth: 6
|
property int strokeWidth: 6 * Theme.scale(Screen)
|
||||||
property bool showText: true
|
property bool showText: true
|
||||||
property string units: "%"
|
property string units: "%"
|
||||||
property string text: Math.round(progress * 100) + units
|
property string text: Math.round(progress * 100) + units
|
||||||
property int textSize: 10
|
property int textSize: 10 * Theme.scale(Screen)
|
||||||
property color textColor: Theme.textPrimary
|
property color textColor: Theme.textPrimary
|
||||||
|
|
||||||
// Notch properties
|
// Notch properties
|
||||||
property bool hasNotch: false
|
property bool hasNotch: false
|
||||||
property real notchSize: 0.25 // Size of the notch as a fraction of the circle
|
property real notchSize: 0.25
|
||||||
property string notchIcon: ""
|
property string notchIcon: ""
|
||||||
property int notchIconSize: 12
|
property int notchIconSize: 12
|
||||||
property color notchIconColor: Theme.accentPrimary
|
property color notchIconColor: Theme.accentPrimary
|
||||||
|
|
@ -32,6 +31,7 @@ Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onPaint: {
|
onPaint: {
|
||||||
|
// Setup canvas context and calculate dimensions
|
||||||
var ctx = getContext("2d")
|
var ctx = getContext("2d")
|
||||||
var centerX = width / 2
|
var centerX = width / 2
|
||||||
var centerY = height / 2
|
var centerY = height / 2
|
||||||
|
|
@ -41,25 +41,22 @@ Rectangle {
|
||||||
var notchStartAngle = -notchAngle / 2
|
var notchStartAngle = -notchAngle / 2
|
||||||
var notchEndAngle = notchAngle / 2
|
var notchEndAngle = notchAngle / 2
|
||||||
|
|
||||||
// Clear canvas
|
|
||||||
ctx.reset()
|
ctx.reset()
|
||||||
|
|
||||||
// Background circle
|
|
||||||
ctx.strokeStyle = backgroundColor
|
ctx.strokeStyle = backgroundColor
|
||||||
ctx.lineWidth = strokeWidth
|
ctx.lineWidth = strokeWidth
|
||||||
ctx.lineCap = "round"
|
ctx.lineCap = "round"
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
if (hasNotch) {
|
if (hasNotch) {
|
||||||
// Draw background circle with notch on the right side
|
// Draw background arc with notch gap
|
||||||
// Draw the arc excluding the notch area (notch is at 0 radians, right side)
|
|
||||||
ctx.arc(centerX, centerY, radius, notchEndAngle, 2 * Math.PI + notchStartAngle)
|
ctx.arc(centerX, centerY, radius, notchEndAngle, 2 * Math.PI + notchStartAngle)
|
||||||
} else {
|
} else {
|
||||||
|
// Draw full background circle
|
||||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI)
|
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI)
|
||||||
}
|
}
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// Progress arc
|
// Draw progress arc
|
||||||
if (progress > 0) {
|
if (progress > 0) {
|
||||||
ctx.strokeStyle = progressColor
|
ctx.strokeStyle = progressColor
|
||||||
ctx.lineWidth = strokeWidth
|
ctx.lineWidth = strokeWidth
|
||||||
|
|
@ -67,15 +64,11 @@ Rectangle {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
if (hasNotch) {
|
if (hasNotch) {
|
||||||
// Calculate progress with notch consideration
|
// Calculate progress arc with notch gap
|
||||||
var availableAngle = 2 * Math.PI - notchAngle
|
var availableAngle = 2 * Math.PI - notchAngle
|
||||||
var progressAngle = availableAngle * progress
|
var progressAngle = availableAngle * progress
|
||||||
|
|
||||||
// Start from where the notch cutout begins (top-right) and go clockwise
|
|
||||||
var adjustedStartAngle = notchEndAngle
|
var adjustedStartAngle = notchEndAngle
|
||||||
var adjustedEndAngle = adjustedStartAngle + progressAngle
|
var adjustedEndAngle = adjustedStartAngle + progressAngle
|
||||||
|
|
||||||
// Ensure we don't exceed the available space
|
|
||||||
if (adjustedEndAngle > 2 * Math.PI + notchStartAngle) {
|
if (adjustedEndAngle > 2 * Math.PI + notchStartAngle) {
|
||||||
adjustedEndAngle = 2 * Math.PI + notchStartAngle
|
adjustedEndAngle = 2 * Math.PI + notchStartAngle
|
||||||
}
|
}
|
||||||
|
|
@ -84,6 +77,7 @@ Rectangle {
|
||||||
ctx.arc(centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle)
|
ctx.arc(centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Draw full progress arc
|
||||||
ctx.arc(centerX, centerY, radius, startAngle, startAngle + (2 * Math.PI * progress))
|
ctx.arc(centerX, centerY, radius, startAngle, startAngle + (2 * Math.PI * progress))
|
||||||
}
|
}
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import qs.Settings
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
property int innerRadius: 34
|
property int innerRadius: 34 * Theme.scale(Screen)
|
||||||
property int outerRadius: 48
|
property int outerRadius: 48 * Theme.scale(Screen)
|
||||||
property color fillColor: "#fff"
|
property color fillColor: "#fff"
|
||||||
property color strokeColor: "#fff"
|
property color strokeColor: "#fff"
|
||||||
property int strokeWidth: 0
|
property int strokeWidth: 0 * Theme.scale(Screen)
|
||||||
property var values: []
|
property var values: []
|
||||||
property int usableOuter: 48
|
property int usableOuter: 48
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ Item {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
property real value: root.values[index]
|
property real value: root.values[index]
|
||||||
property real angle: (index / root.values.length) * 360
|
property real angle: (index / root.values.length) * 360
|
||||||
width: Math.max(2, (root.innerRadius * 2 * Math.PI) / root.values.length - 4)
|
width: Math.max(2 * Theme.scale(Screen), (root.innerRadius * 2 * Math.PI) / root.values.length - 4 * Theme.scale(Screen))
|
||||||
height: Settings.settings.visualizerType === "diamond" ? value * 2 * (usableOuter - root.innerRadius) : value * (usableOuter - root.innerRadius)
|
height: Settings.settings.visualizerType === "diamond" ? value * 2 * (usableOuter - root.innerRadius) : value * (usableOuter - root.innerRadius)
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: root.fillColor
|
color: root.fillColor
|
||||||
|
|
|
||||||
|
|
@ -6,43 +6,43 @@ Shape {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
|
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
|
||||||
property real size: 1.0 // Scale multiplier for entire corner
|
property real size: 1.0 * Theme.scale(Screen) // Scale multiplier for entire corner
|
||||||
property int concaveWidth: 100 * size
|
property int concaveWidth: 100 * size
|
||||||
property int concaveHeight: 60 * size
|
property int concaveHeight: 60 * size
|
||||||
property int offsetX: -20
|
property int offsetX: -20 * Theme.scale(Screen)
|
||||||
property int offsetY: -20
|
property int offsetY: -20 * Theme.scale(Screen)
|
||||||
property color fillColor: Theme.accentPrimary
|
property color fillColor: Theme.accentPrimary
|
||||||
property int arcRadius: 20 * size
|
property int arcRadius: 20 * size
|
||||||
|
|
||||||
property var modelData: null
|
property var modelData: null
|
||||||
|
|
||||||
// Position flags derived from position string
|
// Position flags derived from position string - calculated once
|
||||||
property bool _isTop: position.includes("top")
|
readonly property bool _isTop: position.includes("top")
|
||||||
property bool _isLeft: position.includes("left")
|
readonly property bool _isLeft: position.includes("left")
|
||||||
property bool _isRight: position.includes("right")
|
readonly property bool _isRight: position.includes("right")
|
||||||
property bool _isBottom: position.includes("bottom")
|
readonly property bool _isBottom: position.includes("bottom")
|
||||||
|
|
||||||
// Shift the path vertically if offsetY is negative to pull shape up
|
// Shift the path vertically if offsetY is negative to pull shape up
|
||||||
property real pathOffsetY: Math.min(offsetY, 0)
|
readonly property real pathOffsetY: Math.min(offsetY, 0)
|
||||||
|
|
||||||
// Base coordinates for left corner shape, shifted by pathOffsetY vertically
|
// Base coordinates for left corner shape, shifted by pathOffsetY vertically
|
||||||
property real _baseStartX: 30 * size
|
readonly property real _baseStartX: 30 * size
|
||||||
property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY
|
readonly property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY
|
||||||
property real _baseLineX: 30 * size
|
readonly property real _baseLineX: 30 * size
|
||||||
property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY
|
readonly property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY
|
||||||
property real _baseArcX: 50 * size
|
readonly property real _baseArcX: 50 * size
|
||||||
property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY
|
readonly property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY
|
||||||
|
|
||||||
// Mirror coordinates for right corners
|
// Mirror coordinates for right corners
|
||||||
property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
|
readonly property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
|
||||||
property real _startY: _baseStartY
|
readonly property real _startY: _baseStartY
|
||||||
property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
|
readonly property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
|
||||||
property real _lineY: _baseLineY
|
readonly property real _lineY: _baseLineY
|
||||||
property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
|
readonly property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
|
||||||
property real _arcY: _baseArcY
|
readonly property real _arcY: _baseArcY
|
||||||
|
|
||||||
// Arc direction varies by corner to maintain proper concave shape
|
// Arc direction varies by corner to maintain proper concave shape
|
||||||
property int _arcDirection: {
|
readonly property int _arcDirection: {
|
||||||
if (_isTop && _isLeft) return PathArc.Counterclockwise
|
if (_isTop && _isLeft) return PathArc.Counterclockwise
|
||||||
if (_isTop && _isRight) return PathArc.Clockwise
|
if (_isTop && _isRight) return PathArc.Clockwise
|
||||||
if (_isBottom && _isLeft) return PathArc.Clockwise
|
if (_isBottom && _isLeft) return PathArc.Clockwise
|
||||||
|
|
@ -57,9 +57,10 @@ Shape {
|
||||||
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
|
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
|
||||||
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
|
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
|
||||||
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
// Optimized rendering settings - reduced quality for better performance
|
||||||
layer.enabled: true
|
preferredRendererType: Shape.GeometryRenderer // Use simpler renderer
|
||||||
layer.samples: 4
|
layer.enabled: false // Disable layer rendering to save memory
|
||||||
|
antialiasing: true // Use standard antialiasing instead of MSAA
|
||||||
|
|
||||||
ShapePath {
|
ShapePath {
|
||||||
strokeWidth: 0
|
strokeWidth: 0
|
||||||
|
|
@ -19,7 +19,7 @@ MouseArea {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: 8
|
radius: 8 * Theme.scale(Screen)
|
||||||
color: root.hovering ? Theme.accentPrimary : "transparent"
|
color: root.hovering ? Theme.accentPrimary : "transparent"
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
|
@ -27,7 +27,7 @@ MouseArea {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.icon
|
text: root.icon
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
color: root.hovering ? Theme.onAccent : Theme.textPrimary
|
color: root.hovering ? Theme.onAccent : Theme.textPrimary
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ Item {
|
||||||
property color iconCircleColor: Theme.accentPrimary
|
property color iconCircleColor: Theme.accentPrimary
|
||||||
property color iconTextColor: Theme.backgroundPrimary
|
property color iconTextColor: Theme.backgroundPrimary
|
||||||
property color collapsedIconColor: Theme.textPrimary
|
property color collapsedIconColor: Theme.textPrimary
|
||||||
property int pillHeight: 22
|
property int pillHeight: 22 * Theme.scale(Screen)
|
||||||
property int iconSize: 22
|
property int iconSize: 22 * Theme.scale(Screen)
|
||||||
property int pillPaddingHorizontal: 14
|
property int pillPaddingHorizontal: 14
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ Item {
|
||||||
id: textItem
|
id: textItem
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: revealPill.text
|
text: revealPill.text
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
color: textColor
|
color: textColor
|
||||||
|
|
@ -89,7 +89,7 @@ Item {
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
text: revealPill.icon
|
text: revealPill.icon
|
||||||
color: showPill ? iconTextColor : collapsedIconColor
|
color: showPill ? iconTextColor : collapsedIconColor
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -6,7 +7,7 @@ Item {
|
||||||
property bool running: false
|
property bool running: false
|
||||||
property color color: "white"
|
property color color: "white"
|
||||||
property int size: 16
|
property int size: 16
|
||||||
property int strokeWidth: 2
|
property int strokeWidth: 2 * Theme.scale(Screen)
|
||||||
property int duration: 1000
|
property int duration: 1000
|
||||||
|
|
||||||
implicitWidth: size
|
implicitWidth: size
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ Window {
|
||||||
property Item targetItem: null
|
property Item targetItem: null
|
||||||
property int delay: 300
|
property int delay: 300
|
||||||
|
|
||||||
// New property to control positioning: true => above, false => below
|
|
||||||
property bool positionAbove: true
|
property bool positionAbove: true
|
||||||
|
|
||||||
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
||||||
|
|
@ -34,8 +33,8 @@ Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showNow() {
|
function _showNow() {
|
||||||
width = Math.max(50, tooltipText.implicitWidth + 24)
|
width = Math.max(50 * Theme.scale(Screen), tooltipText.implicitWidth + 24 * Theme.scale(Screen))
|
||||||
height = Math.max(50, tooltipText.implicitHeight + 16)
|
height = Math.max(50 * Theme.scale(Screen), tooltipText.implicitHeight + 16 * Theme.scale(Screen))
|
||||||
|
|
||||||
if (!targetItem) return;
|
if (!targetItem) return;
|
||||||
|
|
||||||
|
|
@ -76,10 +75,10 @@ Window {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: 20
|
radius: 20 * Theme.scale(Screen)
|
||||||
color: Theme.backgroundTertiary || "#222"
|
color: Theme.backgroundTertiary || "#222"
|
||||||
border.color: Theme.outline || "#444"
|
border.color: Theme.outline || "#444"
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
opacity: 0.97
|
opacity: 0.97
|
||||||
z: 1
|
z: 1
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +88,7 @@ Window {
|
||||||
text: tooltipWindow.text
|
text: tooltipWindow.text
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
@ -106,7 +105,7 @@ Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
width = Math.max(minimumWidth, tooltipText.implicitWidth + 24);
|
width = Math.max(minimumWidth * Theme.scale(Screen), tooltipText.implicitWidth + 24 * Theme.scale(Screen));
|
||||||
height = Math.max(minimumHeight, tooltipText.implicitHeight + 16);
|
height = Math.max(minimumHeight * Theme.scale(Screen), tooltipText.implicitHeight + 16 * Theme.scale(Screen));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,8 +17,8 @@ Item {
|
||||||
model: root.tabsModel
|
model: root.tabsModel
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
id: tabWrapper
|
id: tabWrapper
|
||||||
implicitHeight: tab.height
|
implicitHeight: tab.height * Theme.scale(Screen)
|
||||||
implicitWidth: 56
|
implicitWidth: 56 * Theme.scale(Screen)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
|
|
@ -48,7 +48,7 @@ Item {
|
||||||
Text {
|
Text {
|
||||||
text: modelData.icon
|
text: modelData.icon
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 22
|
font.pixelSize: 22 * Theme.scale(Screen)
|
||||||
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444")
|
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444")
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +56,7 @@ Item {
|
||||||
// Label
|
// Label
|
||||||
Text {
|
Text {
|
||||||
text: modelData.label
|
text: modelData.label
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
font.bold: index === root.currentIndex
|
font.bold: index === root.currentIndex
|
||||||
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444")
|
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : tabWrapper.hovered ? (Theme ? Theme.accentPrimary : "#7C3AED") : (Theme ? Theme.textSecondary : "#444")
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
|
@ -64,9 +64,9 @@ Item {
|
||||||
|
|
||||||
// Underline for active tab
|
// Underline for active tab
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 24
|
width: 24 * Theme.scale(Screen)
|
||||||
height: 2
|
height: 2 * Theme.scale(Screen)
|
||||||
radius: 1
|
radius: 1 * Theme.scale(Screen)
|
||||||
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : "transparent"
|
color: index === root.currentIndex ? (Theme ? Theme.accentPrimary : "#7C3AED") : "transparent"
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
Components/ToggleOption.qml
Normal file
88
Components/ToggleOption.qml
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string label: ""
|
||||||
|
property string description: ""
|
||||||
|
property bool value: false
|
||||||
|
property var onToggled: function() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4 * Theme.scale(Screen)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: label
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: description
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: switcher
|
||||||
|
|
||||||
|
width: 52 * Theme.scale(Screen)
|
||||||
|
height: 32 * Theme.scale(Screen)
|
||||||
|
radius: 16 * Theme.scale(Screen)
|
||||||
|
color: value ? Theme.accentPrimary : Theme.surfaceVariant
|
||||||
|
border.color: value ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 2 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28 * Theme.scale(Screen)
|
||||||
|
height: 28 * Theme.scale(Screen)
|
||||||
|
radius: 14 * Theme.scale(Screen)
|
||||||
|
color: Theme.surface
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
y: 2
|
||||||
|
x: value ? switcher.width - width - 2 : 2
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
root.onToggled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: 8 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,9 @@ IpcHandler {
|
||||||
idleInhibitor.toggle()
|
idleInhibitor.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function toggleNotificationPopup(): void {
|
function toggleNotificationPopup(): void {
|
||||||
console.log("[IPC] NotificationPopup toggle() called")
|
console.log("[IPC] NotificationPopup toggle() called")
|
||||||
|
// Use the global toggle function from the notification manager
|
||||||
notificationPopup.togglePopup();
|
notificationPopup.togglePopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import Quickshell.Io
|
||||||
Process {
|
Process {
|
||||||
id: idleRoot
|
id: idleRoot
|
||||||
|
|
||||||
// Example: systemd-inhibit to prevent idle/sleep
|
// Uses systemd-inhibit to prevent idle/sleep
|
||||||
command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"]
|
command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"]
|
||||||
|
|
||||||
// Keep process running in background
|
// Track background process state
|
||||||
property bool isRunning: running
|
property bool isRunning: running
|
||||||
|
|
||||||
onStarted: {
|
onStarted: {
|
||||||
|
|
@ -17,7 +17,7 @@ Process {
|
||||||
console.log("[IdleInhibitor] Process finished:", exitCode)
|
console.log("[IdleInhibitor] Process finished:", exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control functions
|
|
||||||
function start() {
|
function start() {
|
||||||
if (!running) {
|
if (!running) {
|
||||||
console.log("[IdleInhibitor] Starting idle inhibitor...")
|
console.log("[IdleInhibitor] Starting idle inhibitor...")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import qs.Components
|
||||||
Singleton {
|
Singleton {
|
||||||
id: manager
|
id: manager
|
||||||
|
|
||||||
// Properties
|
|
||||||
property var currentPlayer: null
|
property var currentPlayer: null
|
||||||
property real currentPosition: 0
|
property real currentPosition: 0
|
||||||
property int selectedPlayerIndex: 0
|
property int selectedPlayerIndex: 0
|
||||||
|
|
@ -25,14 +25,14 @@ Singleton {
|
||||||
property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
|
property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
|
||||||
property bool hasPlayer: getAvailablePlayers().length > 0
|
property bool hasPlayer: getAvailablePlayers().length > 0
|
||||||
|
|
||||||
// Initialize
|
|
||||||
Item {
|
Item {
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
updateCurrentPlayer()
|
updateCurrentPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns available MPRIS players
|
|
||||||
function getAvailablePlayers() {
|
function getAvailablePlayers() {
|
||||||
if (!Mpris.players || !Mpris.players.values) {
|
if (!Mpris.players || !Mpris.players.values) {
|
||||||
return []
|
return []
|
||||||
|
|
@ -51,14 +51,14 @@ Singleton {
|
||||||
return controllablePlayers
|
return controllablePlayers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns active player or first available
|
|
||||||
function findActivePlayer() {
|
function findActivePlayer() {
|
||||||
let availablePlayers = getAvailablePlayers()
|
let availablePlayers = getAvailablePlayers()
|
||||||
if (availablePlayers.length === 0) {
|
if (availablePlayers.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use selected player if valid, otherwise use first available
|
|
||||||
if (selectedPlayerIndex < availablePlayers.length) {
|
if (selectedPlayerIndex < availablePlayers.length) {
|
||||||
return availablePlayers[selectedPlayerIndex]
|
return availablePlayers[selectedPlayerIndex]
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -67,7 +67,8 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates currentPlayer and currentPosition
|
|
||||||
|
// Switch to the most recently active player
|
||||||
function updateCurrentPlayer() {
|
function updateCurrentPlayer() {
|
||||||
let newPlayer = findActivePlayer()
|
let newPlayer = findActivePlayer()
|
||||||
if (newPlayer !== currentPlayer) {
|
if (newPlayer !== currentPlayer) {
|
||||||
|
|
@ -76,7 +77,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player control functions
|
|
||||||
function playPause() {
|
function playPause() {
|
||||||
if (currentPlayer) {
|
if (currentPlayer) {
|
||||||
if (currentPlayer.isPlaying) {
|
if (currentPlayer.isPlaying) {
|
||||||
|
|
@ -118,6 +119,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seek to position based on ratio (0.0 to 1.0)
|
||||||
function seekByRatio(ratio) {
|
function seekByRatio(ratio) {
|
||||||
if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) {
|
if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) {
|
||||||
let seekPosition = ratio * currentPlayer.length
|
let seekPosition = ratio * currentPlayer.length
|
||||||
|
|
@ -126,7 +128,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates progress bar every second
|
// Update progress bar every second while playing
|
||||||
Timer {
|
Timer {
|
||||||
id: positionTimer
|
id: positionTimer
|
||||||
interval: 1000
|
interval: 1000
|
||||||
|
|
@ -141,14 +143,14 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset position when player state changes
|
// Reset position when switching to inactive player
|
||||||
onCurrentPlayerChanged: {
|
onCurrentPlayerChanged: {
|
||||||
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {
|
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {
|
||||||
currentPosition = 0
|
currentPosition = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reacts to player list changes
|
// Update current player when available players change
|
||||||
Connections {
|
Connections {
|
||||||
target: Mpris.players
|
target: Mpris.players
|
||||||
function onValuesChanged() {
|
function onValuesChanged() {
|
||||||
|
|
|
||||||
348
Services/Network.qml
Normal file
348
Services/Network.qml
Normal file
|
|
@ -0,0 +1,348 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var networks: ({})
|
||||||
|
property string connectingSsid: ""
|
||||||
|
property string connectStatus: ""
|
||||||
|
property string connectStatusSsid: ""
|
||||||
|
property string connectError: ""
|
||||||
|
property string detectedInterface: ""
|
||||||
|
|
||||||
|
function signalIcon(signal) {
|
||||||
|
if (signal >= 80)
|
||||||
|
return "network_wifi";
|
||||||
|
if (signal >= 60)
|
||||||
|
return "network_wifi_3_bar";
|
||||||
|
if (signal >= 40)
|
||||||
|
return "network_wifi_2_bar";
|
||||||
|
if (signal >= 20)
|
||||||
|
return "network_wifi_1_bar";
|
||||||
|
return "wifi_0_bar";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSecured(security) {
|
||||||
|
return security && security.trim() !== "" && security.trim() !== "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshNetworks() {
|
||||||
|
existingNetwork.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectNetwork(ssid, security) {
|
||||||
|
pendingConnect = {
|
||||||
|
ssid: ssid,
|
||||||
|
security: security,
|
||||||
|
password: ""
|
||||||
|
};
|
||||||
|
doConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitPassword(ssid, password) {
|
||||||
|
pendingConnect = {
|
||||||
|
ssid: ssid,
|
||||||
|
security: networks[ssid].security,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
doConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectNetwork(ssid) {
|
||||||
|
disconnectProfileProcess.connectionName = ssid;
|
||||||
|
disconnectProfileProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
property var pendingConnect: null
|
||||||
|
|
||||||
|
function doConnect() {
|
||||||
|
const params = pendingConnect;
|
||||||
|
if (!params)
|
||||||
|
return;
|
||||||
|
|
||||||
|
connectingSsid = params.ssid;
|
||||||
|
connectStatus = "";
|
||||||
|
connectStatusSsid = params.ssid;
|
||||||
|
|
||||||
|
|
||||||
|
const targetNetwork = networks[params.ssid];
|
||||||
|
|
||||||
|
if (targetNetwork && targetNetwork.existing) {
|
||||||
|
upConnectionProcess.profileName = params.ssid;
|
||||||
|
upConnectionProcess.running = true;
|
||||||
|
pendingConnect = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (params.security && params.security !== "--") {
|
||||||
|
getInterfaceProcess.running = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectProcess.security = params.security;
|
||||||
|
connectProcess.ssid = params.ssid;
|
||||||
|
connectProcess.password = params.password;
|
||||||
|
connectProcess.running = true;
|
||||||
|
pendingConnect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
property int refreshInterval: 25000
|
||||||
|
|
||||||
|
// Only refresh when we have an active connection
|
||||||
|
property bool hasActiveConnection: {
|
||||||
|
for (const net in networks) {
|
||||||
|
if (networks[net].connected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
property Timer refreshTimer: Timer {
|
||||||
|
interval: root.refreshInterval
|
||||||
|
// Only run timer when we're connected to a network
|
||||||
|
running: root.hasActiveConnection
|
||||||
|
repeat: true
|
||||||
|
onTriggered: root.refreshNetworks()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force a refresh when menu is opened
|
||||||
|
function onMenuOpened() {
|
||||||
|
refreshNetworks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMenuClosed() {
|
||||||
|
// No need to do anything special on close
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process disconnectProfileProcess: Process {
|
||||||
|
id: disconnectProfileProcess
|
||||||
|
property string connectionName: ""
|
||||||
|
running: false
|
||||||
|
command: ["nmcli", "connection", "down", connectionName]
|
||||||
|
onRunningChanged: {
|
||||||
|
if (!running) {
|
||||||
|
root.refreshNetworks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process existingNetwork: Process {
|
||||||
|
id: existingNetwork
|
||||||
|
running: false
|
||||||
|
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const networksMap = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; ++i) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const parts = line.split(":");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
console.warn("Malformed nmcli output line:", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssid = parts[0];
|
||||||
|
const type = parts[1];
|
||||||
|
|
||||||
|
if (ssid) {
|
||||||
|
networksMap[ssid] = {
|
||||||
|
ssid: ssid,
|
||||||
|
type: type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scanProcess.existingNetwork = networksMap;
|
||||||
|
scanProcess.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process scanProcess: Process {
|
||||||
|
id: scanProcess
|
||||||
|
running: false
|
||||||
|
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
|
||||||
|
|
||||||
|
property var existingNetwork
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const networksMap = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; ++i) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const parts = line.split(":");
|
||||||
|
if (parts.length < 4) {
|
||||||
|
console.warn("Malformed nmcli output line:", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ssid = parts[0];
|
||||||
|
const security = parts[1];
|
||||||
|
const signal = parseInt(parts[2]);
|
||||||
|
const inUse = parts[3] === "*";
|
||||||
|
|
||||||
|
if (ssid) {
|
||||||
|
if (!networksMap[ssid]) {
|
||||||
|
networksMap[ssid] = {
|
||||||
|
ssid: ssid,
|
||||||
|
security: security,
|
||||||
|
signal: signal,
|
||||||
|
connected: inUse,
|
||||||
|
existing: ssid in scanProcess.existingNetwork
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const existingNet = networksMap[ssid];
|
||||||
|
if (inUse) {
|
||||||
|
existingNet.connected = true;
|
||||||
|
}
|
||||||
|
if (signal > existingNet.signal) {
|
||||||
|
existingNet.signal = signal;
|
||||||
|
existingNet.security = security;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.networks = networksMap;
|
||||||
|
scanProcess.existingNetwork = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process connectProcess: Process {
|
||||||
|
id: connectProcess
|
||||||
|
property string ssid: ""
|
||||||
|
property string password: ""
|
||||||
|
property string security: ""
|
||||||
|
running: false
|
||||||
|
command: {
|
||||||
|
if (password) {
|
||||||
|
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password];
|
||||||
|
} else {
|
||||||
|
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.connectingSsid = "";
|
||||||
|
root.connectStatus = "success";
|
||||||
|
root.connectStatusSsid = connectProcess.ssid;
|
||||||
|
root.connectError = "";
|
||||||
|
root.refreshNetworks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.connectingSsid = "";
|
||||||
|
root.connectStatus = "error";
|
||||||
|
root.connectStatusSsid = connectProcess.ssid;
|
||||||
|
root.connectError = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process getInterfaceProcess: Process {
|
||||||
|
id: getInterfaceProcess
|
||||||
|
running: false
|
||||||
|
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
var lines = text.split("\n");
|
||||||
|
for (var i = 0; i < lines.length; ++i) {
|
||||||
|
var parts = lines[i].split(":");
|
||||||
|
if (parts[1] === "wifi" && parts[2] !== "unavailable") {
|
||||||
|
root.detectedInterface = parts[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.detectedInterface) {
|
||||||
|
var params = root.pendingConnect;
|
||||||
|
addConnectionProcess.ifname = root.detectedInterface;
|
||||||
|
addConnectionProcess.ssid = params.ssid;
|
||||||
|
addConnectionProcess.password = params.password;
|
||||||
|
addConnectionProcess.profileName = params.ssid;
|
||||||
|
addConnectionProcess.security = params.security;
|
||||||
|
addConnectionProcess.running = true;
|
||||||
|
} else {
|
||||||
|
root.connectStatus = "error";
|
||||||
|
root.connectStatusSsid = root.pendingConnect.ssid;
|
||||||
|
root.connectError = "No Wi-Fi interface found.";
|
||||||
|
root.connectingSsid = "";
|
||||||
|
root.pendingConnect = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process addConnectionProcess: Process {
|
||||||
|
id: addConnectionProcess
|
||||||
|
property string ifname: ""
|
||||||
|
property string ssid: ""
|
||||||
|
property string password: ""
|
||||||
|
property string profileName: ""
|
||||||
|
property string security: ""
|
||||||
|
running: false
|
||||||
|
command: {
|
||||||
|
var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid];
|
||||||
|
if (security && security !== "--") {
|
||||||
|
cmd.push("wifi-sec.key-mgmt");
|
||||||
|
cmd.push("wpa-psk");
|
||||||
|
cmd.push("wifi-sec.psk");
|
||||||
|
cmd.push(password);
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
upConnectionProcess.profileName = addConnectionProcess.profileName;
|
||||||
|
upConnectionProcess.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
upConnectionProcess.profileName = addConnectionProcess.profileName;
|
||||||
|
upConnectionProcess.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process upConnectionProcess: Process {
|
||||||
|
id: upConnectionProcess
|
||||||
|
property string profileName: ""
|
||||||
|
running: false
|
||||||
|
command: ["nmcli", "connection", "up", "id", profileName]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.connectingSsid = "";
|
||||||
|
root.connectStatus = "success";
|
||||||
|
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "";
|
||||||
|
root.connectError = "";
|
||||||
|
root.pendingConnect = null;
|
||||||
|
root.refreshNetworks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.connectingSsid = "";
|
||||||
|
root.connectStatus = "error";
|
||||||
|
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "";
|
||||||
|
root.connectError = text;
|
||||||
|
root.pendingConnect = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
refreshNetworks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,9 +45,10 @@ Singleton {
|
||||||
property string wallpaperFolder: "/usr/share/wallpapers"
|
property string wallpaperFolder: "/usr/share/wallpapers"
|
||||||
property string currentWallpaper: ""
|
property string currentWallpaper: ""
|
||||||
property string videoPath: "~/Videos/"
|
property string videoPath: "~/Videos/"
|
||||||
|
property bool showActiveWindow: true
|
||||||
property bool showActiveWindowIcon: false
|
property bool showActiveWindowIcon: false
|
||||||
property bool showSystemInfoInBar: false
|
property bool showSystemInfoInBar: false
|
||||||
property bool showCorners: true
|
property bool showCorners: false
|
||||||
property bool showTaskbar: true
|
property bool showTaskbar: true
|
||||||
property bool showMediaInBar: false
|
property bool showMediaInBar: false
|
||||||
property bool useSWWW: false
|
property bool useSWWW: false
|
||||||
|
|
@ -68,6 +69,19 @@ Singleton {
|
||||||
|
|
||||||
property bool showDock: true
|
property bool showDock: true
|
||||||
property bool dockExclusive: false
|
property bool dockExclusive: false
|
||||||
|
property bool wifiEnabled: false
|
||||||
|
property bool bluetoothEnabled: false
|
||||||
|
property int recordingFrameRate: 60
|
||||||
|
property string recordingQuality: "very_high"
|
||||||
|
property string recordingCodec: "h264"
|
||||||
|
property string audioCodec: "opus"
|
||||||
|
property bool showCursor: true
|
||||||
|
property string colorRange: "limited"
|
||||||
|
|
||||||
|
// Monitor/Display Settings
|
||||||
|
property var barMonitors: [] // Array of monitor names to show the bar on
|
||||||
|
property var dockMonitors: [] // Array of monitor names to show the dock on
|
||||||
|
property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,5 +90,7 @@ Singleton {
|
||||||
function onRandomWallpaperChanged() { WallpaperManager.toggleRandomWallpaper() }
|
function onRandomWallpaperChanged() { WallpaperManager.toggleRandomWallpaper() }
|
||||||
function onWallpaperIntervalChanged() { WallpaperManager.restartRandomWallpaperTimer() }
|
function onWallpaperIntervalChanged() { WallpaperManager.restartRandomWallpaperTimer() }
|
||||||
function onWallpaperFolderChanged() { WallpaperManager.loadWallpapers() }
|
function onWallpaperFolderChanged() { WallpaperManager.loadWallpapers() }
|
||||||
|
function onNotificationMonitorsChanged() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,22 @@ import qs.Settings
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Design screen width
|
||||||
|
readonly property int designScreenWidth: 1920
|
||||||
|
|
||||||
|
// Scaling dampening factor - reduces the scaling effect for higher resolutions
|
||||||
|
readonly property real scalingDampening: 0.2
|
||||||
|
|
||||||
|
// Automatic scaling based on screen width
|
||||||
|
function scale(currentScreen) {
|
||||||
|
if (currentScreen !== undefined) {
|
||||||
|
var rawRatio = currentScreen.width / designScreenWidth
|
||||||
|
// Apply dampening to reduce scaling for higher resolutions
|
||||||
|
return Math.min(2.0, 1.0 + (rawRatio - 1.0) * scalingDampening)
|
||||||
|
}
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
function applyOpacity(color, opacity) {
|
function applyOpacity(color, opacity) {
|
||||||
return color.replace("#", "#" + opacity);
|
return color.replace("#", "#" + opacity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
Templates/templates/kitty.conf
Normal file
29
Templates/templates/kitty.conf
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# The kitty terminal template for wallust
|
||||||
|
# Add to wallust config: kitty = { src='kitty.conf', dst='~/.config/kitty/colors.conf'}
|
||||||
|
# And add to kitty config: include colors.conf
|
||||||
|
|
||||||
|
cursor {{ cursor }}
|
||||||
|
|
||||||
|
background {{ background }}
|
||||||
|
foreground {{ foreground }}
|
||||||
|
|
||||||
|
color0 {{ color0 }}
|
||||||
|
color1 {{ color1 }}
|
||||||
|
color2 {{ color2 }}
|
||||||
|
color3 {{ color3 }}
|
||||||
|
color4 {{ color4 }}
|
||||||
|
color5 {{ color5 }}
|
||||||
|
color6 {{ color6 }}
|
||||||
|
color7 {{ color7 }}
|
||||||
|
color8 {{ color8 }}
|
||||||
|
color9 {{ color9 }}
|
||||||
|
color10 {{ color10 }}
|
||||||
|
color11 {{ color11 }}
|
||||||
|
color12 {{ color12 }}
|
||||||
|
color13 {{ color13 }}
|
||||||
|
color14 {{ color14 }}
|
||||||
|
color15 {{ color15 }}
|
||||||
|
|
||||||
|
mark1_foreground {{ color6 | saturate(0.2) }}
|
||||||
|
mark2_foreground {{ color7 | saturate(0.2) }}
|
||||||
|
mark3_foreground {{ color6 | saturate(0.2) }}
|
||||||
292
Templates/templates/niri.kdl
Normal file
292
Templates/templates/niri.kdl
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
// Niri configuration for CachyOS
|
||||||
|
// For documentation and full reference, see: https://github.com/YaLTeR/niri/wiki
|
||||||
|
|
||||||
|
// ────────────── Input Configuration ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
|
||||||
|
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
layout "de" // Use the German keyboard layout
|
||||||
|
}
|
||||||
|
numlock // Enable numlock on startup
|
||||||
|
}
|
||||||
|
|
||||||
|
touchpad {
|
||||||
|
tap // Enable tap-to-click
|
||||||
|
natural-scroll // Enable natural (macOS-style) scrolling
|
||||||
|
}
|
||||||
|
|
||||||
|
focus-follows-mouse // Automatically focus windows under the mouse pointer
|
||||||
|
workspace-auto-back-and-forth // Enable workspace back & forth switching
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Output Configuration ──────────────
|
||||||
|
// You can run `niri msg outputs` to get the correct name for your displays.
|
||||||
|
// You will have to remove "/-" and edit it before it takes effect.
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
|
||||||
|
|
||||||
|
output "DP-1" {
|
||||||
|
mode "2560x1440@359.979" // Set resolution and refresh rate
|
||||||
|
scale 1 // No scaling (use 2 for HiDPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Keybindings ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Key-Bindings
|
||||||
|
|
||||||
|
binds {
|
||||||
|
MOD+SHIFT+ESCAPE { show-hotkey-overlay; }
|
||||||
|
|
||||||
|
// ─── Applications ───
|
||||||
|
MOD+RETURN hotkey-overlay-title="Open Terminal: Kitty" { spawn "kitty"; }
|
||||||
|
MOD+CTRL+RETURN hotkey-overlay-title="Open App Launcher: QS" { spawn "qs" "ipc" "call" "globalIPC" "toggleLauncher"; }
|
||||||
|
MOD+B hotkey-overlay-title="Open Browser: firefox" { spawn "firefox"; }
|
||||||
|
MOD+ALT+L hotkey-overlay-title="Lock Screen: swaylock" { spawn "swaylock"; }
|
||||||
|
|
||||||
|
// Please choose your own file manager
|
||||||
|
MOD+E hotkey-overlay-title="File Manager: Nautilus" { spawn "nautilus"; }
|
||||||
|
|
||||||
|
// ─── Audio Controls ───
|
||||||
|
XF86AUDIORAISEVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||||
|
XF86AUDIOLOWERVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
||||||
|
XF86AUDIOMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
|
||||||
|
XF86AUDIOMICMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
|
||||||
|
|
||||||
|
// ─── Window Movement and Focus ───
|
||||||
|
MOD+Q { close-window; }
|
||||||
|
|
||||||
|
MOD+LEFT { focus-column-left; }
|
||||||
|
MOD+H { focus-column-left; }
|
||||||
|
MOD+RIGHT { focus-column-right; }
|
||||||
|
MOD+L { focus-column-right; }
|
||||||
|
MOD+UP { focus-window-up; }
|
||||||
|
MOD+K { focus-window-up; }
|
||||||
|
MOD+DOWN { focus-window-down; }
|
||||||
|
MOD+J { focus-window-down; }
|
||||||
|
|
||||||
|
MOD+CTRL+LEFT { move-column-left; }
|
||||||
|
MOD+CTRL+H { move-column-left; }
|
||||||
|
MOD+CTRL+RIGHT { move-column-right; }
|
||||||
|
MOD+CTRL+L { move-column-right; }
|
||||||
|
MOD+CTRL+UP { move-window-up; }
|
||||||
|
MOD+CTRL+K { move-window-up; }
|
||||||
|
MOD+CTRL+DOWN { move-window-down; }
|
||||||
|
MOD+CTRL+J { move-window-down; }
|
||||||
|
|
||||||
|
MOD+HOME { focus-column-first; }
|
||||||
|
MOD+END { focus-column-last; }
|
||||||
|
MOD+CTRL+HOME { move-column-to-first; }
|
||||||
|
MOD+CTRL+END { move-column-to-last; }
|
||||||
|
|
||||||
|
MOD+SHIFT+LEFT { focus-monitor-left; }
|
||||||
|
MOD+SHIFT+RIGHT { focus-monitor-right; }
|
||||||
|
MOD+SHIFT+UP { focus-monitor-up; }
|
||||||
|
MOD+SHIFT+DOWN { focus-monitor-down; }
|
||||||
|
|
||||||
|
MOD+SHIFT+CTRL+LEFT { move-column-to-monitor-left; }
|
||||||
|
MOD+SHIFT+CTRL+RIGHT { move-column-to-monitor-right; }
|
||||||
|
MOD+SHIFT+CTRL+UP { move-column-to-monitor-up; }
|
||||||
|
MOD+SHIFT+CTRL+DOWN { move-column-to-monitor-down; }
|
||||||
|
|
||||||
|
// ─── Workspace Switching ───
|
||||||
|
MOD+WHEELSCROLLDOWN cooldown-ms=150 { focus-workspace-down; }
|
||||||
|
MOD+WHEELSCROLLUP cooldown-ms=150 { focus-workspace-up; }
|
||||||
|
MOD+CTRL+WHEELSCROLLDOWN cooldown-ms=150 { move-column-to-workspace-down; }
|
||||||
|
MOD+CTRL+WHEELSCROLLUP cooldown-ms=150 { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
MOD+WHEELSCROLLRIGHT { focus-column-right; }
|
||||||
|
MOD+WHEELSCROLLLEFT { focus-column-left; }
|
||||||
|
MOD+CTRL+WHEELSCROLLRIGHT { move-column-right; }
|
||||||
|
MOD+CTRL+WHEELSCROLLLEFT { move-column-left; }
|
||||||
|
|
||||||
|
MOD+SHIFT+WHEELSCROLLDOWN { focus-column-right; }
|
||||||
|
MOD+SHIFT+WHEELSCROLLUP { focus-column-left; }
|
||||||
|
MOD+CTRL+SHIFT+WHEELSCROLLDOWN { move-column-right; }
|
||||||
|
MOD+CTRL+SHIFT+WHEELSCROLLUP { move-column-left; }
|
||||||
|
|
||||||
|
MOD+1 { focus-workspace 1; }
|
||||||
|
MOD+2 { focus-workspace 2; }
|
||||||
|
MOD+3 { focus-workspace 3; }
|
||||||
|
MOD+4 { focus-workspace 4; }
|
||||||
|
MOD+5 { focus-workspace 5; }
|
||||||
|
MOD+6 { focus-workspace 6; }
|
||||||
|
MOD+7 { focus-workspace 7; }
|
||||||
|
MOD+8 { focus-workspace 8; }
|
||||||
|
MOD+9 { focus-workspace 9; }
|
||||||
|
|
||||||
|
MOD+CTRL+1 { move-column-to-workspace 1; }
|
||||||
|
MOD+CTRL+2 { move-column-to-workspace 2; }
|
||||||
|
MOD+CTRL+3 { move-column-to-workspace 3; }
|
||||||
|
MOD+CTRL+4 { move-column-to-workspace 4; }
|
||||||
|
MOD+CTRL+5 { move-column-to-workspace 5; }
|
||||||
|
MOD+CTRL+6 { move-column-to-workspace 6; }
|
||||||
|
MOD+CTRL+7 { move-column-to-workspace 7; }
|
||||||
|
MOD+CTRL+8 { move-column-to-workspace 8; }
|
||||||
|
MOD+CTRL+9 { move-column-to-workspace 9; }
|
||||||
|
|
||||||
|
MOD+TAB { focus-workspace-previous; }
|
||||||
|
|
||||||
|
// ─── Layout Controls ───
|
||||||
|
MOD+CTRL+F { expand-column-to-available-width; }
|
||||||
|
MOD+C { center-column; }
|
||||||
|
MOD+CTRL+C { center-visible-columns; }
|
||||||
|
MOD+MINUS { set-column-width "-10%"; }
|
||||||
|
MOD+EQUAL { set-column-width "+10%"; }
|
||||||
|
MOD+SHIFT+MINUS { set-window-height "-10%"; }
|
||||||
|
MOD+SHIFT+EQUAL { set-window-height "+10%"; }
|
||||||
|
|
||||||
|
// ─── Modes ───
|
||||||
|
MOD+T { toggle-window-floating; }
|
||||||
|
MOD+F { fullscreen-window; }
|
||||||
|
MOD+W { toggle-column-tabbed-display; }
|
||||||
|
|
||||||
|
// ─── Screenshots ───
|
||||||
|
CTRL+SHIFT+1 { screenshot; }
|
||||||
|
CTRL+SHIFT+2 { screenshot-screen; }
|
||||||
|
CTRL+SHIFT+3 { screenshot-window; }
|
||||||
|
|
||||||
|
// ─── Emergency Escape Key ───
|
||||||
|
// Use this when a fullscreen app blocks your keybinds.
|
||||||
|
// It disables any active keyboard shortcut inhibitor, restoring control.
|
||||||
|
MOD+ESCAPE allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||||
|
|
||||||
|
// ─── Exit / Power ───
|
||||||
|
CTRL+ALT+DELETE { quit; } // Also quits Niri
|
||||||
|
MOD+SHIFT+P { power-off-monitors; } // Turn off screens (useful for OLED or privacy)
|
||||||
|
MOD+O repeat=false { toggle-overview; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Startup Applications ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-at-startup
|
||||||
|
|
||||||
|
spawn-at-startup "/usr/lib/polkit-kde-authentication-agent-1" "&" // Polkit
|
||||||
|
spawn-at-startup "xwayland-satellite" // XWayland support
|
||||||
|
spawn-at-startup "swww-daemon" // Wallpaper daemon
|
||||||
|
spawn-at-startup "swww img" "/usr/share/wallpapers/cachyos-wallpapers/Skyscraper.png" // Set wallpaper
|
||||||
|
spawn-at-startup "qs" // Launch Quickshell
|
||||||
|
spawn-at-startup "vesktop" // Launch Vesktop
|
||||||
|
|
||||||
|
prefer-no-csd // Disable program decorations
|
||||||
|
screenshot-path null // Disable screenshot saving
|
||||||
|
|
||||||
|
// ────────────── Layout Settings ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
||||||
|
|
||||||
|
layout {
|
||||||
|
gaps 16 // Gap between windows
|
||||||
|
center-focused-column "never" // Don’t auto-center focused column
|
||||||
|
|
||||||
|
preset-column-widths {
|
||||||
|
proportion 0.33333
|
||||||
|
proportion 0.5
|
||||||
|
proportion 0.66667
|
||||||
|
}
|
||||||
|
|
||||||
|
focus-ring {
|
||||||
|
width 3
|
||||||
|
active-color "{{ color4 }}"
|
||||||
|
inactive-color "{{ color0 }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
shadow {
|
||||||
|
softness 30
|
||||||
|
spread 5
|
||||||
|
offset x=0 y=5
|
||||||
|
color "#0007"
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color "transparent"
|
||||||
|
|
||||||
|
struts {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Animation Settings ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations
|
||||||
|
animations {
|
||||||
|
workspace-switch {
|
||||||
|
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||||
|
}
|
||||||
|
window-open {
|
||||||
|
duration-ms 200
|
||||||
|
curve "ease-out-quad"
|
||||||
|
}
|
||||||
|
window-close {
|
||||||
|
duration-ms 200
|
||||||
|
curve "ease-out-cubic"
|
||||||
|
}
|
||||||
|
horizontal-view-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=900 epsilon=0.0001
|
||||||
|
}
|
||||||
|
window-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
window-resize {
|
||||||
|
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||||
|
}
|
||||||
|
config-notification-open-close {
|
||||||
|
spring damping-ratio=0.6 stiffness=1200 epsilon=0.001
|
||||||
|
}
|
||||||
|
screenshot-ui-open {
|
||||||
|
duration-ms 300
|
||||||
|
curve "ease-out-quad"
|
||||||
|
}
|
||||||
|
overview-open-close {
|
||||||
|
spring damping-ratio=1.0 stiffness=900 epsilon=0.0001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Named Workspaces ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
|
||||||
|
|
||||||
|
workspace "browser"
|
||||||
|
workspace "chat"
|
||||||
|
|
||||||
|
// ────────────── Window Rules ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
|
||||||
|
|
||||||
|
window-rule {
|
||||||
|
match at-startup=true app-id="vesktop"
|
||||||
|
open-on-workspace "chat"
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
|
||||||
|
window-rule {
|
||||||
|
match app-id="firefox"
|
||||||
|
open-on-workspace "browser"
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||||
|
open-floating true // Always float Firefox PiP windows
|
||||||
|
}
|
||||||
|
|
||||||
|
window-rule {
|
||||||
|
geometry-corner-radius 20 // Set every window radius to 20
|
||||||
|
clip-to-geometry true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Layer Rules ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layer-Rules
|
||||||
|
|
||||||
|
layer-rule {
|
||||||
|
match namespace="^swww-daemon$"
|
||||||
|
place-within-backdrop true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Environment Variables ──────────────
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#environment
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DISPLAY ":1"
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||||
|
QT_QPA_PLATFORM "wayland"
|
||||||
|
QT_WAYLAND_DISABLE_WINDOWDECORATION "1"
|
||||||
|
XDG_SESSION_TYPE "wayland"
|
||||||
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── Misc ──────────────
|
||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,9 @@ import qs.Components
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: taskbarWindow
|
id: taskbarWindow
|
||||||
visible: Settings.settings.showDock
|
visible: Settings.settings.showDock &&
|
||||||
|
(Settings.settings.dockMonitors.includes(modelData.name) ||
|
||||||
|
(Settings.settings.dockMonitors.length === 0))
|
||||||
screen: (typeof modelData !== 'undefined' ? modelData : null)
|
screen: (typeof modelData !== 'undefined' ? modelData : null)
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
anchors.bottom: true
|
anchors.bottom: true
|
||||||
|
|
@ -245,7 +247,7 @@ PanelWindow {
|
||||||
contextMenuVisible = false;
|
contextMenuVisible = false;
|
||||||
contextMenuTarget = null;
|
contextMenuTarget = null;
|
||||||
contextMenuToplevel = null;
|
contextMenuToplevel = null;
|
||||||
hidden = true; // Hide dock when context menu closes by clicking outside
|
hidden = true; // Hide dock when context menu closes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +283,7 @@ PanelWindow {
|
||||||
spacing: 4
|
spacing: 4
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
// Close
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 32
|
height: 32
|
||||||
|
|
@ -300,7 +302,7 @@ PanelWindow {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "close"
|
text: "close"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +310,7 @@ PanelWindow {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Close"
|
text: "Close"
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +324,7 @@ PanelWindow {
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (contextMenuToplevel?.close) contextMenuToplevel.close();
|
if (contextMenuToplevel?.close) contextMenuToplevel.close();
|
||||||
contextMenuVisible = false;
|
contextMenuVisible = false;
|
||||||
hidden = true; // Hide the dock here as well
|
hidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import qs.Components
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
// Test mode
|
|
||||||
property bool testMode: false
|
property bool testMode: false
|
||||||
property int testPercent: 49
|
property int testPercent: 49
|
||||||
property bool testCharging: true
|
property bool testCharging: true
|
||||||
|
|
@ -21,7 +21,7 @@ Item {
|
||||||
height: row.height
|
height: row.height
|
||||||
visible: testMode || (isReady && battery.isLaptopBattery)
|
visible: testMode || (isReady && battery.isLaptopBattery)
|
||||||
|
|
||||||
// Choose icon based on charge and charging state
|
|
||||||
function batteryIcon() {
|
function batteryIcon() {
|
||||||
if (!show)
|
if (!show)
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -32,7 +32,7 @@ Item {
|
||||||
if (percent >= 95)
|
if (percent >= 95)
|
||||||
return "battery_android_full";
|
return "battery_android_full";
|
||||||
|
|
||||||
// Hardcoded battery symbols
|
|
||||||
if (percent >= 85)
|
if (percent >= 85)
|
||||||
return "battery_android_6";
|
return "battery_android_6";
|
||||||
if (percent >= 70)
|
if (percent >= 70)
|
||||||
|
|
@ -58,7 +58,7 @@ Item {
|
||||||
Text {
|
Text {
|
||||||
text: batteryIcon()
|
text: batteryIcon()
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 28
|
font.pixelSize: 28 * Theme.scale(Screen)
|
||||||
color: charging ? Theme.accentPrimary : Theme.textSecondary
|
color: charging ? Theme.accentPrimary : Theme.textSecondary
|
||||||
verticalAlignment: Text.AlignVBottom
|
verticalAlignment: Text.AlignVBottom
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +66,7 @@ Item {
|
||||||
Text {
|
Text {
|
||||||
text: Math.round(percent) + "%"
|
text: Math.round(percent) + "%"
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 18
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
verticalAlignment: Text.AlignVBottom
|
verticalAlignment: Text.AlignVBottom
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,21 +152,21 @@ WlSessionLock {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 30
|
spacing: 30
|
||||||
width: Math.min(parent.width * 0.8, 400)
|
width: Math.min(parent.width * 0.8, 400 * Theme.scale(Screen))
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
width: 80
|
width: 80 * Theme.scale(Screen)
|
||||||
height: 80
|
height: 80 * Theme.scale(Screen)
|
||||||
radius: 40
|
radius: 40 * Theme.scale(Screen)
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: 40
|
radius: 40 * Theme.scale(Screen)
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 3
|
border.width: 3 * Theme.scale(Screen)
|
||||||
z: 2
|
z: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,28 +183,28 @@ WlSessionLock {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
text: Quickshell.env("USER")
|
text: Quickshell.env("USER")
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: 50
|
height: 50 * Theme.scale(Screen)
|
||||||
radius: 25
|
radius: 25 * Theme.scale(Screen)
|
||||||
color: Theme.surface
|
color: Theme.surface
|
||||||
opacity: passwordInput.activeFocus ? 0.8 : 0.3
|
opacity: passwordInput.activeFocus ? 0.8 : 0.3
|
||||||
border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
border.width: 2
|
border.width: 2 * Theme.scale(Screen)
|
||||||
|
|
||||||
TextInput {
|
TextInput {
|
||||||
id: passwordInput
|
id: passwordInput
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 15
|
anchors.margins: 15 * Theme.scale(Screen)
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
horizontalAlignment: TextInput.AlignHCenter
|
horizontalAlignment: TextInput.AlignHCenter
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
passwordCharacter: "●"
|
passwordCharacter: "●"
|
||||||
|
|
@ -218,7 +218,7 @@ WlSessionLock {
|
||||||
text: "Enter password..."
|
text: "Enter password..."
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
visible: !passwordInput.text && !passwordInput.activeFocus
|
visible: !passwordInput.text && !passwordInput.activeFocus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,9 +238,9 @@ WlSessionLock {
|
||||||
id: errorMessageRect
|
id: errorMessageRect
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
width: parent.width * 0.8
|
width: parent.width * 0.8
|
||||||
height: 44
|
height: 44 * Theme.scale(Screen)
|
||||||
color: Theme.overlay
|
color: Theme.overlay
|
||||||
radius: 20
|
radius: 20 * Theme.scale(Screen)
|
||||||
visible: lock.errorMessage !== ""
|
visible: lock.errorMessage !== ""
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|
@ -248,7 +248,7 @@ WlSessionLock {
|
||||||
text: lock.errorMessage
|
text: lock.errorMessage
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
opacity: 1
|
opacity: 1
|
||||||
visible: lock.errorMessage !== ""
|
visible: lock.errorMessage !== ""
|
||||||
}
|
}
|
||||||
|
|
@ -256,13 +256,13 @@ WlSessionLock {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
width: 120
|
width: 120 * Theme.scale(Screen)
|
||||||
height: 44
|
height: 44 * Theme.scale(Screen)
|
||||||
radius: 20
|
radius: 20 * Theme.scale(Screen)
|
||||||
opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5
|
opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5
|
||||||
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface
|
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 2
|
border.width: 2 * Theme.scale(Screen)
|
||||||
enabled: !lock.authenticating
|
enabled: !lock.authenticating
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|
@ -270,7 +270,7 @@ WlSessionLock {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: lock.authenticating ? "..." : "Unlock"
|
text: lock.authenticating ? "..." : "Unlock"
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
||||||
}
|
}
|
||||||
|
|
@ -294,37 +294,13 @@ WlSessionLock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: topRightCorner
|
|
||||||
position: "bottomleft"
|
|
||||||
size: 1.3
|
|
||||||
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
|
||||||
offsetX: screen.width / 2 + 38
|
|
||||||
offsetY: 0
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
z: 50
|
|
||||||
}
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: topLeftCorner
|
|
||||||
position: "bottomright"
|
|
||||||
size: 1.3
|
|
||||||
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
|
||||||
offsetX: -Screen.width / 2 - 38
|
|
||||||
offsetY: 0
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
z: 51
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: infoColumn.width + 32
|
width: infoColumn.width + 32 * Theme.scale(Screen)
|
||||||
height: infoColumn.height + 8
|
height: infoColumn.height + 8 * Theme.scale(Screen)
|
||||||
color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
bottomLeftRadius: 20
|
bottomLeftRadius: 20 * Theme.scale(Screen)
|
||||||
bottomRightRadius: 20
|
bottomRightRadius: 20 * Theme.scale(Screen)
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: infoColumn
|
id: infoColumn
|
||||||
|
|
@ -338,7 +314,7 @@ WlSessionLock {
|
||||||
id: timeText
|
id: timeText
|
||||||
text: Qt.formatDateTime(new Date(), "HH:mm")
|
text: Qt.formatDateTime(new Date(), "HH:mm")
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 48
|
font.pixelSize: 48 * Theme.scale(Screen)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
@ -348,7 +324,7 @@ WlSessionLock {
|
||||||
id: dateText
|
id: dateText
|
||||||
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
|
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
@ -364,7 +340,7 @@ WlSessionLock {
|
||||||
Text {
|
Text {
|
||||||
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
|
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 28
|
font.pixelSize: 28 * Theme.scale(Screen)
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +348,7 @@ WlSessionLock {
|
||||||
Text {
|
Text {
|
||||||
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9 / 5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
|
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9 / 5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 18
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
@ -383,7 +359,7 @@ WlSessionLock {
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
visible: weatherError !== ""
|
visible: weatherError !== ""
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
@ -425,12 +401,12 @@ WlSessionLock {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 48
|
width: 48 * Theme.scale(Screen)
|
||||||
height: 48
|
height: 48 * Theme.scale(Screen)
|
||||||
radius: 24
|
radius: 24 * Theme.scale(Screen)
|
||||||
color: shutdownArea.containsMouse ? Theme.error : "transparent"
|
color: shutdownArea.containsMouse ? Theme.error : "transparent"
|
||||||
border.color: Theme.error
|
border.color: Theme.error
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: shutdownArea
|
id: shutdownArea
|
||||||
|
|
@ -445,18 +421,18 @@ WlSessionLock {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "power_settings_new"
|
text: "power_settings_new"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
color: shutdownArea.containsMouse ? Theme.onAccent : Theme.error
|
color: shutdownArea.containsMouse ? Theme.onAccent : Theme.error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 48
|
width: 48 * Theme.scale(Screen)
|
||||||
height: 48
|
height: 48 * Theme.scale(Screen)
|
||||||
radius: 24
|
radius: 24 * Theme.scale(Screen)
|
||||||
color: rebootArea.containsMouse ? Theme.accentPrimary : "transparent"
|
color: rebootArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: rebootArea
|
id: rebootArea
|
||||||
|
|
@ -471,18 +447,18 @@ WlSessionLock {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "refresh"
|
text: "refresh"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
color: rebootArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
color: rebootArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 48
|
width: 48 * Theme.scale(Screen)
|
||||||
height: 48
|
height: 48 * Theme.scale(Screen)
|
||||||
radius: 24
|
radius: 24 * Theme.scale(Screen)
|
||||||
color: logoutArea.containsMouse ? Theme.accentSecondary : "transparent"
|
color: logoutArea.containsMouse ? Theme.accentSecondary : "transparent"
|
||||||
border.color: Theme.accentSecondary
|
border.color: Theme.accentSecondary
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: logoutArea
|
id: logoutArea
|
||||||
|
|
@ -497,7 +473,7 @@ WlSessionLock {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "exit_to_app"
|
text: "exit_to_app"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
color: logoutArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
|
color: logoutArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,31 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Settings
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
// The popup window
|
|
||||||
PanelWithOverlay {
|
PanelWithOverlay {
|
||||||
id: notificationHistoryWin
|
id: notificationHistoryWin
|
||||||
|
|
||||||
property string historyFilePath: Settings.settingsDir + "notification_history.json"
|
property string historyFilePath: Settings.settingsDir + "notification_history.json"
|
||||||
property bool hasUnread: notificationHistoryWinRect.hasUnread && !notificationHistoryWinRect.visible
|
property bool hasUnread: notificationHistoryWinRect.hasUnread && !notificationHistoryWinRect.visible
|
||||||
function addToHistory(notification) { notificationHistoryWinRect.addToHistory(notification) }
|
|
||||||
|
function addToHistory(notification) {
|
||||||
|
notificationHistoryWinRect.addToHistory(notification);
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: notificationHistoryWinRect
|
id: notificationHistoryWinRect
|
||||||
implicitWidth: 400
|
|
||||||
property int maxPopupHeight: 800
|
property int maxPopupHeight: 800
|
||||||
property int minPopupHeight: 210
|
property int minPopupHeight: 210
|
||||||
property int contentHeight: headerRow.height + historyList.contentHeight + 56
|
property int contentHeight: headerRow.height + historyList.contentHeight + 56
|
||||||
implicitHeight: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight)
|
|
||||||
visible: parent.visible
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.rightMargin: 4
|
|
||||||
color: Theme.backgroundPrimary
|
|
||||||
radius: 20
|
|
||||||
|
|
||||||
property int maxHistory: 100
|
property int maxHistory: 100
|
||||||
property bool hasUnread: false
|
property bool hasUnread: false
|
||||||
|
|
||||||
signal unreadChanged(bool hasUnread)
|
signal unreadChanged(bool hasUnread)
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: historyModel
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: historyFileView
|
|
||||||
path: notificationHistoryWin.historyFilePath
|
|
||||||
blockLoading: true
|
|
||||||
printErrors: true
|
|
||||||
watchChanges: true
|
|
||||||
|
|
||||||
JsonAdapter {
|
|
||||||
id: historyAdapter
|
|
||||||
property var notifications: []
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileChanged: historyFileView.reload()
|
|
||||||
onLoaded: notificationHistoryWinRect.loadHistory()
|
|
||||||
onLoadFailed: function (error) {
|
|
||||||
historyAdapter.notifications = [];
|
|
||||||
historyFileView.writeAdapter();
|
|
||||||
}
|
|
||||||
Component.onCompleted: if (path)
|
|
||||||
reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHasUnread() {
|
function updateHasUnread() {
|
||||||
var unread = false;
|
var unread = false;
|
||||||
for (let i = 0; i < historyModel.count; ++i) {
|
for (let i = 0; i < historyModel.count; ++i) {
|
||||||
|
|
@ -80,9 +50,11 @@ PanelWithOverlay {
|
||||||
if (typeof n === 'object' && n !== null) {
|
if (typeof n === 'object' && n !== null) {
|
||||||
if (n.read === undefined)
|
if (n.read === undefined)
|
||||||
n.read = false;
|
n.read = false;
|
||||||
|
|
||||||
// Mark as read if window is open
|
// Mark as read if window is open
|
||||||
if (notificationHistoryWinRect.visible)
|
if (notificationHistoryWinRect.visible)
|
||||||
n.read = true;
|
n.read = true;
|
||||||
|
|
||||||
historyModel.append(n);
|
historyModel.append(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,19 +67,19 @@ PanelWithOverlay {
|
||||||
const count = Math.min(historyModel.count, maxHistory);
|
const count = Math.min(historyModel.count, maxHistory);
|
||||||
for (let i = 0; i < count; ++i) {
|
for (let i = 0; i < count; ++i) {
|
||||||
let obj = historyModel.get(i);
|
let obj = historyModel.get(i);
|
||||||
if (typeof obj === 'object' && obj !== null) {
|
if (typeof obj === 'object' && obj !== null)
|
||||||
historyArray.push({
|
historyArray.push({
|
||||||
id: obj.id,
|
"id": obj.id,
|
||||||
appName: obj.appName,
|
"appName": obj.appName,
|
||||||
summary: obj.summary,
|
"summary": obj.summary,
|
||||||
body: obj.body,
|
"body": obj.body,
|
||||||
timestamp: obj.timestamp,
|
"timestamp": obj.timestamp,
|
||||||
read: obj.read === undefined ? false : obj.read
|
"read": obj.read === undefined ? false : obj.read
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
historyAdapter.notifications = historyArray;
|
historyAdapter.notifications = historyArray;
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function() {
|
||||||
historyFileView.writeAdapter();
|
historyFileView.writeAdapter();
|
||||||
});
|
});
|
||||||
updateHasUnread();
|
updateHasUnread();
|
||||||
|
|
@ -116,12 +88,12 @@ PanelWithOverlay {
|
||||||
function addToHistory(notification) {
|
function addToHistory(notification) {
|
||||||
if (!notification.id)
|
if (!notification.id)
|
||||||
notification.id = Date.now();
|
notification.id = Date.now();
|
||||||
|
|
||||||
if (!notification.timestamp)
|
if (!notification.timestamp)
|
||||||
notification.timestamp = new Date().toISOString();
|
notification.timestamp = new Date().toISOString();
|
||||||
|
|
||||||
// Mark as read if window is open
|
// Mark as read if window is open
|
||||||
notification.read = visible;
|
notification.read = visible;
|
||||||
|
|
||||||
// Remove duplicate by id
|
// Remove duplicate by id
|
||||||
for (let i = 0; i < historyModel.count; ++i) {
|
for (let i = 0; i < historyModel.count; ++i) {
|
||||||
if (historyModel.get(i).id === notification.id) {
|
if (historyModel.get(i).id === notification.id) {
|
||||||
|
|
@ -129,11 +101,10 @@ PanelWithOverlay {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
historyModel.insert(0, notification);
|
historyModel.insert(0, notification);
|
||||||
|
|
||||||
if (historyModel.count > maxHistory)
|
if (historyModel.count > maxHistory)
|
||||||
historyModel.remove(maxHistory);
|
historyModel.remove(maxHistory);
|
||||||
|
|
||||||
saveHistory();
|
saveHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +117,7 @@ PanelWithOverlay {
|
||||||
function formatTimestamp(ts) {
|
function formatTimestamp(ts) {
|
||||||
if (!ts)
|
if (!ts)
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts));
|
var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts));
|
||||||
var y = date.getFullYear();
|
var y = date.getFullYear();
|
||||||
var m = (date.getMonth() + 1).toString().padStart(2, '0');
|
var m = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
|
@ -155,6 +127,15 @@ PanelWithOverlay {
|
||||||
return `${y}-${m}-${d} ${h}:${min}`;
|
return `${y}-${m}-${d} ${h}:${min}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implicitWidth: 400
|
||||||
|
implicitHeight: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight)
|
||||||
|
visible: parent.visible
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
radius: 20
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
// Mark all as read when popup is opened
|
// Mark all as read when popup is opened
|
||||||
|
|
@ -167,9 +148,46 @@ PanelWithOverlay {
|
||||||
}
|
}
|
||||||
if (changed)
|
if (changed)
|
||||||
saveHistory();
|
saveHistory();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: historyModel
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: historyFileView
|
||||||
|
|
||||||
|
path: notificationHistoryWin.historyFilePath
|
||||||
|
blockLoading: true
|
||||||
|
printErrors: true
|
||||||
|
watchChanges: true
|
||||||
|
onFileChanged: historyFileView.reload()
|
||||||
|
onLoaded: notificationHistoryWinRect.loadHistory()
|
||||||
|
onLoadFailed: function(error) {
|
||||||
|
historyAdapter.notifications = [];
|
||||||
|
historyFileView.writeAdapter();
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (path) {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: historyAdapter
|
||||||
|
|
||||||
|
property var notifications: []
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: notificationHistoryWinRect.width
|
width: notificationHistoryWinRect.width
|
||||||
height: notificationHistoryWinRect.height
|
height: notificationHistoryWinRect.height
|
||||||
|
|
@ -184,6 +202,7 @@ PanelWithOverlay {
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: headerRow
|
id: headerRow
|
||||||
|
|
||||||
spacing: 4
|
spacing: 4
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
@ -193,6 +212,7 @@ PanelWithOverlay {
|
||||||
Layout.preferredHeight: 52
|
Layout.preferredHeight: 52
|
||||||
anchors.leftMargin: 16
|
anchors.leftMargin: 16
|
||||||
anchors.rightMargin: 16
|
anchors.rightMargin: 16
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Notification History"
|
text: "Notification History"
|
||||||
font.pixelSize: 18
|
font.pixelSize: 18
|
||||||
|
|
@ -200,11 +220,14 @@ PanelWithOverlay {
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: clearAllButton
|
id: clearAllButton
|
||||||
|
|
||||||
width: 90
|
width: 90
|
||||||
height: 32
|
height: 32
|
||||||
radius: 16
|
radius: 16
|
||||||
|
|
@ -212,9 +235,11 @@ PanelWithOverlay {
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "delete_sweep"
|
text: "delete_sweep"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
|
|
@ -222,6 +247,7 @@ PanelWithOverlay {
|
||||||
color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Clear"
|
text: "Clear"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|
@ -229,15 +255,20 @@ PanelWithOverlay {
|
||||||
font.bold: true
|
font.bold: true
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: clearAllMouseArea
|
id: clearAllMouseArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: notificationHistoryWinRect.clearHistory()
|
onClicked: notificationHistoryWinRect.clearHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -261,29 +292,36 @@ PanelWithOverlay {
|
||||||
radius: 20
|
radius: 20
|
||||||
z: 0
|
z: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: listContainer
|
id: listContainer
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: 12
|
anchors.topMargin: 12
|
||||||
anchors.bottomMargin: 12
|
anchors.bottomMargin: 12
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: historyList
|
id: historyList
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Math.min(contentHeight, parent.height)
|
height: Math.min(contentHeight, parent.height)
|
||||||
spacing: 12
|
spacing: 12
|
||||||
model: historyModel.count > 0 ? historyModel : placeholderModel
|
model: historyModel.count > 0 ? historyModel : placeholderModel
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: notificationCard.implicitHeight + 12
|
height: notificationCard.implicitHeight + 12
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: notificationCard
|
id: notificationCard
|
||||||
|
|
||||||
width: parent.width - 24
|
width: parent.width - 24
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
color: Theme.backgroundPrimary
|
color: Theme.backgroundPrimary
|
||||||
|
|
@ -292,16 +330,22 @@ PanelWithOverlay {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: 0
|
anchors.margins: 0
|
||||||
implicitHeight: contentColumn.implicitHeight + 20
|
implicitHeight: contentColumn.implicitHeight + 20
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: contentColumn
|
id: contentColumn
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 14
|
anchors.margins: 14
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: headerRow2
|
id: headerRow2
|
||||||
|
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: iconBackground
|
id: iconBackground
|
||||||
|
|
||||||
width: 28
|
width: 28
|
||||||
height: 28
|
height: 28
|
||||||
radius: 20
|
radius: 20
|
||||||
|
|
@ -309,6 +353,7 @@ PanelWithOverlay {
|
||||||
border.color: Qt.darker(Theme.accentPrimary, 1.2)
|
border.color: Qt.darker(Theme.accentPrimary, 1.2)
|
||||||
border.width: 1.2
|
border.width: 1.2
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
|
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
|
||||||
|
|
@ -317,11 +362,15 @@ PanelWithOverlay {
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: Theme.backgroundPrimary
|
color: Theme.backgroundPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: appInfoColumn
|
id: appInfoColumn
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: model.appName || "No Notifications"
|
text: model.appName || "No Notifications"
|
||||||
font.bold: true
|
font.bold: true
|
||||||
|
|
@ -330,6 +379,7 @@ PanelWithOverlay {
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: !model.isPlaceholder
|
visible: !model.isPlaceholder
|
||||||
text: model.timestamp ? notificationHistoryWinRect.formatTimestamp(model.timestamp) : ""
|
text: model.timestamp ? notificationHistoryWinRect.formatTimestamp(model.timestamp) : ""
|
||||||
|
|
@ -338,11 +388,15 @@ PanelWithOverlay {
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
font.pixelSize: Theme.fontSizeCaption
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "")
|
text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "")
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
|
|
@ -351,6 +405,7 @@ PanelWithOverlay {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: model.body || (model.isPlaceholder ? "No notifications to show." : "")
|
text: model.body || (model.isPlaceholder ? "No notifications to show." : "")
|
||||||
color: Theme.textDisabled
|
color: Theme.textDisabled
|
||||||
|
|
@ -359,12 +414,19 @@ PanelWithOverlay {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -375,14 +437,20 @@ PanelWithOverlay {
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
id: placeholderModel
|
id: placeholderModel
|
||||||
|
|
||||||
ListElement {
|
ListElement {
|
||||||
appName: ""
|
appName: ""
|
||||||
summary: ""
|
summary: ""
|
||||||
body: ""
|
body: ""
|
||||||
isPlaceholder: true
|
isPlaceholder: true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,49 @@ Item {
|
||||||
id: root
|
id: root
|
||||||
width: 22; height: 22
|
width: 22; height: 22
|
||||||
property bool isSilence: false
|
property bool isSilence: false
|
||||||
|
property var shell: null
|
||||||
|
|
||||||
// Process for executing CLI commands
|
|
||||||
Process {
|
Process {
|
||||||
id: rightClickProcess
|
id: rightClickProcess
|
||||||
command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"]
|
command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bell icon/button
|
// Timer to check when NotificationHistory is loaded
|
||||||
|
Timer {
|
||||||
|
id: checkHistoryTimer
|
||||||
|
interval: 50
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (shell && shell.notificationHistoryWin) {
|
||||||
|
shell.notificationHistoryWin.visible = true;
|
||||||
|
checkHistoryTimer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: bell
|
id: bell
|
||||||
width: 22; height: 22
|
width: 22; height: 22
|
||||||
Text {
|
Text {
|
||||||
id: bellText
|
id: bellText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: notificationHistoryWin.hasUnread ? "notifications_unread" : "notifications"
|
text: {
|
||||||
|
if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) {
|
||||||
|
return "notifications_unread";
|
||||||
|
} else {
|
||||||
|
return "notifications";
|
||||||
|
}
|
||||||
|
}
|
||||||
font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
font.weight: notificationHistoryWin.hasUnread ? Font.Bold : Font.Normal
|
font.weight: {
|
||||||
color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled)
|
if (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread) {
|
||||||
|
return Font.Bold;
|
||||||
|
} else {
|
||||||
|
return Font.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (shell && shell.notificationHistoryWin && shell.notificationHistoryWin.hasUnread ? Theme.accentPrimary : Theme.textDisabled)
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseAreaBell
|
id: mouseAreaBell
|
||||||
|
|
@ -42,10 +66,18 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton){
|
if (mouse.button === Qt.LeftButton){
|
||||||
notificationHistoryWin.visible = !notificationHistoryWin.visible
|
if (shell) {
|
||||||
return;
|
if (!shell.notificationHistoryWin) {
|
||||||
|
// Use the shell function to load notification history
|
||||||
|
shell.loadNotificationHistory();
|
||||||
|
checkHistoryTimer.start();
|
||||||
|
} else {
|
||||||
|
// Already loaded, just toggle visibility
|
||||||
|
shell.notificationHistoryWin.visible = !shell.notificationHistoryWin.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
onEntered: notificationTooltip.tooltipVisible = true
|
onEntered: notificationTooltip.tooltipVisible = true
|
||||||
onExited: notificationTooltip.tooltipVisible = false
|
onExited: notificationTooltip.tooltipVisible = false
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ PanelWindow {
|
||||||
|
|
||||||
anchors.top: true
|
anchors.top: true
|
||||||
anchors.right: true
|
anchors.right: true
|
||||||
margins.top: -20 // keep as you want
|
margins.top: -20
|
||||||
margins.right: 6
|
margins.right: 6
|
||||||
|
|
||||||
property var notifications: []
|
property var notifications: []
|
||||||
|
|
@ -52,7 +52,7 @@ PanelWindow {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
spacing: window.spacing
|
spacing: window.spacing
|
||||||
width: parent.width
|
width: parent.width
|
||||||
clip: false // prevent clipping during animation
|
clip: false // Prevent clipping during animation
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: notifications
|
model: notifications
|
||||||
|
|
|
||||||
|
|
@ -4,310 +4,389 @@ import Quickshell
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
|
|
||||||
PanelWindow {
|
// Main container that manages multiple notification popups for different monitors
|
||||||
id: window
|
Item {
|
||||||
implicitWidth: 350
|
id: notificationManager
|
||||||
implicitHeight: notificationColumn.implicitHeight
|
anchors.fill: parent
|
||||||
color: "transparent"
|
|
||||||
visible: notificationsVisible && notificationModel.count > 0
|
|
||||||
screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null
|
|
||||||
focusable: false
|
|
||||||
|
|
||||||
property bool barVisible: true
|
// Get list of available monitors/screens
|
||||||
|
property var monitors: Quickshell.screens || []
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("[NotificationPopup] Initialized with", monitors.length, "monitors");
|
||||||
|
for (let i = 0; i < monitors.length; i++) {
|
||||||
|
console.log("[NotificationPopup] Monitor", i, ":", monitors[i].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global visibility state for all notification popups
|
||||||
property bool notificationsVisible: true
|
property bool notificationsVisible: true
|
||||||
|
|
||||||
anchors.top: true
|
|
||||||
anchors.right: true
|
|
||||||
margins.top: 6
|
|
||||||
margins.right: 6
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: notificationModel
|
|
||||||
}
|
|
||||||
|
|
||||||
property int maxVisible: 5
|
|
||||||
property int spacing: 5
|
|
||||||
|
|
||||||
function togglePopup(): void {
|
function togglePopup(): void {
|
||||||
console.log("[NotificationPopup] Current state: " + notificationsVisible);
|
console.log("[NotificationManager] Current state: " + notificationsVisible);
|
||||||
notificationsVisible = !notificationsVisible;
|
notificationsVisible = !notificationsVisible;
|
||||||
console.log("[NotificationPopup] New state: " + notificationsVisible);
|
console.log("[NotificationManager] New state: " + notificationsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNotification(notification) {
|
function addNotification(notification): void {
|
||||||
notificationModel.insert(0, {
|
console.log("[NotificationPopup] Adding notification to popup manager");
|
||||||
id: notification.id,
|
// Add notification to all monitor popups
|
||||||
appName: notification.appName || "Notification",
|
for (let i = 0; i < children.length; i++) {
|
||||||
summary: notification.summary || "",
|
let child = children[i];
|
||||||
body: notification.body || "",
|
if (child.addNotification) {
|
||||||
urgency: notification.urgency || 0,
|
child.addNotification(notification);
|
||||||
rawNotification: notification,
|
|
||||||
appeared: false,
|
|
||||||
dismissed: false
|
|
||||||
});
|
|
||||||
|
|
||||||
while (notificationModel.count > maxVisible) {
|
|
||||||
notificationModel.remove(notificationModel.count - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissNotificationById(id) {
|
|
||||||
for (var i = 0; i < notificationModel.count; i++) {
|
|
||||||
if (notificationModel.get(i).id === id) {
|
|
||||||
dismissNotificationByIndex(i);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissNotificationByIndex(index) {
|
// Create a notification popup for each monitor
|
||||||
if (index >= 0 && index < notificationModel.count) {
|
Repeater {
|
||||||
var notif = notificationModel.get(index);
|
model: notificationManager.monitors
|
||||||
if (!notif.dismissed) {
|
delegate: Item {
|
||||||
notificationModel.set(index, {
|
id: delegateItem
|
||||||
id: notif.id,
|
|
||||||
appName: notif.appName,
|
// Make addNotification accessible from the Item level
|
||||||
summary: notif.summary,
|
function addNotification(notification) {
|
||||||
body: notif.body,
|
if (panelWindow) {
|
||||||
rawNotification: notif.rawNotification,
|
panelWindow.addNotification(notification);
|
||||||
appeared: notif.appeared,
|
}
|
||||||
dismissed: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
PanelWindow {
|
||||||
id: notificationColumn
|
id: panelWindow
|
||||||
anchors.right: parent.right
|
implicitWidth: 350
|
||||||
spacing: window.spacing
|
implicitHeight: Math.max(notificationColumn.height, 0)
|
||||||
width: parent.width
|
color: "transparent"
|
||||||
clip: false
|
visible: notificationManager.notificationsVisible && notificationModel.count > 0 && shouldShowOnThisMonitor
|
||||||
|
screen: modelData
|
||||||
|
focusable: false
|
||||||
|
|
||||||
Repeater {
|
property bool barVisible: true
|
||||||
id: notificationRepeater
|
property bool notificationsVisible: notificationManager.notificationsVisible
|
||||||
model: notificationModel
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
// Check if this monitor should show notifications - make it reactive to settings changes
|
||||||
id: notificationDelegate
|
property bool shouldShowOnThisMonitor: {
|
||||||
width: parent.width
|
let notificationMonitors = Settings.settings.notificationMonitors || [];
|
||||||
color: Theme.backgroundPrimary
|
let currentScreenName = modelData ? modelData.name : "";
|
||||||
radius: 20
|
// Show notifications on all monitors if notificationMonitors is empty or contains "*"
|
||||||
border.color: model.urgency == 2 ? Theme.warning : Theme.outline
|
let shouldShow = notificationMonitors.length === 0 ||
|
||||||
border.width: 1
|
notificationMonitors.includes("*") ||
|
||||||
|
notificationMonitors.includes(currentScreenName);
|
||||||
|
console.log("[NotificationPopup] Monitor", currentScreenName, "should show:", shouldShow, "monitors:", JSON.stringify(notificationMonitors));
|
||||||
|
return shouldShow;
|
||||||
|
}
|
||||||
|
|
||||||
property bool appeared: model.appeared
|
// Watch for changes in notification monitors setting
|
||||||
property bool dismissed: model.dismissed
|
Connections {
|
||||||
property var rawNotification: model.rawNotification
|
target: Settings.settings
|
||||||
|
function onNotificationMonitorsChanged() {
|
||||||
|
// Settings changed, visibility will update automatically
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
x: appeared ? 0 : width
|
anchors.top: true
|
||||||
opacity: dismissed ? 0 : 1
|
anchors.right: true
|
||||||
height: dismissed ? 0 : contentRow.height + 20
|
margins.top: 6
|
||||||
|
margins.right: 6
|
||||||
|
|
||||||
Row {
|
ListModel {
|
||||||
id: contentRow
|
id: notificationModel
|
||||||
anchors.centerIn: parent
|
}
|
||||||
spacing: 10
|
|
||||||
width: parent.width - 20
|
|
||||||
|
|
||||||
// Circular Icon container with border
|
property int maxVisible: 5
|
||||||
Rectangle {
|
property int spacing: 5
|
||||||
id: iconBackground
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
radius: width / 2 // Circular
|
|
||||||
color: Theme.accentPrimary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
border.color: Qt.darker(Theme.accentPrimary, 1.2)
|
|
||||||
border.width: 1.5
|
|
||||||
|
|
||||||
// Get all possible icon sources from notification
|
function addNotification(notification) {
|
||||||
property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""]
|
console.log("[NotificationPopup] Adding notification to monitor popup:", notification.appName);
|
||||||
|
notificationModel.insert(0, {
|
||||||
|
id: notification.id,
|
||||||
|
appName: notification.appName || "Notification",
|
||||||
|
summary: notification.summary || "",
|
||||||
|
body: notification.body || "",
|
||||||
|
urgency: notification.urgency || 0,
|
||||||
|
rawNotification: notification,
|
||||||
|
appeared: false,
|
||||||
|
dismissed: false
|
||||||
|
});
|
||||||
|
|
||||||
// Try to load notification icon
|
while (notificationModel.count > maxVisible) {
|
||||||
IconImage {
|
notificationModel.remove(notificationModel.count - 1);
|
||||||
id: iconImage
|
}
|
||||||
anchors.fill: parent
|
}
|
||||||
anchors.margins: 4
|
|
||||||
asynchronous: true
|
|
||||||
backer.fillMode: Image.PreserveAspectFit
|
|
||||||
source: {
|
|
||||||
for (var i = 0; i < iconBackground.iconSources.length; i++) {
|
|
||||||
var icon = iconBackground.iconSources[i];
|
|
||||||
if (!icon)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (icon.includes("?path=")) {
|
function dismissNotificationById(id) {
|
||||||
const [name, path] = icon.split("?path=");
|
for (var i = 0; i < notificationModel.count; i++) {
|
||||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
if (notificationModel.get(i).id === id) {
|
||||||
return `file://${path}/${fileName}`;
|
dismissNotificationByIndex(i);
|
||||||
}
|
break;
|
||||||
|
|
||||||
if (icon.startsWith('/')) {
|
|
||||||
return "file://" + icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
visible: status === Image.Ready && source.toString() !== ""
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to first letter of app name
|
function dismissNotificationByIndex(index) {
|
||||||
Text {
|
if (index >= 0 && index < notificationModel.count) {
|
||||||
anchors.centerIn: parent
|
var notif = notificationModel.get(index);
|
||||||
visible: !iconImage.visible
|
if (!notif.dismissed) {
|
||||||
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
|
notificationModel.set(index, {
|
||||||
font.family: Theme.fontFamily
|
id: notif.id,
|
||||||
font.pixelSize: Theme.fontSizeBody
|
appName: notif.appName,
|
||||||
font.bold: true
|
summary: notif.summary,
|
||||||
|
body: notif.body,
|
||||||
|
rawNotification: notif.rawNotification,
|
||||||
|
appeared: notif.appeared,
|
||||||
|
dismissed: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: notificationColumn
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: panelWindow.spacing
|
||||||
|
width: parent.width
|
||||||
|
clip: false
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: notificationRepeater
|
||||||
|
model: notificationModel
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: notificationDelegate
|
||||||
|
width: parent.width
|
||||||
color: Theme.backgroundPrimary
|
color: Theme.backgroundPrimary
|
||||||
}
|
radius: 20
|
||||||
}
|
border.color: model.urgency == 2 ? Theme.warning : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
Column {
|
property bool appeared: model.appeared
|
||||||
width: contentRow.width - iconBackground.width - 10
|
property bool dismissed: model.dismissed
|
||||||
spacing: 5
|
property var rawNotification: model.rawNotification
|
||||||
|
|
||||||
Text {
|
x: appeared ? 0 : width
|
||||||
text: model.appName
|
opacity: dismissed ? 0 : 1
|
||||||
width: parent.width
|
height: dismissed ? 0 : Math.max(contentRow.height, 60) + 20
|
||||||
color: Theme.textPrimary
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.bold: true
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: model.summary
|
|
||||||
width: parent.width
|
|
||||||
color: "#eeeeee"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
visible: text !== ""
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: model.body
|
|
||||||
width: parent.width
|
|
||||||
color: "#cccccc"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
visible: text !== ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Row {
|
||||||
interval: 4000
|
id: contentRow
|
||||||
running: !dismissed
|
anchors.centerIn: parent
|
||||||
repeat: false
|
spacing: 10
|
||||||
onTriggered: {
|
width: parent.width - 20
|
||||||
dismissAnimation.start();
|
|
||||||
if (rawNotification)
|
|
||||||
rawNotification.expire();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
// Circular Icon container with border
|
||||||
anchors.fill: parent
|
Rectangle {
|
||||||
onClicked: {
|
id: iconBackground
|
||||||
dismissAnimation.start();
|
width: 36
|
||||||
if (rawNotification)
|
height: 36
|
||||||
rawNotification.dismiss();
|
radius: width / 2
|
||||||
}
|
color: Theme.accentPrimary
|
||||||
}
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
border.color: Qt.darker(Theme.accentPrimary, 1.2)
|
||||||
|
border.width: 1.5
|
||||||
|
|
||||||
ParallelAnimation {
|
// Priority order for notification icons: image > appIcon > icon
|
||||||
id: dismissAnimation
|
property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""]
|
||||||
NumberAnimation {
|
|
||||||
target: notificationDelegate
|
// Load notification icon with fallback handling
|
||||||
property: "opacity"
|
IconImage {
|
||||||
to: 0
|
id: iconImage
|
||||||
duration: 150
|
anchors.fill: parent
|
||||||
}
|
anchors.margins: 4
|
||||||
NumberAnimation {
|
asynchronous: true
|
||||||
target: notificationDelegate
|
backer.fillMode: Image.PreserveAspectFit
|
||||||
property: "height"
|
source: {
|
||||||
to: 0
|
// Try each icon source in priority order
|
||||||
duration: 150
|
for (var i = 0; i < iconBackground.iconSources.length; i++) {
|
||||||
}
|
var icon = iconBackground.iconSources[i];
|
||||||
NumberAnimation {
|
if (!icon)
|
||||||
target: notificationDelegate
|
continue;
|
||||||
property: "x"
|
|
||||||
to: width
|
// Handle special path format from some notifications
|
||||||
duration: 150
|
if (icon.includes("?path=")) {
|
||||||
easing.type: Easing.InQuad
|
const [name, path] = icon.split("?path=");
|
||||||
}
|
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||||
onFinished: {
|
return `file://${path}/${fileName}`;
|
||||||
for (let i = 0; i < notificationModel.count; i++) {
|
}
|
||||||
if (notificationModel.get(i).id === notificationDelegate.id) {
|
|
||||||
notificationModel.remove(i);
|
// Handle absolute file paths
|
||||||
break;
|
if (icon.startsWith('/')) {
|
||||||
|
return "file://" + icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
visible: status === Image.Ready && source.toString() !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: show first letter of app name when no icon available
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !iconImage.visible
|
||||||
|
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeBody
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: contentRow.width - iconBackground.width - 10
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: model.appName
|
||||||
|
width: parent.width
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: model.summary
|
||||||
|
width: parent.width
|
||||||
|
color: "#eeeeee"
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
visible: text !== ""
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: model.body
|
||||||
|
width: parent.width
|
||||||
|
color: "#cccccc"
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeCaption
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
visible: text !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 4000
|
||||||
|
running: !dismissed
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
dismissAnimation.start();
|
||||||
|
if (rawNotification)
|
||||||
|
rawNotification.expire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
dismissAnimation.start();
|
||||||
|
if (rawNotification)
|
||||||
|
rawNotification.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: dismissAnimation
|
||||||
|
NumberAnimation {
|
||||||
|
target: notificationDelegate
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: 150
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: notificationDelegate
|
||||||
|
property: "height"
|
||||||
|
to: 0
|
||||||
|
duration: 150
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: notificationDelegate
|
||||||
|
property: "x"
|
||||||
|
to: width
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.InQuad
|
||||||
|
}
|
||||||
|
onFinished: {
|
||||||
|
for (let i = 0; i < notificationModel.count; i++) {
|
||||||
|
if (notificationModel.get(i).id === notificationDelegate.id) {
|
||||||
|
notificationModel.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: appearAnimation
|
||||||
|
NumberAnimation {
|
||||||
|
target: notificationDelegate
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
duration: 150
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: notificationDelegate
|
||||||
|
property: "height"
|
||||||
|
to: Math.max(contentRow.height, 60) + 20
|
||||||
|
duration: 150
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: notificationDelegate
|
||||||
|
property: "x"
|
||||||
|
to: 0
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: appearTimer
|
||||||
|
interval: 10
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
appearAnimation.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!appeared) {
|
||||||
|
opacity = 0;
|
||||||
|
height = 0;
|
||||||
|
x = width;
|
||||||
|
// Small delay to ensure contentRow has proper height
|
||||||
|
appearTimer.start();
|
||||||
|
for (let i = 0; i < notificationModel.count; i++) {
|
||||||
|
if (notificationModel.get(i).id === notificationDelegate.id) {
|
||||||
|
var oldItem = notificationModel.get(i);
|
||||||
|
notificationModel.set(i, {
|
||||||
|
id: oldItem.id,
|
||||||
|
appName: oldItem.appName,
|
||||||
|
summary: oldItem.summary,
|
||||||
|
body: oldItem.body,
|
||||||
|
rawNotification: oldItem.rawNotification,
|
||||||
|
appeared: true,
|
||||||
|
read: oldItem.read,
|
||||||
|
dismissed: oldItem.dismissed
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ParallelAnimation {
|
Connections {
|
||||||
id: appearAnimation
|
target: Quickshell
|
||||||
NumberAnimation {
|
function onScreensChanged() {
|
||||||
target: notificationDelegate
|
if (panelWindow.screen) {
|
||||||
property: "opacity"
|
x = panelWindow.screen.width - panelWindow.width - 20;
|
||||||
to: 1
|
|
||||||
duration: 150
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: notificationDelegate
|
|
||||||
property: "height"
|
|
||||||
to: contentRow.height + 20
|
|
||||||
duration: 150
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: notificationDelegate
|
|
||||||
property: "x"
|
|
||||||
to: 0
|
|
||||||
duration: 150
|
|
||||||
easing.type: Easing.OutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!appeared) {
|
|
||||||
opacity = 0;
|
|
||||||
height = 0;
|
|
||||||
x = width;
|
|
||||||
appearAnimation.start();
|
|
||||||
for (let i = 0; i < notificationModel.count; i++) {
|
|
||||||
if (notificationModel.get(i).id === notificationDelegate.id) {
|
|
||||||
var oldItem = notificationModel.get(i);
|
|
||||||
notificationModel.set(i, {
|
|
||||||
id: oldItem.id,
|
|
||||||
appName: oldItem.appName,
|
|
||||||
summary: oldItem.summary,
|
|
||||||
body: oldItem.body,
|
|
||||||
rawNotification: oldItem.rawNotification,
|
|
||||||
appeared: true,
|
|
||||||
read: oldItem.read,
|
|
||||||
dismissed: oldItem.dismissed
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: Quickshell
|
|
||||||
function onScreensChanged() {
|
|
||||||
if (window.screen) {
|
|
||||||
x = window.screen.width - width - 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,15 @@ ShellRoot {
|
||||||
cache: true
|
cache: true
|
||||||
smooth: true
|
smooth: true
|
||||||
mipmap: false
|
mipmap: false
|
||||||
visible: wallpaperSource !== "" // Show the original for FastBlur input
|
visible: wallpaperSource !== ""
|
||||||
}
|
}
|
||||||
MultiEffect {
|
MultiEffect {
|
||||||
id: overviewBgBlur
|
id: overviewBgBlur
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: bgImage
|
source: bgImage
|
||||||
blurEnabled: true
|
blurEnabled: true
|
||||||
blur: 0.48 // controls blur strength (0 to 1)
|
blur: 0.48
|
||||||
blurMax: 128 // max blur radius in pixels
|
blurMax: 128
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
|
||||||
500
Widgets/SettingsWindow/SettingsWindow.qml
Normal file
500
Widgets/SettingsWindow/SettingsWindow.qml
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Widgets.SettingsWindow.Tabs
|
||||||
|
import qs.Widgets.SettingsWindow.Tabs.Components
|
||||||
|
|
||||||
|
PanelWithOverlay {
|
||||||
|
id: panelMain
|
||||||
|
|
||||||
|
property int activeTabIndex: 0
|
||||||
|
|
||||||
|
// Function to show wallpaper selector
|
||||||
|
function showWallpaperSelector() {
|
||||||
|
if (wallpaperSelector)
|
||||||
|
wallpaperSelector.show();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to show settings window
|
||||||
|
function showSettings() {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to load component for a specific tab
|
||||||
|
function loadComponentForTab(tabIndex) {
|
||||||
|
const componentMap = {
|
||||||
|
"0": generalSettings,
|
||||||
|
"1": barSettings,
|
||||||
|
"2": timeWeatherSettings,
|
||||||
|
"3": recordingSettings,
|
||||||
|
"4": networkSettings,
|
||||||
|
"5": displaySettings,
|
||||||
|
"6": wallpaperSettings,
|
||||||
|
"7": miscSettings,
|
||||||
|
"8": aboutSettings
|
||||||
|
};
|
||||||
|
const tabNames = ["General", "Bar", "Time & Weather", "Screen Recorder", "Network", "Display", "Wallpaper", "Misc", "About"];
|
||||||
|
if (componentMap[tabIndex]) {
|
||||||
|
settingsLoader.sourceComponent = componentMap[tabIndex];
|
||||||
|
if (tabName)
|
||||||
|
tabName.text = tabNames[tabIndex];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
// Handle activeTabIndex changes
|
||||||
|
onActiveTabIndexChanged: {
|
||||||
|
if (activeTabIndex >= 0 && activeTabIndex <= 8)
|
||||||
|
loadComponentForTab(activeTabIndex);
|
||||||
|
|
||||||
|
}
|
||||||
|
// Add safety checks for component loading
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Ensure we start with a valid tab
|
||||||
|
if (activeTabIndex < 0 || activeTabIndex > 8)
|
||||||
|
activeTabIndex = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
// Cleanup when window is hidden
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
// Reset to default tab when hiding to prevent state issues
|
||||||
|
activeTabIndex = 0;
|
||||||
|
if (tabName)
|
||||||
|
tabName.text = "General";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: generalSettings
|
||||||
|
|
||||||
|
General {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: barSettings
|
||||||
|
|
||||||
|
Bar {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: timeWeatherSettings
|
||||||
|
|
||||||
|
TimeWeather {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: recordingSettings
|
||||||
|
|
||||||
|
ScreenRecorder {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: networkSettings
|
||||||
|
|
||||||
|
Network {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: miscSettings
|
||||||
|
|
||||||
|
Misc {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: aboutSettings
|
||||||
|
|
||||||
|
About {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: displaySettings
|
||||||
|
|
||||||
|
Display {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: wallpaperSettings
|
||||||
|
|
||||||
|
Wallpaper {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: settingsWindowRect
|
||||||
|
|
||||||
|
implicitWidth: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].width * 2 / 3, 1200) * Theme.scale(Screen) : 600 * Theme.scale(Screen)
|
||||||
|
implicitHeight: Quickshell.screens.length > 0 ? Math.min(Quickshell.screens[0].height * 2 / 3, 800) * Theme.scale(Screen) : 400 * Theme.scale(Screen)
|
||||||
|
visible: parent.visible
|
||||||
|
color: "transparent"
|
||||||
|
// Center the settings window on screen
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background rectangle
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 20 * Theme.scale(Screen)
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
source: background
|
||||||
|
anchors.fill: background
|
||||||
|
shadowEnabled: true
|
||||||
|
shadowColor: Theme.shadow
|
||||||
|
shadowOpacity: 0.3
|
||||||
|
shadowHorizontalOffset: 0
|
||||||
|
shadowVerticalOffset: 2
|
||||||
|
shadowBlur: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: settings
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
topRightRadius: 20 * Theme.scale(Screen)
|
||||||
|
bottomRightRadius: 20 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: tabs.right
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
right: parent.right
|
||||||
|
margins: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: headerArea
|
||||||
|
|
||||||
|
height: 48 * Theme.scale(Screen)
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
margins: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 12 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: tabName
|
||||||
|
|
||||||
|
text: wallpaperSelector.visible ? "Select Wallpaper" : (activeTabIndex === 0 ? "General" : activeTabIndex === 1 ? "Bar" : activeTabIndex === 2 ? "Time & Weather" : activeTabIndex === 3 ? "Screen Recorder" : activeTabIndex === 4 ? "Network" : activeTabIndex === 5 ? "Display" : activeTabIndex === 6 ? "Wallpaper" : activeTabIndex === 7 ? "Misc" : activeTabIndex === 8 ? "About" : "General")
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallpaper Selection Button (only visible on Wallpaper tab)
|
||||||
|
Rectangle {
|
||||||
|
width: 160 * Theme.scale(Screen)
|
||||||
|
height: 32 * Theme.scale(Screen)
|
||||||
|
radius: 16 * Theme.scale(Screen)
|
||||||
|
color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
visible: activeTabIndex === 6 // Wallpaper tab index
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "image"
|
||||||
|
font.family: wallpaperButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Select Wallpaper"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: wallpaperButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
// Show the wallpaper selector
|
||||||
|
wallpaperSelector.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32 * Theme.scale(Screen)
|
||||||
|
height: 32 * Theme.scale(Screen)
|
||||||
|
radius: 16 * Theme.scale(Screen)
|
||||||
|
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
// If wallpaper selector is open, close it instead of the settings window
|
||||||
|
if (wallpaperSelector.visible) {
|
||||||
|
wallpaperSelector.hide();
|
||||||
|
} else {
|
||||||
|
panelMain.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: 1 * Theme.scale(Screen)
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.3
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: headerArea.bottom
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
margins: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: settingsContainer
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: headerArea.bottom
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
bottom: parent.bottom
|
||||||
|
topMargin: 32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified single loader approach
|
||||||
|
Loader {
|
||||||
|
id: settingsLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: generalSettings
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallpaper Selector Component - positioned as overlay
|
||||||
|
WallpaperSelector {
|
||||||
|
id: wallpaperSelector
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: tabs
|
||||||
|
|
||||||
|
color: Theme.surface
|
||||||
|
width: parent.width * 0.25
|
||||||
|
height: settingsWindowRect.height
|
||||||
|
topLeftRadius: 20 * Theme.scale(Screen)
|
||||||
|
bottomLeftRadius: 20 * Theme.scale(Screen)
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 0 * Theme.scale(Screen)
|
||||||
|
topPadding: 8 * Theme.scale(Screen)
|
||||||
|
bottomPadding: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: repeater
|
||||||
|
|
||||||
|
model: [{
|
||||||
|
"icon": "tune",
|
||||||
|
"text": "General"
|
||||||
|
}, {
|
||||||
|
"icon": "space_dashboard",
|
||||||
|
"text": "Bar"
|
||||||
|
}, {
|
||||||
|
"icon": "schedule",
|
||||||
|
"text": "Time & Weather"
|
||||||
|
}, {
|
||||||
|
"icon": "photo_camera",
|
||||||
|
"text": "Screen Recorder"
|
||||||
|
}, {
|
||||||
|
"icon": "wifi",
|
||||||
|
"text": "Network"
|
||||||
|
}, {
|
||||||
|
"icon": "monitor",
|
||||||
|
"text": "Display"
|
||||||
|
}, {
|
||||||
|
"icon": "wallpaper",
|
||||||
|
"text": "Wallpaper"
|
||||||
|
}, {
|
||||||
|
"icon": "settings_suggest",
|
||||||
|
"text": "Misc"
|
||||||
|
}, {
|
||||||
|
"icon": "info",
|
||||||
|
"text": "About"
|
||||||
|
}]
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: tabs.width
|
||||||
|
height: 48 * Theme.scale(Screen)
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: activeIndicator
|
||||||
|
|
||||||
|
Layout.leftMargin: 8 * Theme.scale(Screen)
|
||||||
|
Layout.preferredWidth: 3 * Theme.scale(Screen)
|
||||||
|
Layout.preferredHeight: 24 * Theme.scale(Screen)
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
radius: 2 * Theme.scale(Screen)
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
opacity: index === activeTabIndex ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: icon
|
||||||
|
|
||||||
|
text: modelData.icon
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: index === activeTabIndex ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
opacity: index === activeTabIndex ? 1 : 0.8
|
||||||
|
Layout.leftMargin: 20 * Theme.scale(Screen)
|
||||||
|
Layout.preferredWidth: 24 * Theme.scale(Screen)
|
||||||
|
Layout.preferredHeight: 24 * Theme.scale(Screen)
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: label
|
||||||
|
|
||||||
|
text: modelData.text
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: index === activeTabIndex ? Theme.accentPrimary : (tabMouseArea.containsMouse ? Theme.accentPrimary : Theme.textSecondary)
|
||||||
|
font.weight: index === activeTabIndex ? Font.DemiBold : (tabMouseArea.containsMouse ? Font.DemiBold : Font.Normal)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 24 * Theme.scale(Screen)
|
||||||
|
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: 4 * Theme.scale(Screen)
|
||||||
|
Layout.rightMargin: 16 * Theme.scale(Screen)
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tabMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
activeTabIndex = index;
|
||||||
|
loadComponentForTab(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1 * Theme.scale(Screen)
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.6
|
||||||
|
visible: index < (repeater.count - 1)
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
441
Widgets/SettingsWindow/Tabs/About.qml
Normal file
441
Widgets/SettingsWindow/Tabs/About.qml
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string latestVersion: "Unknown"
|
||||||
|
property string currentVersion: "Unknown"
|
||||||
|
property var contributors: []
|
||||||
|
property string githubDataPath: Settings.settingsDir + "github_data.json"
|
||||||
|
|
||||||
|
function loadFromFile() {
|
||||||
|
const now = Date.now();
|
||||||
|
const data = githubData;
|
||||||
|
if (!data.timestamp || (now - data.timestamp > 3.6e+06)) {
|
||||||
|
console.log("[About] Cache expired or missing, fetching new data from GitHub...");
|
||||||
|
fetchFromGitHub();
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
console.log("[About] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)");
|
||||||
|
if (data.version)
|
||||||
|
root.latestVersion = data.version;
|
||||||
|
|
||||||
|
if (data.contributors)
|
||||||
|
root.contributors = data.contributors;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchFromGitHub() {
|
||||||
|
versionProcess.running = true;
|
||||||
|
contributorsProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveData() {
|
||||||
|
githubData.timestamp = Date.now();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
githubDataFile.writeAdapter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: currentVersionProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"]
|
||||||
|
Component.onCompleted: {
|
||||||
|
running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const version = text.trim();
|
||||||
|
if (version && version !== "Unknown") {
|
||||||
|
root.currentVersion = version;
|
||||||
|
} else {
|
||||||
|
currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"];
|
||||||
|
currentVersionProcess.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: githubDataFile
|
||||||
|
|
||||||
|
path: root.githubDataPath
|
||||||
|
blockLoading: true
|
||||||
|
printErrors: true
|
||||||
|
watchChanges: true
|
||||||
|
onFileChanged: githubDataFile.reload()
|
||||||
|
onLoaded: loadFromFile()
|
||||||
|
onLoadFailed: function(error) {
|
||||||
|
console.log("GitHub data file doesn't exist yet, creating it...");
|
||||||
|
githubData.version = "Unknown";
|
||||||
|
githubData.contributors = [];
|
||||||
|
githubData.timestamp = 0;
|
||||||
|
githubDataFile.writeAdapter();
|
||||||
|
fetchFromGitHub();
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (path)
|
||||||
|
reload();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: githubData
|
||||||
|
|
||||||
|
property string version: "Unknown"
|
||||||
|
property var contributors: []
|
||||||
|
property double timestamp: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: versionProcess
|
||||||
|
|
||||||
|
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
if (data.tag_name) {
|
||||||
|
const version = data.tag_name;
|
||||||
|
githubData.version = version;
|
||||||
|
root.latestVersion = version;
|
||||||
|
console.log("[About] Latest version fetched from GitHub:", version);
|
||||||
|
} else {
|
||||||
|
console.log("No tag_name in GitHub response");
|
||||||
|
}
|
||||||
|
saveData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse version:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: contributorsProcess
|
||||||
|
|
||||||
|
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
githubData.contributors = data || [];
|
||||||
|
root.contributors = githubData.contributors;
|
||||||
|
console.log("[About] Contributors data fetched from GitHub:", githubData.contributors.length, "contributors");
|
||||||
|
saveData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse contributors:", e);
|
||||||
|
root.contributors = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Noctalia: quiet by design"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
Layout.bottomMargin: 8 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "It may just be another quickshell setup but it won't get in your way."
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
columns: 2
|
||||||
|
rowSpacing: 4
|
||||||
|
columnSpacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Latest Version:"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.latestVersion
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Installed Version:"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.currentVersion
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.preferredWidth: updateText.implicitWidth + 46
|
||||||
|
Layout.preferredHeight: 32
|
||||||
|
radius: 20
|
||||||
|
color: updateArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 1
|
||||||
|
visible: {
|
||||||
|
if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const latest = root.latestVersion.replace("v", "").split(".");
|
||||||
|
const current = root.currentVersion.replace("v", "").split(".");
|
||||||
|
for (let i = 0; i < Math.max(latest.length, current.length); i++) {
|
||||||
|
const l = parseInt(latest[i] || "0");
|
||||||
|
const c = parseInt(current[i] || "0");
|
||||||
|
if (l > c)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (l < c)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "system_update"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: updateText
|
||||||
|
|
||||||
|
text: "Download latest release"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: updateArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: updateArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 26
|
||||||
|
Layout.bottomMargin: 18
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.leftMargin: 32
|
||||||
|
Layout.rightMargin: 32
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Contributors"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "(" + root.contributors.length + ")"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: contributorsGrid
|
||||||
|
|
||||||
|
Layout.leftMargin: 32
|
||||||
|
Layout.rightMargin: 32
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
width: 200 * 3
|
||||||
|
height: 300
|
||||||
|
cellWidth: 200
|
||||||
|
cellHeight: 100
|
||||||
|
model: root.contributors
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: contributorsGrid.cellWidth - 8
|
||||||
|
height: contributorsGrid.cellHeight - 4
|
||||||
|
radius: 20
|
||||||
|
color: contributorArea.containsMouse ? Theme.highlight : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: avatarImage
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: modelData.avatar_url || ""
|
||||||
|
sourceSize: Qt.size(80, 80)
|
||||||
|
visible: false
|
||||||
|
mipmap: true
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
cache: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: avatarImage
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: mask
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: mask
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
layer.enabled: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: avatarImage.width / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "person"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary
|
||||||
|
visible: !avatarImage.source || avatarImage.status !== Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.login || "Unknown"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: (modelData.contributions || 0) + " commits"
|
||||||
|
font.pixelSize: 11 * Theme.scale(Screen)
|
||||||
|
color: contributorArea.containsMouse ? Theme.backgroundPrimary : Theme.textSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: contributorArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.html_url)
|
||||||
|
Quickshell.execDetached(["xdg-open", modelData.html_url]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
Widgets/SettingsWindow/Tabs/Bar.qml
Normal file
86
Widgets/SettingsWindow/Tabs/Bar.qml
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Elements"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show Active Window"
|
||||||
|
description: "Display the title of the currently focused window below the bar"
|
||||||
|
value: Settings.settings.showActiveWindow
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showActiveWindow = !Settings.settings.showActiveWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show Active Window Icon"
|
||||||
|
description: "Display the icon of the currently focused window"
|
||||||
|
value: Settings.settings.showActiveWindowIcon
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showActiveWindowIcon = !Settings.settings.showActiveWindowIcon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show System Info"
|
||||||
|
description: "Display system information (CPU, RAM, Temperature)"
|
||||||
|
value: Settings.settings.showSystemInfoInBar
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showSystemInfoInBar = !Settings.settings.showSystemInfoInBar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show Taskbar"
|
||||||
|
description: "Display a taskbar showing currently open windows"
|
||||||
|
value: Settings.settings.showTaskbar
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showTaskbar = !Settings.settings.showTaskbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show Media"
|
||||||
|
description: "Display media controls and information"
|
||||||
|
value: Settings.settings.showMediaInBar
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showMediaInBar = !Settings.settings.showMediaInBar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
97
Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml
Normal file
97
Widgets/SettingsWindow/Tabs/Components/UnitSelector.qml
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
width: 64 * Theme.scale(Screen)
|
||||||
|
height: 32 * Theme.scale(Screen)
|
||||||
|
radius: 16 * Theme.scale(Screen)
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
property bool useFahrenheit: Settings.settings.useFahrenheit
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: slider
|
||||||
|
width: parent.width / 2 - 4 * Theme.scale(Screen)
|
||||||
|
height: parent.height - 4 * Theme.scale(Screen)
|
||||||
|
radius: 14 * Theme.scale(Screen)
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
x: 2 + (useFahrenheit ? parent.width / 2 : 0)
|
||||||
|
y: 2
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width / 2
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "°C"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: !useFahrenheit
|
||||||
|
color: !useFahrenheit ? Theme.onAccent : Theme.textPrimary
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: 200 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (useFahrenheit) {
|
||||||
|
Settings.settings.useFahrenheit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width / 2
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "°F"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: useFahrenheit
|
||||||
|
color: useFahrenheit ? Theme.onAccent : Theme.textPrimary
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: 200 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!useFahrenheit) {
|
||||||
|
Settings.settings.useFahrenheit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
Widgets/SettingsWindow/Tabs/Components/WallpaperSelector.qml
Normal file
171
Widgets/SettingsWindow/Tabs/Components/WallpaperSelector.qml
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: wallpaperOverlay
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
// Function to show the overlay and load wallpapers
|
||||||
|
function show() {
|
||||||
|
// Ensure wallpapers are loaded
|
||||||
|
WallpaperManager.loadWallpapers();
|
||||||
|
wallpaperOverlay.visible = true;
|
||||||
|
wallpaperOverlay.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to hide the overlay
|
||||||
|
function hide() {
|
||||||
|
wallpaperOverlay.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
visible: false
|
||||||
|
z: 1000
|
||||||
|
|
||||||
|
// Handle escape key to close
|
||||||
|
Keys.onPressed: function(event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
wallpaperOverlay.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click outside to close
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
wallpaperOverlay.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content area that stops event propagation
|
||||||
|
MouseArea {
|
||||||
|
// Stop event propagation
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 24
|
||||||
|
onClicked: {
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Wallpaper Grid
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: wallpaperGrid
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
cellWidth: Math.max(120 * Theme.scale(Screen), (parent.width / 3) - 12 * Theme.scale(Screen))
|
||||||
|
cellHeight: cellWidth * 0.6
|
||||||
|
model: WallpaperManager.wallpaperList
|
||||||
|
cacheBuffer: 64
|
||||||
|
leftMargin: 8
|
||||||
|
rightMargin: 8
|
||||||
|
topMargin: 8
|
||||||
|
bottomMargin: 8
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
width: wallpaperGrid.cellWidth - 8 * Theme.scale(Screen)
|
||||||
|
height: wallpaperGrid.cellHeight - 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: wallpaperItem
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 3
|
||||||
|
color: Theme.surface
|
||||||
|
radius: 12 * Theme.scale(Screen)
|
||||||
|
border.color: Settings.settings.currentWallpaper === modelData ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 2 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: wallpaperImage
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: modelData
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
sourceSize.width: Math.min(width, 480 * Theme.scale(Screen))
|
||||||
|
sourceSize.height: Math.min(height, 270 * Theme.scale(Screen))
|
||||||
|
opacity: (wallpaperImage.status == Image.Ready) ? 1 : 0
|
||||||
|
// Apply circular mask for rounded corners
|
||||||
|
layer.enabled: true
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: mask
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: mask
|
||||||
|
|
||||||
|
anchors.fill: wallpaperImage
|
||||||
|
layer.enabled: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: wallpaperImage.width
|
||||||
|
height: wallpaperImage.height
|
||||||
|
radius: 12 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
WallpaperManager.changeWallpaper(modelData);
|
||||||
|
wallpaperOverlay.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
373
Widgets/SettingsWindow/Tabs/Display.qml
Normal file
373
Widgets/SettingsWindow/Tabs/Display.qml
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Get list of available monitors/screens
|
||||||
|
property var monitors: Quickshell.screens || []
|
||||||
|
|
||||||
|
// Sorted monitors by name
|
||||||
|
property var sortedMonitors: {
|
||||||
|
let sorted = [...monitors];
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
let nameA = a.name || "Unknown";
|
||||||
|
let nameB = b.name || "Unknown";
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Monitor Selection"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Bar Monitors"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Select which monitors to display the top panel/bar on"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.sortedMonitors
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: barCheckbox
|
||||||
|
property bool isChecked: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Initialize checkbox state from settings
|
||||||
|
let monitors = Settings.settings.barMonitors || [];
|
||||||
|
isChecked = monitors.includes(modelData.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
width: checkboxContent.implicitWidth + 16
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant
|
||||||
|
border.color: isChecked ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: checkboxContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: barCheckbox.isChecked ? "check" : ""
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: barCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
|
||||||
|
visible: barCheckbox.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name || "Unknown"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: barCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
isChecked = !isChecked;
|
||||||
|
|
||||||
|
// Update settings array when checkbox is toggled
|
||||||
|
let monitors = Settings.settings.barMonitors || [];
|
||||||
|
monitors = [...monitors]; // Create copy to trigger reactivity
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
if (!monitors.includes(modelData.name)) {
|
||||||
|
monitors.push(modelData.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
monitors = monitors.filter(name => name !== modelData.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.settings.barMonitors = monitors;
|
||||||
|
console.log("Bar monitors updated:", JSON.stringify(monitors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Dock Monitors"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Select which monitors to display the application dock on"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.sortedMonitors
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: dockCheckbox
|
||||||
|
property bool isChecked: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Initialize with current settings
|
||||||
|
let monitors = Settings.settings.dockMonitors || [];
|
||||||
|
isChecked = monitors.includes(modelData.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
width: checkboxContent.implicitWidth + 16
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant
|
||||||
|
border.color: isChecked ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: checkboxContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: dockCheckbox.isChecked ? "check" : ""
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
|
||||||
|
visible: dockCheckbox.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name || "Unknown"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: dockCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
// Toggle state immediately for UI responsiveness
|
||||||
|
isChecked = !isChecked;
|
||||||
|
|
||||||
|
// Update settings
|
||||||
|
let monitors = Settings.settings.dockMonitors || [];
|
||||||
|
monitors = [...monitors]; // Copy array
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
// Add to array if not already there
|
||||||
|
if (!monitors.includes(modelData.name)) {
|
||||||
|
monitors.push(modelData.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove from array
|
||||||
|
monitors = monitors.filter(name => name !== modelData.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.settings.dockMonitors = monitors;
|
||||||
|
console.log("Dock monitors updated:", JSON.stringify(monitors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Notification Monitors"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Select which monitors to display system notifications on"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.sortedMonitors
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: notificationCheckbox
|
||||||
|
property bool isChecked: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Initialize with current settings
|
||||||
|
let monitors = Settings.settings.notificationMonitors || [];
|
||||||
|
isChecked = monitors.includes(modelData.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
width: checkboxContent.implicitWidth + 16
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: isChecked ? Theme.accentPrimary : Theme.surfaceVariant
|
||||||
|
border.color: isChecked ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: checkboxContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: notificationCheckbox.isChecked ? "check" : ""
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textSecondary
|
||||||
|
visible: notificationCheckbox.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name || "Unknown"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: notificationCheckbox.isChecked ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
// Toggle state immediately for UI responsiveness
|
||||||
|
isChecked = !isChecked;
|
||||||
|
|
||||||
|
// Update settings
|
||||||
|
let monitors = Settings.settings.notificationMonitors || [];
|
||||||
|
monitors = [...monitors]; // Copy array
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
// Add to array if not already there
|
||||||
|
if (!monitors.includes(modelData.name)) {
|
||||||
|
monitors.push(modelData.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove from array
|
||||||
|
monitors = monitors.filter(name => name !== modelData.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.settings.notificationMonitors = monitors;
|
||||||
|
console.log("Notification monitors updated:", JSON.stringify(monitors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
166
Widgets/SettingsWindow/Tabs/General.qml
Normal file
166
Widgets/SettingsWindow/Tabs/General.qml
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Profile"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Profile Image"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 4 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Your profile picture displayed in various places throughout the shell"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8 * Theme.scale(Screen)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 48 * Theme.scale(Screen)
|
||||||
|
height: 48 * Theme.scale(Screen)
|
||||||
|
radius: 24 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
radius: 24 * Theme.scale(Screen)
|
||||||
|
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 2 * Theme.scale(Screen)
|
||||||
|
z: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40 * Theme.scale(Screen)
|
||||||
|
radius: 16 * Theme.scale(Screen)
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: profileImageInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12 * Theme.scale(Screen)
|
||||||
|
anchors.rightMargin: 12 * Theme.scale(Screen)
|
||||||
|
anchors.topMargin: 6 * Theme.scale(Screen)
|
||||||
|
anchors.bottomMargin: 6 * Theme.scale(Screen)
|
||||||
|
text: Settings.settings.profileImage
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
clip: true
|
||||||
|
selectByMouse: true
|
||||||
|
activeFocusOnTab: true
|
||||||
|
inputMethodHints: Qt.ImhUrlCharactersOnly
|
||||||
|
onTextChanged: {
|
||||||
|
Settings.settings.profileImage = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onClicked: profileImageInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 26 * Theme.scale(Screen)
|
||||||
|
Layout.bottomMargin: 18 * Theme.scale(Screen)
|
||||||
|
height: 1 * Theme.scale(Screen)
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "User Interface"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show Corners"
|
||||||
|
description: "Display rounded corners on the edge of the screen"
|
||||||
|
value: Settings.settings.showCorners
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showCorners = !Settings.settings.showCorners;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show Dock"
|
||||||
|
description: "Display a dock at the bottom of the screen for quick access to applications"
|
||||||
|
value: Settings.settings.showDock
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showDock = !Settings.settings.showDock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Dim Desktop"
|
||||||
|
description: "Dim the desktop when panels or menus are open"
|
||||||
|
value: Settings.settings.dimPanels
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.dimPanels = !Settings.settings.dimPanels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
148
Widgets/SettingsWindow/Tabs/Misc.qml
Normal file
148
Widgets/SettingsWindow/Tabs/Misc.qml
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Media"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Visualizer Type"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Choose the style of the audio visualizer"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: visualizerTypeComboBox
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
model: ["radial", "fire", "diamond"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.visualizerType)
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.visualizerType = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 120
|
||||||
|
implicitHeight: 40
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: visualizerTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
leftPadding: 12
|
||||||
|
rightPadding: visualizerTypeComboBox.indicator.width + visualizerTypeComboBox.spacing
|
||||||
|
text: visualizerTypeComboBox.displayText.charAt(0).toUpperCase() + visualizerTypeComboBox.displayText.slice(1)
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator: Text {
|
||||||
|
x: visualizerTypeComboBox.width - width - 12
|
||||||
|
y: visualizerTypeComboBox.topPadding + (visualizerTypeComboBox.availableHeight - height) / 2
|
||||||
|
text: "arrow_drop_down"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: visualizerTypeComboBox.height
|
||||||
|
width: visualizerTypeComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 8
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: visualizerTypeComboBox.popup.visible ? visualizerTypeComboBox.delegateModel : null
|
||||||
|
currentIndex: visualizerTypeComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: visualizerTypeComboBox.width
|
||||||
|
highlighted: visualizerTypeComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: modelData.charAt(0).toUpperCase() + modelData.slice(1)
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
Widgets/SettingsWindow/Tabs/Network.qml
Normal file
86
Widgets/SettingsWindow/Tabs/Network.qml
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Wi-Fi"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Enable Wi-Fi"
|
||||||
|
description: "Turn Wi-Fi radio on or off"
|
||||||
|
value: Settings.settings.wifiEnabled
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.wifiEnabled = !Settings.settings.wifiEnabled;
|
||||||
|
Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.settings.wifiEnabled ? "on" : "off"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 26
|
||||||
|
Layout.bottomMargin: 18
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Bluetooth"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Enable Bluetooth"
|
||||||
|
description: "Turn Bluetooth radio on or off"
|
||||||
|
value: Settings.settings.bluetoothEnabled
|
||||||
|
onToggled: function() {
|
||||||
|
if (Bluetooth.defaultAdapter) {
|
||||||
|
Settings.settings.bluetoothEnabled = !Settings.settings.bluetoothEnabled;
|
||||||
|
Bluetooth.defaultAdapter.enabled = Settings.settings.bluetoothEnabled;
|
||||||
|
if (Bluetooth.defaultAdapter.enabled)
|
||||||
|
Bluetooth.defaultAdapter.discovering = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
Widgets/SettingsWindow/Tabs/Record.qml
Normal file
19
Widgets/SettingsWindow/Tabs/Record.qml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Components
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
spacing: 24
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Coming soon..."
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
Layout.topMargin: 32
|
||||||
|
}
|
||||||
|
}
|
||||||
812
Widgets/SettingsWindow/Tabs/ScreenRecorder.qml
Normal file
812
Widgets/SettingsWindow/Tabs/ScreenRecorder.qml
Normal file
|
|
@ -0,0 +1,812 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
// Text {
|
||||||
|
// text: "Screen Recording"
|
||||||
|
// font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
// font.bold: true
|
||||||
|
// color: Theme.textPrimary
|
||||||
|
// Layout.bottomMargin: 8
|
||||||
|
// }
|
||||||
|
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Output Directory"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Directory where screen recordings will be saved"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 16
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: videoPathInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: videoPathInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.topMargin: 6
|
||||||
|
anchors.bottomMargin: 6
|
||||||
|
text: Settings.settings.videoPath !== undefined ? Settings.settings.videoPath : ""
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
clip: true
|
||||||
|
selectByMouse: true
|
||||||
|
activeFocusOnTab: true
|
||||||
|
inputMethodHints: Qt.ImhUrlCharactersOnly
|
||||||
|
onTextChanged: {
|
||||||
|
Settings.settings.videoPath = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onClicked: videoPathInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Frame Rate"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Target frame rate for screen recordings (default: 60)"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
SpinBox {
|
||||||
|
id: frameRateSpinBox
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
from: 24
|
||||||
|
to: 144
|
||||||
|
value: Settings.settings.recordingFrameRate || 60
|
||||||
|
stepSize: 1
|
||||||
|
onValueChanged: {
|
||||||
|
Settings.settings.recordingFrameRate = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 120
|
||||||
|
implicitHeight: 40
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: frameRateSpinBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: TextInput {
|
||||||
|
text: frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale)
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
selectionColor: Theme.accentPrimary
|
||||||
|
selectedTextColor: Theme.onAccent
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
verticalAlignment: Qt.AlignVCenter
|
||||||
|
readOnly: false
|
||||||
|
selectByMouse: true
|
||||||
|
inputMethodHints: Qt.ImhDigitsOnly
|
||||||
|
onTextChanged: {
|
||||||
|
var newValue = parseInt(text);
|
||||||
|
if (!isNaN(newValue) && newValue >= frameRateSpinBox.from && newValue <= frameRateSpinBox.to)
|
||||||
|
frameRateSpinBox.value = newValue;
|
||||||
|
|
||||||
|
}
|
||||||
|
onEditingFinished: {
|
||||||
|
var newValue = parseInt(text);
|
||||||
|
if (isNaN(newValue) || newValue < frameRateSpinBox.from || newValue > frameRateSpinBox.to)
|
||||||
|
text = frameRateSpinBox.textFromValue(frameRateSpinBox.value, frameRateSpinBox.locale);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
validator: IntValidator {
|
||||||
|
bottom: frameRateSpinBox.from
|
||||||
|
top: frameRateSpinBox.to
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
up.indicator: Rectangle {
|
||||||
|
x: parent.width - width
|
||||||
|
height: parent.height
|
||||||
|
width: height
|
||||||
|
color: "transparent"
|
||||||
|
radius: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "add"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 20 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
down.indicator: Rectangle {
|
||||||
|
x: 0
|
||||||
|
height: parent.height
|
||||||
|
width: height
|
||||||
|
color: "transparent"
|
||||||
|
radius: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "remove"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 20 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Audio Source"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Audio source to capture during recording"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: audioSourceComboBox
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
model: ["default_output", "default_input", "both"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.recordingAudioSource || "default_output")
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.recordingAudioSource = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 120
|
||||||
|
implicitHeight: 40
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: audioSourceComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
leftPadding: 12
|
||||||
|
rightPadding: audioSourceComboBox.indicator.width + audioSourceComboBox.spacing
|
||||||
|
text: {
|
||||||
|
switch (audioSourceComboBox.currentText) {
|
||||||
|
case "default_output":
|
||||||
|
return "System Audio";
|
||||||
|
case "default_input":
|
||||||
|
return "Microphone";
|
||||||
|
case "both":
|
||||||
|
return "System Audio + Microphone";
|
||||||
|
default:
|
||||||
|
return audioSourceComboBox.currentText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator: Text {
|
||||||
|
x: audioSourceComboBox.width - width - 12
|
||||||
|
y: audioSourceComboBox.topPadding + (audioSourceComboBox.availableHeight - height) / 2
|
||||||
|
text: "arrow_drop_down"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: audioSourceComboBox.height
|
||||||
|
width: audioSourceComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: audioSourceComboBox.popup.visible ? audioSourceComboBox.delegateModel : null
|
||||||
|
currentIndex: audioSourceComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: audioSourceComboBox.width
|
||||||
|
highlighted: audioSourceComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "default_output":
|
||||||
|
return "System Audio";
|
||||||
|
case "default_input":
|
||||||
|
return "Microphone";
|
||||||
|
case "both":
|
||||||
|
return "System Audio + Microphone";
|
||||||
|
default:
|
||||||
|
return modelData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Video Quality"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Higher quality results in larger file sizes"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: qualityComboBox
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
model: ["medium", "high", "very_high", "ultra"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.recordingQuality || "very_high")
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.recordingQuality = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 120
|
||||||
|
implicitHeight: 40
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: qualityComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
leftPadding: 12
|
||||||
|
rightPadding: qualityComboBox.indicator.width + qualityComboBox.spacing
|
||||||
|
text: {
|
||||||
|
switch (qualityComboBox.currentText) {
|
||||||
|
case "medium":
|
||||||
|
return "Medium";
|
||||||
|
case "high":
|
||||||
|
return "High";
|
||||||
|
case "very_high":
|
||||||
|
return "Very High";
|
||||||
|
case "ultra":
|
||||||
|
return "Ultra";
|
||||||
|
default:
|
||||||
|
return qualityComboBox.currentText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator: Text {
|
||||||
|
x: qualityComboBox.width - width - 12
|
||||||
|
y: qualityComboBox.topPadding + (qualityComboBox.availableHeight - height) / 2
|
||||||
|
text: "arrow_drop_down"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: qualityComboBox.height
|
||||||
|
width: qualityComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: qualityComboBox.popup.visible ? qualityComboBox.delegateModel : null
|
||||||
|
currentIndex: qualityComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: qualityComboBox.width
|
||||||
|
highlighted: qualityComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "medium":
|
||||||
|
return "Medium";
|
||||||
|
case "high":
|
||||||
|
return "High";
|
||||||
|
case "very_high":
|
||||||
|
return "Very High";
|
||||||
|
case "ultra":
|
||||||
|
return "Ultra";
|
||||||
|
default:
|
||||||
|
return modelData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Video Codec"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Different codecs offer different compression and compatibility"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: codecComboBox
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
model: ["h264", "hevc", "av1", "vp8", "vp9"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.recordingCodec || "h264")
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.recordingCodec = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 120
|
||||||
|
implicitHeight: 40
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: codecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
leftPadding: 12
|
||||||
|
rightPadding: codecComboBox.indicator.width + codecComboBox.spacing
|
||||||
|
text: codecComboBox.currentText.toUpperCase()
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator: Text {
|
||||||
|
x: codecComboBox.width - width - 12
|
||||||
|
y: codecComboBox.topPadding + (codecComboBox.availableHeight - height) / 2
|
||||||
|
text: "arrow_drop_down"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: codecComboBox.height
|
||||||
|
width: codecComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: codecComboBox.popup.visible ? codecComboBox.delegateModel : null
|
||||||
|
currentIndex: codecComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: codecComboBox.width
|
||||||
|
highlighted: codecComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: modelData.toUpperCase()
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Audio Codec"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Opus is recommended for best performance and smallest audio size"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: audioCodecComboBox
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
model: ["opus", "aac"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.audioCodec || "opus")
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.audioCodec = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 120
|
||||||
|
implicitHeight: 40
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: audioCodecComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
leftPadding: 12
|
||||||
|
rightPadding: audioCodecComboBox.indicator.width + audioCodecComboBox.spacing
|
||||||
|
text: audioCodecComboBox.currentText.toUpperCase()
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator: Text {
|
||||||
|
x: audioCodecComboBox.width - width - 12
|
||||||
|
y: audioCodecComboBox.topPadding + (audioCodecComboBox.availableHeight - height) / 2
|
||||||
|
text: "arrow_drop_down"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: audioCodecComboBox.height
|
||||||
|
width: audioCodecComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: audioCodecComboBox.popup.visible ? audioCodecComboBox.delegateModel : null
|
||||||
|
currentIndex: audioCodecComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: audioCodecComboBox.width
|
||||||
|
highlighted: audioCodecComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: modelData.toUpperCase()
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Color Range"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Limited is recommended for better compatibility"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: colorRangeComboBox
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
model: ["limited", "full"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.colorRange || "limited")
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.colorRange = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 120
|
||||||
|
implicitHeight: 40
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: colorRangeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
leftPadding: 12
|
||||||
|
rightPadding: colorRangeComboBox.indicator.width + colorRangeComboBox.spacing
|
||||||
|
text: colorRangeComboBox.currentText.charAt(0).toUpperCase() + colorRangeComboBox.currentText.slice(1)
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator: Text {
|
||||||
|
x: colorRangeComboBox.width - width - 12
|
||||||
|
y: colorRangeComboBox.topPadding + (colorRangeComboBox.availableHeight - height) / 2
|
||||||
|
text: "arrow_drop_down"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: colorRangeComboBox.height
|
||||||
|
width: colorRangeComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: colorRangeComboBox.popup.visible ? colorRangeComboBox.delegateModel : null
|
||||||
|
currentIndex: colorRangeComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: colorRangeComboBox.width
|
||||||
|
highlighted: colorRangeComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: modelData.charAt(0).toUpperCase() + modelData.slice(1)
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Show Cursor"
|
||||||
|
description: "Record mouse cursor in the video"
|
||||||
|
value: Settings.settings.showCursor
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.showCursor = !Settings.settings.showCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 24
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
176
Widgets/SettingsWindow/Tabs/TimeWeather.qml
Normal file
176
Widgets/SettingsWindow/Tabs/TimeWeather.qml
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Widgets.SettingsWindow.Tabs.Components
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Time"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "Use 12 Hour Clock"
|
||||||
|
description: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format"
|
||||||
|
value: Settings.settings.use12HourClock
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.use12HourClock = !Settings.settings.use12HourClock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleOption {
|
||||||
|
label: "US Style Date"
|
||||||
|
description: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY"
|
||||||
|
value: Settings.settings.reverseDayMonth
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.reverseDayMonth = !Settings.settings.reverseDayMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 26
|
||||||
|
Layout.bottomMargin: 18
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Weather"
|
||||||
|
font.pixelSize: 18 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 16 * Theme.scale(Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.bottomMargin: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "City"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Your city name for weather information"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 16
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: cityInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: cityInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.topMargin: 6
|
||||||
|
anchors.bottomMargin: 6
|
||||||
|
text: Settings.settings.weatherCity
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
clip: true
|
||||||
|
focus: true
|
||||||
|
selectByMouse: true
|
||||||
|
activeFocusOnTab: true
|
||||||
|
inputMethodHints: Qt.ImhNone
|
||||||
|
onTextChanged: {
|
||||||
|
Settings.settings.weatherCity = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onClicked: {
|
||||||
|
cityInput.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Temperature Unit"
|
||||||
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Choose between Celsius and Fahrenheit"
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
UnitSelector {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
670
Widgets/SettingsWindow/Tabs/Wallpaper.qml
Normal file
670
Widgets/SettingsWindow/Tabs/Wallpaper.qml
Normal file
|
|
@ -0,0 +1,670 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 16
|
||||||
|
rightPadding: 12
|
||||||
|
clip: true
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scrollView.availableWidth
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Wallpaper Settings"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallpaper Settings Category
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
// Wallpaper Folder
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Wallpaper Folder"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Path to your wallpaper folder"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 16
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: folderInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.topMargin: 6
|
||||||
|
anchors.bottomMargin: 6
|
||||||
|
text: Settings.settings.wallpaperFolder !== undefined ? Settings.settings.wallpaperFolder : ""
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
clip: true
|
||||||
|
selectByMouse: true
|
||||||
|
activeFocusOnTab: true
|
||||||
|
inputMethodHints: Qt.ImhUrlCharactersOnly
|
||||||
|
onTextChanged: {
|
||||||
|
Settings.settings.wallpaperFolder = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onClicked: folderInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 26
|
||||||
|
Layout.bottomMargin: 18
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Automation"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random Wallpaper
|
||||||
|
ToggleOption {
|
||||||
|
label: "Random Wallpaper"
|
||||||
|
description: "Automatically select random wallpapers from the folder"
|
||||||
|
value: Settings.settings.randomWallpaper
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.randomWallpaper = !Settings.settings.randomWallpaper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Wallpaper Theme
|
||||||
|
ToggleOption {
|
||||||
|
label: "Use Wallpaper Theme"
|
||||||
|
description: "Automatically adjust theme colors based on wallpaper"
|
||||||
|
value: Settings.settings.useWallpaperTheme
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.useWallpaperTheme = !Settings.settings.useWallpaperTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallpaper Interval
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Wallpaper Interval"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "How often to change wallpapers automatically (in seconds)"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: Settings.settings.wallpaperInterval + " seconds"
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
id: intervalSlider
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 10
|
||||||
|
to: 900
|
||||||
|
stepSize: 10
|
||||||
|
value: Settings.settings.wallpaperInterval
|
||||||
|
snapMode: Slider.SnapAlways
|
||||||
|
onMoved: {
|
||||||
|
Settings.settings.wallpaperInterval = Math.round(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
x: intervalSlider.leftPadding
|
||||||
|
y: intervalSlider.topPadding + intervalSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 200
|
||||||
|
implicitHeight: 4
|
||||||
|
width: intervalSlider.availableWidth
|
||||||
|
height: implicitHeight
|
||||||
|
radius: 2
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: intervalSlider.visualPosition * parent.width
|
||||||
|
height: parent.height
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
radius: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Rectangle {
|
||||||
|
x: intervalSlider.leftPadding + intervalSlider.visualPosition * (intervalSlider.availableWidth - width)
|
||||||
|
y: intervalSlider.topPadding + intervalSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 20
|
||||||
|
implicitHeight: 20
|
||||||
|
radius: 10
|
||||||
|
color: intervalSlider.pressed ? Theme.surfaceVariant : Theme.surface
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 26
|
||||||
|
Layout.bottomMargin: 18
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "SWWW"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use SWWW
|
||||||
|
ToggleOption {
|
||||||
|
label: "Use SWWW"
|
||||||
|
description: "Use SWWW daemon for advanced wallpaper management"
|
||||||
|
value: Settings.settings.useSWWW
|
||||||
|
onToggled: function() {
|
||||||
|
Settings.settings.useSWWW = !Settings.settings.useSWWW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SWWW Settings (only visible when useSWWW is enabled)
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
visible: Settings.settings.useSWWW
|
||||||
|
|
||||||
|
// Resize Mode
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Resize Mode"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "How SWWW should resize wallpapers to fit the screen"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 16
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: resizeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: resizeComboBox
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.topMargin: 6
|
||||||
|
anchors.bottomMargin: 6
|
||||||
|
model: ["no", "crop", "fit", "stretch"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.wallpaperResize)
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.wallpaperResize = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: resizeComboBox.displayText
|
||||||
|
font: resizeComboBox.font
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: resizeComboBox.height
|
||||||
|
width: resizeComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: resizeComboBox.popup.visible ? resizeComboBox.delegateModel : null
|
||||||
|
currentIndex: resizeComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surface
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: resizeComboBox.width
|
||||||
|
highlighted: resizeComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: modelData
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font: resizeComboBox.font
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.highlighted ? Theme.accentPrimary : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition Type
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Transition Type"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Animation type when switching between wallpapers"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 16
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
border.color: transitionTypeComboBox.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: transitionTypeComboBox
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.topMargin: 6
|
||||||
|
anchors.bottomMargin: 6
|
||||||
|
model: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"]
|
||||||
|
currentIndex: model.indexOf(Settings.settings.transitionType)
|
||||||
|
onActivated: {
|
||||||
|
Settings.settings.transitionType = model[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: transitionTypeComboBox.displayText
|
||||||
|
font: transitionTypeComboBox.font
|
||||||
|
color: Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
popup: Popup {
|
||||||
|
y: transitionTypeComboBox.height
|
||||||
|
width: transitionTypeComboBox.width
|
||||||
|
implicitHeight: contentItem.implicitHeight
|
||||||
|
padding: 1
|
||||||
|
|
||||||
|
contentItem: ListView {
|
||||||
|
clip: true
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
model: transitionTypeComboBox.popup.visible ? transitionTypeComboBox.delegateModel : null
|
||||||
|
currentIndex: transitionTypeComboBox.highlightedIndex
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.surface
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
radius: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: transitionTypeComboBox.width
|
||||||
|
highlighted: transitionTypeComboBox.highlightedIndex === index
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: modelData
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font: transitionTypeComboBox.font
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.highlighted ? Theme.accentPrimary : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition FPS
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Transition FPS"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Frames per second for transition animations"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: Settings.settings.transitionFps + " FPS"
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
id: fpsSlider
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 30
|
||||||
|
to: 500
|
||||||
|
stepSize: 5
|
||||||
|
value: Settings.settings.transitionFps
|
||||||
|
snapMode: Slider.SnapAlways
|
||||||
|
onMoved: {
|
||||||
|
Settings.settings.transitionFps = Math.round(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
x: fpsSlider.leftPadding
|
||||||
|
y: fpsSlider.topPadding + fpsSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 200
|
||||||
|
implicitHeight: 4
|
||||||
|
width: fpsSlider.availableWidth
|
||||||
|
height: implicitHeight
|
||||||
|
radius: 2
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: fpsSlider.visualPosition * parent.width
|
||||||
|
height: parent.height
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
radius: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Rectangle {
|
||||||
|
x: fpsSlider.leftPadding + fpsSlider.visualPosition * (fpsSlider.availableWidth - width)
|
||||||
|
y: fpsSlider.topPadding + fpsSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 20
|
||||||
|
implicitHeight: 20
|
||||||
|
radius: 10
|
||||||
|
color: fpsSlider.pressed ? Theme.surfaceVariant : Theme.surface
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition Duration
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Transition Duration"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Duration of transition animations in seconds"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.textSecondary
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: Settings.settings.transitionDuration.toFixed(3) + " seconds"
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
id: durationSlider
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 0.25
|
||||||
|
to: 10
|
||||||
|
stepSize: 0.05
|
||||||
|
value: Settings.settings.transitionDuration
|
||||||
|
snapMode: Slider.SnapAlways
|
||||||
|
onMoved: {
|
||||||
|
Settings.settings.transitionDuration = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
x: durationSlider.leftPadding
|
||||||
|
y: durationSlider.topPadding + durationSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 200
|
||||||
|
implicitHeight: 4
|
||||||
|
width: durationSlider.availableWidth
|
||||||
|
height: implicitHeight
|
||||||
|
radius: 2
|
||||||
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: durationSlider.visualPosition * parent.width
|
||||||
|
height: parent.height
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
radius: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Rectangle {
|
||||||
|
x: durationSlider.leftPadding + durationSlider.visualPosition * (durationSlider.availableWidth - width)
|
||||||
|
y: durationSlider.topPadding + durationSlider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 20
|
||||||
|
implicitHeight: 20
|
||||||
|
radius: 10
|
||||||
|
color: durationSlider.pressed ? Theme.surfaceVariant : Theme.surface
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ Item {
|
||||||
id: root
|
id: root
|
||||||
property alias panel: bluetoothPanelModal
|
property alias panel: bluetoothPanelModal
|
||||||
|
|
||||||
// For showing error/status messages
|
|
||||||
property string statusMessage: ""
|
property string statusMessage: ""
|
||||||
property bool statusPopupVisible: false
|
property bool statusPopupVisible: false
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ Item {
|
||||||
opacity: 0.12
|
opacity: 0.12
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content area (centered, in a card)
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 640
|
Layout.preferredHeight: 640
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
import qs.Widgets.Sidebar.Panel
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: buttonRoot
|
id: buttonRoot
|
||||||
|
|
@ -45,7 +44,7 @@ Item {
|
||||||
id: iconText
|
id: iconText
|
||||||
text: "dashboard"
|
text: "dashboard"
|
||||||
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
color: sidebarPopup.visible ? Theme.accentPrimary : Theme.textPrimary
|
color: sidebarPopup.visible ? Theme.accentPrimary : Theme.textPrimary
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
z: 1
|
z: 1
|
||||||
|
|
@ -8,15 +8,15 @@ import qs.Services
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: musicCard
|
id: musicCard
|
||||||
width: 360
|
width: 360 * Theme.scale(Screen)
|
||||||
height: 250
|
height: 250 * Theme.scale(Screen)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: card
|
id: card
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Theme.surface
|
color: Theme.surface
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
|
|
||||||
// Show fallback UI if no player is available
|
// Show fallback UI if no player is available
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -26,12 +26,12 @@ Rectangle {
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 16
|
spacing: 16 * Theme.scale(Screen)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "music_note"
|
text: "music_note"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeHeader
|
font.pixelSize: Theme.fontSizeHeader * Theme.scale(Screen)
|
||||||
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ Rectangle {
|
||||||
text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected"
|
text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected"
|
||||||
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
|
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,45 +49,45 @@ Rectangle {
|
||||||
// Main player UI
|
// Main player UI
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 18
|
anchors.margins: 18 * Theme.scale(Screen)
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
visible: !!MusicManager.currentPlayer
|
visible: !!MusicManager.currentPlayer
|
||||||
|
|
||||||
// Player selector
|
// Player selector
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: playerSelector
|
id: playerSelector
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 40 * Theme.scale(Screen)
|
||||||
visible: MusicManager.getAvailablePlayers().length > 1
|
visible: MusicManager.getAvailablePlayers().length > 1
|
||||||
model: MusicManager.getAvailablePlayers()
|
model: MusicManager.getAvailablePlayers()
|
||||||
textRole: "identity"
|
textRole: "identity"
|
||||||
currentIndex: MusicManager.selectedPlayerIndex
|
currentIndex: MusicManager.selectedPlayerIndex
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
implicitWidth: 120
|
implicitWidth: 120 * Theme.scale(Screen)
|
||||||
implicitHeight: 40
|
implicitHeight: 40 * Theme.scale(Screen)
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline
|
border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
radius: 16
|
radius: 16 * Theme.scale(Screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
leftPadding: 12
|
leftPadding: 12 * Theme.scale(Screen)
|
||||||
rightPadding: playerSelector.indicator.width + playerSelector.spacing
|
rightPadding: playerSelector.indicator.width + playerSelector.spacing
|
||||||
text: playerSelector.displayText
|
text: playerSelector.displayText
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator: Text {
|
indicator: Text {
|
||||||
x: playerSelector.width - width - 12
|
x: playerSelector.width - width - 12 * Theme.scale(Screen)
|
||||||
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
|
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
|
||||||
text: "arrow_drop_down"
|
text: "arrow_drop_down"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ Rectangle {
|
||||||
y: playerSelector.height
|
y: playerSelector.height
|
||||||
width: playerSelector.width
|
width: playerSelector.width
|
||||||
implicitHeight: contentItem.implicitHeight
|
implicitHeight: contentItem.implicitHeight
|
||||||
padding: 1
|
padding: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
contentItem: ListView {
|
contentItem: ListView {
|
||||||
clip: true
|
clip: true
|
||||||
|
|
@ -109,8 +109,8 @@ Rectangle {
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
border.color: Theme.outline
|
border.color: Theme.outline
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
radius: 16
|
radius: 16 * Theme.scale(Screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ Rectangle {
|
||||||
width: playerSelector.width
|
width: playerSelector.width
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
text: modelData.identity
|
text: modelData.identity
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13 * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
|
|
@ -136,57 +136,57 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Album art and spectrum
|
// Album art with spectrum visualizer
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
// Album art with spectrum
|
// Album art container with circular spectrum overlay
|
||||||
Item {
|
Item {
|
||||||
id: albumArtContainer
|
id: albumArtContainer
|
||||||
width: 96
|
width: 96 * Theme.scale(Screen)
|
||||||
height: 96 // enough for spectrum and art (will adjust if needed)
|
height: 96 * Theme.scale(Screen) // enough for spectrum and art (will adjust if needed)
|
||||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||||
|
|
||||||
// Spectrum visualizer
|
// Circular spectrum visualizer around album art
|
||||||
CircularSpectrum {
|
CircularSpectrum {
|
||||||
id: spectrum
|
id: spectrum
|
||||||
values: MusicManager.cavaValues
|
values: MusicManager.cavaValues
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
innerRadius: 30 // just outside 60x60 album art
|
innerRadius: 30 * Theme.scale(Screen) // Position just outside 60x60 album art
|
||||||
outerRadius: 48 // how far bars extend
|
outerRadius: 48 * Theme.scale(Screen) // Extend bars outward from album art
|
||||||
fillColor: Theme.accentPrimary
|
fillColor: Theme.accentPrimary
|
||||||
strokeColor: Theme.accentPrimary
|
strokeColor: Theme.accentPrimary
|
||||||
strokeWidth: 0
|
strokeWidth: 0 * Theme.scale(Screen)
|
||||||
z: 0
|
z: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Album art image
|
// Album art image
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: albumArtwork
|
id: albumArtwork
|
||||||
width: 60
|
width: 60 * Theme.scale(Screen)
|
||||||
height: 60
|
height: 60 * Theme.scale(Screen)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: 30 // circle
|
radius: 30 * Theme.scale(Screen) // circle
|
||||||
color: Qt.darker(Theme.surface, 1.1)
|
color: Qt.darker(Theme.surface, 1.1)
|
||||||
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: albumArt
|
id: albumArt
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
anchors.margins: 2 * Theme.scale(Screen)
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
smooth: true
|
smooth: true
|
||||||
mipmap: true
|
mipmap: true
|
||||||
cache: false
|
cache: false
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
sourceSize.width: 60
|
sourceSize.width: 60 * Theme.scale(Screen)
|
||||||
sourceSize.height: 60
|
sourceSize.height: 60 * Theme.scale(Screen)
|
||||||
source: MusicManager.trackArtUrl
|
source: MusicManager.trackArtUrl
|
||||||
visible: source.toString() !== ""
|
visible: source.toString() !== ""
|
||||||
|
|
||||||
// Rounded corners using layer
|
// Apply circular mask for rounded corners
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
maskEnabled: true
|
maskEnabled: true
|
||||||
|
|
@ -208,12 +208,12 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback icon
|
// Fallback icon when no album art available
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "album"
|
text: "album"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeBody
|
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
|
||||||
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
|
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
|
||||||
visible: !albumArt.visible
|
visible: !albumArt.visible
|
||||||
}
|
}
|
||||||
|
|
@ -223,13 +223,13 @@ Rectangle {
|
||||||
// Track metadata
|
// Track metadata
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 4
|
spacing: 4 * Theme.scale(Screen)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: MusicManager.trackTitle
|
text: MusicManager.trackTitle
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall * Theme.scale(Screen)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
|
|
@ -241,7 +241,7 @@ Rectangle {
|
||||||
text: MusicManager.trackArtist
|
text: MusicManager.trackArtist
|
||||||
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8)
|
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
@ -250,7 +250,7 @@ Rectangle {
|
||||||
text: MusicManager.trackAlbum
|
text: MusicManager.trackAlbum
|
||||||
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
|
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
@ -261,8 +261,8 @@ Rectangle {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progressBarBackground
|
id: progressBarBackground
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 6
|
height: 6 * Theme.scale(Screen)
|
||||||
radius: 3
|
radius: 3 * Theme.scale(Screen)
|
||||||
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15)
|
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
|
@ -290,12 +290,12 @@ Rectangle {
|
||||||
// Interactive progress handle
|
// Interactive progress handle
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progressHandle
|
id: progressHandle
|
||||||
width: 12
|
width: 12 * Theme.scale(Screen)
|
||||||
height: 12
|
height: 12 * Theme.scale(Screen)
|
||||||
radius: 6
|
radius: 6 * Theme.scale(Screen)
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
border.color: Qt.lighter(Theme.accentPrimary, 1.3)
|
border.color: Qt.lighter(Theme.accentPrimary, 1.3)
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
|
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -334,18 +334,18 @@ Rectangle {
|
||||||
|
|
||||||
// Media controls
|
// Media controls
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 4
|
spacing: 4 * Theme.scale(Screen)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
// Previous button
|
// Previous button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 28
|
width: 28 * Theme.scale(Screen)
|
||||||
height: 28
|
height: 28 * Theme.scale(Screen)
|
||||||
radius: 14
|
radius: 14 * Theme.scale(Screen)
|
||||||
color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
|
color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
|
||||||
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: previousButton
|
id: previousButton
|
||||||
|
|
@ -360,19 +360,19 @@ Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "skip_previous"
|
text: "skip_previous"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
|
||||||
color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play/Pause button
|
// Play/Pause button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 36
|
width: 36 * Theme.scale(Screen)
|
||||||
height: 36
|
height: 36 * Theme.scale(Screen)
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
|
color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 2
|
border.width: 2 * Theme.scale(Screen)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: playButton
|
id: playButton
|
||||||
|
|
@ -387,19 +387,19 @@ Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: MusicManager.isPlaying ? "pause" : "play_arrow"
|
text: MusicManager.isPlaying ? "pause" : "play_arrow"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeBody
|
font.pixelSize: Theme.fontSizeBody * Theme.scale(Screen)
|
||||||
color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next button
|
// Next button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 28
|
width: 28 * Theme.scale(Screen)
|
||||||
height: 28
|
height: 28 * Theme.scale(Screen)
|
||||||
radius: 14
|
radius: 14 * Theme.scale(Screen)
|
||||||
color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
|
color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
|
||||||
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: nextButton
|
id: nextButton
|
||||||
|
|
@ -414,7 +414,7 @@ Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "skip_next"
|
text: "skip_next"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: Theme.fontSizeCaption
|
font.pixelSize: Theme.fontSizeCaption * Theme.scale(Screen)
|
||||||
color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,13 +3,15 @@ import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Settings
|
|
||||||
import qs.Widgets.Sidebar.Config
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Widgets.SettingsWindow
|
||||||
|
|
||||||
PanelWithOverlay {
|
PanelWithOverlay {
|
||||||
id: sidebarPopup
|
id: sidebarPopup
|
||||||
|
|
||||||
|
property var shell: null
|
||||||
|
|
||||||
function showAt() {
|
function showAt() {
|
||||||
sidebarPopupRect.showAt();
|
sidebarPopupRect.showAt();
|
||||||
}
|
}
|
||||||
|
|
@ -26,18 +28,44 @@ PanelWithOverlay {
|
||||||
sidebarPopupRect.hidePopup();
|
sidebarPopupRect.hidePopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
// Trigger initial weather loading when component is completed
|
||||||
id: sidebarPopupRect
|
Component.onCompleted: {
|
||||||
implicitWidth: 500
|
// Load initial weather data after a short delay to ensure all components are ready
|
||||||
implicitHeight: 800
|
Qt.callLater(function() {
|
||||||
visible: parent.visible
|
if (weather && weather.fetchCityWeather)
|
||||||
color: "transparent"
|
weather.fetchCityWeather();
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
// Access the shell's SettingsWindow instead of creating a new one
|
||||||
|
|
||||||
|
id: sidebarPopupRect
|
||||||
|
|
||||||
// Animation properties
|
|
||||||
property real slideOffset: width
|
property real slideOffset: width
|
||||||
property bool isAnimating: false
|
property bool isAnimating: false
|
||||||
|
property int leftPadding: 20 * Theme.scale(Screen)
|
||||||
|
property int bottomPadding: 20 * Theme.scale(Screen)
|
||||||
|
// Recording properties
|
||||||
|
property bool isRecording: false
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: checkRecordingProcess
|
||||||
|
command: ["pgrep", "-f", "gpu-screen-recorder.*portal"]
|
||||||
|
onExited: function(exitCode, exitStatus) {
|
||||||
|
var isActuallyRecording = exitCode === 0
|
||||||
|
if (isRecording && !isActuallyRecording) {
|
||||||
|
isRecording = isActuallyRecording
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRecordingStatus() {
|
||||||
|
if (isRecording) {
|
||||||
|
checkRecordingProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showAt() {
|
function showAt() {
|
||||||
if (!sidebarPopup.visible) {
|
if (!sidebarPopup.visible) {
|
||||||
|
|
@ -48,26 +76,18 @@ PanelWithOverlay {
|
||||||
slideAnim.running = true;
|
slideAnim.running = true;
|
||||||
if (weather)
|
if (weather)
|
||||||
weather.startWeatherFetch();
|
weather.startWeatherFetch();
|
||||||
|
|
||||||
if (systemWidget)
|
if (systemWidget)
|
||||||
systemWidget.panelVisible = true;
|
systemWidget.panelVisible = true;
|
||||||
if (quickAccessWidget)
|
|
||||||
quickAccessWidget.panelVisible = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hidePopup() {
|
function hidePopup() {
|
||||||
if (sidebarPopupRect.settingsModal && sidebarPopupRect.settingsModal.visible) {
|
if (shell && shell.settingsWindow && shell.settingsWindow.visible)
|
||||||
sidebarPopupRect.settingsModal.visible = false;
|
shell.settingsWindow.visible = false;
|
||||||
}
|
|
||||||
if (wallpaperPanel && wallpaperPanel.visible) {
|
|
||||||
wallpaperPanel.visible = false;
|
|
||||||
}
|
|
||||||
if (sidebarPopupRect.wifiPanelModal && sidebarPopupRect.wifiPanelModal.visible) {
|
|
||||||
sidebarPopupRect.wifiPanelModal.visible = false;
|
|
||||||
}
|
|
||||||
if (sidebarPopupRect.bluetoothPanelModal && sidebarPopupRect.bluetoothPanelModal.visible) {
|
|
||||||
sidebarPopupRect.bluetoothPanelModal.visible = false;
|
|
||||||
}
|
|
||||||
if (sidebarPopup.visible) {
|
if (sidebarPopup.visible) {
|
||||||
slideAnim.from = 0;
|
slideAnim.from = 0;
|
||||||
slideAnim.to = width;
|
slideAnim.to = width;
|
||||||
|
|
@ -75,81 +95,129 @@ PanelWithOverlay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start screen recording using Quickshell.execDetached
|
||||||
|
function startRecording() {
|
||||||
|
var currentDate = new Date();
|
||||||
|
var hours = String(currentDate.getHours()).padStart(2, '0');
|
||||||
|
var minutes = String(currentDate.getMinutes()).padStart(2, '0');
|
||||||
|
var day = String(currentDate.getDate()).padStart(2, '0');
|
||||||
|
var month = String(currentDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
var year = currentDate.getFullYear();
|
||||||
|
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4";
|
||||||
|
var videoPath = Settings.settings.videoPath;
|
||||||
|
if (videoPath && !videoPath.endsWith("/"))
|
||||||
|
videoPath += "/";
|
||||||
|
|
||||||
|
var outputPath = videoPath + filename;
|
||||||
|
var command = "gpu-screen-recorder -w portal" + " -f " + Settings.settings.recordingFrameRate + " -a default_output" + " -k " + Settings.settings.recordingCodec + " -ac " + Settings.settings.audioCodec + " -q " + Settings.settings.recordingQuality + " -cursor " + (Settings.settings.showCursor ? "yes" : "no") + " -cr " + Settings.settings.colorRange + " -o " + outputPath;
|
||||||
|
Quickshell.execDetached(["sh", "-c", command]);
|
||||||
|
isRecording = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop recording using Quickshell.execDetached
|
||||||
|
function stopRecording() {
|
||||||
|
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
|
||||||
|
// Optionally, force kill after a delay
|
||||||
|
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect);
|
||||||
|
cleanupTimer.triggered.connect(function() {
|
||||||
|
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]);
|
||||||
|
cleanupTimer.destroy();
|
||||||
|
});
|
||||||
|
isRecording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: 500 * Theme.scale(Screen)
|
||||||
|
implicitHeight: 700 * Theme.scale(Screen)
|
||||||
|
visible: parent.visible
|
||||||
|
color: "transparent"
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
// Clean up processes on destruction
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (isRecording)
|
||||||
|
stopRecording();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: slideAnim
|
id: slideAnim
|
||||||
|
|
||||||
target: sidebarPopupRect
|
target: sidebarPopupRect
|
||||||
property: "slideOffset"
|
property: "slideOffset"
|
||||||
duration: 300
|
duration: 300
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
|
|
||||||
onStopped: {
|
onStopped: {
|
||||||
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
|
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
|
||||||
sidebarPopup.visible = false;
|
sidebarPopup.visible = false;
|
||||||
// Stop monitoring and background tasks when hidden
|
|
||||||
if (weather)
|
if (weather)
|
||||||
weather.stopWeatherFetch();
|
weather.stopWeatherFetch();
|
||||||
|
|
||||||
if (systemWidget)
|
if (systemWidget)
|
||||||
systemWidget.panelVisible = false;
|
systemWidget.panelVisible = false;
|
||||||
if (quickAccessWidget)
|
|
||||||
quickAccessWidget.panelVisible = false;
|
|
||||||
}
|
}
|
||||||
sidebarPopupRect.isAnimating = false;
|
sidebarPopupRect.isAnimating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStarted: {
|
onStarted: {
|
||||||
sidebarPopupRect.isAnimating = true;
|
sidebarPopupRect.isAnimating = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property int leftPadding: 20
|
|
||||||
property int bottomPadding: 20
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: mainRectangle
|
id: mainRectangle
|
||||||
|
|
||||||
width: sidebarPopupRect.width - sidebarPopupRect.leftPadding
|
width: sidebarPopupRect.width - sidebarPopupRect.leftPadding
|
||||||
height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding
|
height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding
|
||||||
anchors.top: sidebarPopupRect.top
|
anchors.top: sidebarPopupRect.top
|
||||||
x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset
|
x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset
|
||||||
y: 0
|
y: 0
|
||||||
color: Theme.backgroundPrimary
|
color: Theme.backgroundPrimary
|
||||||
bottomLeftRadius: 20
|
bottomLeftRadius: 20 * Theme.scale(Screen)
|
||||||
z: 0
|
z: 0
|
||||||
|
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
enabled: !sidebarPopupRect.isAnimating
|
enabled: !sidebarPopupRect.isAnimating
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 300
|
duration: 300
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property alias settingsModal: settingsModal
|
|
||||||
property alias wifiPanelModal: wifiPanel.panel
|
|
||||||
property alias bluetoothPanelModal: bluetoothPanel.panel
|
// SettingsIcon component
|
||||||
SettingsModal {
|
SettingsIcon {
|
||||||
id: settingsModal
|
id: settingsModal
|
||||||
|
|
||||||
|
onWeatherRefreshRequested: {
|
||||||
|
if (weather && weather.fetchCityWeather)
|
||||||
|
weather.fetchCityWeather();
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: mainRectangle
|
anchors.fill: mainRectangle
|
||||||
x: sidebarPopupRect.slideOffset
|
x: sidebarPopupRect.slideOffset
|
||||||
|
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
|
||||||
Behavior on x {
|
|
||||||
enabled: !sidebarPopupRect.isAnimating
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 300
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 20
|
anchors.margins: 20 * Theme.scale(Screen)
|
||||||
spacing: 16
|
spacing: 4 * Theme.scale(Screen)
|
||||||
|
|
||||||
System {
|
PowerMenu {
|
||||||
id: systemWidget
|
id: systemWidget
|
||||||
|
settingsModal: settingsModal
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
z: 3
|
z: 3
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +230,7 @@ PanelWithOverlay {
|
||||||
|
|
||||||
// Music and System Monitor row
|
// Music and System Monitor row
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
|
|
@ -174,241 +242,147 @@ PanelWithOverlay {
|
||||||
id: systemMonitor
|
id: systemMonitor
|
||||||
z: 2
|
z: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Power profile, Wifi and Bluetooth row
|
// Power profile, Record and Wallpaper row
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.alignment: Qt.AlignLeft
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.preferredHeight: 80
|
spacing: 10 * Theme.scale(Screen)
|
||||||
spacing: 16
|
Layout.preferredHeight: 80 * Theme.scale(Screen)
|
||||||
z: 3
|
z: 3
|
||||||
|
|
||||||
PowerProfile {
|
PowerProfile {
|
||||||
Layout.alignment: Qt.AlignLeft
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.preferredHeight: 80
|
Layout.preferredHeight: 80 * Theme.scale(Screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network card containing Wifi and Bluetooth
|
// Record and Wallpaper card
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.preferredHeight: 80
|
Layout.preferredHeight: 80 * Theme.scale(Screen)
|
||||||
Layout.preferredWidth: 140
|
Layout.preferredWidth: 140 * Theme.scale(Screen)
|
||||||
Layout.fillWidth: false
|
Layout.fillWidth: false
|
||||||
color: Theme.surface
|
color: Theme.surface
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 20
|
spacing: 20 * Theme.scale(Screen)
|
||||||
|
|
||||||
// Wifi button
|
// Record button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: wifiButton
|
id: recordButton
|
||||||
width: 36
|
|
||||||
height: 36
|
width: 36 * Theme.scale(Screen)
|
||||||
radius: 18
|
height: 36 * Theme.scale(Screen)
|
||||||
|
radius: 18 * Theme.scale(Screen)
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
color: wifiButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
color: sidebarPopupRect.isRecording ? Theme.accentPrimary :
|
||||||
|
(recordButtonArea.containsMouse ? Theme.accentPrimary : "transparent")
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "wifi"
|
text: "photo_camera"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 22
|
font.pixelSize: 22 * Theme.scale(Screen)
|
||||||
color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
|
color: sidebarPopupRect.isRecording || recordButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: wifiButtonArea
|
id: recordButtonArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: wifiPanel.showAt()
|
onClicked: {
|
||||||
|
if (sidebarPopupRect.isRecording) {
|
||||||
|
sidebarPopupRect.stopRecording();
|
||||||
|
sidebarPopup.dismiss();
|
||||||
|
} else {
|
||||||
|
sidebarPopupRect.startRecording();
|
||||||
|
sidebarPopup.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledTooltip {
|
StyledTooltip {
|
||||||
text: "Wifi"
|
text: sidebarPopupRect.isRecording ? "Stop Recording" : "Start Recording"
|
||||||
targetItem: wifiButtonArea
|
targetItem: recordButtonArea
|
||||||
tooltipVisible: wifiButtonArea.containsMouse
|
tooltipVisible: recordButtonArea.containsMouse
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bluetooth button
|
// Wallpaper button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bluetoothButton
|
id: wallpaperButton
|
||||||
width: 36
|
|
||||||
height: 36
|
width: 36 * Theme.scale(Screen)
|
||||||
radius: 18
|
height: 36 * Theme.scale(Screen)
|
||||||
|
radius: 18 * Theme.scale(Screen)
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "bluetooth"
|
text: "image"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 22
|
font.pixelSize: 22 * Theme.scale(Screen)
|
||||||
color: bluetoothButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
|
color: wallpaperButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: bluetoothButtonArea
|
id: wallpaperButtonArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: bluetoothPanel.showAt()
|
onClicked: {
|
||||||
|
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) {
|
||||||
|
settingsModal.openSettings(6);
|
||||||
|
sidebarPopup.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledTooltip {
|
StyledTooltip {
|
||||||
text: "Bluetooth"
|
text: "Wallpaper"
|
||||||
targetItem: bluetoothButtonArea
|
targetItem: wallpaperButtonArea
|
||||||
tooltipVisible: bluetoothButtonArea.containsMouse
|
tooltipVisible: wallpaperButtonArea.containsMouse
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hidden panel components for modal functionality
|
|
||||||
WifiPanel {
|
|
||||||
id: wifiPanel
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
BluetoothPanel {
|
|
||||||
id: bluetoothPanel
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuickAccess widget
|
|
||||||
QuickAccess {
|
|
||||||
id: quickAccessWidget
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.topMargin: -16
|
|
||||||
z: 2
|
|
||||||
isRecording: sidebarPopupRect.isRecording
|
|
||||||
|
|
||||||
onRecordingRequested: {
|
|
||||||
sidebarPopupRect.startRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
onStopRecordingRequested: {
|
|
||||||
sidebarPopupRect.stopRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
onRecordingStateMismatch: function (actualState) {
|
|
||||||
isRecording = actualState;
|
|
||||||
quickAccessWidget.isRecording = actualState;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSettingsRequested: {
|
|
||||||
settingsModal.visible = true;
|
|
||||||
}
|
|
||||||
onWallpaperRequested: {
|
|
||||||
wallpaperPanel.visible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recording properties
|
Behavior on x {
|
||||||
property bool isRecording: false
|
|
||||||
|
|
||||||
// Start screen recording using Quickshell.execDetached
|
|
||||||
function startRecording() {
|
|
||||||
var currentDate = new Date();
|
|
||||||
var hours = String(currentDate.getHours()).padStart(2, '0');
|
|
||||||
var minutes = String(currentDate.getMinutes()).padStart(2, '0');
|
|
||||||
var day = String(currentDate.getDate()).padStart(2, '0');
|
|
||||||
var month = String(currentDate.getMonth() + 1).padStart(2, '0');
|
|
||||||
var year = currentDate.getFullYear();
|
|
||||||
|
|
||||||
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4";
|
|
||||||
var videoPath = Settings.settings.videoPath;
|
|
||||||
if (videoPath && !videoPath.endsWith("/")) {
|
|
||||||
videoPath += "/";
|
|
||||||
}
|
|
||||||
var outputPath = videoPath + filename;
|
|
||||||
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath;
|
|
||||||
Quickshell.execDetached(["sh", "-c", command]);
|
|
||||||
isRecording = true;
|
|
||||||
quickAccessWidget.isRecording = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop recording using Quickshell.execDetached
|
|
||||||
function stopRecording() {
|
|
||||||
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
|
|
||||||
// Optionally, force kill after a delay
|
|
||||||
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect);
|
|
||||||
cleanupTimer.triggered.connect(function () {
|
|
||||||
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]);
|
|
||||||
cleanupTimer.destroy();
|
|
||||||
});
|
|
||||||
isRecording = false;
|
|
||||||
quickAccessWidget.isRecording = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up processes on destruction
|
|
||||||
Component.onDestruction: {
|
|
||||||
if (isRecording) {
|
|
||||||
stopRecording();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: sidebarCornerLeft
|
|
||||||
position: "bottomright"
|
|
||||||
size: 1.1
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
anchors.top: mainRectangle.top
|
|
||||||
offsetX: -447 + sidebarPopupRect.slideOffset
|
|
||||||
offsetY: 0
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
|
|
||||||
Behavior on offsetX {
|
|
||||||
enabled: !sidebarPopupRect.isAnimating
|
enabled: !sidebarPopupRect.isAnimating
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 300
|
duration: 300
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Corners {
|
|
||||||
id: sidebarCornerBottom
|
|
||||||
position: "bottomright"
|
|
||||||
size: 1.1
|
|
||||||
fillColor: Theme.backgroundPrimary
|
|
||||||
offsetX: 33 + sidebarPopupRect.slideOffset
|
|
||||||
offsetY: 46
|
|
||||||
visible: Settings.settings.showCorners
|
|
||||||
|
|
||||||
Behavior on offsetX {
|
|
||||||
enabled: !sidebarPopupRect.isAnimating
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 300
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WallpaperPanel {
|
|
||||||
id: wallpaperPanel
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (parent) {
|
|
||||||
anchors.top = parent.top;
|
|
||||||
anchors.right = parent.right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,396 +1,23 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Services
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Widgets.LockScreen
|
import qs.Widgets.LockScreen
|
||||||
import qs.Helpers
|
|
||||||
import qs.Services
|
|
||||||
import qs.Components
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: systemWidget
|
id: systemWidget
|
||||||
width: 440
|
|
||||||
height: 80
|
|
||||||
color: "transparent"
|
|
||||||
anchors.horizontalCenterOffset: -2
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: card
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Theme.surface
|
|
||||||
radius: 18
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 18
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
// User info row
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
// Profile image
|
|
||||||
Rectangle {
|
|
||||||
width: 48
|
|
||||||
height: 48
|
|
||||||
radius: 24
|
|
||||||
color: Theme.accentPrimary
|
|
||||||
|
|
||||||
// Border
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
radius: 24
|
|
||||||
border.color: Theme.accentPrimary
|
|
||||||
border.width: 2
|
|
||||||
z: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Avatar {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User info text
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 4
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: Quickshell.env("USER")
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 16
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "System Uptime: " + uptimeText
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: Theme.textSecondary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spacer
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// System menu button
|
|
||||||
Rectangle {
|
|
||||||
id: systemButton
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent"
|
|
||||||
border.color: Theme.accentPrimary
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "power_settings_new"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 16
|
|
||||||
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: systemButtonArea
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: {
|
|
||||||
systemMenu.visible = !systemMenu.visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StyledTooltip {
|
|
||||||
id: systemTooltip
|
|
||||||
text: "System"
|
|
||||||
targetItem: systemButton
|
|
||||||
tooltipVisible: systemButtonArea.containsMouse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelWithOverlay {
|
|
||||||
id: systemMenu
|
|
||||||
anchors.top: systemButton.bottom
|
|
||||||
anchors.right: systemButton.right
|
|
||||||
// System menu popup
|
|
||||||
Rectangle {
|
|
||||||
|
|
||||||
width: 160
|
|
||||||
height: 220
|
|
||||||
color: Theme.surface
|
|
||||||
radius: 8
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
visible: true
|
|
||||||
z: 9999
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
// Position below system button
|
|
||||||
anchors.rightMargin: 32
|
|
||||||
anchors.topMargin: systemButton.y + systemButton.height + 48
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
// Lock button
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
radius: 6
|
|
||||||
color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "lock_outline"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 16
|
|
||||||
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Lock Screen"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: lockButtonArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
lockScreen.locked = true;
|
|
||||||
systemMenu.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suspend button
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
radius: 6
|
|
||||||
color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "bedtime"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 16
|
|
||||||
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Suspend"
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendButtonArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
suspend();
|
|
||||||
systemMenu.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reboot button
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
radius: 6
|
|
||||||
color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "refresh"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 16
|
|
||||||
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Reboot"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootButtonArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
reboot();
|
|
||||||
systemMenu.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout button
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
radius: 6
|
|
||||||
color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "exit_to_app"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 16
|
|
||||||
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Logout"
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutButtonArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
logout();
|
|
||||||
systemMenu.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown button
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
radius: 6
|
|
||||||
color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "power_settings_new"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 16
|
|
||||||
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Shutdown"
|
|
||||||
font.pixelSize: 14
|
|
||||||
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: shutdownButtonArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
shutdown();
|
|
||||||
systemMenu.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Properties
|
|
||||||
property string uptimeText: "--:--"
|
property string uptimeText: "--:--"
|
||||||
|
property bool panelVisible: false
|
||||||
// Process to get uptime
|
property var settingsModal: null
|
||||||
Process {
|
|
||||||
id: uptimeProcess
|
|
||||||
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
|
|
||||||
running: false
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
uptimeText = this.text.trim();
|
|
||||||
uptimeProcess.running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: shutdownProcess
|
|
||||||
command: ["shutdown", "-h", "now"]
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: rebootProcess
|
|
||||||
command: ["reboot"]
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: suspendProcess
|
|
||||||
command: ["systemctl", "suspend"]
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: logoutProcessNiri
|
|
||||||
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: logoutProcessHyprland
|
|
||||||
command: ["hyprctl", "dispatch", "exit"]
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: logoutProcess
|
id: logoutProcess
|
||||||
|
|
@ -399,14 +26,12 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
if (WorkspaceManager.isNiri) {
|
if (WorkspaceManager.isNiri)
|
||||||
logoutProcessNiri.running = true;
|
logoutProcessNiri.running = true;
|
||||||
} else if (WorkspaceManager.isHyprland) {
|
else if (WorkspaceManager.isHyprland)
|
||||||
logoutProcessHyprland.running = true;
|
logoutProcessHyprland.running = true;
|
||||||
} else {
|
else
|
||||||
// fallback or error
|
|
||||||
console.warn("No supported compositor detected for logout");
|
console.warn("No supported compositor detected for logout");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function suspend() {
|
function suspend() {
|
||||||
|
|
@ -421,33 +46,479 @@ Rectangle {
|
||||||
rebootProcess.running = true;
|
rebootProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool panelVisible: false
|
function updateSystemInfo() {
|
||||||
|
uptimeProcess.running = true;
|
||||||
// Trigger initial update when panel becomes visible
|
}
|
||||||
onPanelVisibleChanged: {
|
|
||||||
if (panelVisible) {
|
width: 440 * Theme.scale(Screen)
|
||||||
updateSystemInfo();
|
height: 80 * Theme.scale(Screen)
|
||||||
}
|
color: "transparent"
|
||||||
|
anchors.horizontalCenterOffset: -2
|
||||||
|
onPanelVisibleChanged: {
|
||||||
|
if (panelVisible)
|
||||||
|
updateSystemInfo();
|
||||||
|
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
uptimeProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.surface
|
||||||
|
radius: 18 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 18 * Theme.scale(Screen)
|
||||||
|
spacing: 12 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 12 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 48 * Theme.scale(Screen)
|
||||||
|
height: 48 * Theme.scale(Screen)
|
||||||
|
radius: 24 * Theme.scale(Screen)
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
radius: 24 * Theme.scale(Screen)
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 2 * Theme.scale(Screen)
|
||||||
|
z: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 4 * Theme.scale(Screen)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: Quickshell.env("USER")
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "System Uptime: " + uptimeText
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
|
color: Theme.textSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: settingsButton
|
||||||
|
|
||||||
|
width: 32 * Theme.scale(Screen)
|
||||||
|
height: 32 * Theme.scale(Screen)
|
||||||
|
radius: 16 * Theme.scale(Screen)
|
||||||
|
color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.accentPrimary : "transparent"
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.horizontalCenterOffset: -1
|
||||||
|
text: "settings"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: settingsButtonArea.containsMouse || settingsButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: settingsButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings)
|
||||||
|
settingsModal.openSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledTooltip {
|
||||||
|
id: settingsTooltip
|
||||||
|
|
||||||
|
text: "Settings"
|
||||||
|
targetItem: settingsButton
|
||||||
|
tooltipVisible: settingsButtonArea.containsMouse
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: systemButton
|
||||||
|
|
||||||
|
width: 32 * Theme.scale(Screen)
|
||||||
|
height: 32 * Theme.scale(Screen)
|
||||||
|
radius: 16 * Theme.scale(Screen)
|
||||||
|
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent"
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "power_settings_new"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: systemButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
systemMenu.visible = !systemMenu.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledTooltip {
|
||||||
|
id: systemTooltip
|
||||||
|
|
||||||
|
text: "Power Menu"
|
||||||
|
targetItem: systemButton
|
||||||
|
tooltipVisible: systemButtonArea.containsMouse
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWithOverlay {
|
||||||
|
id: systemMenu
|
||||||
|
|
||||||
|
anchors.top: systemButton.bottom
|
||||||
|
anchors.right: systemButton.right
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 160 * Theme.scale(Screen)
|
||||||
|
height: 220 * Theme.scale(Screen)
|
||||||
|
color: Theme.surface
|
||||||
|
radius: 8 * Theme.scale(Screen)
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1 * Theme.scale(Screen)
|
||||||
|
visible: true
|
||||||
|
z: 9999
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 32 * Theme.scale(Screen)
|
||||||
|
anchors.topMargin: systemButton.y + systemButton.height + 48 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
// Prevent closing when clicking in the panel bg
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8 * Theme.scale(Screen)
|
||||||
|
spacing: 4 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36 * Theme.scale(Screen)
|
||||||
|
radius: 6 * Theme.scale(Screen)
|
||||||
|
color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12 * Theme.scale(Screen)
|
||||||
|
spacing: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "lock_outline"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Lock Screen"
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: lockButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
lockScreen.locked = true;
|
||||||
|
systemMenu.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36 * Theme.scale(Screen)
|
||||||
|
radius: 6 * Theme.scale(Screen)
|
||||||
|
color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12 * Theme.scale(Screen)
|
||||||
|
spacing: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "bedtime"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Suspend"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: suspendButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
suspend();
|
||||||
|
systemMenu.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36 * Theme.scale(Screen)
|
||||||
|
radius: 6 * Theme.scale(Screen)
|
||||||
|
color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12 * Theme.scale(Screen)
|
||||||
|
spacing: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "refresh"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Reboot"
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rebootButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
reboot();
|
||||||
|
systemMenu.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36 * Theme.scale(Screen)
|
||||||
|
radius: 6 * Theme.scale(Screen)
|
||||||
|
color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12 * Theme.scale(Screen)
|
||||||
|
spacing: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "exit_to_app"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Logout"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: logoutButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
logout();
|
||||||
|
systemMenu.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36 * Theme.scale(Screen)
|
||||||
|
radius: 6 * Theme.scale(Screen)
|
||||||
|
color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12 * Theme.scale(Screen)
|
||||||
|
spacing: 8 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "power_settings_new"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16 * Theme.scale(Screen)
|
||||||
|
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Shutdown"
|
||||||
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
|
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: shutdownButtonArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
shutdown();
|
||||||
|
systemMenu.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: uptimeProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
uptimeText = this.text.trim();
|
||||||
|
uptimeProcess.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: shutdownProcess
|
||||||
|
|
||||||
|
command: ["shutdown", "-h", "now"]
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: rebootProcess
|
||||||
|
|
||||||
|
command: ["reboot"]
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: suspendProcess
|
||||||
|
|
||||||
|
command: ["systemctl", "suspend"]
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: logoutProcessNiri
|
||||||
|
|
||||||
|
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: logoutProcessHyprland
|
||||||
|
|
||||||
|
command: ["hyprctl", "dispatch", "exit"]
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: logoutProcess
|
||||||
|
|
||||||
|
command: ["loginctl", "terminate-user", Quickshell.env("USER")]
|
||||||
|
running: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer to update uptime - only runs when panel is visible
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 60000 // Update every minute
|
interval: 60000
|
||||||
repeat: true
|
repeat: true
|
||||||
running: panelVisible
|
running: panelVisible
|
||||||
onTriggered: updateSystemInfo()
|
onTriggered: updateSystemInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
uptimeProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSystemInfo() {
|
|
||||||
uptimeProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add lockscreen instance (hidden by default)
|
|
||||||
LockScreen {
|
LockScreen {
|
||||||
id: lockScreen
|
id: lockScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,22 +7,22 @@ import qs.Components
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: card
|
id: card
|
||||||
width: 200
|
width: 200 * Theme.scale(Screen)
|
||||||
height: 70
|
height: 70 * Theme.scale(Screen)
|
||||||
color: Theme.surface
|
color: Theme.surface
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 20
|
spacing: 20 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
|
||||||
// Performance
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 36; height: 36
|
width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance)
|
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance)
|
||||||
? Theme.accentPrimary
|
? Theme.accentPrimary
|
||||||
: (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
|
: (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
|
||||||
|
|
@ -33,7 +33,7 @@ Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "speed"
|
text: "speed"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 22
|
font.pixelSize: 22 * Theme.scale(Screen)
|
||||||
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse
|
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse
|
||||||
? Theme.backgroundPrimary
|
? Theme.backgroundPrimary
|
||||||
: Theme.accentPrimary
|
: Theme.accentPrimary
|
||||||
|
|
@ -63,12 +63,12 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Balanced
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 36; height: 36
|
width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced)
|
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced)
|
||||||
? Theme.accentPrimary
|
? Theme.accentPrimary
|
||||||
: (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
|
: (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
|
||||||
|
|
@ -79,7 +79,7 @@ Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "balance"
|
text: "balance"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 22
|
font.pixelSize: 22 * Theme.scale(Screen)
|
||||||
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse
|
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse
|
||||||
? Theme.backgroundPrimary
|
? Theme.backgroundPrimary
|
||||||
: Theme.accentPrimary
|
: Theme.accentPrimary
|
||||||
|
|
@ -109,12 +109,12 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Power Saver
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 36; height: 36
|
width: 36 * Theme.scale(Screen); height: 36 * Theme.scale(Screen)
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
border.color: Theme.accentPrimary
|
border.color: Theme.accentPrimary
|
||||||
border.width: 1
|
border.width: 1 * Theme.scale(Screen)
|
||||||
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver)
|
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver)
|
||||||
? Theme.accentPrimary
|
? Theme.accentPrimary
|
||||||
: (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
|
: (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
|
||||||
|
|
@ -125,7 +125,7 @@ Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "eco"
|
text: "eco"
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 22
|
font.pixelSize: 22 * Theme.scale(Screen)
|
||||||
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse
|
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse
|
||||||
? Theme.backgroundPrimary
|
? Theme.backgroundPrimary
|
||||||
: Theme.accentPrimary
|
: Theme.accentPrimary
|
||||||
94
Widgets/SidePanel/SettingsIcon.qml
Normal file
94
Widgets/SidePanel/SettingsIcon.qml
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets.SettingsWindow
|
||||||
|
import qs.Components
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: settingsModal
|
||||||
|
implicitWidth: 480 * Theme.scale(Screen)
|
||||||
|
implicitHeight: 780 * Theme.scale(Screen)
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
anchors.top: true
|
||||||
|
anchors.right: true
|
||||||
|
margins.right: 0
|
||||||
|
margins.top: 0
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
|
// Signal to request weather refresh
|
||||||
|
signal weatherRefreshRequested()
|
||||||
|
|
||||||
|
// Property to track the settings window instance
|
||||||
|
property var settingsWindow: null
|
||||||
|
|
||||||
|
// Function to open the modal and initialize temp values
|
||||||
|
function openSettings(initialTabIndex) {
|
||||||
|
if (!settingsWindow) {
|
||||||
|
// Create new window
|
||||||
|
settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues
|
||||||
|
if (settingsWindow) {
|
||||||
|
// Set the initial tab if provided
|
||||||
|
if (typeof initialTabIndex === 'number' && initialTabIndex >= 0 && initialTabIndex <= 8) {
|
||||||
|
settingsWindow.activeTabIndex = initialTabIndex;
|
||||||
|
}
|
||||||
|
settingsWindow.visible = true;
|
||||||
|
|
||||||
|
// Show wallpaper selector if opening wallpaper tab (after window is visible)
|
||||||
|
if (typeof initialTabIndex === 'number' && initialTabIndex === 6) {
|
||||||
|
Qt.callLater(function() {
|
||||||
|
if (settingsWindow && settingsWindow.showWallpaperSelector) {
|
||||||
|
settingsWindow.showWallpaperSelector();
|
||||||
|
}
|
||||||
|
}, 100); // Small delay to ensure window is fully loaded
|
||||||
|
}
|
||||||
|
// Handle window closure
|
||||||
|
settingsWindow.visibleChanged.connect(function() {
|
||||||
|
if (settingsWindow && !settingsWindow.visible) {
|
||||||
|
// Trigger weather refresh when settings close
|
||||||
|
weatherRefreshRequested();
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sidebarPopup.dismiss();
|
||||||
|
} else if (settingsWindow.visible) {
|
||||||
|
// Close and destroy window
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.visible = false;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to close the modal and release focus
|
||||||
|
function closeSettings() {
|
||||||
|
if (settingsWindow) {
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.visible = false;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: settingsComponent
|
||||||
|
SettingsWindow {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up on destruction
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (settingsWindow) {
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
81
Widgets/SidePanel/SettingsModal.qml
Normal file
81
Widgets/SidePanel/SettingsModal.qml
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets.SettingsWindow
|
||||||
|
import qs.Components
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: settingsModal
|
||||||
|
implicitWidth: 480 * Theme.scale(Screen)
|
||||||
|
implicitHeight: 780 * Theme.scale(Screen)
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
anchors.top: true
|
||||||
|
anchors.right: true
|
||||||
|
margins.right: 0
|
||||||
|
margins.top: 0
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
|
// Property to track the settings window instance
|
||||||
|
property var settingsWindow: null
|
||||||
|
|
||||||
|
// Function to open the modal and initialize temp values
|
||||||
|
function openSettings() {
|
||||||
|
if (!settingsWindow) {
|
||||||
|
// Create new window
|
||||||
|
settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues
|
||||||
|
if (settingsWindow) {
|
||||||
|
settingsWindow.visible = true;
|
||||||
|
// Handle window closure
|
||||||
|
settingsWindow.visibleChanged.connect(function() {
|
||||||
|
if (settingsWindow && !settingsWindow.visible) {
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (settingsWindow.visible) {
|
||||||
|
// Close and destroy window
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.visible = false;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to close the modal and release focus
|
||||||
|
function closeSettings() {
|
||||||
|
if (settingsWindow) {
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.visible = false;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: settingsComponent
|
||||||
|
SettingsWindow {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up on destruction
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (settingsWindow) {
|
||||||
|
var windowToDestroy = settingsWindow;
|
||||||
|
settingsWindow = null;
|
||||||
|
windowToDestroy.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh weather data when hidden
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) {
|
||||||
|
weather.fetchCityWeather();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,35 +8,37 @@ import qs.Settings
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: systemMonitor
|
id: systemMonitor
|
||||||
width: 70
|
width: 70 * Theme.scale(Screen)
|
||||||
height: 250
|
height: 250 * Theme.scale(Screen)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
|
// Track visibility state for panel integration
|
||||||
property bool isVisible: false
|
property bool isVisible: false
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: card
|
id: card
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Theme.surface
|
color: Theme.surface
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 8
|
anchors.margins: 8 * Theme.scale(Screen)
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
// CPU Usage
|
|
||||||
|
// CPU usage indicator with circular progress bar
|
||||||
Item {
|
Item {
|
||||||
width: 50; height: 50
|
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
|
||||||
CircularProgressBar {
|
CircularProgressBar {
|
||||||
id: cpuBar
|
id: cpuBar
|
||||||
progress: Sysinfo.cpuUsage / 100
|
progress: Sysinfo.cpuUsage / 100
|
||||||
size: 50
|
size: 50 * Theme.scale(Screen)
|
||||||
strokeWidth: 4
|
strokeWidth: 4 * Theme.scale(Screen)
|
||||||
hasNotch: true
|
hasNotch: true
|
||||||
notchIcon: "speed"
|
notchIcon: "speed"
|
||||||
notchIconSize: 14
|
notchIconSize: 14 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
@ -55,18 +57,19 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cpu Temp
|
|
||||||
|
// CPU temperature indicator with circular progress bar
|
||||||
Item {
|
Item {
|
||||||
width: 50; height: 50
|
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
|
||||||
CircularProgressBar {
|
CircularProgressBar {
|
||||||
id: tempBar
|
id: tempBar
|
||||||
progress: Sysinfo.cpuTemp / 100
|
progress: Sysinfo.cpuTemp / 100
|
||||||
size: 50
|
size: 50 * Theme.scale(Screen)
|
||||||
strokeWidth: 4
|
strokeWidth: 4 * Theme.scale(Screen)
|
||||||
hasNotch: true
|
hasNotch: true
|
||||||
units: "°C"
|
units: "°C"
|
||||||
notchIcon: "thermometer"
|
notchIcon: "thermometer"
|
||||||
notchIconSize: 14
|
notchIconSize: 14 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
@ -85,17 +88,18 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory Usage
|
|
||||||
|
// Memory usage indicator with circular progress bar
|
||||||
Item {
|
Item {
|
||||||
width: 50; height: 50
|
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
|
||||||
CircularProgressBar {
|
CircularProgressBar {
|
||||||
id: memBar
|
id: memBar
|
||||||
progress: Sysinfo.memoryUsagePer / 100
|
progress: Sysinfo.memoryUsagePer / 100
|
||||||
size: 50
|
size: 50 * Theme.scale(Screen)
|
||||||
strokeWidth: 4
|
strokeWidth: 4 * Theme.scale(Screen)
|
||||||
hasNotch: true
|
hasNotch: true
|
||||||
notchIcon: "memory"
|
notchIcon: "memory"
|
||||||
notchIconSize: 14
|
notchIconSize: 14 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
@ -114,17 +118,18 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disk Usage
|
|
||||||
|
// Disk usage indicator with circular progress bar
|
||||||
Item {
|
Item {
|
||||||
width: 50; height: 50
|
width: 50 * Theme.scale(Screen); height: 50 * Theme.scale(Screen)
|
||||||
CircularProgressBar {
|
CircularProgressBar {
|
||||||
id: diskBar
|
id: diskBar
|
||||||
progress: Sysinfo.diskUsage / 100
|
progress: Sysinfo.diskUsage / 100
|
||||||
size: 50
|
size: 50 * Theme.scale(Screen)
|
||||||
strokeWidth: 4
|
strokeWidth: 4 * Theme.scale(Screen)
|
||||||
hasNotch: true
|
hasNotch: true
|
||||||
notchIcon: "storage"
|
notchIcon: "storage"
|
||||||
notchIconSize: 14
|
notchIconSize: 14 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
@ -2,12 +2,12 @@ import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
import "../../../Helpers/Weather.js" as WeatherHelper
|
import "../../Helpers/Weather.js" as WeatherHelper
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: weatherRoot
|
id: weatherRoot
|
||||||
width: 440
|
width: 440 * Theme.scale(Screen)
|
||||||
height: 180
|
height: 180 * Theme.scale(Screen)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
anchors.horizontalCenterOffset: -2
|
anchors.horizontalCenterOffset: -2
|
||||||
|
|
||||||
|
|
@ -15,6 +15,20 @@ Rectangle {
|
||||||
property var weatherData: null
|
property var weatherData: null
|
||||||
property string errorString: ""
|
property string errorString: ""
|
||||||
property bool isVisible: false
|
property bool isVisible: false
|
||||||
|
property int lastFetchTime: 0
|
||||||
|
property bool isLoading: false
|
||||||
|
|
||||||
|
// Auto-refetch weather when city changes
|
||||||
|
Connections {
|
||||||
|
target: Settings.settings
|
||||||
|
function onWeatherCityChanged() {
|
||||||
|
if (isVisible && city !== "") {
|
||||||
|
// Force refresh when city changes
|
||||||
|
lastFetchTime = 0;
|
||||||
|
fetchCityWeather();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
|
|
@ -23,20 +37,42 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchCityWeather() {
|
function fetchCityWeather() {
|
||||||
|
if (!city || city.trim() === "") {
|
||||||
|
errorString = "No city configured";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should fetch new data (avoid fetching too frequently)
|
||||||
|
var currentTime = Date.now();
|
||||||
|
var timeSinceLastFetch = currentTime - lastFetchTime;
|
||||||
|
|
||||||
|
// Only skip if we have recent data AND lastFetchTime is not 0 (initial state)
|
||||||
|
if (lastFetchTime > 0 && timeSinceLastFetch < 60000) { // 1 minute
|
||||||
|
return; // Skip if last fetch was less than 1 minute ago
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
errorString = "";
|
||||||
|
|
||||||
WeatherHelper.fetchCityWeather(city,
|
WeatherHelper.fetchCityWeather(city,
|
||||||
function(result) {
|
function(result) {
|
||||||
weatherData = result.weather;
|
weatherData = result.weather;
|
||||||
|
lastFetchTime = currentTime;
|
||||||
errorString = "";
|
errorString = "";
|
||||||
|
isLoading = false;
|
||||||
},
|
},
|
||||||
function(err) {
|
function(err) {
|
||||||
errorString = err;
|
errorString = err;
|
||||||
|
isLoading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startWeatherFetch() {
|
function startWeatherFetch() {
|
||||||
isVisible = true
|
isVisible = true
|
||||||
fetchCityWeather()
|
// Force refresh when panel opens, regardless of time check
|
||||||
|
lastFetchTime = 0;
|
||||||
|
fetchCityWeather();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopWeatherFetch() {
|
function stopWeatherFetch() {
|
||||||
|
|
@ -47,81 +83,90 @@ Rectangle {
|
||||||
id: card
|
id: card
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Theme.surface
|
color: Theme.surface
|
||||||
radius: 18
|
radius: 18 * Theme.scale(Screen)
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 18
|
anchors.margins: 18 * Theme.scale(Screen)
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
|
||||||
// Current weather row
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
// Weather icon and basic info section
|
|
||||||
RowLayout {
|
|
||||||
spacing: 12
|
|
||||||
Layout.preferredWidth: 140
|
|
||||||
|
|
||||||
// Weather icon
|
RowLayout {
|
||||||
|
spacing: 12 * Theme.scale(Screen)
|
||||||
|
Layout.preferredWidth: 140 * Theme.scale(Screen)
|
||||||
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: weatherIcon
|
id: weatherIcon
|
||||||
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
|
text: isLoading ? "sync" : (weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud")
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 28
|
font.pixelSize: 28 * Theme.scale(Screen)
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
color: Theme.accentPrimary
|
color: isLoading ? Theme.accentPrimary : Theme.accentPrimary
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
// Add rotation animation for loading state
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: isLoading
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
loops: Animation.Infinite
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 2
|
spacing: 2 * Theme.scale(Screen)
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 4
|
spacing: 4 * Theme.scale(Screen)
|
||||||
Text {
|
Text {
|
||||||
text: city
|
text: city
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14 * Theme.scale(Screen)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : ""
|
text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : ""
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
leftPadding: 2
|
leftPadding: 2 * Theme.scale(Screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
|
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 24
|
font.pixelSize: 24 * Theme.scale(Screen)
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Spacer to push content to the right
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separator line
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 1
|
height: 1 * Theme.scale(Screen)
|
||||||
color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12)
|
color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 2
|
Layout.topMargin: 2 * Theme.scale(Screen)
|
||||||
Layout.bottomMargin: 2
|
Layout.bottomMargin: 2 * Theme.scale(Screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5-day forecast row
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12 * Theme.scale(Screen)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
visible: weatherData && weatherData.daily && weatherData.daily.time
|
visible: weatherData && weatherData.daily && weatherData.daily.time
|
||||||
|
|
@ -129,31 +174,31 @@ Rectangle {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0
|
model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0
|
||||||
delegate: ColumnLayout {
|
delegate: ColumnLayout {
|
||||||
spacing: 2
|
spacing: 2 * Theme.scale(Screen)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Text {
|
Text {
|
||||||
// Day of the week (e.g., Mon)
|
|
||||||
text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd")
|
text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd")
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
color: Theme.textSecondary
|
color: Theme.textSecondary
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
// Material Symbol icon
|
|
||||||
text: materialSymbolForCode(weatherData.daily.weathercode[index])
|
text: materialSymbolForCode(weatherData.daily.weathercode[index])
|
||||||
font.family: "Material Symbols Outlined"
|
font.family: "Material Symbols Outlined"
|
||||||
font.pixelSize: 22
|
font.pixelSize: 22 * Theme.scale(Screen)
|
||||||
color: Theme.accentPrimary
|
color: Theme.accentPrimary
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
// High/low temp
|
|
||||||
text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°")
|
text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°")
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12 * Theme.scale(Screen)
|
||||||
color: Theme.textPrimary
|
color: Theme.textPrimary
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
@ -162,29 +207,29 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error message
|
|
||||||
Text {
|
Text {
|
||||||
text: errorString
|
text: errorString
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
visible: errorString !== ""
|
visible: errorString !== ""
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10 * Theme.scale(Screen)
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weather code to Material Symbol ligature mapping
|
|
||||||
function materialSymbolForCode(code) {
|
function materialSymbolForCode(code) {
|
||||||
if (code === 0) return "sunny"; // Clear
|
if (code === 0) return "sunny";
|
||||||
if (code === 1 || code === 2) return "partly_cloudy_day"; // Mainly clear/partly cloudy
|
if (code === 1 || code === 2) return "partly_cloudy_day";
|
||||||
if (code === 3) return "cloud"; // Overcast
|
if (code === 3) return "cloud";
|
||||||
if (code >= 45 && code <= 48) return "foggy"; // Fog
|
if (code >= 45 && code <= 48) return "foggy";
|
||||||
if (code >= 51 && code <= 67) return "rainy"; // Drizzle
|
if (code >= 51 && code <= 67) return "rainy";
|
||||||
if (code >= 71 && code <= 77) return "weather_snowy"; // Snow
|
if (code >= 71 && code <= 77) return "weather_snowy";
|
||||||
if (code >= 80 && code <= 82) return "rainy"; // Rain showers
|
if (code >= 80 && code <= 82) return "rainy";
|
||||||
if (code >= 95 && code <= 99) return "thunderstorm"; // Thunderstorm
|
if (code >= 95 && code <= 99) return "thunderstorm";
|
||||||
return "cloud";
|
return "cloud";
|
||||||
}
|
}
|
||||||
function weatherDescriptionForCode(code) {
|
function weatherDescriptionForCode(code) {
|
||||||
|
|
@ -204,19 +204,19 @@ Item {
|
||||||
|
|
||||||
wifiLogic.connectingSsid = params.ssid;
|
wifiLogic.connectingSsid = params.ssid;
|
||||||
|
|
||||||
// Find the target network in our networks data
|
|
||||||
const targetNetwork = wifiLogic.networks[params.ssid];
|
const targetNetwork = wifiLogic.networks[params.ssid];
|
||||||
|
|
||||||
// Check if profile already exists using existing field
|
|
||||||
if (targetNetwork && targetNetwork.existing) {
|
if (targetNetwork && targetNetwork.existing) {
|
||||||
// Profile exists, just bring it up (no password prompt)
|
|
||||||
upConnectionProcess.profileName = params.ssid;
|
upConnectionProcess.profileName = params.ssid;
|
||||||
upConnectionProcess.running = true;
|
upConnectionProcess.running = true;
|
||||||
wifiLogic.pendingConnect = null;
|
wifiLogic.pendingConnect = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No existing profile, proceed with normal connection flow
|
|
||||||
if (params.security && params.security !== "--") {
|
if (params.security && params.security !== "--") {
|
||||||
getInterfaceProcess.running = true;
|
getInterfaceProcess.running = true;
|
||||||
return;
|
return;
|
||||||
|
|
@ -232,7 +232,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect, delete profile, refresh
|
|
||||||
Process {
|
Process {
|
||||||
id: disconnectProfileProcess
|
id: disconnectProfileProcess
|
||||||
property string connectionName: ""
|
property string connectionName: ""
|
||||||
|
|
@ -291,7 +291,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Handles connecting to a Wi-Fi network, with or without password
|
|
||||||
Process {
|
Process {
|
||||||
id: connectProcess
|
id: connectProcess
|
||||||
property string ssid: ""
|
property string ssid: ""
|
||||||
|
|
@ -336,7 +336,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds the correct Wi-Fi interface for connection
|
|
||||||
Process {
|
Process {
|
||||||
id: getInterfaceProcess
|
id: getInterfaceProcess
|
||||||
running: false
|
running: false
|
||||||
|
|
@ -370,7 +370,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a new Wi-Fi connection profile
|
|
||||||
Process {
|
Process {
|
||||||
id: addConnectionProcess
|
id: addConnectionProcess
|
||||||
property string ifname: ""
|
property string ifname: ""
|
||||||
|
|
@ -403,7 +403,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brings up the new connection profile and finalizes connection state
|
|
||||||
Process {
|
Process {
|
||||||
id: upConnectionProcess
|
id: upConnectionProcess
|
||||||
property string profileName: ""
|
property string profileName: ""
|
||||||
|
|
@ -436,7 +436,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wifi button (no background card)
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: wifiButton
|
id: wifiButton
|
||||||
width: 36
|
width: 36
|
||||||
|
|
@ -516,8 +516,8 @@ Item {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
visible: false
|
visible: false
|
||||||
running: false
|
running: false
|
||||||
color: Theme.accentPrimary // Assuming Spinner supports color property
|
color: Theme.accentPrimary
|
||||||
size: 22 // Based on the existing Spinner usage
|
size: 22
|
||||||
}
|
}
|
||||||
IconButton {
|
IconButton {
|
||||||
id: refreshButton
|
id: refreshButton
|
||||||
|
|
@ -704,7 +704,7 @@ Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// Toggle the action panel for this network
|
|
||||||
if (wifiLogic.actionPanelSsid === modelData.ssid) {
|
if (wifiLogic.actionPanelSsid === modelData.ssid) {
|
||||||
wifiLogic.actionPanelSsid = ""; // Close if already open
|
wifiLogic.actionPanelSsid = ""; // Close if already open
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -791,7 +791,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Action panel for network connection controls
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: modelData.ssid === wifiLogic.actionPanelSsid
|
visible: modelData.ssid === wifiLogic.actionPanelSsid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -806,7 +806,7 @@ Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.margins: 12
|
||||||
spacing: 10
|
spacing: 10
|
||||||
// Password field for new secured networks
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 36
|
Layout.preferredHeight: 36
|
||||||
|
|
@ -830,7 +830,7 @@ Item {
|
||||||
inputMethodHints: Qt.ImhNone
|
inputMethodHints: Qt.ImhNone
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
// Connect with the entered password
|
|
||||||
wifiLogic.pendingConnect = {
|
wifiLogic.pendingConnect = {
|
||||||
ssid: modelData.ssid,
|
ssid: modelData.ssid,
|
||||||
security: modelData.security,
|
security: modelData.security,
|
||||||
|
|
@ -843,7 +843,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Connect/Disconnect button
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.preferredWidth: 80
|
Layout.preferredWidth: 80
|
||||||
Layout.preferredHeight: 36
|
Layout.preferredHeight: 36
|
||||||
|
|
@ -861,12 +861,12 @@ Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData.connected) {
|
if (modelData.connected) {
|
||||||
// Disconnect from network
|
|
||||||
wifiLogic.disconnectNetwork(modelData.ssid);
|
wifiLogic.disconnectNetwork(modelData.ssid);
|
||||||
} else {
|
} else {
|
||||||
// For secured networks, check if we need password
|
|
||||||
if (wifiLogic.isSecured(modelData.security) && !modelData.existing) {
|
if (wifiLogic.isSecured(modelData.security) && !modelData.existing) {
|
||||||
// If password field is visible and has content, use it
|
|
||||||
if (actionPanelPasswordField.text.length > 0) {
|
if (actionPanelPasswordField.text.length > 0) {
|
||||||
wifiLogic.pendingConnect = {
|
wifiLogic.pendingConnect = {
|
||||||
ssid: modelData.ssid,
|
ssid: modelData.ssid,
|
||||||
|
|
@ -875,10 +875,9 @@ Item {
|
||||||
};
|
};
|
||||||
wifiLogic.doConnect();
|
wifiLogic.doConnect();
|
||||||
}
|
}
|
||||||
// For new networks without password entered, we might want to show an error or handle differently
|
|
||||||
// For now, we'll just close the panel
|
|
||||||
} else {
|
} else {
|
||||||
// Connect to open network
|
|
||||||
wifiLogic.connectNetwork(modelData.ssid, modelData.security);
|
wifiLogic.connectNetwork(modelData.ssid, modelData.security);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import qs.Settings
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
property alias title: headerText.text
|
|
||||||
property bool expanded: false // Hidden by default
|
|
||||||
default property alias content: contentItem.children
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 44
|
|
||||||
radius: 12
|
|
||||||
color: Theme.surface
|
|
||||||
border.color: Theme.accentPrimary
|
|
||||||
border.width: 2
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
spacing: 8
|
|
||||||
Item { width: 2 }
|
|
||||||
Text {
|
|
||||||
id: headerText
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeBody
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
}
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
Rectangle {
|
|
||||||
width: 32; height: 32
|
|
||||||
color: "transparent"
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: expanded ? "expand_less" : "expand_more"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: Theme.fontSizeBody
|
|
||||||
color: Theme.accentPrimary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: expanded = !expanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Item { height: 8 }
|
|
||||||
ColumnLayout {
|
|
||||||
id: contentItem
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: expanded
|
|
||||||
spacing: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Settings
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: weatherSettingsCard
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 320
|
|
||||||
color: Theme.surface
|
|
||||||
radius: 18
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 18
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
// Weather Settings Header
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "wb_sunny"
|
|
||||||
font.family: "Material Symbols Outlined"
|
|
||||||
font.pixelSize: 20
|
|
||||||
color: Theme.accentPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Weather Settings"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 16
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weather City Setting
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 8
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "City"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 13
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 40
|
|
||||||
radius: 16
|
|
||||||
color: Theme.surfaceVariant
|
|
||||||
border.color: cityInput.activeFocus ? Theme.accentPrimary : Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
TextInput {
|
|
||||||
id: cityInput
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.leftMargin: 12
|
|
||||||
anchors.rightMargin: 12
|
|
||||||
anchors.topMargin: 6
|
|
||||||
anchors.bottomMargin: 6
|
|
||||||
text: Settings.settings.weatherCity
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 13
|
|
||||||
color: Theme.textPrimary
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
clip: true
|
|
||||||
focus: true
|
|
||||||
selectByMouse: true
|
|
||||||
activeFocusOnTab: true
|
|
||||||
inputMethodHints: Qt.ImhNone
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
Settings.settings.weatherCity = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.IBeamCursor
|
|
||||||
onClicked: {
|
|
||||||
cityInput.forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temperature Unit Setting
|
|
||||||
RowLayout {
|
|
||||||
spacing: 12
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Temperature Unit"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 13
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Material 3 Switch
|
|
||||||
Rectangle {
|
|
||||||
id: customSwitch
|
|
||||||
width: 52
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Theme.accentPrimary
|
|
||||||
border.color: Theme.accentPrimary
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: thumb
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: Theme.surface
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
y: 2
|
|
||||||
x: Settings.settings.useFahrenheit ? customSwitch.width - width - 2 : 2
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: Settings.settings.useFahrenheit ? "\u00b0F" : "\u00b0C"
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
Settings.settings.useFahrenheit = !Settings.settings.useFahrenheit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random Wallpaper Setting
|
|
||||||
RowLayout {
|
|
||||||
spacing: 8
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Use 12 Hour Clock"
|
|
||||||
font.pixelSize: 13
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Material 3 Switch
|
|
||||||
Rectangle {
|
|
||||||
id: use12HourClockSwitch
|
|
||||||
width: 52
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Settings.settings.use12HourClock ? Theme.accentPrimary : Theme.surfaceVariant
|
|
||||||
border.color: Settings.settings.use12HourClock ? Theme.accentPrimary : Theme.outline
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: randomWallpaperThumb
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: Theme.surface
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
y: 2
|
|
||||||
x: Settings.settings.use12HourClock ? use12HourClockSwitch.width - width - 2 : 2
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
Settings.settings.use12HourClock = !Settings.settings.use12HourClock;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse Day Month Setting
|
|
||||||
RowLayout {
|
|
||||||
spacing: 8
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "US Style Date"
|
|
||||||
font.pixelSize: 13
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.textPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Material 3 Switch
|
|
||||||
Rectangle {
|
|
||||||
id: reverseDayMonthSwitch
|
|
||||||
width: 52
|
|
||||||
height: 32
|
|
||||||
radius: 16
|
|
||||||
color: Settings.settings.reverseDayMonth ? Theme.accentPrimary : Theme.surfaceVariant
|
|
||||||
border.color: Settings.settings.reverseDayMonth ? Theme.accentPrimary : Theme.outline
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: reverseDayMonthThumb
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: Theme.surface
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
y: 2
|
|
||||||
x: Settings.settings.reverseDayMonth ? reverseDayMonthSwitch.width - width - 2 : 2
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
Settings.settings.reverseDayMonth = !Settings.settings.reverseDayMonth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
qmlls.ini
Normal file
0
qmlls.ini
Normal file
76
shell.qml
76
shell.qml
|
|
@ -9,19 +9,26 @@ import qs.Bar.Modules
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Widgets.LockScreen
|
import qs.Widgets.LockScreen
|
||||||
import qs.Widgets.Notification
|
import qs.Widgets.Notification
|
||||||
|
import qs.Widgets.SettingsWindow
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
|
||||||
import "./Helpers/IdleInhibitor.qml"
|
|
||||||
import "./Helpers/IPCHandlers.qml"
|
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias appLauncherPanel: appLauncherPanel
|
property alias appLauncherPanel: appLauncherPanel
|
||||||
property var notificationHistoryWin: notificationHistoryWin
|
property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null
|
||||||
|
property var settingsWindow: null
|
||||||
property bool pendingReload: false
|
property bool pendingReload: false
|
||||||
|
|
||||||
|
// Function to load notification history
|
||||||
|
function loadNotificationHistory() {
|
||||||
|
if (!notificationHistoryLoader.active) {
|
||||||
|
notificationHistoryLoader.loading = true;
|
||||||
|
}
|
||||||
|
return notificationHistoryLoader;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to round value to nearest step
|
// Helper function to round value to nearest step
|
||||||
function roundToStep(value, step) {
|
function roundToStep(value, step) {
|
||||||
return Math.round(value / step) * step;
|
return Math.round(value / step) * step;
|
||||||
|
|
@ -47,10 +54,22 @@ Scope {
|
||||||
Quickshell.shell = root;
|
Quickshell.shell = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Background {}
|
||||||
|
Overview {}
|
||||||
|
|
||||||
Bar {
|
Bar {
|
||||||
id: bar
|
id: bar
|
||||||
shell: root
|
shell: root
|
||||||
property var notificationHistoryWin: notificationHistoryWin
|
property var notificationHistoryWin: notificationHistoryLoader.active ? notificationHistoryLoader.item : null
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
Dock {
|
||||||
|
id: dock
|
||||||
|
property var modelData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dock {
|
Dock {
|
||||||
|
|
@ -79,13 +98,14 @@ Scope {
|
||||||
NotificationServer {
|
NotificationServer {
|
||||||
id: notificationServer
|
id: notificationServer
|
||||||
onNotification: function (notification) {
|
onNotification: function (notification) {
|
||||||
console.log("Notification received:", notification.appName);
|
console.log("[Notification] Received notification:", notification.appName, "-", notification.summary);
|
||||||
notification.tracked = true;
|
notification.tracked = true;
|
||||||
if (notificationPopup.notificationsVisible) {
|
if (notificationPopup.notificationsVisible) {
|
||||||
|
// Add notification to the popup manager
|
||||||
notificationPopup.addNotification(notification);
|
notificationPopup.addNotification(notification);
|
||||||
}
|
}
|
||||||
if (notificationHistoryWin) {
|
if (notificationHistoryLoader.active && notificationHistoryLoader.item) {
|
||||||
notificationHistoryWin.addToHistory({
|
notificationHistoryLoader.item.addToHistory({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
appName: notification.appName || "Notification",
|
appName: notification.appName || "Notification",
|
||||||
summary: notification.summary || "",
|
summary: notification.summary || "",
|
||||||
|
|
@ -99,11 +119,35 @@ Scope {
|
||||||
|
|
||||||
NotificationPopup {
|
NotificationPopup {
|
||||||
id: notificationPopup
|
id: notificationPopup
|
||||||
barVisible: bar.visible
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationHistory {
|
// LazyLoader for NotificationHistory - only load when needed
|
||||||
id: notificationHistoryWin
|
LazyLoader {
|
||||||
|
id: notificationHistoryLoader
|
||||||
|
loading: false
|
||||||
|
component: NotificationHistory {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centralized LazyLoader for SettingsWindow - prevents crashes on multiple opens
|
||||||
|
LazyLoader {
|
||||||
|
id: settingsWindowLoader
|
||||||
|
loading: false
|
||||||
|
component: SettingsWindow {
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.settingsWindow = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to safely show/hide settings window
|
||||||
|
function toggleSettingsWindow() {
|
||||||
|
if (!settingsWindowLoader.active) {
|
||||||
|
settingsWindowLoader.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsWindowLoader.item) {
|
||||||
|
settingsWindowLoader.item.visible = !settingsWindowLoader.item.visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reference to the default audio sink from Pipewire
|
// Reference to the default audio sink from Pipewire
|
||||||
|
|
@ -144,14 +188,15 @@ Scope {
|
||||||
function onScreensChanged() {
|
function onScreensChanged() {
|
||||||
if (lockScreen.locked) {
|
if (lockScreen.locked) {
|
||||||
pendingReload = true;
|
pendingReload = true;
|
||||||
} else {
|
} /*else {
|
||||||
reloadTimer.restart();
|
reloadTimer.restart();
|
||||||
}
|
} */
|
||||||
|
// ^commented out for now to fix QS crash on monitor wake.
|
||||||
|
// if it reintroduces the notification bug (https://github.com/Ly-sec/Noctalia/issues/32)...
|
||||||
|
// we need to find a different fix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEW: Keep volume property in sync with actual Pipewire audio sink volume ---
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: defaultAudioSink ? defaultAudioSink.audio : null
|
target: defaultAudioSink ? defaultAudioSink.audio : null
|
||||||
function onVolumeChanged() {
|
function onVolumeChanged() {
|
||||||
|
|
@ -172,4 +217,5 @@ Scope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue