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 Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
|
@ -116,9 +117,7 @@ Singleton {
|
|||
id: adapter
|
||||
|
||||
// bar
|
||||
property JsonObject bar
|
||||
|
||||
bar: JsonObject {
|
||||
property JsonObject bar: JsonObject {
|
||||
property string position: "top" // Possible values: "top", "bottom"
|
||||
property bool showActiveWindowIcon: true
|
||||
property bool alwaysShowBatteryPercentage: false
|
||||
|
|
@ -130,14 +129,12 @@ Singleton {
|
|||
widgets: JsonObject {
|
||||
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
|
||||
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
|
||||
property JsonObject general
|
||||
|
||||
general: JsonObject {
|
||||
property JsonObject general: JsonObject {
|
||||
property string avatarImage: defaultAvatar
|
||||
property bool dimDesktop: false
|
||||
property bool showScreenCorners: false
|
||||
|
|
@ -145,9 +142,7 @@ Singleton {
|
|||
}
|
||||
|
||||
// location
|
||||
property JsonObject location
|
||||
|
||||
location: JsonObject {
|
||||
property JsonObject location: JsonObject {
|
||||
property string name: "Tokyo"
|
||||
property bool useFahrenheit: false
|
||||
property bool reverseDayMonth: false
|
||||
|
|
@ -156,9 +151,7 @@ Singleton {
|
|||
}
|
||||
|
||||
// screen recorder
|
||||
property JsonObject screenRecorder
|
||||
|
||||
screenRecorder: JsonObject {
|
||||
property JsonObject screenRecorder: JsonObject {
|
||||
property string directory: "~/Videos"
|
||||
property int frameRate: 60
|
||||
property string audioCodec: "opus"
|
||||
|
|
@ -171,9 +164,7 @@ Singleton {
|
|||
}
|
||||
|
||||
// wallpaper
|
||||
property JsonObject wallpaper
|
||||
|
||||
wallpaper: JsonObject {
|
||||
property JsonObject wallpaper: JsonObject {
|
||||
property string directory: "/usr/share/wallpapers"
|
||||
property string current: ""
|
||||
property bool isRandom: false
|
||||
|
|
@ -194,9 +185,7 @@ Singleton {
|
|||
}
|
||||
|
||||
// applauncher
|
||||
property JsonObject appLauncher
|
||||
|
||||
appLauncher: JsonObject {
|
||||
property JsonObject appLauncher: JsonObject {
|
||||
// When disabled, Launcher hides clipboard command and ignores cliphist
|
||||
property bool enableClipboardHistory: true
|
||||
// Position: center, top_left, top_right, bottom_left, bottom_right
|
||||
|
|
@ -205,43 +194,34 @@ Singleton {
|
|||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock
|
||||
|
||||
dock: JsonObject {
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
// network
|
||||
property JsonObject network
|
||||
|
||||
network: JsonObject {
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool bluetoothEnabled: true
|
||||
}
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications
|
||||
|
||||
notifications: JsonObject {
|
||||
property JsonObject notifications: JsonObject {
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
// audio
|
||||
property JsonObject audio
|
||||
|
||||
audio: JsonObject {
|
||||
property JsonObject audio: JsonObject {
|
||||
property bool showMiniplayerAlbumArt: false
|
||||
property bool showMiniplayerCava: false
|
||||
property string visualizerType: "linear"
|
||||
property int volumeStep: 5
|
||||
property int cavaFrameRate: 60
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui
|
||||
|
||||
ui: JsonObject {
|
||||
property JsonObject ui: JsonObject {
|
||||
property string fontDefault: "Roboto" // Default font for all text
|
||||
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
|
||||
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
|
||||
|
|
@ -259,15 +239,11 @@ Singleton {
|
|||
}
|
||||
|
||||
// brightness
|
||||
property JsonObject brightness
|
||||
|
||||
brightness: JsonObject {
|
||||
property JsonObject brightness: JsonObject {
|
||||
property int brightnessStep: 5
|
||||
}
|
||||
|
||||
property JsonObject colorSchemes
|
||||
|
||||
colorSchemes: JsonObject {
|
||||
property JsonObject colorSchemes: JsonObject {
|
||||
property bool useWallpaperColors: false
|
||||
property string predefinedScheme: ""
|
||||
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 Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
|
@ -47,6 +48,7 @@ Variants {
|
|||
layer.enabled: true
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Left Section - Dynamic Widgets
|
||||
Row {
|
||||
id: leftSection
|
||||
|
|
@ -60,21 +62,19 @@ Variants {
|
|||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: Loader {
|
||||
id: leftWidgetLoader
|
||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
||||
active: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
||||
} else if (status === Loader.Ready) {
|
||||
widgetLoader.onWidgetLoaded(modelData)
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Center Section - Dynamic Widgets
|
||||
Row {
|
||||
id: centerSection
|
||||
|
|
@ -87,21 +87,19 @@ Variants {
|
|||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: Loader {
|
||||
id: centerWidgetLoader
|
||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
||||
active: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
||||
} else if (status === Loader.Ready) {
|
||||
widgetLoader.onWidgetLoaded(modelData)
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Right Section - Dynamic Widgets
|
||||
Row {
|
||||
id: rightSection
|
||||
|
|
@ -115,35 +113,17 @@ Variants {
|
|||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: Loader {
|
||||
id: rightWidgetLoader
|
||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
||||
active: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
||||
} else if (status === Loader.Ready) {
|
||||
widgetLoader.onWidgetLoaded(modelData)
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property bool showingFullTitle: false
|
||||
property int lastWindowIndex: -1
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: getTitle() !== ""
|
||||
|
||||
property bool showingFullTitle: false
|
||||
property int lastWindowIndex: -1
|
||||
|
||||
// Timer to hide full title after window switch
|
||||
Timer {
|
||||
id: fullTitleTimer
|
||||
interval: Style.animationSlow * 4 // Show full title for 2 seconds
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
showingFullTitle = false
|
||||
|
|
@ -40,8 +43,9 @@ Row {
|
|||
}
|
||||
|
||||
function getTitle() {
|
||||
const focusedWindow = CompositorService.getFocusedWindow()
|
||||
return focusedWindow ? (focusedWindow.title || focusedWindow.appId || "") : ""
|
||||
// Use the service's focusedWindowTitle property which is updated immediately
|
||||
// when WindowOpenedOrChanged events are received
|
||||
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||
}
|
||||
|
||||
function getAppIcon() {
|
||||
|
|
@ -62,6 +66,7 @@ Row {
|
|||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
visible: root.visible
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
|
|
@ -100,10 +105,10 @@ Row {
|
|||
NText {
|
||||
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
|
||||
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||
300 * scaling) : Math.min(
|
||||
400 * scaling) : Math.min(
|
||||
fullTitleMetrics.contentWidth, 150 * scaling)
|
||||
text: getTitle()
|
||||
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,89 +6,100 @@ import qs.Commons
|
|||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NPill {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Test mode
|
||||
property bool testMode: false
|
||||
property int testPercent: 49
|
||||
property bool testCharging: false
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
property var battery: UPower.displayDevice
|
||||
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
property bool show: isReady && percent > 0
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
// Choose icon based on charge and charging state
|
||||
function batteryIcon() {
|
||||
NPill {
|
||||
id: pill
|
||||
|
||||
if (charging)
|
||||
return "battery_android_bolt"
|
||||
// Test mode
|
||||
property bool testMode: false
|
||||
property int testPercent: 49
|
||||
property bool testCharging: false
|
||||
|
||||
if (percent >= 95)
|
||||
return "battery_android_full"
|
||||
property var battery: UPower.displayDevice
|
||||
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
|
||||
// Hardcoded battery symbols
|
||||
if (percent >= 85)
|
||||
return "battery_android_6"
|
||||
if (percent >= 70)
|
||||
return "battery_android_5"
|
||||
if (percent >= 55)
|
||||
return "battery_android_4"
|
||||
if (percent >= 40)
|
||||
return "battery_android_3"
|
||||
if (percent >= 25)
|
||||
return "battery_android_2"
|
||||
if (percent >= 10)
|
||||
return "battery_android_1"
|
||||
if (percent >= 0)
|
||||
return "battery_android_0"
|
||||
}
|
||||
// Choose icon based on charge and charging state
|
||||
function batteryIcon() {
|
||||
|
||||
visible: testMode || (isReady && battery.isLaptopBattery)
|
||||
if (!isReady || !battery.isLaptopBattery)
|
||||
return "battery_android_alert"
|
||||
|
||||
icon: root.batteryIcon()
|
||||
text: Math.round(root.percent) + "%"
|
||||
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
||||
forceShown: Settings.data.bar.alwaysShowBatteryPercentage
|
||||
tooltipText: {
|
||||
let lines = []
|
||||
if (charging)
|
||||
return "battery_android_bolt"
|
||||
|
||||
if (testMode) {
|
||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
|
||||
if (percent >= 95)
|
||||
return "battery_android_full"
|
||||
|
||||
// Hardcoded battery symbols
|
||||
if (percent >= 85)
|
||||
return "battery_android_6"
|
||||
if (percent >= 70)
|
||||
return "battery_android_5"
|
||||
if (percent >= 55)
|
||||
return "battery_android_4"
|
||||
if (percent >= 40)
|
||||
return "battery_android_3"
|
||||
if (percent >= 25)
|
||||
return "battery_android_2"
|
||||
if (percent >= 10)
|
||||
return "battery_android_1"
|
||||
if (percent >= 0)
|
||||
return "battery_android_0"
|
||||
}
|
||||
|
||||
icon: batteryIcon()
|
||||
text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-"
|
||||
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
||||
forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage
|
||||
disableOpen: (!isReady || !battery.isLaptopBattery)
|
||||
tooltipText: {
|
||||
let lines = []
|
||||
|
||||
if (testMode) {
|
||||
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345))
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
if (!isReady || !battery.isLaptopBattery) {
|
||||
return "No Battery Detected"
|
||||
}
|
||||
|
||||
if (battery.timeToEmpty > 0) {
|
||||
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
|
||||
}
|
||||
|
||||
if (battery.timeToFull > 0) {
|
||||
lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
|
||||
}
|
||||
|
||||
if (battery.changeRate !== undefined) {
|
||||
const rate = battery.changeRate
|
||||
if (rate > 0) {
|
||||
lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed(
|
||||
2) + " W")
|
||||
} else if (rate < 0) {
|
||||
lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W")
|
||||
} else {
|
||||
lines.push("Estimating...")
|
||||
}
|
||||
} else {
|
||||
lines.push(charging ? "Charging" : "Discharging")
|
||||
}
|
||||
|
||||
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
|
||||
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
if (!root.isReady) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (root.battery.timeToEmpty > 0) {
|
||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(root.battery.timeToEmpty))
|
||||
}
|
||||
|
||||
if (root.battery.timeToFull > 0) {
|
||||
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(root.battery.timeToFull))
|
||||
}
|
||||
|
||||
if (root.battery.changeRate !== undefined) {
|
||||
const rate = root.battery.changeRate
|
||||
if (rate > 0) {
|
||||
lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
|
||||
2) + " W")
|
||||
} else if (rate < 0) {
|
||||
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W")
|
||||
} else {
|
||||
lines.push("Estimating...")
|
||||
}
|
||||
} else {
|
||||
lines.push(root.charging ? "Charging" : "Discharging")
|
||||
}
|
||||
|
||||
if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) {
|
||||
lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%")
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ import qs.Widgets
|
|||
NIconButton {
|
||||
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
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
|
|
@ -28,7 +31,5 @@ NIconButton {
|
|||
}
|
||||
}
|
||||
tooltipText: "Bluetooth Devices"
|
||||
onClicked: {
|
||||
bluetoothPanel.toggle(screen)
|
||||
}
|
||||
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,16 @@ import qs.Widgets
|
|||
Item {
|
||||
id: root
|
||||
|
||||
width: pill.width
|
||||
height: pill.height
|
||||
visible: getMonitor() !== null
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstBrightnessReceived: false
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
visible: getMonitor() !== null
|
||||
|
||||
function getMonitor() {
|
||||
return BrightnessService.getMonitorForScreen(screen) || null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Clock Icon with attached calendar
|
||||
Rectangle {
|
||||
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)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
// Clock Icon with attached calendar
|
||||
NClock {
|
||||
id: clock
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
|
@ -24,7 +29,7 @@ Rectangle {
|
|||
}
|
||||
|
||||
onEntered: {
|
||||
if (!calendarPanel.active) {
|
||||
if (!PanelService.getPanel("calendarPanel")?.active) {
|
||||
tooltip.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +38,7 @@ Rectangle {
|
|||
}
|
||||
onClicked: {
|
||||
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 {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: MediaService.currentPlayer !== null
|
||||
width: MediaService.currentPlayer !== null ? implicitWidth : 0
|
||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
||||
|
||||
function getTitle() {
|
||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||
|
|
@ -144,7 +148,7 @@ Row {
|
|||
NText {
|
||||
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
|
||||
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import qs.Widgets
|
|||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
icon: "notifications"
|
||||
tooltipText: "Notification History"
|
||||
|
|
@ -17,8 +20,5 @@ NIconButton {
|
|||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
onClicked: {
|
||||
notificationHistoryPanel.toggle(screen)
|
||||
}
|
||||
onClicked: PanelService.getPanel("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.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Screen Recording Indicator
|
||||
NIconButton {
|
||||
id: screenRecordingIndicator
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
visible: ScreenRecorderService.isRecording
|
||||
icon: "videocam"
|
||||
tooltipText: "Screen Recording Active\nClick To Stop Recording"
|
||||
sizeMultiplier: 0.8
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
visible: ScreenRecorderService.isRecording
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
onClicked: ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
NIconButton {
|
||||
id: sidePanelToggle
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
icon: "widgets"
|
||||
tooltipText: "Open Side Panel"
|
||||
sizeMultiplier: 0.8
|
||||
|
|
@ -14,5 +19,5 @@ NIconButton {
|
|||
colorBorderHover: Color.transparent
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: sidePanel.toggle(screen)
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import qs.Widgets
|
|||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,20 @@ import Quickshell
|
|||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
readonly property real itemSize: 24 * scaling
|
||||
|
||||
visible: SystemTray.items.values.length > 0
|
||||
width: tray.width + Style.marginM * scaling * 2
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
implicitWidth: tray.width + Style.marginM * scaling * 2
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
|
|
@ -134,9 +139,7 @@ Rectangle {
|
|||
function open() {
|
||||
visible = true
|
||||
|
||||
// Register into the panel service
|
||||
// so this will autoclose if we open another panel
|
||||
PanelService.registerOpen(trayPanel)
|
||||
PanelService.willOpenPanel(trayPanel)
|
||||
}
|
||||
|
||||
function close() {
|
||||
|
|
@ -152,7 +155,7 @@ Rectangle {
|
|||
|
||||
Loader {
|
||||
id: trayMenu
|
||||
source: "TrayMenu.qml"
|
||||
source: "../Extras/TrayMenu.qml"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,15 @@ import qs.Widgets
|
|||
Item {
|
||||
id: root
|
||||
|
||||
width: pill.width
|
||||
height: pill.height
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstVolumeReceived: false
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
function getIcon() {
|
||||
if (AudioService.muted) {
|
||||
return "volume_off"
|
||||
|
|
@ -64,6 +67,7 @@ Item {
|
|||
}
|
||||
}
|
||||
onClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
||||
settingsPanel.open(screen)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ import qs.Widgets
|
|||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
visible: Settings.data.network.wifiEnabled
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
|
||||
Component.onCompleted: {
|
||||
|
|
@ -27,6 +32,8 @@ NIconButton {
|
|||
|
||||
icon: {
|
||||
try {
|
||||
if (NetworkService.ethernet)
|
||||
return "lan"
|
||||
let connected = false
|
||||
let signalStrength = 0
|
||||
for (const net in NetworkService.networks) {
|
||||
|
|
@ -36,17 +43,17 @@ NIconButton {
|
|||
break
|
||||
}
|
||||
}
|
||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi"
|
||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||
} catch (error) {
|
||||
Logger.error("WiFi", "Error getting icon:", error)
|
||||
return "wifi"
|
||||
return "signal_wifi_bad"
|
||||
}
|
||||
}
|
||||
tooltipText: "WiFi Networks"
|
||||
tooltipText: "Network / WiFi"
|
||||
onClicked: {
|
||||
try {
|
||||
Logger.log("WiFi", "Button clicked, toggling panel")
|
||||
wifiPanel.toggle(screen)
|
||||
PanelService.getPanel("wifiPanel")?.toggle(screen)
|
||||
} catch (error) {
|
||||
Logger.error("WiFi", "Error toggling panel:", error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import qs.Services
|
|||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen: null
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
property bool isDestroying: false
|
||||
property bool hovered: false
|
||||
|
||||
|
|
@ -23,7 +27,8 @@ Item {
|
|||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
width: {
|
||||
implicitHeight: Math.round(36 * scaling)
|
||||
implicitWidth: {
|
||||
let total = 0
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i)
|
||||
|
|
@ -39,34 +44,35 @@ Item {
|
|||
return total
|
||||
}
|
||||
|
||||
height: Math.round(36 * scaling)
|
||||
|
||||
Component.onCompleted: {
|
||||
localWorkspaces.clear()
|
||||
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)
|
||||
}
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces
|
||||
updateWorkspaceFocus()
|
||||
refreshWorkspaces()
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
root.isDestroying = true
|
||||
}
|
||||
|
||||
onScreenChanged: refreshWorkspaces()
|
||||
|
||||
Connections {
|
||||
target: WorkspaceService
|
||||
function onWorkspacesChanged() {
|
||||
localWorkspaces.clear()
|
||||
refreshWorkspaces()
|
||||
}
|
||||
}
|
||||
|
||||
function refreshWorkspaces() {
|
||||
localWorkspaces.clear()
|
||||
if (screen !== null) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
workspaceRepeater.model = localWorkspaces
|
||||
updateWorkspaceFocus()
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces
|
||||
updateWorkspaceFocus()
|
||||
}
|
||||
|
||||
function triggerUnifiedWave() {
|
||||
|
|
@ -74,6 +80,17 @@ Item {
|
|||
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 {
|
||||
id: masterAnimation
|
||||
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 {
|
||||
id: workspaceBackground
|
||||
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()
|
||||
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 {
|
||||
width: (Style.baseWidgetSize * scaling)
|
||||
height: (Style.baseWidgetSize * scaling)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ Item {
|
|||
|
||||
IpcHandler {
|
||||
target: "settings"
|
||||
|
||||
function toggle() {
|
||||
settingsPanel.toggle(Quickshell.screens[0])
|
||||
}
|
||||
|
|
@ -16,43 +15,64 @@ Item {
|
|||
|
||||
IpcHandler {
|
||||
target: "notifications"
|
||||
|
||||
function toggleHistory() {
|
||||
notificationHistoryPanel.toggle(Quickshell.screens[0])
|
||||
}
|
||||
|
||||
function toggleDoNotDisturb() {// TODO
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "idleInhibitor"
|
||||
|
||||
function toggle() {
|
||||
return IdleInhibitorService.manualToggle()
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility, should be removed soon(tmc)
|
||||
IpcHandler {
|
||||
target: "appLauncher"
|
||||
|
||||
function toggle() {
|
||||
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 {
|
||||
target: "launcher"
|
||||
|
||||
function toggle() {
|
||||
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 {
|
||||
target: "lockScreen"
|
||||
|
||||
function toggle() {
|
||||
// Only lock if not already locked (prevents the red screen issue)
|
||||
// Note: No unlock via IPC for security reasons
|
||||
|
|
@ -64,13 +84,25 @@ Item {
|
|||
|
||||
IpcHandler {
|
||||
target: "brightness"
|
||||
|
||||
function increase() {
|
||||
BrightnessService.increaseBrightness()
|
||||
}
|
||||
|
||||
function decrease() {
|
||||
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 {
|
||||
// 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
|
||||
var processed = expression.trim(
|
||||
|
|
|
|||
|
|
@ -24,10 +24,30 @@ NPanel {
|
|||
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
|
||||
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: {
|
||||
// Reset state when panel opens to avoid sticky modes
|
||||
if (searchText === "") {
|
||||
searchText = ""
|
||||
selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
// Reset search bar when launcher is closed
|
||||
searchText = ""
|
||||
selectedIndex = 0
|
||||
shouldResetCursor = true
|
||||
}
|
||||
|
||||
// Import modular components
|
||||
|
|
@ -50,7 +70,6 @@ NPanel {
|
|||
|
||||
// Properties
|
||||
property var desktopEntries: DesktopEntries.applications.values
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
|
||||
// Refresh clipboard when user starts typing clipboard commands
|
||||
|
|
@ -141,15 +160,11 @@ NPanel {
|
|||
|
||||
// Command execution functions
|
||||
function executeCalcCommand() {
|
||||
searchText = ">calc "
|
||||
searchInput.text = searchText
|
||||
searchInput.cursorPosition = searchText.length
|
||||
setSearchText(">calc ")
|
||||
}
|
||||
|
||||
function executeClipCommand() {
|
||||
searchText = ">clip "
|
||||
searchInput.text = searchText
|
||||
searchInput.cursorPosition = searchText.length
|
||||
setSearchText(">clip ")
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
|
|
@ -252,10 +267,20 @@ NPanel {
|
|||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: searchText
|
||||
onTextChanged: {
|
||||
searchText = text
|
||||
// Update the parent searchText property
|
||||
if (searchText !== text) {
|
||||
searchText = text
|
||||
}
|
||||
// Defer selectedIndex reset to avoid binding loops
|
||||
Qt.callLater(() => selectedIndex = 0)
|
||||
|
||||
// Reset cursor position if needed
|
||||
if (shouldResetCursor && text === "") {
|
||||
cursorPosition = 0
|
||||
shouldResetCursor = false
|
||||
}
|
||||
}
|
||||
selectedTextColor: Color.mOnSurface
|
||||
selectionColor: Color.mPrimary
|
||||
|
|
@ -266,10 +291,14 @@ NPanel {
|
|||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
Component.onCompleted: {
|
||||
// Focus the search bar by default
|
||||
// Focus the search bar by default and set cursor position
|
||||
Qt.callLater(() => {
|
||||
selectedIndex = 0
|
||||
searchInput.forceActiveFocus()
|
||||
// Set cursor to end if there's already text
|
||||
if (searchText && searchText.length > 0) {
|
||||
searchInput.cursorPosition = searchText.length
|
||||
}
|
||||
})
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -308,6 +308,45 @@ ColumnLayout {
|
|||
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
|
||||
Layout.fillWidth: true
|
||||
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -72,7 +71,7 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
|
|
@ -111,7 +110,6 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
NToggle {
|
||||
label: "Show Active Window's Icon"
|
||||
description: "Display the app icon next to the title of the currently focused window."
|
||||
|
|
@ -130,7 +128,6 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginL * scaling
|
||||
|
|
@ -144,13 +141,14 @@ ColumnLayout {
|
|||
|
||||
NText {
|
||||
text: "Widgets Positioning"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.bottomMargin: Style.marginS * scaling
|
||||
}
|
||||
|
||||
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
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
|
|
@ -165,7 +163,7 @@ ColumnLayout {
|
|||
spacing: Style.marginM * scaling
|
||||
|
||||
// Left Section
|
||||
NWidgetCard {
|
||||
NSectionEditor {
|
||||
sectionName: "Left"
|
||||
widgetModel: Settings.data.bar.widgets.left
|
||||
availableWidgets: availableWidgets
|
||||
|
|
@ -176,7 +174,7 @@ ColumnLayout {
|
|||
}
|
||||
|
||||
// Center Section
|
||||
NWidgetCard {
|
||||
NSectionEditor {
|
||||
sectionName: "Center"
|
||||
widgetModel: Settings.data.bar.widgets.center
|
||||
availableWidgets: availableWidgets
|
||||
|
|
@ -187,7 +185,7 @@ ColumnLayout {
|
|||
}
|
||||
|
||||
// Right Section
|
||||
NWidgetCard {
|
||||
NSectionEditor {
|
||||
sectionName: "Right"
|
||||
widgetModel: Settings.data.bar.widgets.right
|
||||
availableWidgets: availableWidgets
|
||||
|
|
@ -204,13 +202,13 @@ ColumnLayout {
|
|||
|
||||
// Helper functions
|
||||
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]
|
||||
if (sectionArray) {
|
||||
// Create a new array to avoid modifying the original
|
||||
var newArray = sectionArray.slice()
|
||||
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
|
||||
Settings.data.bar.widgets[section] = newArray
|
||||
|
|
@ -218,21 +216,27 @@ ColumnLayout {
|
|||
}
|
||||
|
||||
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]
|
||||
|
||||
//Logger.log("BarTab", "Current section array:", JSON.stringify(sectionArray))
|
||||
if (sectionArray && index >= 0 && index < sectionArray.length) {
|
||||
// Create a new array to avoid modifying the original
|
||||
var newArray = sectionArray.slice()
|
||||
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
|
||||
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) {
|
||||
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]
|
||||
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0
|
||||
&& toIndex < sectionArray.length) {
|
||||
|
|
@ -242,36 +246,26 @@ ColumnLayout {
|
|||
var item = newArray[fromIndex]
|
||||
newArray.splice(fromIndex, 1)
|
||||
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
|
||||
Settings.data.bar.widgets[section] = newArray
|
||||
}
|
||||
}
|
||||
|
||||
// Widget loader for discovering available widgets
|
||||
WidgetLoader {
|
||||
id: widgetLoader
|
||||
}
|
||||
|
||||
// Base list model for all combo boxes
|
||||
ListModel {
|
||||
id: availableWidgets
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
discoverWidgets()
|
||||
}
|
||||
|
||||
// Automatically discover available widgets using WidgetLoader
|
||||
function discoverWidgets() {
|
||||
// Fill out availableWidgets ListModel
|
||||
availableWidgets.clear()
|
||||
|
||||
// Use WidgetLoader to discover available widgets
|
||||
const discoveredWidgets = widgetLoader.discoverAvailableWidgets()
|
||||
|
||||
// Add discovered widgets to the ListModel
|
||||
discoveredWidgets.forEach(widget => {
|
||||
availableWidgets.append(widget)
|
||||
})
|
||||
BarWidgetRegistry.getAvailableWidgets().forEach(entry => {
|
||||
availableWidgets.append({
|
||||
"key": entry,
|
||||
"name": entry
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,10 +125,22 @@ ColumnLayout {
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -160,8 +160,6 @@ NBox {
|
|||
height: 90 * scaling
|
||||
radius: width * 0.5
|
||||
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
|
||||
|
||||
NImageCircled {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ NBox {
|
|||
icon: "image"
|
||||
tooltipText: "Open Wallpaper Selector"
|
||||
onClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
||||
settingsPanel.open(screen)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,10 @@ NBox {
|
|||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginS * scaling
|
||||
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
|
||||
}
|
||||
NIcon {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ Variants {
|
|||
readonly property real scaling: ScalingService.scale(screen)
|
||||
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
|
||||
anchors {
|
||||
top: Settings.data.bar.position === "top"
|
||||
|
|
@ -51,11 +55,15 @@ Variants {
|
|||
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
|
||||
|
||||
Component.onCompleted: {
|
||||
// Register this toast with the service
|
||||
ToastService.currentToast = toast
|
||||
// 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
|
||||
ToastService.allToasts.push(toast)
|
||||
|
||||
// Connect dismissal signal
|
||||
toast.dismissed.connect(ToastService.onToastDismissed)
|
||||
// Connect dismissal signal
|
||||
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
|
||||
- `brightnessctl` - For internal/laptop monitor brightness
|
||||
- `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
|
||||
qs
|
||||
|
||||
# Toggle launcher
|
||||
qs ipc call appLauncher toggle
|
||||
# Launcher
|
||||
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
|
||||
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 💜!
|
||||
* 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: ({
|
||||
"general": {
|
||||
"bars": barsCount,
|
||||
"framerate": 60,
|
||||
"framerate": Settings.data.audio.cavaFrameRate,
|
||||
"autosens": 1,
|
||||
"sensitivity": 100,
|
||||
"lower_cutoff_freq": 50,
|
||||
|
|
@ -38,7 +38,7 @@ Singleton {
|
|||
id: process
|
||||
stdinEnabled: true
|
||||
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))
|
||||
command: ["cava", "-p", "/dev/stdin"]
|
||||
onExited: {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Singleton {
|
|||
property ListModel workspaces: ListModel {}
|
||||
property var windows: []
|
||||
property int focusedWindowIndex: -1
|
||||
property string focusedWindowTitle: "(No active window)"
|
||||
property string focusedWindowTitle: "n/a"
|
||||
property bool inOverview: false
|
||||
|
||||
// Generic events
|
||||
|
|
@ -27,6 +27,7 @@ Singleton {
|
|||
signal activeWindowChanged
|
||||
signal overviewStateChanged
|
||||
signal windowListChanged
|
||||
signal windowTitleChanged
|
||||
|
||||
// Compositor detection
|
||||
Component.onCompleted: {
|
||||
|
|
@ -308,9 +309,18 @@ Singleton {
|
|||
|
||||
// Update focused window index if this window is focused
|
||||
if (newWindow.isFocused) {
|
||||
const oldFocusedIndex = focusedWindowIndex
|
||||
focusedWindowIndex = windows.findIndex(w => w.id === windowData.id)
|
||||
updateFocusedWindowTitle()
|
||||
activeWindowChanged()
|
||||
|
||||
// Only emit activeWindowChanged if the focused window actually changed
|
||||
if (oldFocusedIndex !== focusedWindowIndex) {
|
||||
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()
|
||||
|
|
@ -449,11 +459,17 @@ Singleton {
|
|||
}
|
||||
|
||||
function updateFocusedWindowTitle() {
|
||||
const oldTitle = focusedWindowTitle
|
||||
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
||||
focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)"
|
||||
} else {
|
||||
focusedWindowTitle = "(No active window)"
|
||||
}
|
||||
|
||||
// Emit signal if title actually changed
|
||||
if (oldTitle !== focusedWindowTitle) {
|
||||
windowTitleChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// Generic workspace switching
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
pragma Singleton
|
||||
|
||||
// GitHub API logic and caching
|
||||
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 Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
pragma Singleton
|
||||
|
||||
// Weather logic and caching
|
||||
Singleton {
|
||||
|
|
@ -109,8 +110,8 @@ Singleton {
|
|||
|
||||
// --------------------------------
|
||||
function _geocodeLocation(locationName, callback, errorCallback) {
|
||||
Logger.log("Location", "Geocoding from api.open-meteo.com")
|
||||
var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(
|
||||
Logger.log("Location", "Geocoding location name")
|
||||
var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(
|
||||
locationName) + "&language=en&format=json"
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.onreadystatechange = function () {
|
||||
|
|
@ -119,8 +120,8 @@ Singleton {
|
|||
try {
|
||||
var geoData = JSON.parse(xhr.responseText)
|
||||
// Logger.logJSON.stringify(geoData))
|
||||
if (geoData.results && geoData.results.length > 0) {
|
||||
callback(geoData.results[0].latitude, geoData.results[0].longitude)
|
||||
if (geoData.lat != null) {
|
||||
callback(geoData.lat, geoData.lng)
|
||||
} else {
|
||||
errorCallback("Location", "could not resolve location name")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Singleton {
|
|||
property string detectedInterface: ""
|
||||
property string lastConnectedNetwork: ""
|
||||
property bool isLoading: false
|
||||
property bool ethernet: false
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Network", "Service started")
|
||||
|
|
@ -43,6 +44,7 @@ Singleton {
|
|||
|
||||
function refreshNetworks() {
|
||||
isLoading = true
|
||||
checkEthernet.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 {
|
||||
id: addConnectionProcess
|
||||
property string ifname: ""
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import Quickshell.Services.Notifications
|
||||
pragma Singleton
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
|
|
|||
|
|
@ -1,20 +1,38 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// A ref. to the sidePanel, so it's accessible from other services
|
||||
property var sidePanel: null
|
||||
|
||||
// A ref. to the lockScreen, so it's accessible from other services
|
||||
// A ref. to the lockScreen, so it's accessible from anywhere
|
||||
// This is not a panel...
|
||||
property var lockScreen: null
|
||||
|
||||
// Currently opened panel
|
||||
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) {
|
||||
openedPanel.close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,22 @@ Singleton {
|
|||
videoDir += "/"
|
||||
}
|
||||
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)
|
||||
Quickshell.execDetached(["sh", "-c", command])
|
||||
Logger.log("ScreenRecorder", "Started recording")
|
||||
//Logger.log("ScreenRecorder", command)
|
||||
}
|
||||
|
||||
// Stop recording using Quickshell.execDetached
|
||||
|
|
|
|||
|
|
@ -1,18 +1,77 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Queue of pending toast messages
|
||||
property var messageQueue: []
|
||||
property bool isShowingToast: false
|
||||
|
||||
// Reference to the current toast instance (set by ToastManager)
|
||||
property var currentToast: null
|
||||
// Reference to all toast instances (set by ToastManager)
|
||||
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
|
||||
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
|
||||
function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) {
|
||||
var checkProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
id: checkProc
|
||||
command: ["which", "${command}"]
|
||||
running: true
|
||||
// Store callback for use in the process
|
||||
commandCheckCallback = onSuccess
|
||||
commandCheckSuccessMessage = successMessage
|
||||
commandCheckFailMessage = failMessage
|
||||
|
||||
property var onSuccessCallback: null
|
||||
property bool hasFinished: false
|
||||
|
||||
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
|
||||
// Start the command check process
|
||||
commandCheckProcess.command = ["which", command]
|
||||
commandCheckProcess.running = true
|
||||
}
|
||||
|
||||
// Simple function to show a random toast (useful for testing or fun messages)
|
||||
|
|
@ -95,37 +131,14 @@ QtObject {
|
|||
|
||||
// Generic command runner with toast feedback
|
||||
function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) {
|
||||
var fullCommand = [command].concat(args || [])
|
||||
var runProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
id: runProc
|
||||
command: ${JSON.stringify(fullCommand)}
|
||||
running: true
|
||||
// Store callback for use in the process
|
||||
commandRunCallback = onSuccess
|
||||
commandRunSuccessMessage = successMessage
|
||||
commandRunFailMessage = failMessage
|
||||
|
||||
property var onSuccessCallback: null
|
||||
property bool hasFinished: false
|
||||
|
||||
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
|
||||
// Start the command run process
|
||||
commandRunProcess.command = [command].concat(args || [])
|
||||
commandRunProcess.running = true
|
||||
}
|
||||
|
||||
// Check if a file/directory exists
|
||||
|
|
@ -135,18 +148,10 @@ QtObject {
|
|||
|
||||
// Show toast after a delay (useful for delayed feedback)
|
||||
function delayedToast(message, type = "notice", delayMs = 1000) {
|
||||
var timer = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
Timer {
|
||||
interval: ${delayMs}
|
||||
repeat: false
|
||||
running: true
|
||||
onTriggered: {
|
||||
ToastService.showToast("${message}", "${type}")
|
||||
destroy()
|
||||
}
|
||||
}
|
||||
`, root)
|
||||
delayedToastMessage = message
|
||||
delayedToastType = type
|
||||
delayedToastTimer.interval = delayMs
|
||||
delayedToastTimer.restart()
|
||||
}
|
||||
|
||||
// Generic method to show a toast
|
||||
|
|
@ -171,7 +176,7 @@ QtObject {
|
|||
|
||||
// Process the message queue
|
||||
function processQueue() {
|
||||
if (messageQueue.length === 0 || !currentToast) {
|
||||
if (messageQueue.length === 0 || allToasts.length === 0) {
|
||||
isShowingToast = false
|
||||
return
|
||||
}
|
||||
|
|
@ -184,24 +189,37 @@ QtObject {
|
|||
var toastData = messageQueue.shift()
|
||||
isShowingToast = true
|
||||
|
||||
// Configure and show toast
|
||||
currentToast.label = toastData.label
|
||||
currentToast.description = toastData.description
|
||||
currentToast.type = toastData.type
|
||||
currentToast.persistent = toastData.persistent
|
||||
currentToast.duration = toastData.duration
|
||||
currentToast.show()
|
||||
// Configure and show toast on all screens
|
||||
for (var i = 0; i < allToasts.length; i++) {
|
||||
var toast = allToasts[i]
|
||||
toast.label = toastData.label
|
||||
toast.description = toastData.description
|
||||
toast.type = toastData.type
|
||||
toast.persistent = toastData.persistent
|
||||
toast.duration = toastData.duration
|
||||
toast.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a toast is dismissed
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
isShowingToast = false
|
||||
if (allDismissed) {
|
||||
isShowingToast = false
|
||||
|
||||
// Small delay before showing next toast
|
||||
Qt.callLater(function () {
|
||||
processQueue()
|
||||
})
|
||||
// Small delay before showing next toast
|
||||
Qt.callLater(function () {
|
||||
processQueue()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all pending messages
|
||||
|
|
@ -212,8 +230,10 @@ QtObject {
|
|||
|
||||
// Hide current toast
|
||||
function hideCurrentToast() {
|
||||
if (currentToast && isShowingToast) {
|
||||
currentToast.hide()
|
||||
if (isShowingToast) {
|
||||
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 {
|
||||
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 description: ""
|
||||
property ListModel model: {
|
||||
|
||||
}
|
||||
property string currentKey: ''
|
||||
property string currentKey: ""
|
||||
property string placeholder: ""
|
||||
|
||||
signal selected(string key)
|
||||
|
|
@ -39,7 +39,8 @@ ColumnLayout {
|
|||
|
||||
ComboBox {
|
||||
id: combo
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.preferredWidth: 320 * scaling
|
||||
Layout.preferredHeight: height
|
||||
model: model
|
||||
currentIndex: findIndexByKey(currentKey)
|
||||
|
|
@ -128,5 +129,13 @@ ColumnLayout {
|
|||
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 closed
|
||||
|
||||
Component.onCompleted: {
|
||||
// console.log("Oh Yeah")
|
||||
// console.log(objectName)
|
||||
PanelService.registerPanel(root)
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
function toggle(aScreen) {
|
||||
if (!active || isClosing) {
|
||||
|
|
@ -53,7 +59,7 @@ Loader {
|
|||
opacityValue = 1.0
|
||||
}
|
||||
|
||||
PanelService.registerOpen(root)
|
||||
PanelService.willOpenPanel(root)
|
||||
|
||||
active = true
|
||||
root.opened()
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ Item {
|
|||
property color collapsedIconColor: Color.mOnSurface
|
||||
property real sizeMultiplier: 0.8
|
||||
property bool autoHide: false
|
||||
// When true, keep the pill expanded regardless of hover state
|
||||
property bool forceShown: false
|
||||
property bool forceOpen: false
|
||||
property bool disableOpen: false
|
||||
|
||||
// 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 hidden
|
||||
|
|
@ -85,7 +86,7 @@ Item {
|
|||
height: iconSize
|
||||
radius: width * 0.5
|
||||
// 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.right: parent.right
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ Item {
|
|||
text: root.icon
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -194,18 +195,21 @@ Item {
|
|||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
if (!forceShown) {
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed()
|
||||
}
|
||||
tooltip.show()
|
||||
root.entered()
|
||||
}
|
||||
onExited: {
|
||||
if (!forceShown) {
|
||||
root.exited()
|
||||
if (!forceOpen) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
root.exited()
|
||||
}
|
||||
onClicked: {
|
||||
root.clicked()
|
||||
|
|
@ -226,7 +230,7 @@ Item {
|
|||
}
|
||||
|
||||
function hide() {
|
||||
if (forceShown) {
|
||||
if (forceOpen) {
|
||||
return
|
||||
}
|
||||
if (showPill) {
|
||||
|
|
@ -245,8 +249,8 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
onForceShownChanged: {
|
||||
if (forceShown) {
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.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.Toast
|
||||
import qs.Modules.WiFiPanel
|
||||
import qs.Modules.ArchUpdaterPanel
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
|
|
@ -39,55 +40,67 @@ ShellRoot {
|
|||
Bar {}
|
||||
Dock {}
|
||||
|
||||
Launcher {
|
||||
id: launcherPanel
|
||||
}
|
||||
|
||||
SidePanel {
|
||||
id: sidePanel
|
||||
}
|
||||
|
||||
Calendar {
|
||||
id: calendarPanel
|
||||
}
|
||||
|
||||
SettingsPanel {
|
||||
id: settingsPanel
|
||||
}
|
||||
|
||||
Notification {
|
||||
id: notification
|
||||
}
|
||||
|
||||
NotificationHistoryPanel {
|
||||
id: notificationHistoryPanel
|
||||
}
|
||||
|
||||
LockScreen {
|
||||
id: lockScreen
|
||||
}
|
||||
|
||||
PowerPanel {
|
||||
id: powerPanel
|
||||
}
|
||||
|
||||
WiFiPanel {
|
||||
id: wifiPanel
|
||||
}
|
||||
|
||||
BluetoothPanel {
|
||||
id: bluetoothPanel
|
||||
}
|
||||
|
||||
ToastManager {}
|
||||
|
||||
IPCManager {}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Save a ref. to our sidePanel so we can access it from services
|
||||
PanelService.sidePanel = sidePanel
|
||||
// ------------------------------
|
||||
// All the panels
|
||||
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
|
||||
|
||||
// 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