Merge branch 'noctalia-dev:main' into fix/heuristic-lookup
This commit is contained in:
commit
81182aa65b
56 changed files with 2680 additions and 1646 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -116,9 +117,7 @@ Singleton {
|
||||||
id: adapter
|
id: adapter
|
||||||
|
|
||||||
// bar
|
// bar
|
||||||
property JsonObject bar
|
property JsonObject bar: JsonObject {
|
||||||
|
|
||||||
bar: JsonObject {
|
|
||||||
property string position: "top" // Possible values: "top", "bottom"
|
property string position: "top" // Possible values: "top", "bottom"
|
||||||
property bool showActiveWindowIcon: true
|
property bool showActiveWindowIcon: true
|
||||||
property bool alwaysShowBatteryPercentage: false
|
property bool alwaysShowBatteryPercentage: false
|
||||||
|
|
@ -130,14 +129,12 @@ Singleton {
|
||||||
widgets: JsonObject {
|
widgets: JsonObject {
|
||||||
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
|
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
|
||||||
property list<string> center: ["Workspace"]
|
property list<string> center: ["Workspace"]
|
||||||
property list<string> right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
|
property list<string> right: ["ScreenRecorderIndicator", "Tray", "ArchUpdater", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// general
|
// general
|
||||||
property JsonObject general
|
property JsonObject general: JsonObject {
|
||||||
|
|
||||||
general: JsonObject {
|
|
||||||
property string avatarImage: defaultAvatar
|
property string avatarImage: defaultAvatar
|
||||||
property bool dimDesktop: false
|
property bool dimDesktop: false
|
||||||
property bool showScreenCorners: false
|
property bool showScreenCorners: false
|
||||||
|
|
@ -145,9 +142,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// location
|
// location
|
||||||
property JsonObject location
|
property JsonObject location: JsonObject {
|
||||||
|
|
||||||
location: JsonObject {
|
|
||||||
property string name: "Tokyo"
|
property string name: "Tokyo"
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool reverseDayMonth: false
|
property bool reverseDayMonth: false
|
||||||
|
|
@ -156,9 +151,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// screen recorder
|
// screen recorder
|
||||||
property JsonObject screenRecorder
|
property JsonObject screenRecorder: JsonObject {
|
||||||
|
|
||||||
screenRecorder: JsonObject {
|
|
||||||
property string directory: "~/Videos"
|
property string directory: "~/Videos"
|
||||||
property int frameRate: 60
|
property int frameRate: 60
|
||||||
property string audioCodec: "opus"
|
property string audioCodec: "opus"
|
||||||
|
|
@ -171,9 +164,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wallpaper
|
// wallpaper
|
||||||
property JsonObject wallpaper
|
property JsonObject wallpaper: JsonObject {
|
||||||
|
|
||||||
wallpaper: JsonObject {
|
|
||||||
property string directory: "/usr/share/wallpapers"
|
property string directory: "/usr/share/wallpapers"
|
||||||
property string current: ""
|
property string current: ""
|
||||||
property bool isRandom: false
|
property bool isRandom: false
|
||||||
|
|
@ -194,9 +185,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// applauncher
|
// applauncher
|
||||||
property JsonObject appLauncher
|
property JsonObject appLauncher: JsonObject {
|
||||||
|
|
||||||
appLauncher: JsonObject {
|
|
||||||
// When disabled, Launcher hides clipboard command and ignores cliphist
|
// When disabled, Launcher hides clipboard command and ignores cliphist
|
||||||
property bool enableClipboardHistory: true
|
property bool enableClipboardHistory: true
|
||||||
// Position: center, top_left, top_right, bottom_left, bottom_right
|
// Position: center, top_left, top_right, bottom_left, bottom_right
|
||||||
|
|
@ -205,43 +194,34 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// dock
|
// dock
|
||||||
property JsonObject dock
|
property JsonObject dock: JsonObject {
|
||||||
|
|
||||||
dock: JsonObject {
|
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
property bool exclusive: false
|
property bool exclusive: false
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// network
|
// network
|
||||||
property JsonObject network
|
property JsonObject network: JsonObject {
|
||||||
|
|
||||||
network: JsonObject {
|
|
||||||
property bool wifiEnabled: true
|
property bool wifiEnabled: true
|
||||||
property bool bluetoothEnabled: true
|
property bool bluetoothEnabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
property JsonObject notifications
|
property JsonObject notifications: JsonObject {
|
||||||
|
|
||||||
notifications: JsonObject {
|
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
property JsonObject audio
|
property JsonObject audio: JsonObject {
|
||||||
|
|
||||||
audio: JsonObject {
|
|
||||||
property bool showMiniplayerAlbumArt: false
|
property bool showMiniplayerAlbumArt: false
|
||||||
property bool showMiniplayerCava: false
|
property bool showMiniplayerCava: false
|
||||||
property string visualizerType: "linear"
|
property string visualizerType: "linear"
|
||||||
property int volumeStep: 5
|
property int volumeStep: 5
|
||||||
|
property int cavaFrameRate: 60
|
||||||
}
|
}
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
property JsonObject ui
|
property JsonObject ui: JsonObject {
|
||||||
|
|
||||||
ui: JsonObject {
|
|
||||||
property string fontDefault: "Roboto" // Default font for all text
|
property string fontDefault: "Roboto" // Default font for all text
|
||||||
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
|
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
|
||||||
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
|
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
|
||||||
|
|
@ -259,15 +239,11 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// brightness
|
// brightness
|
||||||
property JsonObject brightness
|
property JsonObject brightness: JsonObject {
|
||||||
|
|
||||||
brightness: JsonObject {
|
|
||||||
property int brightnessStep: 5
|
property int brightnessStep: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject colorSchemes
|
property JsonObject colorSchemes: JsonObject {
|
||||||
|
|
||||||
colorSchemes: JsonObject {
|
|
||||||
property bool useWallpaperColors: false
|
property bool useWallpaperColors: false
|
||||||
property string predefinedScheme: ""
|
property string predefinedScheme: ""
|
||||||
property bool darkMode: true
|
property bool darkMode: true
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Commons
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// Signal emitted when widget loading status changes
|
|
||||||
signal widgetLoaded(string widgetName)
|
|
||||||
signal widgetFailed(string widgetName, string error)
|
|
||||||
signal loadingComplete(int total, int loaded, int failed)
|
|
||||||
|
|
||||||
// Properties to track loading status
|
|
||||||
property int totalWidgets: 0
|
|
||||||
property int loadedWidgets: 0
|
|
||||||
property int failedWidgets: 0
|
|
||||||
|
|
||||||
// Auto-discover widget components
|
|
||||||
function getWidgetComponent(widgetName) {
|
|
||||||
if (!widgetName || widgetName.trim() === "") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const widgetPath = `../Modules/Bar/Widgets/${widgetName}.qml`
|
|
||||||
|
|
||||||
// Try to load the widget directly from file
|
|
||||||
const component = Qt.createComponent(widgetPath)
|
|
||||||
if (component.status === Component.Ready) {
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMsg = `Failed to load ${widgetName}.qml widget, status: ${component.status}, error: ${component.errorString(
|
|
||||||
)}`
|
|
||||||
Logger.error("WidgetLoader", errorMsg)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize loading tracking
|
|
||||||
function initializeLoading(widgetList) {
|
|
||||||
totalWidgets = widgetList.length
|
|
||||||
loadedWidgets = 0
|
|
||||||
failedWidgets = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track widget loading success
|
|
||||||
function onWidgetLoaded(widgetName) {
|
|
||||||
loadedWidgets++
|
|
||||||
widgetLoaded(widgetName)
|
|
||||||
|
|
||||||
if (loadedWidgets + failedWidgets === totalWidgets) {
|
|
||||||
Logger.log("WidgetLoader", `Loaded ${loadedWidgets} widgets`)
|
|
||||||
loadingComplete(totalWidgets, loadedWidgets, failedWidgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track widget loading failure
|
|
||||||
function onWidgetFailed(widgetName, error) {
|
|
||||||
failedWidgets++
|
|
||||||
widgetFailed(widgetName, error)
|
|
||||||
|
|
||||||
if (loadedWidgets + failedWidgets === totalWidgets) {
|
|
||||||
loadingComplete(totalWidgets, loadedWidgets, failedWidgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is where you should add your Modules/Bar/Widgets/
|
|
||||||
// so it gets registered in the BarTab
|
|
||||||
function discoverAvailableWidgets() {
|
|
||||||
const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "MediaMini", "NotificationHistory", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"]
|
|
||||||
|
|
||||||
const availableWidgets = []
|
|
||||||
|
|
||||||
widgetFiles.forEach(widgetName => {
|
|
||||||
// Test if the widget can be loaded
|
|
||||||
const component = getWidgetComponent(widgetName)
|
|
||||||
if (component) {
|
|
||||||
availableWidgets.push({
|
|
||||||
"key": widgetName,
|
|
||||||
"name": widgetName
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sort alphabetically
|
|
||||||
availableWidgets.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
|
|
||||||
return availableWidgets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
230
Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml
Normal file
230
Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NPanel {
|
||||||
|
id: root
|
||||||
|
panelWidth: 380 * scaling
|
||||||
|
panelHeight: 500 * scaling
|
||||||
|
panelAnchorRight: true
|
||||||
|
|
||||||
|
// Auto-refresh when service updates
|
||||||
|
Connections {
|
||||||
|
target: ArchUpdaterService
|
||||||
|
function onUpdatePackagesChanged() {
|
||||||
|
// Force UI update when packages change
|
||||||
|
if (root.visible) {
|
||||||
|
// Small delay to ensure data is fully updated
|
||||||
|
Qt.callLater(() => {
|
||||||
|
// Force a UI update by triggering a property change
|
||||||
|
ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panelContent: Rectangle {
|
||||||
|
color: Color.mSurface
|
||||||
|
radius: Style.radiusL * scaling
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginL * scaling
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
text: "system_update"
|
||||||
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "System Updates"
|
||||||
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
tooltipText: "Close"
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update summary
|
||||||
|
Text {
|
||||||
|
text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length
|
||||||
|
!== 1 ? "s" : "") + " can be updated"
|
||||||
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package selection info
|
||||||
|
Text {
|
||||||
|
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected"
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package list
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
radius: Style.radiusM * scaling
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: packageListView
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginS * scaling
|
||||||
|
clip: true
|
||||||
|
model: ArchUpdaterService.updatePackages
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: packageListView.width
|
||||||
|
height: 50 * scaling
|
||||||
|
color: Color.transparent
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginS * scaling
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
|
// Checkbox for selection
|
||||||
|
NIconButton {
|
||||||
|
id: checkbox
|
||||||
|
icon: "check_box_outline_blank"
|
||||||
|
onClicked: {
|
||||||
|
const isSelected = ArchUpdaterService.isPackageSelected(modelData.name)
|
||||||
|
if (isSelected) {
|
||||||
|
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||||
|
icon = "check_box_outline_blank"
|
||||||
|
colorFg = Color.mOnSurfaceVariant
|
||||||
|
} else {
|
||||||
|
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||||
|
icon = "check_box"
|
||||||
|
colorFg = Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorFg: Color.mOnSurfaceVariant
|
||||||
|
Layout.preferredWidth: 30 * scaling
|
||||||
|
Layout.preferredHeight: 30 * scaling
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Set initial state
|
||||||
|
if (ArchUpdaterService.isPackageSelected(modelData.name)) {
|
||||||
|
icon = "check_box"
|
||||||
|
colorFg = Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package info
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginXXS * scaling
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.oldVersion + " → " + modelData.newVersion
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "refresh"
|
||||||
|
tooltipText: "Check for updates"
|
||||||
|
onClicked: {
|
||||||
|
ArchUpdaterService.doPoll()
|
||||||
|
}
|
||||||
|
colorBg: Color.mSurfaceVariant
|
||||||
|
colorFg: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 35 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update"
|
||||||
|
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
|
||||||
|
enabled: !ArchUpdaterService.updateInProgress
|
||||||
|
onClicked: {
|
||||||
|
ArchUpdaterService.runUpdate()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary
|
||||||
|
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 35 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings"
|
||||||
|
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
|
||||||
|
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
|
||||||
|
onClicked: {
|
||||||
|
if (ArchUpdaterService.selectedPackagesCount > 0) {
|
||||||
|
ArchUpdaterService.runSelectiveUpdate()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||||
|
> 0 ? Color.mSecondary : Color.mSurfaceVariant)
|
||||||
|
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||||
|
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 35 * scaling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.UPower
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
@ -47,6 +48,7 @@ Variants {
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
// Left Section - Dynamic Widgets
|
// Left Section - Dynamic Widgets
|
||||||
Row {
|
Row {
|
||||||
id: leftSection
|
id: leftSection
|
||||||
|
|
@ -60,21 +62,19 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Settings.data.bar.widgets.left
|
model: Settings.data.bar.widgets.left
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: leftWidgetLoader
|
|
||||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
|
||||||
active: true
|
active: true
|
||||||
|
sourceComponent: NWidgetLoader {
|
||||||
|
widgetName: modelData
|
||||||
|
widgetProps: {
|
||||||
|
"screen": screen
|
||||||
|
}
|
||||||
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Loader.Error) {
|
|
||||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
|
||||||
} else if (status === Loader.Ready) {
|
|
||||||
widgetLoader.onWidgetLoaded(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
// Center Section - Dynamic Widgets
|
// Center Section - Dynamic Widgets
|
||||||
Row {
|
Row {
|
||||||
id: centerSection
|
id: centerSection
|
||||||
|
|
@ -87,21 +87,19 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Settings.data.bar.widgets.center
|
model: Settings.data.bar.widgets.center
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: centerWidgetLoader
|
|
||||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
|
||||||
active: true
|
active: true
|
||||||
|
sourceComponent: NWidgetLoader {
|
||||||
|
widgetName: modelData
|
||||||
|
widgetProps: {
|
||||||
|
"screen": screen
|
||||||
|
}
|
||||||
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Loader.Error) {
|
|
||||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
|
||||||
} else if (status === Loader.Ready) {
|
|
||||||
widgetLoader.onWidgetLoaded(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
// Right Section - Dynamic Widgets
|
// Right Section - Dynamic Widgets
|
||||||
Row {
|
Row {
|
||||||
id: rightSection
|
id: rightSection
|
||||||
|
|
@ -115,35 +113,17 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Settings.data.bar.widgets.right
|
model: Settings.data.bar.widgets.right
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: rightWidgetLoader
|
|
||||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
|
||||||
active: true
|
active: true
|
||||||
|
sourceComponent: NWidgetLoader {
|
||||||
|
widgetName: modelData
|
||||||
|
widgetProps: {
|
||||||
|
"screen": screen
|
||||||
|
}
|
||||||
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Loader.Error) {
|
|
||||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
|
||||||
} else if (status === Loader.Ready) {
|
|
||||||
widgetLoader.onWidgetLoaded(modelData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Widget loader instance
|
|
||||||
WidgetLoader {
|
|
||||||
id: widgetLoader
|
|
||||||
|
|
||||||
onWidgetFailed: function (widgetName, error) {
|
|
||||||
Logger.error("Bar", `Widget failed: ${widgetName} - ${error}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize widget loading tracking
|
|
||||||
Component.onCompleted: {
|
|
||||||
const allWidgets = [...Settings.data.bar.widgets.left, ...Settings.data.bar.widgets.center, ...Settings.data.bar.widgets.right]
|
|
||||||
widgetLoader.initializeLoading(allWidgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,20 @@ import qs.Widgets
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
property bool showingFullTitle: false
|
||||||
|
property int lastWindowIndex: -1
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
visible: getTitle() !== ""
|
visible: getTitle() !== ""
|
||||||
|
|
||||||
property bool showingFullTitle: false
|
|
||||||
property int lastWindowIndex: -1
|
|
||||||
|
|
||||||
// Timer to hide full title after window switch
|
// Timer to hide full title after window switch
|
||||||
Timer {
|
Timer {
|
||||||
id: fullTitleTimer
|
id: fullTitleTimer
|
||||||
interval: Style.animationSlow * 4 // Show full title for 2 seconds
|
interval: 2000
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
showingFullTitle = false
|
showingFullTitle = false
|
||||||
|
|
@ -40,8 +43,9 @@ Row {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTitle() {
|
function getTitle() {
|
||||||
const focusedWindow = CompositorService.getFocusedWindow()
|
// Use the service's focusedWindowTitle property which is updated immediately
|
||||||
return focusedWindow ? (focusedWindow.title || focusedWindow.appId || "") : ""
|
// when WindowOpenedOrChanged events are received
|
||||||
|
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppIcon() {
|
function getAppIcon() {
|
||||||
|
|
@ -62,6 +66,7 @@ Row {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
// Let the Rectangle size itself based on its content (the Row)
|
// Let the Rectangle size itself based on its content (the Row)
|
||||||
|
visible: root.visible
|
||||||
width: row.width + Style.marginM * scaling * 2
|
width: row.width + Style.marginM * scaling * 2
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
height: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
|
|
@ -100,10 +105,10 @@ Row {
|
||||||
NText {
|
NText {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
|
||||||
// If hovered or just switched window, show up to 300 pixels
|
// If hovered or just switched window, show up to 400 pixels
|
||||||
// If not hovered show up to 150 pixels
|
// If not hovered show up to 150 pixels
|
||||||
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||||
300 * scaling) : Math.min(
|
400 * scaling) : Math.min(
|
||||||
fullTitleMetrics.contentWidth, 150 * scaling)
|
fullTitleMetrics.contentWidth, 150 * scaling)
|
||||||
text: getTitle()
|
text: getTitle()
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
|
|
||||||
75
Modules/Bar/Widgets/ArchUpdater.qml
Normal file
75
Modules/Bar/Widgets/ArchUpdater.qml
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
colorBg: Color.mSurfaceVariant
|
||||||
|
colorFg: Color.mOnSurface
|
||||||
|
colorBorder: Color.transparent
|
||||||
|
colorBorderHover: Color.transparent
|
||||||
|
|
||||||
|
// Enhanced icon states with better visual feedback
|
||||||
|
icon: {
|
||||||
|
if (ArchUpdaterService.busy)
|
||||||
|
return "sync"
|
||||||
|
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||||
|
// Show different icons based on update count
|
||||||
|
const count = ArchUpdaterService.updatePackages.length
|
||||||
|
if (count > 50)
|
||||||
|
return "system_update_alt" // Many updates
|
||||||
|
if (count > 10)
|
||||||
|
return "system_update" // Moderate updates
|
||||||
|
return "system_update" // Few updates
|
||||||
|
}
|
||||||
|
return "task_alt"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced tooltip with more information
|
||||||
|
tooltipText: {
|
||||||
|
if (ArchUpdaterService.busy)
|
||||||
|
return "Checking for updates…"
|
||||||
|
|
||||||
|
var count = ArchUpdaterService.updatePackages.length
|
||||||
|
if (count === 0)
|
||||||
|
return "System is up to date ✓"
|
||||||
|
|
||||||
|
var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:")
|
||||||
|
|
||||||
|
var list = ArchUpdaterService.updatePackages || []
|
||||||
|
var s = ""
|
||||||
|
var limit = Math.min(list.length, 8)
|
||||||
|
// Reduced to 8 for better readability
|
||||||
|
for (var i = 0; i < limit; ++i) {
|
||||||
|
var p = list[i]
|
||||||
|
s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion)
|
||||||
|
}
|
||||||
|
if (list.length > 8)
|
||||||
|
s += "\n… and " + (list.length - 8) + " more"
|
||||||
|
|
||||||
|
return header + "\n\n" + s + "\n\nClick to update system"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced click behavior with confirmation
|
||||||
|
onClicked: {
|
||||||
|
if (ArchUpdaterService.busy)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||||
|
// Show confirmation dialog for updates
|
||||||
|
PanelService.getPanel("archUpdaterPanel").toggle(screen)
|
||||||
|
} else {
|
||||||
|
// Just refresh if no updates available
|
||||||
|
ArchUpdaterService.doPoll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,18 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
NPill {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
implicitWidth: pill.width
|
||||||
|
implicitHeight: pill.height
|
||||||
|
|
||||||
|
NPill {
|
||||||
|
id: pill
|
||||||
|
|
||||||
// Test mode
|
// Test mode
|
||||||
property bool testMode: false
|
property bool testMode: false
|
||||||
property int testPercent: 49
|
property int testPercent: 49
|
||||||
|
|
@ -18,11 +27,13 @@ NPill {
|
||||||
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||||
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||||
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||||
property bool show: isReady && percent > 0
|
|
||||||
|
|
||||||
// Choose icon based on charge and charging state
|
// Choose icon based on charge and charging state
|
||||||
function batteryIcon() {
|
function batteryIcon() {
|
||||||
|
|
||||||
|
if (!isReady || !battery.isLaptopBattery)
|
||||||
|
return "battery_android_alert"
|
||||||
|
|
||||||
if (charging)
|
if (charging)
|
||||||
return "battery_android_bolt"
|
return "battery_android_bolt"
|
||||||
|
|
||||||
|
|
@ -46,49 +57,49 @@ NPill {
|
||||||
return "battery_android_0"
|
return "battery_android_0"
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: testMode || (isReady && battery.isLaptopBattery)
|
icon: batteryIcon()
|
||||||
|
text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-"
|
||||||
icon: root.batteryIcon()
|
|
||||||
text: Math.round(root.percent) + "%"
|
|
||||||
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
||||||
forceShown: Settings.data.bar.alwaysShowBatteryPercentage
|
forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage
|
||||||
|
disableOpen: (!isReady || !battery.isLaptopBattery)
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
let lines = []
|
let lines = []
|
||||||
|
|
||||||
if (testMode) {
|
if (testMode) {
|
||||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
|
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345))
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.isReady) {
|
if (!isReady || !battery.isLaptopBattery) {
|
||||||
return ""
|
return "No Battery Detected"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.battery.timeToEmpty > 0) {
|
if (battery.timeToEmpty > 0) {
|
||||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(root.battery.timeToEmpty))
|
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.battery.timeToFull > 0) {
|
if (battery.timeToFull > 0) {
|
||||||
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(root.battery.timeToFull))
|
lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.battery.changeRate !== undefined) {
|
if (battery.changeRate !== undefined) {
|
||||||
const rate = root.battery.changeRate
|
const rate = battery.changeRate
|
||||||
if (rate > 0) {
|
if (rate > 0) {
|
||||||
lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
|
lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed(
|
||||||
2) + " W")
|
2) + " W")
|
||||||
} else if (rate < 0) {
|
} else if (rate < 0) {
|
||||||
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W")
|
lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W")
|
||||||
} else {
|
} else {
|
||||||
lines.push("Estimating...")
|
lines.push("Estimating...")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(root.charging ? "Charging" : "Discharging")
|
lines.push(charging ? "Charging" : "Discharging")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) {
|
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
|
||||||
lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%")
|
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
|
||||||
}
|
}
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,11 @@ import qs.Widgets
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
sizeMultiplier: 0.8
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
visible: Settings.data.network.bluetoothEnabled
|
||||||
|
sizeMultiplier: 0.8
|
||||||
colorBg: Color.mSurfaceVariant
|
colorBg: Color.mSurfaceVariant
|
||||||
colorFg: Color.mOnSurface
|
colorFg: Color.mOnSurface
|
||||||
colorBorder: Color.transparent
|
colorBorder: Color.transparent
|
||||||
|
|
@ -28,7 +31,5 @@ NIconButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltipText: "Bluetooth Devices"
|
tooltipText: "Bluetooth Devices"
|
||||||
onClicked: {
|
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen)
|
||||||
bluetoothPanel.toggle(screen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,16 @@ import qs.Widgets
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
width: pill.width
|
property ShellScreen screen
|
||||||
height: pill.height
|
property real scaling: ScalingService.scale(screen)
|
||||||
visible: getMonitor() !== null
|
|
||||||
|
|
||||||
// Used to avoid opening the pill on Quickshell startup
|
// Used to avoid opening the pill on Quickshell startup
|
||||||
property bool firstBrightnessReceived: false
|
property bool firstBrightnessReceived: false
|
||||||
|
|
||||||
|
implicitWidth: pill.width
|
||||||
|
implicitHeight: pill.height
|
||||||
|
visible: getMonitor() !== null
|
||||||
|
|
||||||
function getMonitor() {
|
function getMonitor() {
|
||||||
return BrightnessService.getMonitorForScreen(screen) || null
|
return BrightnessService.getMonitorForScreen(screen) || null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
// Clock Icon with attached calendar
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
width: clock.width + Style.marginM * 2 * scaling
|
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
implicitWidth: clock.width + Style.marginM * 2 * scaling
|
||||||
|
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
// Clock Icon with attached calendar
|
||||||
NClock {
|
NClock {
|
||||||
id: clock
|
id: clock
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -24,7 +29,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (!calendarPanel.active) {
|
if (!PanelService.getPanel("calendarPanel")?.active) {
|
||||||
tooltip.show()
|
tooltip.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +38,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
tooltip.hide()
|
tooltip.hide()
|
||||||
calendarPanel.toggle(screen)
|
PanelService.getPanel("calendarPanel")?.toggle(screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
Modules/Bar/Widgets/KeyboardLayout.qml
Normal file
36
Modules/Bar/Widgets/KeyboardLayout.qml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
// Use the shared service for keyboard layout
|
||||||
|
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||||
|
|
||||||
|
width: pill.width
|
||||||
|
height: pill.height
|
||||||
|
|
||||||
|
NPill {
|
||||||
|
id: pill
|
||||||
|
icon: "keyboard_alt"
|
||||||
|
iconCircleColor: Color.mPrimary
|
||||||
|
collapsedIconColor: Color.mOnSurface
|
||||||
|
autoHide: false // Important to be false so we can hover as long as we want
|
||||||
|
text: currentLayout
|
||||||
|
tooltipText: "Keyboard Layout: " + currentLayout
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
// You could open keyboard settings here if needed
|
||||||
|
// For now, just show the current layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,10 +9,14 @@ import qs.Widgets
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
visible: MediaService.currentPlayer !== null
|
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||||
width: MediaService.currentPlayer !== null ? implicitWidth : 0
|
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
||||||
|
|
||||||
function getTitle() {
|
function getTitle() {
|
||||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||||
|
|
@ -144,7 +148,7 @@ Row {
|
||||||
NText {
|
NText {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
|
||||||
// If hovered or just switched window, show up to 300 pixels
|
// If hovered or just switched window, show up to 400 pixels
|
||||||
// If not hovered show up to 150 pixels
|
// If not hovered show up to 150 pixels
|
||||||
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||||
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
|
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ import qs.Widgets
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
icon: "notifications"
|
icon: "notifications"
|
||||||
tooltipText: "Notification History"
|
tooltipText: "Notification History"
|
||||||
|
|
@ -17,8 +20,5 @@ NIconButton {
|
||||||
colorFg: Color.mOnSurface
|
colorFg: Color.mOnSurface
|
||||||
colorBorder: Color.transparent
|
colorBorder: Color.transparent
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen)
|
||||||
onClicked: {
|
|
||||||
notificationHistoryPanel.toggle(screen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
Modules/Bar/Widgets/PowerProfile.qml
Normal file
60
Modules/Bar/Widgets/PowerProfile.qml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
property var powerProfiles: PowerProfiles
|
||||||
|
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
|
||||||
|
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
visible: hasPP
|
||||||
|
|
||||||
|
function profileIcon() {
|
||||||
|
if (!hasPP)
|
||||||
|
return "balance"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Performance)
|
||||||
|
return "speed"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||||
|
return "balance"
|
||||||
|
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||||
|
return "eco"
|
||||||
|
}
|
||||||
|
|
||||||
|
function profileName() {
|
||||||
|
if (!hasPP)
|
||||||
|
return "Unknown"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Performance)
|
||||||
|
return "Performance"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||||
|
return "Balanced"
|
||||||
|
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||||
|
return "Power Saver"
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeProfile() {
|
||||||
|
if (!hasPP)
|
||||||
|
return
|
||||||
|
if (powerProfiles.profile === PowerProfile.Performance)
|
||||||
|
powerProfiles.profile = PowerProfile.PowerSaver
|
||||||
|
else if (powerProfiles.profile === PowerProfile.Balanced)
|
||||||
|
powerProfiles.profile = PowerProfile.Performance
|
||||||
|
else if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||||
|
powerProfiles.profile = PowerProfile.Balanced
|
||||||
|
}
|
||||||
|
|
||||||
|
icon: root.profileIcon()
|
||||||
|
tooltipText: root.profileName()
|
||||||
|
colorBg: Color.mSurfaceVariant
|
||||||
|
colorFg: Color.mOnSurface
|
||||||
|
colorBorder: Color.transparent
|
||||||
|
colorBorderHover: Color.transparent
|
||||||
|
onClicked: root.changeProfile()
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
// Screen Recording Indicator
|
// Screen Recording Indicator
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: screenRecordingIndicator
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
visible: ScreenRecorderService.isRecording
|
||||||
icon: "videocam"
|
icon: "videocam"
|
||||||
tooltipText: "Screen Recording Active\nClick To Stop Recording"
|
tooltipText: "Screen Recording Active\nClick To Stop Recording"
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
colorBg: Color.mPrimary
|
colorBg: Color.mPrimary
|
||||||
colorFg: Color.mOnPrimary
|
colorFg: Color.mOnPrimary
|
||||||
visible: ScreenRecorderService.isRecording
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: {
|
onClicked: ScreenRecorderService.toggleRecording()
|
||||||
ScreenRecorderService.toggleRecording()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: sidePanelToggle
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
icon: "widgets"
|
icon: "widgets"
|
||||||
tooltipText: "Open Side Panel"
|
tooltipText: "Open Side Panel"
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
|
|
@ -14,5 +19,5 @@ NIconButton {
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: sidePanel.toggle(screen)
|
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import qs.Widgets
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,20 @@ import Quickshell
|
||||||
import Quickshell.Services.SystemTray
|
import Quickshell.Services.SystemTray
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
|
import qs.Modules.Bar.Extras
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
readonly property real itemSize: 24 * scaling
|
readonly property real itemSize: 24 * scaling
|
||||||
|
|
||||||
visible: SystemTray.items.values.length > 0
|
visible: SystemTray.items.values.length > 0
|
||||||
width: tray.width + Style.marginM * scaling * 2
|
implicitWidth: tray.width + Style.marginM * scaling * 2
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
|
@ -134,9 +139,7 @@ Rectangle {
|
||||||
function open() {
|
function open() {
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
// Register into the panel service
|
PanelService.willOpenPanel(trayPanel)
|
||||||
// so this will autoclose if we open another panel
|
|
||||||
PanelService.registerOpen(trayPanel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
|
@ -152,7 +155,7 @@ Rectangle {
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: trayMenu
|
id: trayMenu
|
||||||
source: "TrayMenu.qml"
|
source: "../Extras/TrayMenu.qml"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,15 @@ import qs.Widgets
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
width: pill.width
|
property ShellScreen screen
|
||||||
height: pill.height
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
// Used to avoid opening the pill on Quickshell startup
|
// Used to avoid opening the pill on Quickshell startup
|
||||||
property bool firstVolumeReceived: false
|
property bool firstVolumeReceived: false
|
||||||
|
|
||||||
|
implicitWidth: pill.width
|
||||||
|
implicitHeight: pill.height
|
||||||
|
|
||||||
function getIcon() {
|
function getIcon() {
|
||||||
if (AudioService.muted) {
|
if (AudioService.muted) {
|
||||||
return "volume_off"
|
return "volume_off"
|
||||||
|
|
@ -64,6 +67,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
||||||
settingsPanel.open(screen)
|
settingsPanel.open(screen)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import qs.Widgets
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
visible: Settings.data.network.wifiEnabled
|
||||||
|
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|
@ -27,6 +32,8 @@ NIconButton {
|
||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
try {
|
try {
|
||||||
|
if (NetworkService.ethernet)
|
||||||
|
return "lan"
|
||||||
let connected = false
|
let connected = false
|
||||||
let signalStrength = 0
|
let signalStrength = 0
|
||||||
for (const net in NetworkService.networks) {
|
for (const net in NetworkService.networks) {
|
||||||
|
|
@ -36,17 +43,17 @@ NIconButton {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi"
|
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error("WiFi", "Error getting icon:", error)
|
Logger.error("WiFi", "Error getting icon:", error)
|
||||||
return "wifi"
|
return "signal_wifi_bad"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltipText: "WiFi Networks"
|
tooltipText: "Network / WiFi"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
try {
|
try {
|
||||||
Logger.log("WiFi", "Button clicked, toggling panel")
|
Logger.log("WiFi", "Button clicked, toggling panel")
|
||||||
wifiPanel.toggle(screen)
|
PanelService.getPanel("wifiPanel")?.toggle(screen)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error("WiFi", "Error toggling panel:", error)
|
Logger.error("WiFi", "Error toggling panel:", error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ import qs.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen: null
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
property bool isDestroying: false
|
property bool isDestroying: false
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
|
|
||||||
|
|
@ -23,7 +27,8 @@ Item {
|
||||||
|
|
||||||
signal workspaceChanged(int workspaceId, color accentColor)
|
signal workspaceChanged(int workspaceId, color accentColor)
|
||||||
|
|
||||||
width: {
|
implicitHeight: Math.round(36 * scaling)
|
||||||
|
implicitWidth: {
|
||||||
let total = 0
|
let total = 0
|
||||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||||
const ws = localWorkspaces.get(i)
|
const ws = localWorkspaces.get(i)
|
||||||
|
|
@ -39,41 +44,53 @@ Item {
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
height: Math.round(36 * scaling)
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
localWorkspaces.clear()
|
refreshWorkspaces()
|
||||||
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
|
||||||
const ws = WorkspaceService.workspaces.get(i)
|
|
||||||
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
|
||||||
localWorkspaces.append(ws)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
root.isDestroying = true
|
||||||
}
|
}
|
||||||
workspaceRepeater.model = localWorkspaces
|
|
||||||
updateWorkspaceFocus()
|
onScreenChanged: refreshWorkspaces()
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: WorkspaceService
|
target: WorkspaceService
|
||||||
function onWorkspacesChanged() {
|
function onWorkspacesChanged() {
|
||||||
|
refreshWorkspaces()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshWorkspaces() {
|
||||||
localWorkspaces.clear()
|
localWorkspaces.clear()
|
||||||
|
if (screen !== null) {
|
||||||
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
||||||
const ws = WorkspaceService.workspaces.get(i)
|
const ws = WorkspaceService.workspaces.get(i)
|
||||||
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
||||||
localWorkspaces.append(ws)
|
localWorkspaces.append(ws)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
workspaceRepeater.model = localWorkspaces
|
workspaceRepeater.model = localWorkspaces
|
||||||
updateWorkspaceFocus()
|
updateWorkspaceFocus()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function triggerUnifiedWave() {
|
function triggerUnifiedWave() {
|
||||||
effectColor = Color.mPrimary
|
effectColor = Color.mPrimary
|
||||||
masterAnimation.restart()
|
masterAnimation.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateWorkspaceFocus() {
|
||||||
|
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||||
|
const ws = localWorkspaces.get(i)
|
||||||
|
if (ws.isFocused === true) {
|
||||||
|
root.triggerUnifiedWave()
|
||||||
|
root.workspaceChanged(ws.id, Color.mPrimary)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
id: masterAnimation
|
id: masterAnimation
|
||||||
PropertyAction {
|
PropertyAction {
|
||||||
|
|
@ -101,17 +118,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWorkspaceFocus() {
|
|
||||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
|
||||||
const ws = localWorkspaces.get(i)
|
|
||||||
if (ws.isFocused === true) {
|
|
||||||
root.triggerUnifiedWave()
|
|
||||||
root.workspaceChanged(ws.id, Color.mPrimary)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: workspaceBackground
|
id: workspaceBackground
|
||||||
width: parent.width - Style.marginS * scaling * 2
|
width: parent.width - Style.marginS * scaling * 2
|
||||||
|
|
@ -254,8 +260,4 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
root.isDestroying = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,17 +104,6 @@ NPanel {
|
||||||
year: Time.date.getFullYear()
|
year: Time.date.getFullYear()
|
||||||
locale: Qt.locale() // Use system locale
|
locale: Qt.locale() // Use system locale
|
||||||
|
|
||||||
// Optionally, update when the panel becomes visible
|
|
||||||
Connections {
|
|
||||||
target: calendarPanel
|
|
||||||
function onVisibleChanged() {
|
|
||||||
if (calendarPanel.visible) {
|
|
||||||
grid.month = Time.date.getMonth()
|
|
||||||
grid.year = Time.date.getFullYear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: (Style.baseWidgetSize * scaling)
|
width: (Style.baseWidgetSize * scaling)
|
||||||
height: (Style.baseWidgetSize * scaling)
|
height: (Style.baseWidgetSize * scaling)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ Item {
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "settings"
|
target: "settings"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
settingsPanel.toggle(Quickshell.screens[0])
|
settingsPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
|
@ -16,43 +15,64 @@ Item {
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "notifications"
|
target: "notifications"
|
||||||
|
|
||||||
function toggleHistory() {
|
function toggleHistory() {
|
||||||
notificationHistoryPanel.toggle(Quickshell.screens[0])
|
notificationHistoryPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDoNotDisturb() {// TODO
|
function toggleDoNotDisturb() {// TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "idleInhibitor"
|
target: "idleInhibitor"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
return IdleInhibitorService.manualToggle()
|
return IdleInhibitorService.manualToggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For backward compatibility, should be removed soon(tmc)
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "appLauncher"
|
target: "appLauncher"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
launcherPanel.toggle(Quickshell.screens[0])
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
function clipboard() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set clipboard mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">clip ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function calculator() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set calculator mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">calc ")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "launcher"
|
target: "launcher"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
launcherPanel.toggle(Quickshell.screens[0])
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
function clipboard() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set clipboard mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">clip ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function calculator() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set calculator mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">calc ")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "lockScreen"
|
target: "lockScreen"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
// Only lock if not already locked (prevents the red screen issue)
|
// Only lock if not already locked (prevents the red screen issue)
|
||||||
// Note: No unlock via IPC for security reasons
|
// Note: No unlock via IPC for security reasons
|
||||||
|
|
@ -64,13 +84,25 @@ Item {
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "brightness"
|
target: "brightness"
|
||||||
|
|
||||||
function increase() {
|
function increase() {
|
||||||
BrightnessService.increaseBrightness()
|
BrightnessService.increaseBrightness()
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrease() {
|
function decrease() {
|
||||||
BrightnessService.decreaseBrightness()
|
BrightnessService.decreaseBrightness()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "powerPanel"
|
||||||
|
function toggle() {
|
||||||
|
powerPanel.toggle(Quickshell.screens[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "sidePanel"
|
||||||
|
function toggle() {
|
||||||
|
sidePanel.toggle(Quickshell.screens[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ QtObject {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to basic evaluation
|
// Fallback to basic evaluation
|
||||||
console.log("AdvancedMath not available, using basic eval")
|
Logger.warn("Calculator", "AdvancedMath not available, using basic eval")
|
||||||
|
|
||||||
// Basic preprocessing for common functions
|
// Basic preprocessing for common functions
|
||||||
var processed = expression.trim(
|
var processed = expression.trim(
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,31 @@ NPanel {
|
||||||
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
|
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
|
||||||
panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right"))
|
panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right"))
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
property string searchText: ""
|
||||||
|
property bool shouldResetCursor: false
|
||||||
|
|
||||||
|
// Add function to set search text programmatically
|
||||||
|
function setSearchText(text) {
|
||||||
|
searchText = text
|
||||||
|
// The searchInput will automatically update via the text binding
|
||||||
|
// Focus and cursor position will be handled by the TextField's Component.onCompleted
|
||||||
|
}
|
||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
// Reset state when panel opens to avoid sticky modes
|
// Reset state when panel opens to avoid sticky modes
|
||||||
|
if (searchText === "") {
|
||||||
searchText = ""
|
searchText = ""
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
// Reset search bar when launcher is closed
|
||||||
|
searchText = ""
|
||||||
|
selectedIndex = 0
|
||||||
|
shouldResetCursor = true
|
||||||
|
}
|
||||||
|
|
||||||
// Import modular components
|
// Import modular components
|
||||||
Calculator {
|
Calculator {
|
||||||
|
|
@ -50,7 +70,6 @@ NPanel {
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
property var desktopEntries: DesktopEntries.applications.values
|
property var desktopEntries: DesktopEntries.applications.values
|
||||||
property string searchText: ""
|
|
||||||
property int selectedIndex: 0
|
property int selectedIndex: 0
|
||||||
|
|
||||||
// Refresh clipboard when user starts typing clipboard commands
|
// Refresh clipboard when user starts typing clipboard commands
|
||||||
|
|
@ -141,15 +160,11 @@ NPanel {
|
||||||
|
|
||||||
// Command execution functions
|
// Command execution functions
|
||||||
function executeCalcCommand() {
|
function executeCalcCommand() {
|
||||||
searchText = ">calc "
|
setSearchText(">calc ")
|
||||||
searchInput.text = searchText
|
|
||||||
searchInput.cursorPosition = searchText.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeClipCommand() {
|
function executeClipCommand() {
|
||||||
searchText = ">clip "
|
setSearchText(">clip ")
|
||||||
searchInput.text = searchText
|
|
||||||
searchInput.cursorPosition = searchText.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation functions
|
// Navigation functions
|
||||||
|
|
@ -252,10 +267,20 @@ NPanel {
|
||||||
anchors.leftMargin: Style.marginS * scaling
|
anchors.leftMargin: Style.marginS * scaling
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: searchText
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
|
// Update the parent searchText property
|
||||||
|
if (searchText !== text) {
|
||||||
searchText = text
|
searchText = text
|
||||||
|
}
|
||||||
// Defer selectedIndex reset to avoid binding loops
|
// Defer selectedIndex reset to avoid binding loops
|
||||||
Qt.callLater(() => selectedIndex = 0)
|
Qt.callLater(() => selectedIndex = 0)
|
||||||
|
|
||||||
|
// Reset cursor position if needed
|
||||||
|
if (shouldResetCursor && text === "") {
|
||||||
|
cursorPosition = 0
|
||||||
|
shouldResetCursor = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
selectedTextColor: Color.mOnSurface
|
selectedTextColor: Color.mOnSurface
|
||||||
selectionColor: Color.mPrimary
|
selectionColor: Color.mPrimary
|
||||||
|
|
@ -266,10 +291,14 @@ NPanel {
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Focus the search bar by default
|
// Focus the search bar by default and set cursor position
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
searchInput.forceActiveFocus()
|
searchInput.forceActiveFocus()
|
||||||
|
// Set cursor to end if there's already text
|
||||||
|
if (searchText && searchText.length > 0) {
|
||||||
|
searchInput.cursorPosition = searchText.length
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Keys.onDownPressed: selectNext()
|
Keys.onDownPressed: selectNext()
|
||||||
|
|
|
||||||
94
Modules/LockScreen/LockContext.qml
Normal file
94
Modules/LockScreen/LockContext.qml
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pam
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
signal unlocked
|
||||||
|
signal failed
|
||||||
|
|
||||||
|
property string currentText: ""
|
||||||
|
property bool unlockInProgress: false
|
||||||
|
property bool showFailure: false
|
||||||
|
property string errorMessage: ""
|
||||||
|
property bool pamAvailable: typeof PamContext !== "undefined"
|
||||||
|
|
||||||
|
onCurrentTextChanged: {
|
||||||
|
if (currentText !== "") {
|
||||||
|
showFailure = false
|
||||||
|
errorMessage = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryUnlock() {
|
||||||
|
if (!pamAvailable) {
|
||||||
|
errorMessage = "PAM not available"
|
||||||
|
showFailure = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentText === "") {
|
||||||
|
errorMessage = "Password required"
|
||||||
|
showFailure = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.unlockInProgress = true
|
||||||
|
errorMessage = ""
|
||||||
|
showFailure = false
|
||||||
|
|
||||||
|
Logger.log("LockContext", "Starting PAM authentication for user:", pam.user)
|
||||||
|
pam.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
PamContext {
|
||||||
|
id: pam
|
||||||
|
config: "login"
|
||||||
|
user: Quickshell.env("USER")
|
||||||
|
|
||||||
|
onPamMessage: {
|
||||||
|
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:",
|
||||||
|
responseRequired)
|
||||||
|
|
||||||
|
if (messageIsError) {
|
||||||
|
errorMessage = message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseRequired) {
|
||||||
|
Logger.log("LockContext", "Responding to PAM with password")
|
||||||
|
respond(root.currentText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onResponseRequiredChanged: {
|
||||||
|
Logger.log("LockContext", "Response required changed:", responseRequired)
|
||||||
|
if (responseRequired && root.unlockInProgress) {
|
||||||
|
Logger.log("LockContext", "Automatically responding to PAM")
|
||||||
|
respond(root.currentText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleted: result => {
|
||||||
|
Logger.log("LockContext", "PAM completed with result:", result)
|
||||||
|
if (result === PamResult.Success) {
|
||||||
|
Logger.log("LockContext", "Authentication successful")
|
||||||
|
root.unlocked()
|
||||||
|
} else {
|
||||||
|
Logger.log("LockContext", "Authentication failed")
|
||||||
|
errorMessage = "Authentication failed"
|
||||||
|
showFailure = true
|
||||||
|
root.failed()
|
||||||
|
}
|
||||||
|
root.unlockInProgress = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onError: {
|
||||||
|
Logger.log("LockContext", "PAM error:", error, "message:", message)
|
||||||
|
errorMessage = message || "Authentication error"
|
||||||
|
showFailure = true
|
||||||
|
root.unlockInProgress = false
|
||||||
|
root.failed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,135 +17,55 @@ Loader {
|
||||||
id: lockScreen
|
id: lockScreen
|
||||||
active: false
|
active: false
|
||||||
|
|
||||||
// Log state changes to help debug lock screen issues
|
|
||||||
onActiveChanged: {
|
|
||||||
Logger.log("LockScreen", "State changed:", active)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow a small grace period after unlocking so the compositor releases the lock surfaces
|
|
||||||
Timer {
|
Timer {
|
||||||
id: unloadAfterUnlockTimer
|
id: unloadAfterUnlockTimer
|
||||||
interval: 250
|
interval: 250
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
Logger.log("LockScreen", "Unload timer triggered - deactivating")
|
|
||||||
lockScreen.active = false
|
lockScreen.active = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleUnloadAfterUnlock() {
|
function scheduleUnloadAfterUnlock() {
|
||||||
Logger.log("LockScreen", "Scheduling unload after unlock")
|
|
||||||
unloadAfterUnlockTimer.start()
|
unloadAfterUnlockTimer.start()
|
||||||
}
|
}
|
||||||
sourceComponent: Component {
|
|
||||||
WlSessionLock {
|
|
||||||
id: lock
|
|
||||||
|
|
||||||
// Tie session lock to loader visibility
|
sourceComponent: Component {
|
||||||
|
Item {
|
||||||
|
id: lockContainer
|
||||||
|
|
||||||
|
// Create the lock context
|
||||||
|
LockContext {
|
||||||
|
id: lockContext
|
||||||
|
onUnlocked: {
|
||||||
|
lockSession.locked = false
|
||||||
|
lockScreen.scheduleUnloadAfterUnlock()
|
||||||
|
lockContext.currentText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WlSessionLock {
|
||||||
|
id: lockSession
|
||||||
locked: lockScreen.active
|
locked: lockScreen.active
|
||||||
|
|
||||||
property string errorMessage: ""
|
|
||||||
property bool authenticating: false
|
|
||||||
property string password: ""
|
|
||||||
property bool pamAvailable: typeof PamContext !== "undefined"
|
|
||||||
|
|
||||||
function unlockAttempt() {
|
|
||||||
Logger.log("LockScreen", "Unlock attempt started")
|
|
||||||
|
|
||||||
// Real PAM authentication
|
|
||||||
if (!pamAvailable) {
|
|
||||||
lock.errorMessage = "PAM authentication not available."
|
|
||||||
Logger.log("LockScreen", "PAM not available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!lock.password) {
|
|
||||||
lock.errorMessage = "Password required."
|
|
||||||
Logger.log("LockScreen", "No password entered")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Logger.log("LockScreen", "Starting PAM authentication")
|
|
||||||
lock.authenticating = true
|
|
||||||
lock.errorMessage = ""
|
|
||||||
|
|
||||||
Logger.log("LockScreen", "About to create PAM context with userName:", Quickshell.env("USER"))
|
|
||||||
var pam = Qt.createQmlObject(
|
|
||||||
'import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }',
|
|
||||||
lock)
|
|
||||||
Logger.log("LockScreen", "PamContext created", pam)
|
|
||||||
|
|
||||||
pam.onCompleted.connect(function (result) {
|
|
||||||
Logger.log("LockScreen", "PAM completed with result:", result)
|
|
||||||
lock.authenticating = false
|
|
||||||
if (result === PamResult.Success) {
|
|
||||||
Logger.log("LockScreen", "Authentication successful, unlocking")
|
|
||||||
// First release the Wayland session lock, then unload after a short delay
|
|
||||||
lock.locked = false
|
|
||||||
lockScreen.scheduleUnloadAfterUnlock()
|
|
||||||
lock.password = ""
|
|
||||||
lock.errorMessage = ""
|
|
||||||
} else {
|
|
||||||
Logger.log("LockScreen", "Authentication failed")
|
|
||||||
lock.errorMessage = "Authentication failed."
|
|
||||||
lock.password = ""
|
|
||||||
}
|
|
||||||
pam.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
pam.onError.connect(function (error) {
|
|
||||||
Logger.log("LockScreen", "PAM error:", error)
|
|
||||||
lock.authenticating = false
|
|
||||||
lock.errorMessage = pam.message || "Authentication error."
|
|
||||||
lock.password = ""
|
|
||||||
pam.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
pam.onPamMessage.connect(function () {
|
|
||||||
Logger.log("LockScreen", "PAM message:", pam.message, "isError:", pam.messageIsError)
|
|
||||||
if (pam.messageIsError) {
|
|
||||||
lock.errorMessage = pam.message
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
pam.onResponseRequiredChanged.connect(function () {
|
|
||||||
Logger.log("LockScreen", "PAM response required:", pam.responseRequired)
|
|
||||||
if (pam.responseRequired && lock.authenticating) {
|
|
||||||
Logger.log("LockScreen", "Responding to PAM with password")
|
|
||||||
pam.respond(lock.password)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var started = pam.start()
|
|
||||||
Logger.log("LockScreen", "PAM start result:", started)
|
|
||||||
}
|
|
||||||
|
|
||||||
WlSessionLockSurface {
|
WlSessionLockSurface {
|
||||||
// Battery indicator component
|
|
||||||
|
|
||||||
// WlSessionLockSurface provides a screen variable for the current screen.
|
|
||||||
// Also we use a different scaling algorithm based on the resolution, as the design is full screen.
|
|
||||||
readonly property real scaling: ScalingService.dynamicScale(screen)
|
readonly property real scaling: ScalingService.dynamicScale(screen)
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: batteryIndicator
|
id: batteryIndicator
|
||||||
|
|
||||||
// Import UPower for battery data
|
|
||||||
property var battery: UPower.displayDevice
|
property var battery: UPower.displayDevice
|
||||||
property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent
|
property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent
|
||||||
property real percent: isReady ? (battery.percentage * 100) : 0
|
property real percent: isReady ? (battery.percentage * 100) : 0
|
||||||
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
|
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
|
||||||
property bool batteryVisible: isReady && percent > 0
|
property bool batteryVisible: isReady && percent > 0
|
||||||
|
|
||||||
// Choose icon based on charge and charging state
|
|
||||||
function getIcon() {
|
function getIcon() {
|
||||||
if (!batteryVisible)
|
if (!batteryVisible)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
if (charging)
|
if (charging)
|
||||||
return "battery_android_bolt"
|
return "battery_android_bolt"
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -163,7 +83,12 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wallpaper image
|
Item {
|
||||||
|
id: keyboardLayout
|
||||||
|
property string currentLayout: (typeof KeyboardLayoutService !== 'undefined'
|
||||||
|
&& KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: lockBgImage
|
id: lockBgImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -174,18 +99,14 @@ Loader {
|
||||||
mipmap: false
|
mipmap: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blurred background
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
|
|
||||||
// Simple blur effect
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.samples: 4
|
layer.samples: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animated gradient overlay
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
|
|
@ -207,7 +128,6 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtle animated particles
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: 20
|
model: 20
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -233,11 +153,9 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main content - Centered design
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
// Top section - Time, date, and user info
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
@ -245,7 +163,6 @@ Loader {
|
||||||
anchors.topMargin: 80 * scaling
|
anchors.topMargin: 80 * scaling
|
||||||
spacing: 40 * scaling
|
spacing: 40 * scaling
|
||||||
|
|
||||||
// Time display - Large and prominent with pulse animation
|
|
||||||
Column {
|
Column {
|
||||||
spacing: Style.marginXS * scaling
|
spacing: Style.marginXS * scaling
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
@ -287,12 +204,10 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User section with animated avatar
|
|
||||||
Column {
|
Column {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
// Animated avatar with glow effect or audio visualizer
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 108 * scaling
|
width: 108 * scaling
|
||||||
height: 108 * scaling
|
height: 108 * scaling
|
||||||
|
|
@ -303,31 +218,25 @@ Loader {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
z: 10
|
z: 10
|
||||||
|
|
||||||
// Circular audio visualizer when music is playing
|
|
||||||
Loader {
|
Loader {
|
||||||
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear"
|
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 160 * scaling
|
width: 160 * scaling
|
||||||
height: 160 * scaling
|
height: 160 * scaling
|
||||||
|
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: CavaService.values.length
|
model: CavaService.values.length
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
property real linearAngle: (index / CavaService.values.length) * 2 * Math.PI
|
property real linearAngle: (index / CavaService.values.length) * 2 * Math.PI
|
||||||
property real linearRadius: 70 * scaling
|
property real linearRadius: 70 * scaling
|
||||||
property real linearBarLength: Math.max(2, CavaService.values[index] * 30 * scaling)
|
property real linearBarLength: Math.max(2, CavaService.values[index] * 30 * scaling)
|
||||||
property real linearBarWidth: 3 * scaling
|
property real linearBarWidth: 3 * scaling
|
||||||
|
|
||||||
width: linearBarWidth
|
width: linearBarWidth
|
||||||
height: linearBarLength
|
height: linearBarLength
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
radius: linearBarWidth * 0.5
|
radius: linearBarWidth * 0.5
|
||||||
|
|
||||||
x: parent.width * 0.5 + Math.cos(linearAngle) * linearRadius - width * 0.5
|
x: parent.width * 0.5 + Math.cos(linearAngle) * linearRadius - width * 0.5
|
||||||
y: parent.height * 0.5 + Math.sin(linearAngle) * linearRadius - height * 0.5
|
y: parent.height * 0.5 + Math.sin(linearAngle) * linearRadius - height * 0.5
|
||||||
|
|
||||||
transform: Rotation {
|
transform: Rotation {
|
||||||
origin.x: linearBarWidth * 0.5
|
origin.x: linearBarWidth * 0.5
|
||||||
origin.y: linearBarLength * 0.5
|
origin.y: linearBarLength * 0.5
|
||||||
|
|
@ -343,28 +252,23 @@ Loader {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 160 * scaling
|
width: 160 * scaling
|
||||||
height: 160 * scaling
|
height: 160 * scaling
|
||||||
|
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: CavaService.values.length * 2
|
model: CavaService.values.length * 2
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length
|
property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length
|
||||||
* 2 - 1 - index)
|
* 2 - 1 - index)
|
||||||
property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI
|
property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI
|
||||||
property real mirroredRadius: 70 * scaling
|
property real mirroredRadius: 70 * scaling
|
||||||
property real mirroredBarLength: Math.max(2,
|
property real mirroredBarLength: Math.max(
|
||||||
CavaService.values[mirroredValueIndex] * 30 * scaling)
|
2, CavaService.values[mirroredValueIndex] * 30 * scaling)
|
||||||
property real mirroredBarWidth: 3 * scaling
|
property real mirroredBarWidth: 3 * scaling
|
||||||
|
|
||||||
width: mirroredBarWidth
|
width: mirroredBarWidth
|
||||||
height: mirroredBarLength
|
height: mirroredBarLength
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
radius: mirroredBarWidth * 0.5
|
radius: mirroredBarWidth * 0.5
|
||||||
|
|
||||||
x: parent.width * 0.5 + Math.cos(mirroredAngle) * mirroredRadius - width * 0.5
|
x: parent.width * 0.5 + Math.cos(mirroredAngle) * mirroredRadius - width * 0.5
|
||||||
y: parent.height * 0.5 + Math.sin(mirroredAngle) * mirroredRadius - height * 0.5
|
y: parent.height * 0.5 + Math.sin(mirroredAngle) * mirroredRadius - height * 0.5
|
||||||
|
|
||||||
transform: Rotation {
|
transform: Rotation {
|
||||||
origin.x: mirroredBarWidth * 0.5
|
origin.x: mirroredBarWidth * 0.5
|
||||||
origin.y: mirroredBarLength * 0.5
|
origin.y: mirroredBarLength * 0.5
|
||||||
|
|
@ -380,65 +284,49 @@ Loader {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 160 * scaling
|
width: 160 * scaling
|
||||||
height: 160 * scaling
|
height: 160 * scaling
|
||||||
|
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
Canvas {
|
Canvas {
|
||||||
id: waveCanvas
|
id: waveCanvas
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
|
|
||||||
onPaint: {
|
onPaint: {
|
||||||
var ctx = getContext("2d")
|
var ctx = getContext("2d")
|
||||||
ctx.reset()
|
ctx.reset()
|
||||||
|
if (CavaService.values.length === 0)
|
||||||
if (CavaService.values.length === 0) {
|
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
ctx.strokeStyle = Color.mPrimary
|
ctx.strokeStyle = Color.mPrimary
|
||||||
ctx.lineWidth = 2 * scaling
|
ctx.lineWidth = 2 * scaling
|
||||||
ctx.lineCap = "round"
|
ctx.lineCap = "round"
|
||||||
|
|
||||||
var centerX = width * 0.5
|
var centerX = width * 0.5
|
||||||
var centerY = height * 0.5
|
var centerY = height * 0.5
|
||||||
var baseRadius = 60 * scaling
|
var baseRadius = 60 * scaling
|
||||||
var maxAmplitude = 20 * scaling
|
var maxAmplitude = 20 * scaling
|
||||||
|
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
||||||
for (var i = 0; i <= CavaService.values.length; i++) {
|
for (var i = 0; i <= CavaService.values.length; i++) {
|
||||||
var index = i % CavaService.values.length
|
var index = i % CavaService.values.length
|
||||||
var angle = (i / CavaService.values.length) * 2 * Math.PI
|
var angle = (i / CavaService.values.length) * 2 * Math.PI
|
||||||
var amplitude = CavaService.values[index] * maxAmplitude
|
var amplitude = CavaService.values[index] * maxAmplitude
|
||||||
var radius = baseRadius + amplitude
|
var radius = baseRadius + amplitude
|
||||||
|
|
||||||
var x = centerX + Math.cos(angle) * radius
|
var x = centerX + Math.cos(angle) * radius
|
||||||
var y = centerY + Math.sin(angle) * radius
|
var y = centerY + Math.sin(angle) * radius
|
||||||
|
if (i === 0)
|
||||||
if (i === 0) {
|
|
||||||
ctx.moveTo(x, y)
|
ctx.moveTo(x, y)
|
||||||
} else {
|
else
|
||||||
ctx.lineTo(x, y)
|
ctx.lineTo(x, y)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ctx.closePath()
|
ctx.closePath()
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 16 // ~60 FPS
|
interval: 16
|
||||||
running: true
|
running: true
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: waveCanvas.requestPaint()
|
||||||
waveCanvas.requestPaint()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Glow effect when no music is playing
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width + 24 * scaling
|
width: parent.width + 24 * scaling
|
||||||
|
|
@ -449,7 +337,6 @@ Loader {
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
z: -1
|
z: -1
|
||||||
visible: !MediaService.isPlaying
|
visible: !MediaService.isPlaying
|
||||||
|
|
||||||
SequentialAnimation on scale {
|
SequentialAnimation on scale {
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|
@ -473,7 +360,6 @@ Loader {
|
||||||
fallbackIcon: "person"
|
fallbackIcon: "person"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover animation
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
@ -491,19 +377,17 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centered terminal section
|
|
||||||
Item {
|
Item {
|
||||||
width: 720 * scaling
|
width: 720 * scaling
|
||||||
height: 280 * scaling
|
height: 280 * scaling
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
anchors.verticalCenterOffset: 50 * scaling
|
||||||
|
|
||||||
// Futuristic Terminal-Style Input
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 280 * scaling
|
height: 280 * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
// Terminal background with scanlines
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: terminalBackground
|
id: terminalBackground
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -512,7 +396,6 @@ Loader {
|
||||||
border.color: Color.mPrimary
|
border.color: Color.mPrimary
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
|
|
||||||
// Scanline effect
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: 20
|
model: 20
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -521,7 +404,6 @@ Loader {
|
||||||
color: Color.applyOpacity(Color.mPrimary, "1A")
|
color: Color.applyOpacity(Color.mPrimary, "1A")
|
||||||
y: index * 10 * scaling
|
y: index * 10 * scaling
|
||||||
opacity: Style.opacityMedium
|
opacity: Style.opacityMedium
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
SequentialAnimation on opacity {
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|
@ -536,7 +418,6 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminal header
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 40 * scaling
|
height: 40 * scaling
|
||||||
|
|
@ -561,17 +442,14 @@ Loader {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery indicator
|
|
||||||
Row {
|
Row {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
visible: batteryIndicator.batteryVisible
|
visible: batteryIndicator.batteryVisible
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: batteryIndicator.getIcon()
|
text: batteryIndicator.getIcon()
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: Math.round(batteryIndicator.percent) + "%"
|
text: Math.round(batteryIndicator.percent) + "%"
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
|
|
@ -580,10 +458,25 @@ Loader {
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
NText {
|
||||||
|
text: keyboardLayout.currentLayout
|
||||||
|
color: Color.mOnSurface
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
}
|
||||||
|
NIcon {
|
||||||
|
text: "keyboard_alt"
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: Color.mOnSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminal content area
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
@ -593,13 +486,12 @@ Loader {
|
||||||
anchors.topMargin: 70 * scaling
|
anchors.topMargin: 70 * scaling
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Welcome back typing effect
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "root@noctalia:~$"
|
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
font.family: Settings.data.ui.fontFixed
|
font.family: Settings.data.ui.fontFixed
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
|
@ -631,13 +523,12 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command line with integrated password input
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "root@noctalia:~$"
|
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
font.family: Settings.data.ui.fontFixed
|
font.family: Settings.data.ui.fontFixed
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
|
@ -651,7 +542,6 @@ Loader {
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
// Integrated password input (invisible, just for functionality)
|
|
||||||
TextInput {
|
TextInput {
|
||||||
id: passwordInput
|
id: passwordInput
|
||||||
width: 0
|
width: 0
|
||||||
|
|
@ -664,16 +554,14 @@ Loader {
|
||||||
passwordCharacter: "*"
|
passwordCharacter: "*"
|
||||||
passwordMaskDelay: 0
|
passwordMaskDelay: 0
|
||||||
|
|
||||||
text: lock.password
|
text: lockContext.currentText
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
lock.password = text
|
lockContext.currentText = text
|
||||||
// Terminal typing sound effect (visual)
|
|
||||||
typingEffect.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: function (event) {
|
Keys.onPressed: function (event) {
|
||||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
lock.unlockAttempt()
|
lockContext.tryUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -682,7 +570,6 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visual password display with integrated cursor
|
|
||||||
NText {
|
NText {
|
||||||
id: asterisksText
|
id: asterisksText
|
||||||
text: "*".repeat(passwordInput.text.length)
|
text: "*".repeat(passwordInput.text.length)
|
||||||
|
|
@ -691,7 +578,6 @@ Loader {
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
visible: passwordInput.activeFocus
|
visible: passwordInput.activeFocus
|
||||||
|
|
||||||
// Typing effect animation
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
id: typingEffect
|
id: typingEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|
@ -709,7 +595,6 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blinking cursor positioned right after the asterisks
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 8 * scaling
|
width: 8 * scaling
|
||||||
height: 20 * scaling
|
height: 20 * scaling
|
||||||
|
|
@ -732,16 +617,29 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status messages
|
|
||||||
NText {
|
NText {
|
||||||
text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "")
|
text: {
|
||||||
color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent)
|
if (lockContext.unlockInProgress)
|
||||||
|
return "Authenticating..."
|
||||||
|
if (lockContext.showFailure && lockContext.errorMessage)
|
||||||
|
return lockContext.errorMessage
|
||||||
|
if (lockContext.showFailure)
|
||||||
|
return "Authentication failed."
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
color: {
|
||||||
|
if (lockContext.unlockInProgress)
|
||||||
|
return Color.mPrimary
|
||||||
|
if (lockContext.showFailure)
|
||||||
|
return Color.mError
|
||||||
|
return Color.transparent
|
||||||
|
}
|
||||||
font.family: "DejaVu Sans Mono"
|
font.family: "DejaVu Sans Mono"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
SequentialAnimation on opacity {
|
||||||
running: lock.authenticating
|
running: lockContext.unlockInProgress
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
to: 1.0
|
to: 1.0
|
||||||
|
|
@ -754,7 +652,6 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute button
|
|
||||||
Row {
|
Row {
|
||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
Layout.bottomMargin: -10 * scaling
|
Layout.bottomMargin: -10 * scaling
|
||||||
|
|
@ -762,14 +659,15 @@ Loader {
|
||||||
width: 120 * scaling
|
width: 120 * scaling
|
||||||
height: 40 * scaling
|
height: 40 * scaling
|
||||||
radius: Style.radiusS * scaling
|
radius: Style.radiusS * scaling
|
||||||
color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33")
|
color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary,
|
||||||
|
"33")
|
||||||
border.color: Color.mPrimary
|
border.color: Color.mPrimary
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
enabled: !lock.authenticating
|
enabled: !lockContext.unlockInProgress
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: lock.authenticating ? "EXECUTING" : "EXECUTE"
|
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
|
||||||
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||||
font.family: Settings.data.ui.fontFixed
|
font.family: Settings.data.ui.fontFixed
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
|
@ -780,7 +678,9 @@ Loader {
|
||||||
id: executeButtonArea
|
id: executeButtonArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: lock.unlockAttempt()
|
onClicked: {
|
||||||
|
lockContext.tryUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
SequentialAnimation on scale {
|
SequentialAnimation on scale {
|
||||||
running: executeButtonArea.containsMouse
|
running: executeButtonArea.containsMouse
|
||||||
|
|
@ -801,10 +701,9 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processing animation
|
|
||||||
SequentialAnimation on scale {
|
SequentialAnimation on scale {
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
running: lock.authenticating
|
running: lockContext.unlockInProgress
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
to: 1.02
|
to: 1.02
|
||||||
duration: 600
|
duration: 600
|
||||||
|
|
@ -820,7 +719,6 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminal glow effect
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
|
|
@ -846,182 +744,91 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced power buttons with hover effects
|
// Power buttons at bottom
|
||||||
Row {
|
Row {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: 50 * scaling
|
anchors.margins: 50 * scaling
|
||||||
spacing: 20 * scaling
|
spacing: 20 * scaling
|
||||||
|
|
||||||
// Shutdown with enhanced styling
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 64 * scaling
|
width: 60 * scaling
|
||||||
height: 64 * scaling
|
height: 60 * scaling
|
||||||
radius: Style.radiusL * scaling
|
radius: width * 0.5
|
||||||
color: shutdownArea.containsMouse ? Color.applyOpacity(Color.mError,
|
color: powerButtonArea.containsMouse ? Color.mError : Color.applyOpacity(Color.mError, "33")
|
||||||
"DD") : Color.applyOpacity(Color.mError, "22")
|
|
||||||
border.color: Color.mError
|
border.color: Color.mError
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
|
|
||||||
// Glow effect
|
NIcon {
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width + 10 * scaling
|
text: "power_settings_new"
|
||||||
height: parent.height + 10 * scaling
|
font.pointSize: Style.fontSizeXL * scaling
|
||||||
radius: width * 0.5
|
color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError
|
||||||
color: Color.transparent
|
|
||||||
opacity: shutdownArea.containsMouse ? 1 : 0
|
|
||||||
z: -1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Style.animationFast
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: shutdownArea
|
id: powerButtonArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Qt.createQmlObject(
|
CompositorService.shutdown()
|
||||||
'import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NIcon {
|
|
||||||
text: "power_settings_new"
|
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
|
||||||
color: shutdownArea.containsMouse ? Color.mOnPrimary : Color.mError
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Style.animationFast
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scale: shutdownArea.containsMouse ? 1.1 : 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reboot with enhanced styling
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 64 * scaling
|
width: 60 * scaling
|
||||||
height: 64 * scaling
|
height: 60 * scaling
|
||||||
radius: Style.radiusL * scaling
|
radius: width * 0.5
|
||||||
color: rebootArea.containsMouse ? Color.applyOpacity(Color.mPrimary,
|
color: restartButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33")
|
||||||
"DD") : Color.applyOpacity(Color.mPrimary, "22")
|
|
||||||
border.color: Color.mPrimary
|
border.color: Color.mPrimary
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
|
|
||||||
// Glow effect
|
NIcon {
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width + 10 * scaling
|
text: "restart_alt"
|
||||||
height: parent.height + 10 * scaling
|
font.pointSize: Style.fontSizeXL * scaling
|
||||||
radius: width * 0.5
|
color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||||
color: Color.transparent
|
|
||||||
opacity: rebootArea.containsMouse ? 1 : 0
|
|
||||||
z: -1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Style.animationMedium
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: rebootArea
|
id: restartButtonArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock)
|
CompositorService.reboot()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NIcon {
|
|
||||||
text: "refresh"
|
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
|
||||||
color: rebootArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Style.animationMedium
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scale: rebootArea.containsMouse ? 1.1 : 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout with enhanced styling
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 64 * scaling
|
width: 60 * scaling
|
||||||
height: 64 * scaling
|
height: 60 * scaling
|
||||||
radius: Style.radiusL * scaling
|
radius: width * 0.5
|
||||||
color: logoutArea.containsMouse ? Color.applyOpacity(Color.mSecondary,
|
color: suspendButtonArea.containsMouse ? Color.mSecondary : Color.applyOpacity(Color.mSecondary, "33")
|
||||||
"DD") : Color.applyOpacity(Color.mSecondary, "22")
|
|
||||||
border.color: Color.mSecondary
|
border.color: Color.mSecondary
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
|
|
||||||
// Glow effect
|
NIcon {
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width + 10 * scaling
|
text: "bedtime"
|
||||||
height: parent.height + 10 * scaling
|
font.pointSize: Style.fontSizeXL * scaling
|
||||||
radius: width * 0.5
|
color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary
|
||||||
color: Color.transparent
|
|
||||||
opacity: logoutArea.containsMouse ? 1 : 0
|
|
||||||
z: -1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Style.animationMedium
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: logoutArea
|
id: suspendButtonArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Qt.createQmlObject(
|
CompositorService.suspend()
|
||||||
'import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env(
|
}
|
||||||
"USER") + '"]; running: true }', lock)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NIcon {
|
|
||||||
text: "exit_to_app"
|
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
|
||||||
color: logoutArea.containsMouse ? Color.mOnPrimary : Color.mSecondary
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Style.animationFast
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scale: logoutArea.containsMouse ? 1.1 : 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timer for updating time
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 1000
|
interval: 1000
|
||||||
running: true
|
running: true
|
||||||
|
|
@ -1034,4 +841,5 @@ Loader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,45 @@ ColumnLayout {
|
||||||
Settings.data.audio.visualizerType = key
|
Settings.data.audio.visualizerType = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NComboBox {
|
||||||
|
label: "Frame Rate"
|
||||||
|
description: "Target frame rate for audio visualizer. (default: 60)"
|
||||||
|
model: ListModel {
|
||||||
|
ListElement {
|
||||||
|
key: "30"
|
||||||
|
name: "30 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "60"
|
||||||
|
name: "60 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "100"
|
||||||
|
name: "100 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "120"
|
||||||
|
name: "120 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "144"
|
||||||
|
name: "144 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "165"
|
||||||
|
name: "165 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "240"
|
||||||
|
name: "240 FPS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentKey: Settings.data.audio.cavaFrameRate
|
||||||
|
onSelected: key => {
|
||||||
|
Settings.data.audio.cavaFrameRate = key
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -111,7 +110,6 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Show Active Window's Icon"
|
label: "Show Active Window's Icon"
|
||||||
description: "Display the app icon next to the title of the currently focused window."
|
description: "Display the app icon next to the title of the currently focused window."
|
||||||
|
|
@ -130,7 +128,6 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginL * scaling
|
Layout.topMargin: Style.marginL * scaling
|
||||||
|
|
@ -144,13 +141,14 @@ ColumnLayout {
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "Widgets Positioning"
|
text: "Widgets Positioning"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
|
Layout.bottomMargin: Style.marginS * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "Add, remove, or reorder widgets in each section of the bar using the control buttons."
|
text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
@ -165,7 +163,7 @@ ColumnLayout {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Left Section
|
// Left Section
|
||||||
NWidgetCard {
|
NSectionEditor {
|
||||||
sectionName: "Left"
|
sectionName: "Left"
|
||||||
widgetModel: Settings.data.bar.widgets.left
|
widgetModel: Settings.data.bar.widgets.left
|
||||||
availableWidgets: availableWidgets
|
availableWidgets: availableWidgets
|
||||||
|
|
@ -176,7 +174,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center Section
|
// Center Section
|
||||||
NWidgetCard {
|
NSectionEditor {
|
||||||
sectionName: "Center"
|
sectionName: "Center"
|
||||||
widgetModel: Settings.data.bar.widgets.center
|
widgetModel: Settings.data.bar.widgets.center
|
||||||
availableWidgets: availableWidgets
|
availableWidgets: availableWidgets
|
||||||
|
|
@ -187,7 +185,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right Section
|
// Right Section
|
||||||
NWidgetCard {
|
NSectionEditor {
|
||||||
sectionName: "Right"
|
sectionName: "Right"
|
||||||
widgetModel: Settings.data.bar.widgets.right
|
widgetModel: Settings.data.bar.widgets.right
|
||||||
availableWidgets: availableWidgets
|
availableWidgets: availableWidgets
|
||||||
|
|
@ -204,13 +202,13 @@ ColumnLayout {
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function addWidgetToSection(widgetName, section) {
|
function addWidgetToSection(widgetName, section) {
|
||||||
console.log("Adding widget", widgetName, "to section", section)
|
//Logger.log("BarTab", "Adding widget", widgetName, "to section", section)
|
||||||
var sectionArray = Settings.data.bar.widgets[section]
|
var sectionArray = Settings.data.bar.widgets[section]
|
||||||
if (sectionArray) {
|
if (sectionArray) {
|
||||||
// Create a new array to avoid modifying the original
|
// Create a new array to avoid modifying the original
|
||||||
var newArray = sectionArray.slice()
|
var newArray = sectionArray.slice()
|
||||||
newArray.push(widgetName)
|
newArray.push(widgetName)
|
||||||
console.log("Widget added. New array:", JSON.stringify(newArray))
|
//Logger.log("BarTab", "Widget added. New array:", JSON.stringify(newArray))
|
||||||
|
|
||||||
// Assign the new array
|
// Assign the new array
|
||||||
Settings.data.bar.widgets[section] = newArray
|
Settings.data.bar.widgets[section] = newArray
|
||||||
|
|
@ -218,21 +216,27 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeWidgetFromSection(section, index) {
|
function removeWidgetFromSection(section, index) {
|
||||||
console.log("Removing widget from section", section, "at index", index)
|
// Logger.log("BarTab", "Removing widget from section", section, "at index", index)
|
||||||
var sectionArray = Settings.data.bar.widgets[section]
|
var sectionArray = Settings.data.bar.widgets[section]
|
||||||
|
|
||||||
|
//Logger.log("BarTab", "Current section array:", JSON.stringify(sectionArray))
|
||||||
if (sectionArray && index >= 0 && index < sectionArray.length) {
|
if (sectionArray && index >= 0 && index < sectionArray.length) {
|
||||||
// Create a new array to avoid modifying the original
|
// Create a new array to avoid modifying the original
|
||||||
var newArray = sectionArray.slice()
|
var newArray = sectionArray.slice()
|
||||||
newArray.splice(index, 1)
|
newArray.splice(index, 1)
|
||||||
console.log("Widget removed. New array:", JSON.stringify(newArray))
|
//Logger.log("BarTab", "Widget removed. New array:", JSON.stringify(newArray))
|
||||||
|
|
||||||
// Assign the new array
|
// Assign the new array
|
||||||
Settings.data.bar.widgets[section] = newArray
|
Settings.data.bar.widgets[section] = newArray
|
||||||
|
} else {
|
||||||
|
|
||||||
|
//Logger.log("BarTab", "Invalid section or index:", section, index, "array length:",
|
||||||
|
// sectionArray ? sectionArray.length : "null")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorderWidgetInSection(section, fromIndex, toIndex) {
|
function reorderWidgetInSection(section, fromIndex, toIndex) {
|
||||||
console.log("Reordering widget in section", section, "from", fromIndex, "to", toIndex)
|
//Logger.log("BarTab", "Reordering widget in section", section, "from", fromIndex, "to", toIndex)
|
||||||
var sectionArray = Settings.data.bar.widgets[section]
|
var sectionArray = Settings.data.bar.widgets[section]
|
||||||
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0
|
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0
|
||||||
&& toIndex < sectionArray.length) {
|
&& toIndex < sectionArray.length) {
|
||||||
|
|
@ -242,36 +246,26 @@ ColumnLayout {
|
||||||
var item = newArray[fromIndex]
|
var item = newArray[fromIndex]
|
||||||
newArray.splice(fromIndex, 1)
|
newArray.splice(fromIndex, 1)
|
||||||
newArray.splice(toIndex, 0, item)
|
newArray.splice(toIndex, 0, item)
|
||||||
console.log("Widget reordered. New array:", JSON.stringify(newArray))
|
Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray))
|
||||||
|
|
||||||
// Assign the new array
|
// Assign the new array
|
||||||
Settings.data.bar.widgets[section] = newArray
|
Settings.data.bar.widgets[section] = newArray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget loader for discovering available widgets
|
// Base list model for all combo boxes
|
||||||
WidgetLoader {
|
|
||||||
id: widgetLoader
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
id: availableWidgets
|
id: availableWidgets
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
discoverWidgets()
|
// Fill out availableWidgets ListModel
|
||||||
}
|
|
||||||
|
|
||||||
// Automatically discover available widgets using WidgetLoader
|
|
||||||
function discoverWidgets() {
|
|
||||||
availableWidgets.clear()
|
availableWidgets.clear()
|
||||||
|
BarWidgetRegistry.getAvailableWidgets().forEach(entry => {
|
||||||
// Use WidgetLoader to discover available widgets
|
availableWidgets.append({
|
||||||
const discoveredWidgets = widgetLoader.discoverAvailableWidgets()
|
"key": entry,
|
||||||
|
"name": entry
|
||||||
// Add discovered widgets to the ListModel
|
})
|
||||||
discoveredWidgets.forEach(widget => {
|
|
||||||
availableWidgets.append(widget)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,10 +125,22 @@ ColumnLayout {
|
||||||
key: "60"
|
key: "60"
|
||||||
name: "60 FPS"
|
name: "60 FPS"
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "100"
|
||||||
|
name: "100 FPS"
|
||||||
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
key: "120"
|
key: "120"
|
||||||
name: "120 FPS"
|
name: "120 FPS"
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "144"
|
||||||
|
name: "144 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "165"
|
||||||
|
name: "165 FPS"
|
||||||
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
key: "240"
|
key: "240"
|
||||||
name: "240 FPS"
|
name: "240 FPS"
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,6 @@ NBox {
|
||||||
height: 90 * scaling
|
height: 90 * scaling
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
color: trackArt.visible ? Color.mPrimary : Color.transparent
|
color: trackArt.visible ? Color.mPrimary : Color.transparent
|
||||||
border.color: trackArt.visible ? Color.mOutline : Color.transparent
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
NImageCircled {
|
NImageCircled {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ NBox {
|
||||||
icon: "image"
|
icon: "image"
|
||||||
tooltipText: "Open Wallpaper Selector"
|
tooltipText: "Open Wallpaper Selector"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
||||||
settingsPanel.open(screen)
|
settingsPanel.open(screen)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,10 @@ NBox {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
NText {
|
NText {
|
||||||
text: Qt.formatDateTime(new Date(LocationService.data.weather.daily.time[index]), "ddd")
|
text: {
|
||||||
|
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
|
||||||
|
return Qt.formatDateTime(weatherDate, "ddd")
|
||||||
|
}
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
}
|
}
|
||||||
NIcon {
|
NIcon {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ Variants {
|
||||||
readonly property real scaling: ScalingService.scale(screen)
|
readonly property real scaling: ScalingService.scale(screen)
|
||||||
screen: modelData
|
screen: modelData
|
||||||
|
|
||||||
|
// Only show on screens that have notifications enabled
|
||||||
|
visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|
||||||
|
|| (Settings.data.notifications.monitors.length === 0)) : false
|
||||||
|
|
||||||
// Position based on bar location, like Notification popup does
|
// Position based on bar location, like Notification popup does
|
||||||
anchors {
|
anchors {
|
||||||
top: Settings.data.bar.position === "top"
|
top: Settings.data.bar.position === "top"
|
||||||
|
|
@ -51,12 +55,16 @@ Variants {
|
||||||
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
|
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
// Only register toasts for screens that have notifications enabled
|
||||||
|
if (modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|
||||||
|
|| (Settings.data.notifications.monitors.length === 0)) : false) {
|
||||||
// Register this toast with the service
|
// Register this toast with the service
|
||||||
ToastService.currentToast = toast
|
ToastService.allToasts.push(toast)
|
||||||
|
|
||||||
// Connect dismissal signal
|
// Connect dismissal signal
|
||||||
toast.dismissed.connect(ToastService.onToastDismissed)
|
toast.dismissed.connect(ToastService.onToastDismissed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
README.md
28
README.md
|
|
@ -79,6 +79,7 @@ Features a modern modular architecture with a status bar, notification system, c
|
||||||
- `gpu-screen-recorder` - Screen recording functionality
|
- `gpu-screen-recorder` - Screen recording functionality
|
||||||
- `brightnessctl` - For internal/laptop monitor brightness
|
- `brightnessctl` - For internal/laptop monitor brightness
|
||||||
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
|
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
|
||||||
|
If you want to use the ArchUpdater Widget, make sure you have any polkit agent installed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -100,8 +101,30 @@ mkdir -p ~/.config/quickshell && curl -sL https://github.com/noctalia-dev/noctal
|
||||||
# Start the shell
|
# Start the shell
|
||||||
qs
|
qs
|
||||||
|
|
||||||
# Toggle launcher
|
# Launcher
|
||||||
qs ipc call appLauncher toggle
|
qs ipc call launcher toggle
|
||||||
|
|
||||||
|
# SidePanel
|
||||||
|
qs ipc call sidePanel toggle
|
||||||
|
|
||||||
|
# Clipboard History
|
||||||
|
qs ipc call launcher clipboard
|
||||||
|
|
||||||
|
# Calculator
|
||||||
|
qs ipc call launcher calculator
|
||||||
|
|
||||||
|
# Brightness
|
||||||
|
qs ipc call brightness increase
|
||||||
|
qs ipc call brightness decrease
|
||||||
|
|
||||||
|
# Power Panel
|
||||||
|
qs ipc call powerPanel toggle
|
||||||
|
|
||||||
|
# Idle Inhibitor
|
||||||
|
qs ipc call idleInhibitor toggle
|
||||||
|
|
||||||
|
# Settings Window
|
||||||
|
qs ipc call settings toggle
|
||||||
|
|
||||||
# Toggle lock screen
|
# Toggle lock screen
|
||||||
qs ipc call lockScreen toggle
|
qs ipc call lockScreen toggle
|
||||||
|
|
@ -249,6 +272,7 @@ While I actually didn't want to accept donations, more and more people are askin
|
||||||
|
|
||||||
Thank you to everyone who supports me and this project 💜!
|
Thank you to everyone who supports me and this project 💜!
|
||||||
* Gohma
|
* Gohma
|
||||||
|
* <a href="https://pika-os.com/" target="_blank">PikaOS</a>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
156
Services/ArchUpdaterService.qml
Normal file
156
Services/ArchUpdaterService.qml
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: updateService
|
||||||
|
|
||||||
|
// Core properties
|
||||||
|
readonly property bool busy: checkupdatesProcess.running
|
||||||
|
readonly property int updates: updatePackages.length
|
||||||
|
property var updatePackages: []
|
||||||
|
property var selectedPackages: []
|
||||||
|
property int selectedPackagesCount: 0
|
||||||
|
property bool updateInProgress: false
|
||||||
|
|
||||||
|
// Process for checking updates
|
||||||
|
Process {
|
||||||
|
id: checkupdatesProcess
|
||||||
|
command: ["checkupdates"]
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (exitCode !== 0 && exitCode !== 2) {
|
||||||
|
console.warn("[UpdateService] checkupdates failed (code:", exitCode, ")")
|
||||||
|
updatePackages = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
parseCheckupdatesOutput(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse checkupdates output
|
||||||
|
function parseCheckupdatesOutput(output) {
|
||||||
|
const lines = output.trim().split('\n').filter(line => line.trim())
|
||||||
|
const packages = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
|
||||||
|
if (m) {
|
||||||
|
packages.push({
|
||||||
|
"name": m[1],
|
||||||
|
"oldVersion": m[2],
|
||||||
|
"newVersion": m[3],
|
||||||
|
"description": `${m[1]} ${m[2]} -> ${m[3]}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePackages = packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
function doPoll() {
|
||||||
|
if (busy)
|
||||||
|
return
|
||||||
|
checkupdatesProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all packages
|
||||||
|
function runUpdate() {
|
||||||
|
if (updates === 0) {
|
||||||
|
doPoll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInProgress = true
|
||||||
|
Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"])
|
||||||
|
|
||||||
|
// Refresh after updates with multiple attempts
|
||||||
|
refreshAfterUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selected packages
|
||||||
|
function runSelectiveUpdate() {
|
||||||
|
if (selectedPackages.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
updateInProgress = true
|
||||||
|
const command = ["pkexec", "pacman", "-S", "--noconfirm"].concat(selectedPackages)
|
||||||
|
Quickshell.execDetached(command)
|
||||||
|
|
||||||
|
// Clear selection and refresh
|
||||||
|
selectedPackages = []
|
||||||
|
selectedPackagesCount = 0
|
||||||
|
refreshAfterUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package selection functions
|
||||||
|
function togglePackageSelection(packageName) {
|
||||||
|
const index = selectedPackages.indexOf(packageName)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedPackages.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
selectedPackages.push(packageName)
|
||||||
|
}
|
||||||
|
selectedPackagesCount = selectedPackages.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllPackages() {
|
||||||
|
selectedPackages = updatePackages.map(pkg => pkg.name)
|
||||||
|
selectedPackagesCount = selectedPackages.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAllPackages() {
|
||||||
|
selectedPackages = []
|
||||||
|
selectedPackagesCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPackageSelected(packageName) {
|
||||||
|
return selectedPackages.indexOf(packageName) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robust refresh after updates
|
||||||
|
function refreshAfterUpdate() {
|
||||||
|
// First refresh attempt after 3 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
// Second refresh attempt after 8 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
}, 8000)
|
||||||
|
|
||||||
|
// Third refresh attempt after 15 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
updateInProgress = false
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
// Final refresh attempt after 30 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
}, 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification helper
|
||||||
|
function notify(title, body) {
|
||||||
|
Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-poll every 15 minutes
|
||||||
|
Timer {
|
||||||
|
interval: 15 * 60 * 1000 // 15 minutes
|
||||||
|
repeat: true
|
||||||
|
running: true
|
||||||
|
onTriggered: doPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
Component.onCompleted: doPoll()
|
||||||
|
}
|
||||||
99
Services/BarWidgetRegistry.qml
Normal file
99
Services/BarWidgetRegistry.qml
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Modules.Bar.Widgets
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Widget registry object mapping widget names to components
|
||||||
|
property var widgets: ({
|
||||||
|
"ActiveWindow": activeWindowComponent,
|
||||||
|
// "ArchUpdater": archUpdaterComponent,
|
||||||
|
"Battery": batteryComponent,
|
||||||
|
"Bluetooth": bluetoothComponent,
|
||||||
|
"Brightness": brightnessComponent,
|
||||||
|
"Clock": clockComponent,
|
||||||
|
"KeyboardLayout": keyboardLayoutComponent,
|
||||||
|
"MediaMini": mediaMiniComponent,
|
||||||
|
"NotificationHistory": notificationHistoryComponent,
|
||||||
|
"PowerProfile": powerProfileComponent,
|
||||||
|
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
|
||||||
|
"SidePanelToggle": sidePanelToggleComponent,
|
||||||
|
"SystemMonitor": systemMonitorComponent,
|
||||||
|
"Tray": trayComponent,
|
||||||
|
"Volume": volumeComponent,
|
||||||
|
"WiFi": wiFiComponent,
|
||||||
|
"Workspace": workspaceComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
// Component definitions - these are loaded once at startup
|
||||||
|
property Component activeWindowComponent: Component {
|
||||||
|
ActiveWindow {}
|
||||||
|
}
|
||||||
|
// property Component archUpdaterComponent: Component {
|
||||||
|
// ArchUpdater {}
|
||||||
|
// }
|
||||||
|
property Component batteryComponent: Component {
|
||||||
|
Battery {}
|
||||||
|
}
|
||||||
|
property Component bluetoothComponent: Component {
|
||||||
|
Bluetooth {}
|
||||||
|
}
|
||||||
|
property Component brightnessComponent: Component {
|
||||||
|
Brightness {}
|
||||||
|
}
|
||||||
|
property Component clockComponent: Component {
|
||||||
|
Clock {}
|
||||||
|
}
|
||||||
|
property Component keyboardLayoutComponent: Component {
|
||||||
|
KeyboardLayout {}
|
||||||
|
}
|
||||||
|
property Component mediaMiniComponent: Component {
|
||||||
|
MediaMini {}
|
||||||
|
}
|
||||||
|
property Component notificationHistoryComponent: Component {
|
||||||
|
NotificationHistory {}
|
||||||
|
}
|
||||||
|
property Component powerProfileComponent: Component {
|
||||||
|
PowerProfile {}
|
||||||
|
}
|
||||||
|
property Component screenRecorderIndicatorComponent: Component {
|
||||||
|
ScreenRecorderIndicator {}
|
||||||
|
}
|
||||||
|
property Component sidePanelToggleComponent: Component {
|
||||||
|
SidePanelToggle {}
|
||||||
|
}
|
||||||
|
property Component systemMonitorComponent: Component {
|
||||||
|
SystemMonitor {}
|
||||||
|
}
|
||||||
|
property Component trayComponent: Component {
|
||||||
|
Tray {}
|
||||||
|
}
|
||||||
|
property Component volumeComponent: Component {
|
||||||
|
Volume {}
|
||||||
|
}
|
||||||
|
property Component wiFiComponent: Component {
|
||||||
|
WiFi {}
|
||||||
|
}
|
||||||
|
property Component workspaceComponent: Component {
|
||||||
|
Workspace {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Helper function to get widget component by name
|
||||||
|
function getWidget(name) {
|
||||||
|
return widgets[name] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if widget exists
|
||||||
|
function hasWidget(name) {
|
||||||
|
return name in widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of available widget names
|
||||||
|
function getAvailableWidgets() {
|
||||||
|
return Object.keys(widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ Singleton {
|
||||||
property var config: ({
|
property var config: ({
|
||||||
"general": {
|
"general": {
|
||||||
"bars": barsCount,
|
"bars": barsCount,
|
||||||
"framerate": 60,
|
"framerate": Settings.data.audio.cavaFrameRate,
|
||||||
"autosens": 1,
|
"autosens": 1,
|
||||||
"sensitivity": 100,
|
"sensitivity": 100,
|
||||||
"lower_cutoff_freq": 50,
|
"lower_cutoff_freq": 50,
|
||||||
|
|
@ -38,7 +38,7 @@ Singleton {
|
||||||
id: process
|
id: process
|
||||||
stdinEnabled: true
|
stdinEnabled: true
|
||||||
running: (Settings.data.audio.visualizerType !== "none")
|
running: (Settings.data.audio.visualizerType !== "none")
|
||||||
&& (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava
|
&& (PanelService.getPanel("sidePanel").active || Settings.data.audio.showMiniplayerCava
|
||||||
|| (PanelService.lockScreen && PanelService.lockScreen.active))
|
|| (PanelService.lockScreen && PanelService.lockScreen.active))
|
||||||
command: ["cava", "-p", "/dev/stdin"]
|
command: ["cava", "-p", "/dev/stdin"]
|
||||||
onExited: {
|
onExited: {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Singleton {
|
||||||
property ListModel workspaces: ListModel {}
|
property ListModel workspaces: ListModel {}
|
||||||
property var windows: []
|
property var windows: []
|
||||||
property int focusedWindowIndex: -1
|
property int focusedWindowIndex: -1
|
||||||
property string focusedWindowTitle: "(No active window)"
|
property string focusedWindowTitle: "n/a"
|
||||||
property bool inOverview: false
|
property bool inOverview: false
|
||||||
|
|
||||||
// Generic events
|
// Generic events
|
||||||
|
|
@ -27,6 +27,7 @@ Singleton {
|
||||||
signal activeWindowChanged
|
signal activeWindowChanged
|
||||||
signal overviewStateChanged
|
signal overviewStateChanged
|
||||||
signal windowListChanged
|
signal windowListChanged
|
||||||
|
signal windowTitleChanged
|
||||||
|
|
||||||
// Compositor detection
|
// Compositor detection
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|
@ -308,10 +309,19 @@ Singleton {
|
||||||
|
|
||||||
// Update focused window index if this window is focused
|
// Update focused window index if this window is focused
|
||||||
if (newWindow.isFocused) {
|
if (newWindow.isFocused) {
|
||||||
|
const oldFocusedIndex = focusedWindowIndex
|
||||||
focusedWindowIndex = windows.findIndex(w => w.id === windowData.id)
|
focusedWindowIndex = windows.findIndex(w => w.id === windowData.id)
|
||||||
updateFocusedWindowTitle()
|
updateFocusedWindowTitle()
|
||||||
|
|
||||||
|
// Only emit activeWindowChanged if the focused window actually changed
|
||||||
|
if (oldFocusedIndex !== focusedWindowIndex) {
|
||||||
activeWindowChanged()
|
activeWindowChanged()
|
||||||
}
|
}
|
||||||
|
} else if (existingIndex >= 0 && existingIndex === focusedWindowIndex) {
|
||||||
|
// If this is the currently focused window (but not newly focused),
|
||||||
|
// still update the title in case it changed, but don't emit activeWindowChanged
|
||||||
|
updateFocusedWindowTitle()
|
||||||
|
}
|
||||||
|
|
||||||
windowListChanged()
|
windowListChanged()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -449,11 +459,17 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFocusedWindowTitle() {
|
function updateFocusedWindowTitle() {
|
||||||
|
const oldTitle = focusedWindowTitle
|
||||||
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
||||||
focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)"
|
focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)"
|
||||||
} else {
|
} else {
|
||||||
focusedWindowTitle = "(No active window)"
|
focusedWindowTitle = "(No active window)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit signal if title actually changed
|
||||||
|
if (oldTitle !== focusedWindowTitle) {
|
||||||
|
windowTitleChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic workspace switching
|
// Generic workspace switching
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
// GitHub API logic and caching
|
// GitHub API logic and caching
|
||||||
Singleton {
|
Singleton {
|
||||||
|
|
|
||||||
112
Services/KeyboardLayoutService.qml
Normal file
112
Services/KeyboardLayoutService.qml
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string currentLayout: "Unknown"
|
||||||
|
property int updateInterval: 1000 // Update every second
|
||||||
|
|
||||||
|
// Timer to periodically update the layout
|
||||||
|
Timer {
|
||||||
|
id: updateTimer
|
||||||
|
interval: updateInterval
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process to get current keyboard layout using niri msg (Wayland native)
|
||||||
|
Process {
|
||||||
|
id: niriLayoutProcess
|
||||||
|
running: false
|
||||||
|
command: ["niri", "msg", "-j", "keyboard-layouts"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text)
|
||||||
|
const layoutName = data.names[data.current_idx]
|
||||||
|
root.currentLayout = mapLayoutNameToCode(layoutName)
|
||||||
|
} catch (e) {
|
||||||
|
root.currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process to get current keyboard layout using hyprctl (Hyprland)
|
||||||
|
Process {
|
||||||
|
id: hyprlandLayoutProcess
|
||||||
|
running: false
|
||||||
|
command: ["hyprctl", "-j", "devices"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text)
|
||||||
|
// Find the main keyboard and get its active keymap
|
||||||
|
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
||||||
|
if (mainKeyboard && mainKeyboard.active_keymap) {
|
||||||
|
root.currentLayout = mapLayoutNameToCode(mainKeyboard.active_keymap)
|
||||||
|
} else {
|
||||||
|
root.currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
root.currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout name to ISO code mapping
|
||||||
|
property var layoutMap: {
|
||||||
|
"German": "de",
|
||||||
|
"English (US)": "us",
|
||||||
|
"English (UK)": "gb",
|
||||||
|
"French": "fr",
|
||||||
|
"Spanish": "es",
|
||||||
|
"Italian": "it",
|
||||||
|
"Portuguese (Brazil)": "br",
|
||||||
|
"Portuguese": "pt",
|
||||||
|
"Russian": "ru",
|
||||||
|
"Polish": "pl",
|
||||||
|
"Swedish": "se",
|
||||||
|
"Norwegian": "no",
|
||||||
|
"Danish": "dk",
|
||||||
|
"Finnish": "fi",
|
||||||
|
"Hungarian": "hu",
|
||||||
|
"Turkish": "tr",
|
||||||
|
"Czech": "cz",
|
||||||
|
"Slovak": "sk",
|
||||||
|
"Japanese": "jp",
|
||||||
|
"Korean": "kr",
|
||||||
|
"Chinese": "cn"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map layout names to ISO codes
|
||||||
|
function mapLayoutNameToCode(layoutName) {
|
||||||
|
return layoutMap[layoutName] || layoutName // fallback to raw name if not found
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Logger.log("KeyboardLayout", "Service started")
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLayout() {
|
||||||
|
if (CompositorService.isHyprland) {
|
||||||
|
hyprlandLayoutProcess.running = true
|
||||||
|
} else if (CompositorService.isNiri) {
|
||||||
|
niriLayoutProcess.running = true
|
||||||
|
} else {
|
||||||
|
currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
// Weather logic and caching
|
// Weather logic and caching
|
||||||
Singleton {
|
Singleton {
|
||||||
|
|
@ -109,8 +110,8 @@ Singleton {
|
||||||
|
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
function _geocodeLocation(locationName, callback, errorCallback) {
|
function _geocodeLocation(locationName, callback, errorCallback) {
|
||||||
Logger.log("Location", "Geocoding from api.open-meteo.com")
|
Logger.log("Location", "Geocoding location name")
|
||||||
var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(
|
var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(
|
||||||
locationName) + "&language=en&format=json"
|
locationName) + "&language=en&format=json"
|
||||||
var xhr = new XMLHttpRequest()
|
var xhr = new XMLHttpRequest()
|
||||||
xhr.onreadystatechange = function () {
|
xhr.onreadystatechange = function () {
|
||||||
|
|
@ -119,8 +120,8 @@ Singleton {
|
||||||
try {
|
try {
|
||||||
var geoData = JSON.parse(xhr.responseText)
|
var geoData = JSON.parse(xhr.responseText)
|
||||||
// Logger.logJSON.stringify(geoData))
|
// Logger.logJSON.stringify(geoData))
|
||||||
if (geoData.results && geoData.results.length > 0) {
|
if (geoData.lat != null) {
|
||||||
callback(geoData.results[0].latitude, geoData.results[0].longitude)
|
callback(geoData.lat, geoData.lng)
|
||||||
} else {
|
} else {
|
||||||
errorCallback("Location", "could not resolve location name")
|
errorCallback("Location", "could not resolve location name")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ Singleton {
|
||||||
property string detectedInterface: ""
|
property string detectedInterface: ""
|
||||||
property string lastConnectedNetwork: ""
|
property string lastConnectedNetwork: ""
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
|
property bool ethernet: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Logger.log("Network", "Service started")
|
Logger.log("Network", "Service started")
|
||||||
|
|
@ -43,6 +44,7 @@ Singleton {
|
||||||
|
|
||||||
function refreshNetworks() {
|
function refreshNetworks() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
checkEthernet.running = true
|
||||||
existingNetwork.running = true
|
existingNetwork.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,6 +418,24 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property Process checkEthernet: Process {
|
||||||
|
id: checkEthernet
|
||||||
|
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] === "ethernet" && parts[2] === "connected") {
|
||||||
|
root.ethernet = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property Process addConnectionProcess: Process {
|
property Process addConnectionProcess: Process {
|
||||||
id: addConnectionProcess
|
id: addConnectionProcess
|
||||||
property string ifname: ""
|
property string ifname: ""
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,38 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// A ref. to the sidePanel, so it's accessible from other services
|
// A ref. to the lockScreen, so it's accessible from anywhere
|
||||||
property var sidePanel: null
|
// This is not a panel...
|
||||||
|
|
||||||
// A ref. to the lockScreen, so it's accessible from other services
|
|
||||||
property var lockScreen: null
|
property var lockScreen: null
|
||||||
|
|
||||||
// Currently opened panel
|
// Currently opened panel
|
||||||
property var openedPanel: null
|
property var openedPanel: null
|
||||||
|
|
||||||
function registerOpen(panel) {
|
property var registeredPanels: ({})
|
||||||
|
|
||||||
|
// Register this panel
|
||||||
|
function registerPanel(panel) {
|
||||||
|
registeredPanels[panel.objectName] = panel
|
||||||
|
Logger.log("PanelService", "Registered:", panel.objectName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a panel
|
||||||
|
function getPanel(name) {
|
||||||
|
return registeredPanels[name] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a panel exists
|
||||||
|
function hasPanel(name) {
|
||||||
|
return name in registeredPanels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to keep only one panel open at any time
|
||||||
|
function willOpenPanel(panel) {
|
||||||
if (openedPanel && openedPanel != panel) {
|
if (openedPanel && openedPanel != panel) {
|
||||||
openedPanel.close()
|
openedPanel.close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,22 @@ Singleton {
|
||||||
videoDir += "/"
|
videoDir += "/"
|
||||||
}
|
}
|
||||||
outputPath = videoDir + filename
|
outputPath = videoDir + filename
|
||||||
var command = `gpu-screen-recorder -w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath}`
|
var flags = `-w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath}`
|
||||||
|
var command = `
|
||||||
|
_gpuscreenrecorder_flatpak_installed() {
|
||||||
|
flatpak list --app | grep -q "com.dec05eba.gpu_screen_recorder"
|
||||||
|
}
|
||||||
|
if command -v gpu-screen-recorder >/dev/null 2>&1; then
|
||||||
|
gpu-screen-recorder ${flags}
|
||||||
|
elif command -v flatpak >/dev/null 2>&1 && _gpuscreenrecorder_flatpak_installed; then
|
||||||
|
flatpak run --command=gpu-screen-recorder --file-forwarding com.dec05eba.gpu_screen_recorder ${flags}
|
||||||
|
else
|
||||||
|
notify-send "gpu-screen-recorder not installed!" -u critical
|
||||||
|
fi`
|
||||||
|
|
||||||
//Logger.log("ScreenRecorder", command)
|
//Logger.log("ScreenRecorder", command)
|
||||||
Quickshell.execDetached(["sh", "-c", command])
|
Quickshell.execDetached(["sh", "-c", command])
|
||||||
Logger.log("ScreenRecorder", "Started recording")
|
Logger.log("ScreenRecorder", "Started recording")
|
||||||
//Logger.log("ScreenRecorder", command)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop recording using Quickshell.execDetached
|
// Stop recording using Quickshell.execDetached
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,77 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
|
|
||||||
QtObject {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Queue of pending toast messages
|
// Queue of pending toast messages
|
||||||
property var messageQueue: []
|
property var messageQueue: []
|
||||||
property bool isShowingToast: false
|
property bool isShowingToast: false
|
||||||
|
|
||||||
// Reference to the current toast instance (set by ToastManager)
|
// Reference to all toast instances (set by ToastManager)
|
||||||
property var currentToast: null
|
property var allToasts: []
|
||||||
|
|
||||||
|
// Properties for command checking
|
||||||
|
property var commandCheckCallback: null
|
||||||
|
property string commandCheckSuccessMessage: ""
|
||||||
|
property string commandCheckFailMessage: ""
|
||||||
|
|
||||||
|
// Properties for command running
|
||||||
|
property var commandRunCallback: null
|
||||||
|
property string commandRunSuccessMessage: ""
|
||||||
|
property string commandRunFailMessage: ""
|
||||||
|
|
||||||
|
// Properties for delayed toast
|
||||||
|
property string delayedToastMessage: ""
|
||||||
|
property string delayedToastType: "notice"
|
||||||
|
|
||||||
|
// Process for command checking
|
||||||
|
Process {
|
||||||
|
id: commandCheckProcess
|
||||||
|
command: ["which", "test"]
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
showNotice(commandCheckSuccessMessage)
|
||||||
|
if (commandCheckCallback)
|
||||||
|
commandCheckCallback()
|
||||||
|
} else {
|
||||||
|
showWarning(commandCheckFailMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process for command running
|
||||||
|
Process {
|
||||||
|
id: commandRunProcess
|
||||||
|
command: ["echo", "test"]
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
showNotice(commandRunSuccessMessage)
|
||||||
|
if (commandRunCallback)
|
||||||
|
commandRunCallback()
|
||||||
|
} else {
|
||||||
|
showWarning(commandRunFailMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer for delayed toast
|
||||||
|
Timer {
|
||||||
|
id: delayedToastTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
showToast(delayedToastMessage, delayedToastType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Methods to show different types of messages
|
// Methods to show different types of messages
|
||||||
function showNotice(label, description = "", persistent = false, duration = 3000) {
|
function showNotice(label, description = "", persistent = false, duration = 3000) {
|
||||||
|
|
@ -25,37 +84,14 @@ QtObject {
|
||||||
|
|
||||||
// Utility function to check if a command exists and show appropriate toast
|
// Utility function to check if a command exists and show appropriate toast
|
||||||
function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) {
|
function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) {
|
||||||
var checkProcess = Qt.createQmlObject(`
|
// Store callback for use in the process
|
||||||
import QtQuick
|
commandCheckCallback = onSuccess
|
||||||
import Quickshell.Io
|
commandCheckSuccessMessage = successMessage
|
||||||
Process {
|
commandCheckFailMessage = failMessage
|
||||||
id: checkProc
|
|
||||||
command: ["which", "${command}"]
|
|
||||||
running: true
|
|
||||||
|
|
||||||
property var onSuccessCallback: null
|
// Start the command check process
|
||||||
property bool hasFinished: false
|
commandCheckProcess.command = ["which", command]
|
||||||
|
commandCheckProcess.running = true
|
||||||
onExited: {
|
|
||||||
if (!hasFinished) {
|
|
||||||
hasFinished = true
|
|
||||||
if (exitCode === 0) {
|
|
||||||
ToastService.showNotice("${successMessage}")
|
|
||||||
if (onSuccessCallback) onSuccessCallback()
|
|
||||||
} else {
|
|
||||||
ToastService.showWarning("${failMessage}")
|
|
||||||
}
|
|
||||||
checkProc.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback collectors to prevent issues
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
stderr: StdioCollector {}
|
|
||||||
}
|
|
||||||
`, root)
|
|
||||||
|
|
||||||
checkProcess.onSuccessCallback = onSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple function to show a random toast (useful for testing or fun messages)
|
// Simple function to show a random toast (useful for testing or fun messages)
|
||||||
|
|
@ -95,37 +131,14 @@ QtObject {
|
||||||
|
|
||||||
// Generic command runner with toast feedback
|
// Generic command runner with toast feedback
|
||||||
function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) {
|
function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) {
|
||||||
var fullCommand = [command].concat(args || [])
|
// Store callback for use in the process
|
||||||
var runProcess = Qt.createQmlObject(`
|
commandRunCallback = onSuccess
|
||||||
import QtQuick
|
commandRunSuccessMessage = successMessage
|
||||||
import Quickshell.Io
|
commandRunFailMessage = failMessage
|
||||||
Process {
|
|
||||||
id: runProc
|
|
||||||
command: ${JSON.stringify(fullCommand)}
|
|
||||||
running: true
|
|
||||||
|
|
||||||
property var onSuccessCallback: null
|
// Start the command run process
|
||||||
property bool hasFinished: false
|
commandRunProcess.command = [command].concat(args || [])
|
||||||
|
commandRunProcess.running = true
|
||||||
onExited: {
|
|
||||||
if (!hasFinished) {
|
|
||||||
hasFinished = true
|
|
||||||
if (exitCode === 0) {
|
|
||||||
ToastService.showNotice("${successMessage}")
|
|
||||||
if (onSuccessCallback) onSuccessCallback()
|
|
||||||
} else {
|
|
||||||
ToastService.showWarning("${failMessage}")
|
|
||||||
}
|
|
||||||
runProc.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
stderr: StdioCollector {}
|
|
||||||
}
|
|
||||||
`, root)
|
|
||||||
|
|
||||||
runProcess.onSuccessCallback = onSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a file/directory exists
|
// Check if a file/directory exists
|
||||||
|
|
@ -135,18 +148,10 @@ QtObject {
|
||||||
|
|
||||||
// Show toast after a delay (useful for delayed feedback)
|
// Show toast after a delay (useful for delayed feedback)
|
||||||
function delayedToast(message, type = "notice", delayMs = 1000) {
|
function delayedToast(message, type = "notice", delayMs = 1000) {
|
||||||
var timer = Qt.createQmlObject(`
|
delayedToastMessage = message
|
||||||
import QtQuick
|
delayedToastType = type
|
||||||
Timer {
|
delayedToastTimer.interval = delayMs
|
||||||
interval: ${delayMs}
|
delayedToastTimer.restart()
|
||||||
repeat: false
|
|
||||||
running: true
|
|
||||||
onTriggered: {
|
|
||||||
ToastService.showToast("${message}", "${type}")
|
|
||||||
destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic method to show a toast
|
// Generic method to show a toast
|
||||||
|
|
@ -171,7 +176,7 @@ QtObject {
|
||||||
|
|
||||||
// Process the message queue
|
// Process the message queue
|
||||||
function processQueue() {
|
function processQueue() {
|
||||||
if (messageQueue.length === 0 || !currentToast) {
|
if (messageQueue.length === 0 || allToasts.length === 0) {
|
||||||
isShowingToast = false
|
isShowingToast = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -184,18 +189,30 @@ QtObject {
|
||||||
var toastData = messageQueue.shift()
|
var toastData = messageQueue.shift()
|
||||||
isShowingToast = true
|
isShowingToast = true
|
||||||
|
|
||||||
// Configure and show toast
|
// Configure and show toast on all screens
|
||||||
currentToast.label = toastData.label
|
for (var i = 0; i < allToasts.length; i++) {
|
||||||
currentToast.description = toastData.description
|
var toast = allToasts[i]
|
||||||
currentToast.type = toastData.type
|
toast.label = toastData.label
|
||||||
currentToast.persistent = toastData.persistent
|
toast.description = toastData.description
|
||||||
currentToast.duration = toastData.duration
|
toast.type = toastData.type
|
||||||
currentToast.show()
|
toast.persistent = toastData.persistent
|
||||||
|
toast.duration = toastData.duration
|
||||||
|
toast.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when a toast is dismissed
|
// Called when a toast is dismissed
|
||||||
function onToastDismissed() {
|
function onToastDismissed() {
|
||||||
|
// Check if all toasts are dismissed
|
||||||
|
var allDismissed = true
|
||||||
|
for (var i = 0; i < allToasts.length; i++) {
|
||||||
|
if (allToasts[i].visible) {
|
||||||
|
allDismissed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allDismissed) {
|
||||||
isShowingToast = false
|
isShowingToast = false
|
||||||
|
|
||||||
// Small delay before showing next toast
|
// Small delay before showing next toast
|
||||||
|
|
@ -203,6 +220,7 @@ QtObject {
|
||||||
processQueue()
|
processQueue()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear all pending messages
|
// Clear all pending messages
|
||||||
function clearQueue() {
|
function clearQueue() {
|
||||||
|
|
@ -212,8 +230,10 @@ QtObject {
|
||||||
|
|
||||||
// Hide current toast
|
// Hide current toast
|
||||||
function hideCurrentToast() {
|
function hideCurrentToast() {
|
||||||
if (currentToast && isShowingToast) {
|
if (isShowingToast) {
|
||||||
currentToast.hide()
|
for (var i = 0; i < allToasts.length; i++) {
|
||||||
|
allToasts[i].hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
// Generic card container
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
implicitWidth: childrenRect.width
|
|
||||||
implicitHeight: childrenRect.height
|
|
||||||
|
|
||||||
color: Color.mSurface
|
|
||||||
radius: Style.radiusM * scaling
|
|
||||||
border.color: Color.mOutline
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
}
|
|
||||||
|
|
@ -8,14 +8,14 @@ import qs.Widgets
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property real preferredHeight: Style.baseWidgetSize * 1.25 * scaling
|
readonly property real preferredHeight: Style.baseWidgetSize * 1.35 * scaling
|
||||||
|
|
||||||
property string label: ""
|
property string label: ""
|
||||||
property string description: ""
|
property string description: ""
|
||||||
property ListModel model: {
|
property ListModel model: {
|
||||||
|
|
||||||
}
|
}
|
||||||
property string currentKey: ''
|
property string currentKey: ""
|
||||||
property string placeholder: ""
|
property string placeholder: ""
|
||||||
|
|
||||||
signal selected(string key)
|
signal selected(string key)
|
||||||
|
|
@ -39,7 +39,8 @@ ColumnLayout {
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: combo
|
id: combo
|
||||||
Layout.fillWidth: true
|
|
||||||
|
Layout.preferredWidth: 320 * scaling
|
||||||
Layout.preferredHeight: height
|
Layout.preferredHeight: height
|
||||||
model: model
|
model: model
|
||||||
currentIndex: findIndexByKey(currentKey)
|
currentIndex: findIndexByKey(currentKey)
|
||||||
|
|
@ -128,5 +129,13 @@ ColumnLayout {
|
||||||
radius: Style.radiusM * scaling
|
radius: Style.radiusM * scaling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the currentIndex if the currentKey is changed externalyu
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onCurrentKeyChanged() {
|
||||||
|
combo.currentIndex = root.findIndexByKey(currentKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ Loader {
|
||||||
signal opened
|
signal opened
|
||||||
signal closed
|
signal closed
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// console.log("Oh Yeah")
|
||||||
|
// console.log(objectName)
|
||||||
|
PanelService.registerPanel(root)
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
function toggle(aScreen) {
|
function toggle(aScreen) {
|
||||||
if (!active || isClosing) {
|
if (!active || isClosing) {
|
||||||
|
|
@ -53,7 +59,7 @@ Loader {
|
||||||
opacityValue = 1.0
|
opacityValue = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelService.registerOpen(root)
|
PanelService.willOpenPanel(root)
|
||||||
|
|
||||||
active = true
|
active = true
|
||||||
root.opened()
|
root.opened()
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@ Item {
|
||||||
property color collapsedIconColor: Color.mOnSurface
|
property color collapsedIconColor: Color.mOnSurface
|
||||||
property real sizeMultiplier: 0.8
|
property real sizeMultiplier: 0.8
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
// When true, keep the pill expanded regardless of hover state
|
property bool forceOpen: false
|
||||||
property bool forceShown: false
|
property bool disableOpen: false
|
||||||
|
|
||||||
// Effective shown state (true if hovered/animated open or forced)
|
// Effective shown state (true if hovered/animated open or forced)
|
||||||
readonly property bool effectiveShown: forceShown || showPill
|
readonly property bool effectiveShown: forceOpen || showPill
|
||||||
|
|
||||||
signal shown
|
signal shown
|
||||||
signal hidden
|
signal hidden
|
||||||
|
|
@ -85,7 +86,7 @@ Item {
|
||||||
height: iconSize
|
height: iconSize
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
// When forced shown, match pill background; otherwise use accent when hovered
|
// When forced shown, match pill background; otherwise use accent when hovered
|
||||||
color: forceShown ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
|
@ -100,7 +101,7 @@ Item {
|
||||||
text: root.icon
|
text: root.icon
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
// When forced shown, use pill text color; otherwise accent color when hovered
|
// When forced shown, use pill text color; otherwise accent color when hovered
|
||||||
color: forceShown ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -194,18 +195,21 @@ Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (!forceShown) {
|
root.entered()
|
||||||
|
tooltip.show()
|
||||||
|
if (disableOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!forceOpen) {
|
||||||
showDelayed()
|
showDelayed()
|
||||||
}
|
}
|
||||||
tooltip.show()
|
|
||||||
root.entered()
|
|
||||||
}
|
}
|
||||||
onExited: {
|
onExited: {
|
||||||
if (!forceShown) {
|
root.exited()
|
||||||
|
if (!forceOpen) {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
tooltip.hide()
|
tooltip.hide()
|
||||||
root.exited()
|
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.clicked()
|
root.clicked()
|
||||||
|
|
@ -226,7 +230,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
if (forceShown) {
|
if (forceOpen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (showPill) {
|
if (showPill) {
|
||||||
|
|
@ -245,8 +249,8 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onForceShownChanged: {
|
onForceOpenChanged: {
|
||||||
if (forceShown) {
|
if (forceOpen) {
|
||||||
// Immediately lock open without animations
|
// Immediately lock open without animations
|
||||||
showAnim.stop()
|
showAnim.stop()
|
||||||
hideAnim.stop()
|
hideAnim.stop()
|
||||||
|
|
|
||||||
319
Widgets/NSectionEditor.qml
Normal file
319
Widgets/NSectionEditor.qml
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NBox {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string sectionName: ""
|
||||||
|
property var widgetModel: []
|
||||||
|
property var availableWidgets: []
|
||||||
|
property var scrollView: null
|
||||||
|
|
||||||
|
signal addWidget(string widgetName, string section)
|
||||||
|
signal removeWidget(string section, int index)
|
||||||
|
signal reorderWidget(string section, int fromIndex, int toIndex)
|
||||||
|
|
||||||
|
color: Color.mSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumHeight: {
|
||||||
|
var widgetCount = widgetModel.length
|
||||||
|
if (widgetCount === 0)
|
||||||
|
return 140 * scaling
|
||||||
|
|
||||||
|
var availableWidth = scrollView ? scrollView.availableWidth - (Style.marginM * scaling * 2) : 400 * scaling
|
||||||
|
var avgWidgetWidth = 150 * scaling
|
||||||
|
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
|
||||||
|
var rows = Math.ceil(widgetCount / widgetsPerRow)
|
||||||
|
|
||||||
|
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate widget color from name checksum
|
||||||
|
function getWidgetColor(name) {
|
||||||
|
const totalSum = name.split('').reduce((acc, character) => {
|
||||||
|
return acc + character.charCodeAt(0)
|
||||||
|
}, 0)
|
||||||
|
switch (totalSum % 5) {
|
||||||
|
case 0:
|
||||||
|
return Color.mPrimary
|
||||||
|
case 1:
|
||||||
|
return Color.mSecondary
|
||||||
|
case 2:
|
||||||
|
return Color.mTertiary
|
||||||
|
case 3:
|
||||||
|
return Color.mError
|
||||||
|
case 4:
|
||||||
|
return Color.mOnSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginM * scaling
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: sectionName + " Section"
|
||||||
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mSecondary
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
NComboBox {
|
||||||
|
id: comboBox
|
||||||
|
model: availableWidgets
|
||||||
|
label: ""
|
||||||
|
description: ""
|
||||||
|
placeholder: "Add widget to the " + sectionName.toLowerCase() + " section..."
|
||||||
|
onSelected: key => {
|
||||||
|
comboBox.currentKey = key
|
||||||
|
}
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "add"
|
||||||
|
|
||||||
|
colorBg: Color.mPrimary
|
||||||
|
colorFg: Color.mOnPrimary
|
||||||
|
colorBgHover: Color.mSecondary
|
||||||
|
colorFgHover: Color.mOnSecondary
|
||||||
|
enabled: comboBox.selectedKey !== ""
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: Style.marginS * scaling
|
||||||
|
onClicked: {
|
||||||
|
if (comboBox.currentKey !== "") {
|
||||||
|
addWidget(comboBox.currentKey, sectionName.toLowerCase())
|
||||||
|
comboBox.currentKey = "battery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and Drop Widget Area
|
||||||
|
Flow {
|
||||||
|
id: widgetFlow
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.minimumHeight: 65 * scaling
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
flow: Flow.LeftToRight
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: widgetModel
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: widgetItem
|
||||||
|
required property int index
|
||||||
|
required property string modelData
|
||||||
|
|
||||||
|
width: widgetContent.implicitWidth + 16 * scaling
|
||||||
|
height: 40 * scaling
|
||||||
|
radius: Style.radiusL * scaling
|
||||||
|
color: root.getWidgetColor(modelData)
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
|
// Drag properties
|
||||||
|
Drag.keys: ["widget"]
|
||||||
|
Drag.active: mouseArea.drag.active
|
||||||
|
Drag.hotSpot.x: width / 2
|
||||||
|
Drag.hotSpot.y: height / 2
|
||||||
|
|
||||||
|
// Store the widget index for drag operations
|
||||||
|
property int widgetIndex: index
|
||||||
|
|
||||||
|
// Visual feedback during drag
|
||||||
|
states: State {
|
||||||
|
when: mouseArea.drag.active
|
||||||
|
PropertyChanges {
|
||||||
|
target: widgetItem
|
||||||
|
scale: 1.1
|
||||||
|
opacity: 0.9
|
||||||
|
z: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: widgetContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: modelData
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
color: Color.mOnPrimary
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
size: 20 * scaling
|
||||||
|
colorBorder: Color.applyOpacity(Color.mOutline, "40")
|
||||||
|
colorBg: Color.mOnSurface
|
||||||
|
colorFg: Color.mOnPrimary
|
||||||
|
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
||||||
|
colorFgHover: Color.mOnPrimary
|
||||||
|
onClicked: {
|
||||||
|
removeWidget(sectionName.toLowerCase(), index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse area for drag and drop
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
drag.target: parent
|
||||||
|
|
||||||
|
onPressed: mouse => {
|
||||||
|
// Check if the click is on the close button area
|
||||||
|
const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling
|
||||||
|
const closeButtonY = widgetContent.y
|
||||||
|
const closeButtonWidth = 20 * scaling
|
||||||
|
const closeButtonHeight = 20 * scaling
|
||||||
|
|
||||||
|
if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth
|
||||||
|
&& mouseY >= closeButtonY && mouseY <= closeButtonY + closeButtonHeight) {
|
||||||
|
// Click is on the close button, don't start drag
|
||||||
|
mouse.accepted = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.log("NSectionEditor", `Started dragging widget: ${modelData} at index ${index}`)
|
||||||
|
// Bring to front when starting drag
|
||||||
|
widgetItem.z = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: {
|
||||||
|
Logger.log("NSectionEditor", `Released widget: ${modelData} at index ${index}`)
|
||||||
|
// Reset z-index when drag ends
|
||||||
|
widgetItem.z = 0
|
||||||
|
|
||||||
|
// Get the global mouse position
|
||||||
|
const globalDropX = mouseArea.mouseX + widgetItem.x + widgetFlow.x
|
||||||
|
const globalDropY = mouseArea.mouseY + widgetItem.y + widgetFlow.y
|
||||||
|
|
||||||
|
// Find which widget the drop position is closest to
|
||||||
|
let targetIndex = -1
|
||||||
|
let minDistance = Infinity
|
||||||
|
|
||||||
|
for (var i = 0; i < widgetModel.length; i++) {
|
||||||
|
if (i !== index) {
|
||||||
|
// Get the position of other widgets
|
||||||
|
const otherWidget = widgetFlow.children[i]
|
||||||
|
if (otherWidget && otherWidget.widgetIndex !== undefined) {
|
||||||
|
// Calculate the center of the other widget
|
||||||
|
const otherCenterX = otherWidget.x + otherWidget.width / 2 + widgetFlow.x
|
||||||
|
const otherCenterY = otherWidget.y + otherWidget.height / 2 + widgetFlow.y
|
||||||
|
|
||||||
|
// Calculate distance to the center of this widget
|
||||||
|
const distance = Math.sqrt(Math.pow(globalDropX - otherCenterX,
|
||||||
|
2) + Math.pow(globalDropY - otherCenterY, 2))
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance
|
||||||
|
targetIndex = otherWidget.widgetIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only reorder if we found a valid target and it's different from current position
|
||||||
|
if (targetIndex !== -1 && targetIndex !== index) {
|
||||||
|
const fromIndex = index
|
||||||
|
const toIndex = targetIndex
|
||||||
|
Logger.log(
|
||||||
|
"NSectionEditor",
|
||||||
|
`Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed(
|
||||||
|
2)})`)
|
||||||
|
reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex)
|
||||||
|
} else {
|
||||||
|
Logger.log("NSectionEditor", `No valid drop target found for widget at index ${index}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop zone at the beginning (positioned absolutely)
|
||||||
|
DropArea {
|
||||||
|
id: startDropZone
|
||||||
|
width: 40 * scaling
|
||||||
|
height: 40 * scaling
|
||||||
|
x: widgetFlow.x
|
||||||
|
y: widgetFlow.y + (widgetFlow.height - height) / 2
|
||||||
|
keys: ["widget"]
|
||||||
|
z: 1001 // Above the Flow
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: startDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent
|
||||||
|
border.color: startDropZone.containsDrag ? Color.mPrimary : Color.transparent
|
||||||
|
border.width: startDropZone.containsDrag ? 2 : 0
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntered: function (drag) {
|
||||||
|
Logger.log("NSectionEditor", "Entered start drop zone")
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropped: function (drop) {
|
||||||
|
Logger.log("NSectionEditor", "Dropped on start zone")
|
||||||
|
if (drop.source && drop.source.widgetIndex !== undefined) {
|
||||||
|
const fromIndex = drop.source.widgetIndex
|
||||||
|
const toIndex = 0 // Insert at the beginning
|
||||||
|
if (fromIndex !== toIndex) {
|
||||||
|
Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to beginning`)
|
||||||
|
reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop zone at the end (positioned absolutely)
|
||||||
|
DropArea {
|
||||||
|
id: endDropZone
|
||||||
|
width: 40 * scaling
|
||||||
|
height: 40 * scaling
|
||||||
|
x: widgetFlow.x + widgetFlow.width - width
|
||||||
|
y: widgetFlow.y + (widgetFlow.height - height) / 2
|
||||||
|
keys: ["widget"]
|
||||||
|
z: 1001 // Above the Flow
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: endDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent
|
||||||
|
border.color: endDropZone.containsDrag ? Color.mPrimary : Color.transparent
|
||||||
|
border.width: endDropZone.containsDrag ? 2 : 0
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntered: function (drag) {
|
||||||
|
Logger.log("NSectionEditor", "Entered end drop zone")
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropped: function (drop) {
|
||||||
|
Logger.log("NSectionEditor", "Dropped on end zone")
|
||||||
|
if (drop.source && drop.source.widgetIndex !== undefined) {
|
||||||
|
const fromIndex = drop.source.widgetIndex
|
||||||
|
const toIndex = widgetModel.length // Insert at the end
|
||||||
|
if (fromIndex !== toIndex) {
|
||||||
|
Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to end`)
|
||||||
|
reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
NCard {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string sectionName: ""
|
|
||||||
property var widgetModel: []
|
|
||||||
property var availableWidgets: []
|
|
||||||
property var scrollView: null
|
|
||||||
|
|
||||||
signal addWidget(string widgetName, string section)
|
|
||||||
signal removeWidget(string section, int index)
|
|
||||||
signal reorderWidget(string section, int fromIndex, int toIndex)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: {
|
|
||||||
var widgetCount = widgetModel.length
|
|
||||||
if (widgetCount === 0)
|
|
||||||
return 140 * scaling
|
|
||||||
|
|
||||||
var availableWidth = scrollView ? scrollView.availableWidth - (Style.marginM * scaling * 2) : 400 * scaling
|
|
||||||
var avgWidgetWidth = 150 * scaling
|
|
||||||
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
|
|
||||||
var rows = Math.ceil(widgetCount / widgetsPerRow)
|
|
||||||
|
|
||||||
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Style.marginM * scaling
|
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: sectionName + " Section"
|
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mOnSurface
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
NComboBox {
|
|
||||||
id: comboBox
|
|
||||||
width: 120 * scaling
|
|
||||||
model: availableWidgets
|
|
||||||
label: ""
|
|
||||||
description: ""
|
|
||||||
placeholder: "Add widget to " + sectionName.toLowerCase() + " section"
|
|
||||||
onSelected: key => {
|
|
||||||
comboBox.selectedKey = key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "add"
|
|
||||||
size: 24 * scaling
|
|
||||||
colorBg: Color.mPrimary
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.mPrimaryContainer
|
|
||||||
colorFgHover: Color.mOnPrimaryContainer
|
|
||||||
enabled: comboBox.selectedKey !== ""
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
onClicked: {
|
|
||||||
if (comboBox.selectedKey !== "") {
|
|
||||||
addWidget(comboBox.selectedKey, sectionName.toLowerCase())
|
|
||||||
comboBox.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.minimumHeight: 65 * scaling
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
flow: Flow.LeftToRight
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: widgetModel
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: widgetContent.implicitWidth + 16 * scaling
|
|
||||||
height: 48 * scaling
|
|
||||||
radius: Style.radiusS * scaling
|
|
||||||
color: Color.mPrimary
|
|
||||||
border.color: Color.mOutline
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: widgetContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Style.marginXS * scaling
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "chevron_left"
|
|
||||||
size: 20 * scaling
|
|
||||||
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
|
||||||
colorFgHover: Color.mOnPrimary
|
|
||||||
enabled: index > 0
|
|
||||||
onClicked: {
|
|
||||||
if (index > 0) {
|
|
||||||
reorderWidget(sectionName.toLowerCase(), index, index - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: modelData
|
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
|
||||||
color: Color.mOnPrimary
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "chevron_right"
|
|
||||||
size: 20 * scaling
|
|
||||||
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
|
||||||
colorFgHover: Color.mOnPrimary
|
|
||||||
enabled: index < widgetModel.length - 1
|
|
||||||
onClicked: {
|
|
||||||
if (index < widgetModel.length - 1) {
|
|
||||||
reorderWidget(sectionName.toLowerCase(), index, index + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "close"
|
|
||||||
size: 20 * scaling
|
|
||||||
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
|
||||||
colorFgHover: Color.mOnPrimary
|
|
||||||
onClicked: {
|
|
||||||
removeWidget(sectionName.toLowerCase(), index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
Widgets/NWidgetLoader.qml
Normal file
46
Widgets/NWidgetLoader.qml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string widgetName: ""
|
||||||
|
property var widgetProps: ({})
|
||||||
|
property bool enabled: true
|
||||||
|
|
||||||
|
// Don't reserve space unless the loaded widget is really visible
|
||||||
|
implicitWidth: loader.item ? loader.item.visible ? loader.item.implicitWidth : 0 : 0
|
||||||
|
implicitHeight: loader.item ? loader.item.visible ? loader.item.implicitHeight : 0 : 0
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: loader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: enabled && widgetName !== ""
|
||||||
|
sourceComponent: {
|
||||||
|
if (!active) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return BarWidgetRegistry.getWidget(widgetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
if (item && widgetProps) {
|
||||||
|
// Apply properties to loaded widget
|
||||||
|
for (var prop in widgetProps) {
|
||||||
|
if (item.hasOwnProperty(prop)) {
|
||||||
|
item[prop] = widgetProps[prop]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
onWidgetNameChanged: {
|
||||||
|
if (widgetName && !BarWidgetRegistry.hasWidget(widgetName)) {
|
||||||
|
Logger.warn("WidgetLoader", "Widget not found in registry:", widgetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
shell.qml
85
shell.qml
|
|
@ -27,6 +27,7 @@ import qs.Modules.PowerPanel
|
||||||
import qs.Modules.SidePanel
|
import qs.Modules.SidePanel
|
||||||
import qs.Modules.Toast
|
import qs.Modules.Toast
|
||||||
import qs.Modules.WiFiPanel
|
import qs.Modules.WiFiPanel
|
||||||
|
import qs.Modules.ArchUpdaterPanel
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
|
|
@ -39,55 +40,67 @@ ShellRoot {
|
||||||
Bar {}
|
Bar {}
|
||||||
Dock {}
|
Dock {}
|
||||||
|
|
||||||
Launcher {
|
|
||||||
id: launcherPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
SidePanel {
|
|
||||||
id: sidePanel
|
|
||||||
}
|
|
||||||
|
|
||||||
Calendar {
|
|
||||||
id: calendarPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsPanel {
|
|
||||||
id: settingsPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
Notification {
|
Notification {
|
||||||
id: notification
|
id: notification
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationHistoryPanel {
|
|
||||||
id: notificationHistoryPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
LockScreen {
|
LockScreen {
|
||||||
id: lockScreen
|
id: lockScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
PowerPanel {
|
|
||||||
id: powerPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiPanel {
|
|
||||||
id: wifiPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothPanel {
|
|
||||||
id: bluetoothPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
ToastManager {}
|
ToastManager {}
|
||||||
|
|
||||||
IPCManager {}
|
IPCManager {}
|
||||||
|
|
||||||
Component.onCompleted: {
|
// ------------------------------
|
||||||
// Save a ref. to our sidePanel so we can access it from services
|
// All the panels
|
||||||
PanelService.sidePanel = sidePanel
|
Launcher {
|
||||||
|
id: launcherPanel
|
||||||
|
objectName: "launcherPanel"
|
||||||
|
}
|
||||||
|
|
||||||
// Save a ref. to our lockScreen so we can access it from services
|
SidePanel {
|
||||||
|
id: sidePanel
|
||||||
|
objectName: "sidePanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar {
|
||||||
|
id: calendarPanel
|
||||||
|
objectName: "calendarPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsPanel {
|
||||||
|
id: settingsPanel
|
||||||
|
objectName: "settingsPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationHistoryPanel {
|
||||||
|
id: notificationHistoryPanel
|
||||||
|
objectName: "notificationHistoryPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerPanel {
|
||||||
|
id: powerPanel
|
||||||
|
objectName: "powerPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiPanel {
|
||||||
|
id: wifiPanel
|
||||||
|
objectName: "wifiPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothPanel {
|
||||||
|
id: bluetoothPanel
|
||||||
|
objectName: "bluetoothPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
ArchUpdaterPanel {
|
||||||
|
id: archUpdaterPanel
|
||||||
|
objectName: "archUpdaterPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Save a ref. to our lockScreen so we can access it easily
|
||||||
PanelService.lockScreen = lockScreen
|
PanelService.lockScreen = lockScreen
|
||||||
|
|
||||||
// Ensure our singleton is created as soon as possible so we start fetching weather asap
|
// Ensure our singleton is created as soon as possible so we start fetching weather asap
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue