Merge branch 'main' into never
This commit is contained in:
commit
b03f877c27
58 changed files with 2910 additions and 2147 deletions
|
|
@ -510,7 +510,7 @@
|
|||
}
|
||||
|
||||
.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before {
|
||||
content: "Message #general" !important;
|
||||
content: "send a message" !important;
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -278,12 +278,14 @@ Singleton {
|
|||
property string position: "center"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> pinnedExecs: []
|
||||
property bool useApp2Unit: false
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
|
|
@ -295,6 +297,7 @@ Singleton {
|
|||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool doNotDisturb: false
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ Singleton {
|
|||
property int fontWeightBold: 700
|
||||
|
||||
// Radii
|
||||
property int radiusXXS: 4 * Settings.data.general.radiusRatio
|
||||
property int radiusXS: 8 * Settings.data.general.radiusRatio
|
||||
property int radiusS: 12 * Settings.data.general.radiusRatio
|
||||
property int radiusM: 16 * Settings.data.general.radiusRatio
|
||||
|
|
|
|||
|
|
@ -78,23 +78,34 @@ Singleton {
|
|||
}
|
||||
|
||||
// Format an easy to read approximate duration ex: 4h32m
|
||||
// Used to display the time remaining on the Battery widget
|
||||
// Used to display the time remaining on the Battery widget, computer uptime, etc..
|
||||
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
|
||||
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
|
||||
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
|
||||
return '0s'
|
||||
}
|
||||
|
||||
var str = ""
|
||||
if (hours) {
|
||||
str += hours.toString() + "h"
|
||||
}
|
||||
if (minutes) {
|
||||
str += minutes.toString() + "m"
|
||||
}
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds)
|
||||
|
||||
const days = Math.floor(totalSeconds / 86400)
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
const parts = []
|
||||
if (days)
|
||||
parts.push(`${days}d`)
|
||||
if (hours)
|
||||
parts.push(`${hours}h`)
|
||||
if (minutes)
|
||||
parts.push(`${minutes}m`)
|
||||
|
||||
// Only show seconds if no hours and no minutes
|
||||
if (!hours && !minutes) {
|
||||
str += seconds.toString() + "s"
|
||||
parts.push(`${seconds}s`)
|
||||
}
|
||||
return str
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ Variants {
|
|||
transitionProgress = 0.0
|
||||
Qt.callLater(() => {
|
||||
currentWallpaper.asynchronous = true
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,38 +2,45 @@ import QtQuick
|
|||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
readonly property real minWidth: 160
|
||||
readonly property real maxWidth: 400
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: getTitle() !== ""
|
||||
|
||||
function getTitle() {
|
||||
// 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() {
|
||||
// Try CompositorService first
|
||||
const focusedWindow = CompositorService.getFocusedWindow()
|
||||
if (!focusedWindow || !focusedWindow.appId)
|
||||
return ""
|
||||
if (focusedWindow && focusedWindow.appId) {
|
||||
return Icons.iconForAppId(focusedWindow.appId.toLowerCase())
|
||||
}
|
||||
|
||||
return Icons.iconForAppId(focusedWindow.appId)
|
||||
// Fallback to ToplevelManager
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel) {
|
||||
const activeToplevel = ToplevelManager.activeToplevel
|
||||
if (activeToplevel.appId) {
|
||||
return Icons.iconForAppId(activeToplevel.appId.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
// A hidden text element to safely measure the full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
visible: false
|
||||
|
|
@ -43,15 +50,13 @@ Row {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
id: windowTitleRect
|
||||
visible: root.visible
|
||||
width: row.width + Style.marginM * 2 * scaling
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
|
|
@ -59,16 +64,16 @@ Row {
|
|||
anchors.rightMargin: Style.marginS * scaling
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
width: Style.fontSizeL * scaling * 1.2
|
||||
height: Style.fontSizeL * scaling * 1.2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
|
||||
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
|
||||
|
||||
IconImage {
|
||||
|
|
@ -83,26 +88,24 @@ Row {
|
|||
|
||||
NText {
|
||||
id: titleText
|
||||
|
||||
// For short titles, show full. For long titles, truncate and expand on hover
|
||||
width: {
|
||||
Layout.preferredWidth: {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mSecondary
|
||||
clip: true
|
||||
|
||||
Behavior on width {
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutCubic
|
||||
|
|
@ -120,4 +123,14 @@ Row {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onActiveWindowChanged() {
|
||||
windowIcon.source = Qt.binding(getAppIcon)
|
||||
}
|
||||
function onWindowListChanged() {
|
||||
windowIcon.source = Qt.binding(getAppIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Rectangle {
|
|||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: Time.dateString
|
||||
text: `${Time.dateString}.`
|
||||
target: clock
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ NIconButton {
|
|||
} else {
|
||||
var lines = []
|
||||
if (userLeftClickExec !== "") {
|
||||
lines.push(`Left click: <i>${userLeftClickExec}</i>`)
|
||||
lines.push(`Left click: <i>${userLeftClickExec}</i>.`)
|
||||
}
|
||||
if (userRightClickExec !== "") {
|
||||
lines.push(`Right click: <i>${userRightClickExec}</i>`)
|
||||
lines.push(`Right click: <i>${userRightClickExec}</i>.`)
|
||||
}
|
||||
if (userMiddleClickExec !== "") {
|
||||
lines.push(`Middle click: <i>${userMiddleClickExec}</i>`)
|
||||
lines.push(`Middle click: <i>${userMiddleClickExec}</i>.`)
|
||||
}
|
||||
return lines.join("<br/>")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
|
|
@ -6,7 +7,7 @@ import qs.Commons
|
|||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
|
@ -18,12 +19,13 @@ Row {
|
|||
// Use the shared service for keyboard layout
|
||||
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||
|
||||
width: pill.width
|
||||
height: pill.height
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||
icon: "keyboard_alt"
|
||||
iconCircleColor: Color.mPrimary
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import qs.Commons
|
|||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
|
@ -15,10 +15,10 @@ Row {
|
|||
readonly property real minWidth: 160
|
||||
readonly property real maxWidth: 400
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
||||
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
||||
|
||||
function getTitle() {
|
||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||
|
|
@ -35,15 +35,13 @@ Row {
|
|||
Rectangle {
|
||||
id: mediaMini
|
||||
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
width: row.width + Style.marginM * 2 * scaling
|
||||
Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
||||
Item {
|
||||
id: anchor
|
||||
|
|
@ -61,7 +59,7 @@ Row {
|
|||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
|
||||
&& MediaService.isPlaying && MediaService.trackLength > 0
|
||||
&& MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: LinearSpectrum {
|
||||
|
|
@ -71,42 +69,42 @@ Row {
|
|||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
|
||||
&& MediaService.isPlaying && MediaService.trackLength > 0
|
||||
z: 0
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
|
||||
&& MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: MirroredSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
|
||||
&& MediaService.isPlaying && MediaService.trackLength > 0
|
||||
z: 0
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
sourceComponent: MirroredSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
|
||||
&& MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
z: 1 // Above the visualizer
|
||||
|
|
@ -116,17 +114,18 @@ Row {
|
|||
text: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: Settings.data.audio.showMiniplayerAlbumArt
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: Math.round(18 * scaling)
|
||||
height: Math.round(18 * scaling)
|
||||
Layout.preferredWidth: Math.round(18 * scaling)
|
||||
Layout.preferredHeight: Math.round(18 * scaling)
|
||||
|
||||
NImageCircled {
|
||||
id: trackArt
|
||||
|
|
@ -142,23 +141,23 @@ Row {
|
|||
NText {
|
||||
id: titleText
|
||||
|
||||
// For short titles, show full. For long titles, truncate and expand on hover
|
||||
width: {
|
||||
Layout.preferredWidth: {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mTertiary
|
||||
|
||||
Behavior on width {
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutCubic
|
||||
|
|
@ -205,10 +204,10 @@ Row {
|
|||
text: {
|
||||
var str = ""
|
||||
if (MediaService.canGoNext) {
|
||||
str += "Right click for next\n"
|
||||
str += "Right click for next.\n"
|
||||
}
|
||||
if (MediaService.canGoPrevious) {
|
||||
str += "Middle click for previous\n"
|
||||
str += "Middle click for previous."
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ Item {
|
|||
AudioService.setInputMuted(!AudioService.inputMuted)
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["pwvucontrol"]);
|
||||
Quickshell.execDetached(["pwvucontrol"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ NIconButton {
|
|||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
|
||||
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled" : "disabled"}\nLeft click to toggle.\nRight click to access settings.`
|
||||
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled." : "disabled."}\nLeft click to toggle.\nRight click to access settings.`
|
||||
onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
|
||||
|
||||
onRightClicked: {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ NIconButton {
|
|||
property real scaling: 1.0
|
||||
|
||||
sizeRatio: 0.8
|
||||
icon: "notifications"
|
||||
tooltipText: "Notification history"
|
||||
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this)
|
||||
|
||||
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ NIconButton {
|
|||
property real scaling: 1.0
|
||||
|
||||
icon: Settings.data.bar.useDistroLogo ? "" : "widgets"
|
||||
tooltipText: "Open side panel"
|
||||
tooltipText: "Open side panel."
|
||||
sizeRatio: 0.8
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
|
|
|
|||
56
Modules/Bar/Widgets/Spacer.qml
Normal file
56
Modules/Bar/Widgets/Spacer.qml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Widget properties passed from Bar.qml
|
||||
property var screen
|
||||
property real scaling: 1.0
|
||||
|
||||
property string barSection: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Get user settings from Settings data - make it reactive
|
||||
property var widgetSettings: {
|
||||
var section = barSection.replace("Section", "").toLowerCase()
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// Use settings or defaults from BarWidgetRegistry
|
||||
readonly property int userWidth: {
|
||||
var section = barSection.replace("Section", "").toLowerCase()
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex].width || BarWidgetRegistry.widgetMetadata["Spacer"].width
|
||||
}
|
||||
}
|
||||
return BarWidgetRegistry.widgetMetadata["Spacer"].width
|
||||
}
|
||||
|
||||
// Set the width based on user settings
|
||||
implicitWidth: userWidth * scaling
|
||||
implicitHeight: Style.barHeight * scaling
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
// Optional: Add a subtle visual indicator in debug mode
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint
|
||||
visible: Settings.data.general.debugMode || false
|
||||
radius: 2 * scaling
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +1,146 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
RowLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
Row {
|
||||
id: cpuUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
// CPU Usage Component
|
||||
RowLayout {
|
||||
id: cpuUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NIcon {
|
||||
id: cpuUsageIcon
|
||||
text: "speed"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
id: cpuUsageText
|
||||
text: `${SystemStatService.cpuUsage}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NIcon {
|
||||
id: cpuUsageIcon
|
||||
text: "speed"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// CPU Temperature Component
|
||||
Row {
|
||||
id: cpuTempLayout
|
||||
// spacing is thin here to compensate for the vertical thermometer icon
|
||||
spacing: Style.marginXXS * scaling
|
||||
NText {
|
||||
id: cpuUsageText
|
||||
text: `${SystemStatService.cpuUsage}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "thermometer"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// CPU Temperature Component
|
||||
RowLayout {
|
||||
id: cpuTempLayout
|
||||
// spacing is thin here to compensate for the vertical thermometer icon
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.cpuTemp}°C`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NIcon {
|
||||
text: "thermometer"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Memory Usage Component
|
||||
Row {
|
||||
id: memoryUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
NText {
|
||||
text: `${SystemStatService.cpuTemp}°C`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "memory"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// Memory Usage Component
|
||||
RowLayout {
|
||||
id: memoryUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.memoryUsageGb}G`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NIcon {
|
||||
text: "memory"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Network Download Speed Component
|
||||
Row {
|
||||
id: networkDownloadLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
visible: Settings.data.bar.showNetworkStats
|
||||
NText {
|
||||
text: `${SystemStatService.memoryUsageGb}G`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "download"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// Network Download Speed Component
|
||||
RowLayout {
|
||||
id: networkDownloadLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: Settings.data.bar.showNetworkStats
|
||||
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NIcon {
|
||||
text: "download"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Network Upload Speed Component
|
||||
Row {
|
||||
id: networkUploadLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
visible: Settings.data.bar.showNetworkStats
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "upload"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// Network Upload Speed Component
|
||||
RowLayout {
|
||||
id: networkUploadLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: Settings.data.bar.showNetworkStats
|
||||
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NIcon {
|
||||
text: "upload"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pragma ComponentBehavior
|
|||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
|
|
@ -17,15 +18,14 @@ Rectangle {
|
|||
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
|
||||
|
||||
// Always visible when there are toplevels
|
||||
implicitWidth: taskbarRow.width + Style.marginM * scaling * 2
|
||||
implicitWidth: taskbarLayout.implicitWidth + Style.marginM * scaling * 2
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
Row {
|
||||
id: taskbarRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
RowLayout {
|
||||
id: taskbarLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * root.scaling
|
||||
|
||||
Repeater {
|
||||
|
|
@ -35,8 +35,10 @@ Rectangle {
|
|||
required property Toplevel modelData
|
||||
property Toplevel toplevel: modelData
|
||||
property bool isActive: ToplevelManager.activeToplevel === modelData
|
||||
width: root.itemSize
|
||||
height: root.itemSize
|
||||
|
||||
Layout.preferredWidth: root.itemSize
|
||||
Layout.preferredHeight: root.itemSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
Rectangle {
|
||||
id: iconBackground
|
||||
|
|
@ -89,7 +91,7 @@ Rectangle {
|
|||
|
||||
NTooltip {
|
||||
id: taskbarTooltip
|
||||
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App"
|
||||
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
|
||||
target: taskbarItem
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,26 +26,26 @@ Rectangle {
|
|||
}
|
||||
|
||||
visible: SystemTray.items.values.length > 0
|
||||
implicitWidth: tray.width + Style.marginM * scaling * 2
|
||||
implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Row {
|
||||
id: tray
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
RowLayout {
|
||||
id: trayLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
width: itemSize
|
||||
height: itemSize
|
||||
Layout.preferredWidth: itemSize
|
||||
Layout.preferredHeight: itemSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
visible: modelData
|
||||
|
||||
IconImage {
|
||||
|
|
@ -146,13 +146,14 @@ Rectangle {
|
|||
|
||||
function open() {
|
||||
visible = true
|
||||
|
||||
PanelService.willOpenPanel(trayPanel)
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false
|
||||
trayMenu.item.hideMenu()
|
||||
if (trayMenu.item) {
|
||||
trayMenu.item.hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking outside of the rectangle to close
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ Item {
|
|||
collapsedIconColor: Color.mOnSurface
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.volume * 100) + "%"
|
||||
tooltipText: "Volume: " + Math.round(
|
||||
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
|
||||
tooltipText: "Volume: " + Math.round(AudioService.volume * 100)
|
||||
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
|
|
@ -85,7 +85,7 @@ Item {
|
|||
AudioService.setMuted(!AudioService.muted)
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["pwvucontrol"]);
|
||||
Quickshell.execDetached(["pwvucontrol"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,8 @@ NIconButton {
|
|||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
visible: Settings.data.network.wifiEnabled
|
||||
|
||||
sizeRatio: 0.8
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("WiFi", "Widget component completed")
|
||||
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
|
||||
if (NetworkService) {
|
||||
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
|
||||
}
|
||||
}
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
|
|
@ -32,7 +22,7 @@ NIconButton {
|
|||
|
||||
icon: {
|
||||
try {
|
||||
if (NetworkService.ethernet) {
|
||||
if (NetworkService.ethernetConnected) {
|
||||
return "lan"
|
||||
}
|
||||
let connected = false
|
||||
|
|
@ -46,10 +36,10 @@ NIconButton {
|
|||
}
|
||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||
} catch (error) {
|
||||
Logger.error("WiFi", "Error getting icon:", error)
|
||||
Logger.error("Wi-Fi", "Error getting icon:", error)
|
||||
return "signal_wifi_bad"
|
||||
}
|
||||
}
|
||||
tooltipText: "Network / Wi-Fi"
|
||||
tooltipText: "Network / Wi-Fi."
|
||||
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import qs.Services
|
|||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen: null
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
property bool isDestroying: false
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ Variants {
|
|||
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.namespace: "noctalia-dock"
|
||||
|
||||
property bool autoHide: Settings.data.dock.autoHide
|
||||
property bool hidden: autoHide
|
||||
property int hideDelay: 500
|
||||
|
|
@ -128,9 +130,9 @@ Variants {
|
|||
|
||||
Rectangle {
|
||||
id: dockContainer
|
||||
width: dock.width + 48 * scaling
|
||||
width: dockLayout.implicitWidth + 48 * scaling
|
||||
height: iconSize * 1.4 * scaling
|
||||
color: Color.mSurface
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: dockSpacing
|
||||
|
|
@ -176,7 +178,7 @@ Variants {
|
|||
|
||||
Item {
|
||||
id: dock
|
||||
width: runningAppsRow.width
|
||||
width: dockLayout.implicitWidth
|
||||
height: parent.height - (20 * scaling)
|
||||
anchors.centerIn: parent
|
||||
|
||||
|
|
@ -192,10 +194,10 @@ Variants {
|
|||
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
|
||||
}
|
||||
|
||||
Row {
|
||||
id: runningAppsRow
|
||||
RowLayout {
|
||||
id: dockLayout
|
||||
spacing: Style.marginL * scaling
|
||||
height: parent.height
|
||||
Layout.preferredHeight: parent.height
|
||||
anchors.centerIn: parent
|
||||
|
||||
Repeater {
|
||||
|
|
@ -203,8 +205,10 @@ Variants {
|
|||
|
||||
delegate: Rectangle {
|
||||
id: appButton
|
||||
width: iconSize * scaling
|
||||
height: iconSize * scaling
|
||||
Layout.preferredWidth: iconSize * scaling
|
||||
Layout.preferredHeight: iconSize * scaling
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
color: Color.transparent
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ Item {
|
|||
function toggleHistory() {
|
||||
notificationHistoryPanel.toggle(getActiveScreen())
|
||||
}
|
||||
function toggleDoNotDisturb() {// TODO
|
||||
function toggleDND() {
|
||||
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -243,52 +243,45 @@ NPanel {
|
|||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Item {
|
||||
id: searchInputWrap
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
|
||||
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
anchors.fill: parent
|
||||
inputMaxWidth: Number.MAX_SAFE_INTEGER
|
||||
fontSize: Style.fontSizeL * scaling
|
||||
fontWeight: Style.fontWeightSemiBold
|
||||
|
||||
fontSize: Style.fontSizeL * scaling
|
||||
fontWeight: Style.fontWeightSemiBold
|
||||
text: searchText
|
||||
placeholderText: "Search entries... or use > for commands"
|
||||
|
||||
text: searchText
|
||||
placeholderText: "Search entries... or use > for commands"
|
||||
onTextChanged: searchText = text
|
||||
|
||||
onTextChanged: searchText = text
|
||||
Component.onCompleted: {
|
||||
if (searchInput.inputItem && searchInput.inputItem.visible) {
|
||||
searchInput.inputItem.forceActiveFocus()
|
||||
|
||||
Component.onCompleted: {
|
||||
if (searchInput.inputItem && searchInput.inputItem.visible) {
|
||||
searchInput.inputItem.forceActiveFocus()
|
||||
|
||||
// Override the TextField's default Home/End behavior
|
||||
searchInput.inputItem.Keys.priority = Keys.BeforeItem
|
||||
searchInput.inputItem.Keys.onPressed.connect(function (event) {
|
||||
// Intercept Home and End BEFORE the TextField handles them
|
||||
if (event.key === Qt.Key_Home) {
|
||||
ui.selectFirst()
|
||||
event.accepted = true
|
||||
return
|
||||
} else if (event.key === Qt.Key_End) {
|
||||
ui.selectLast()
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
})
|
||||
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
|
||||
ui.selectNext()
|
||||
})
|
||||
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
|
||||
ui.selectPrevious()
|
||||
})
|
||||
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
|
||||
ui.activate()
|
||||
})
|
||||
}
|
||||
// Override the TextField's default Home/End behavior
|
||||
searchInput.inputItem.Keys.priority = Keys.BeforeItem
|
||||
searchInput.inputItem.Keys.onPressed.connect(function (event) {
|
||||
// Intercept Home and End BEFORE the TextField handles them
|
||||
if (event.key === Qt.Key_Home) {
|
||||
ui.selectFirst()
|
||||
event.accepted = true
|
||||
return
|
||||
} else if (event.key === Qt.Key_End) {
|
||||
ui.selectLast()
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
})
|
||||
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
|
||||
ui.selectNext()
|
||||
})
|
||||
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
|
||||
ui.selectPrevious()
|
||||
})
|
||||
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
|
||||
ui.activate()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,11 @@ Item {
|
|||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
|
||||
if (app.execute) {
|
||||
|
||||
if (Settings.data.appLauncher.useApp2Unit && app.id) {
|
||||
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
|
||||
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
|
||||
} else if (app.execute) {
|
||||
app.execute()
|
||||
} else if (app.exec) {
|
||||
// Fallback to manual execution
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ Loader {
|
|||
anchors.topMargin: 80 * scaling
|
||||
spacing: 40 * scaling
|
||||
|
||||
Column {
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
|
|
@ -168,6 +168,7 @@ Loader {
|
|||
font.letterSpacing: -2 * scaling
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
|
|
@ -192,22 +193,23 @@ Loader {
|
|||
font.weight: Font.Light
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
width: timeText.width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: timeText.implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Rectangle {
|
||||
width: 108 * scaling
|
||||
height: 108 * scaling
|
||||
Layout.preferredWidth: 108 * scaling
|
||||
Layout.preferredHeight: 108 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
radius: width * 0.5
|
||||
color: Color.transparent
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderL * scaling)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
z: 10
|
||||
|
||||
Loader {
|
||||
|
|
@ -375,377 +377,371 @@ Loader {
|
|||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 50 * scaling
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 280 * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
id: terminalBackground
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusM * scaling
|
||||
color: Qt.alpha(Color.mSurface, 0.9)
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
|
||||
Repeater {
|
||||
model: 20
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.alpha(Color.mPrimary, 0.1)
|
||||
y: index * 10 * scaling
|
||||
opacity: Style.opacityMedium
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: 2000 + Math.random() * 1000
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.1
|
||||
duration: 2000 + Math.random() * 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: terminalBackground
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusM * scaling
|
||||
color: Qt.alpha(Color.mSurface, 0.9)
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
|
||||
Repeater {
|
||||
model: 20
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40 * scaling
|
||||
color: Qt.alpha(Color.mPrimary, 0.2)
|
||||
topLeftRadius: Style.radiusS * scaling
|
||||
topRightRadius: Style.radiusS * scaling
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
anchors.leftMargin: Style.marginL * scaling
|
||||
anchors.rightMargin: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "SECURE TERMINAL"
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Style.marginS * scaling
|
||||
visible: batteryIndicator.batteryVisible
|
||||
NIcon {
|
||||
text: batteryIndicator.getIcon()
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
NText {
|
||||
text: Math.round(batteryIndicator.percent) + "%"
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Style.marginS * scaling
|
||||
NText {
|
||||
text: keyboardLayout.currentLayout
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
NIcon {
|
||||
text: "keyboard_alt"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Style.marginL * scaling
|
||||
anchors.topMargin: 70 * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
NText {
|
||||
id: welcomeText
|
||||
text: ""
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
property int currentIndex: 0
|
||||
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
|
||||
|
||||
Timer {
|
||||
interval: Style.animationFast
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (parent.currentIndex < parent.fullText.length) {
|
||||
parent.text = parent.fullText.substring(0, parent.currentIndex + 1)
|
||||
parent.currentIndex++
|
||||
} else {
|
||||
running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "sudo unlock-session"
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: passwordInput
|
||||
width: 0
|
||||
height: 0
|
||||
visible: false
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
echoMode: TextInput.Password
|
||||
passwordCharacter: "*"
|
||||
passwordMaskDelay: 0
|
||||
|
||||
text: lockContext.currentText
|
||||
onTextChanged: {
|
||||
lockContext.currentText = text
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
lockContext.tryUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: asterisksText
|
||||
text: "*".repeat(passwordInput.text.length)
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
visible: passwordInput.activeFocus
|
||||
|
||||
SequentialAnimation {
|
||||
id: typingEffect
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.01
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 8 * scaling
|
||||
height: 20 * scaling
|
||||
color: Color.mPrimary
|
||||
visible: passwordInput.activeFocus
|
||||
Layout.leftMargin: -Style.marginS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 500
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.0
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (lockContext.unlockInProgress)
|
||||
return "Authenticating..."
|
||||
if (lockContext.showFailure && lockContext.errorMessage)
|
||||
return lockContext.errorMessage
|
||||
if (lockContext.showFailure)
|
||||
return "Authentication failed."
|
||||
return ""
|
||||
}
|
||||
color: {
|
||||
if (lockContext.unlockInProgress)
|
||||
return Color.mPrimary
|
||||
if (lockContext.showFailure)
|
||||
return Color.mError
|
||||
return Color.transparent
|
||||
}
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: lockContext.unlockInProgress
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 800
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.5
|
||||
duration: 800
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.bottomMargin: -10 * scaling
|
||||
Rectangle {
|
||||
width: 120 * scaling
|
||||
height: 40 * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
enabled: !lockContext.unlockInProgress
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
|
||||
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: executeButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
lockContext.tryUnlock()
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: executeButtonArea.containsMouse
|
||||
NumberAnimation {
|
||||
to: 1.05
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: !executeButtonArea.containsMouse
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
running: lockContext.unlockInProgress
|
||||
NumberAnimation {
|
||||
to: 1.02
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Color.transparent
|
||||
border.color: Qt.alpha(Color.mPrimary, 0.3)
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
z: -1
|
||||
|
||||
height: 1
|
||||
color: Qt.alpha(Color.mPrimary, 0.1)
|
||||
y: index * 10 * scaling
|
||||
opacity: Style.opacityMedium
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutQuad
|
||||
duration: 2000 + Math.random() * 1000
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.2
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutQuad
|
||||
to: 0.1
|
||||
duration: 2000 + Math.random() * 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40 * scaling
|
||||
color: Qt.alpha(Color.mPrimary, 0.2)
|
||||
topLeftRadius: Style.radiusS * scaling
|
||||
topRightRadius: Style.radiusS * scaling
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
anchors.leftMargin: Style.marginL * scaling
|
||||
anchors.rightMargin: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "SECURE TERMINAL"
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
visible: batteryIndicator.batteryVisible
|
||||
NIcon {
|
||||
text: batteryIndicator.getIcon()
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
NText {
|
||||
text: Math.round(batteryIndicator.percent) + "%"
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
NText {
|
||||
text: keyboardLayout.currentLayout
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
NIcon {
|
||||
text: "keyboard_alt"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Style.marginL * scaling
|
||||
anchors.topMargin: 70 * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
NText {
|
||||
id: welcomeText
|
||||
text: ""
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
property int currentIndex: 0
|
||||
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
|
||||
|
||||
Timer {
|
||||
interval: Style.animationFast
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (parent.currentIndex < parent.fullText.length) {
|
||||
parent.text = parent.fullText.substring(0, parent.currentIndex + 1)
|
||||
parent.currentIndex++
|
||||
} else {
|
||||
running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: Quickshell.env("USER") + "@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "sudo unlock-session"
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: passwordInput
|
||||
width: 0
|
||||
height: 0
|
||||
visible: false
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
echoMode: TextInput.Password
|
||||
passwordCharacter: "*"
|
||||
passwordMaskDelay: 0
|
||||
|
||||
text: lockContext.currentText
|
||||
onTextChanged: {
|
||||
lockContext.currentText = text
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
lockContext.tryUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: asterisksText
|
||||
text: "*".repeat(passwordInput.text.length)
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
visible: passwordInput.activeFocus
|
||||
|
||||
SequentialAnimation {
|
||||
id: typingEffect
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.01
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 8 * scaling
|
||||
height: 20 * scaling
|
||||
color: Color.mPrimary
|
||||
visible: passwordInput.activeFocus
|
||||
Layout.leftMargin: -Style.marginS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 500
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.0
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (lockContext.unlockInProgress)
|
||||
return "Authenticating..."
|
||||
if (lockContext.showFailure && lockContext.errorMessage)
|
||||
return lockContext.errorMessage
|
||||
if (lockContext.showFailure)
|
||||
return "Authentication failed."
|
||||
return ""
|
||||
}
|
||||
color: {
|
||||
if (lockContext.unlockInProgress)
|
||||
return Color.mPrimary
|
||||
if (lockContext.showFailure)
|
||||
return Color.mError
|
||||
return Color.transparent
|
||||
}
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: lockContext.unlockInProgress
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 800
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.5
|
||||
duration: 800
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.bottomMargin: -10 * scaling
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 120 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
enabled: !lockContext.unlockInProgress
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
|
||||
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: executeButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
lockContext.tryUnlock()
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: executeButtonArea.containsMouse
|
||||
NumberAnimation {
|
||||
to: 1.05
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: !executeButtonArea.containsMouse
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
running: lockContext.unlockInProgress
|
||||
NumberAnimation {
|
||||
to: 1.02
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Color.transparent
|
||||
border.color: Qt.alpha(Color.mPrimary, 0.3)
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
z: -1
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.2
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power buttons at bottom
|
||||
Row {
|
||||
// Power buttons at bottom right
|
||||
RowLayout {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 50 * scaling
|
||||
spacing: 20 * scaling
|
||||
|
||||
Rectangle {
|
||||
width: 60 * scaling
|
||||
height: 60 * scaling
|
||||
Layout.preferredWidth: 60 * scaling
|
||||
Layout.preferredHeight: 60 * scaling
|
||||
radius: width * 0.5
|
||||
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
|
||||
border.color: Color.mError
|
||||
|
|
@ -769,8 +765,8 @@ Loader {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
width: 60 * scaling
|
||||
height: 60 * scaling
|
||||
Layout.preferredWidth: 60 * scaling
|
||||
Layout.preferredHeight: 60 * scaling
|
||||
radius: width * 0.5
|
||||
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
|
||||
border.color: Color.mPrimary
|
||||
|
|
@ -794,8 +790,8 @@ Loader {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
width: 60 * scaling
|
||||
height: 60 * scaling
|
||||
Layout.preferredWidth: 60 * scaling
|
||||
Layout.preferredHeight: 60 * scaling
|
||||
radius: width * 0.5
|
||||
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
|
||||
border.color: Color.mSecondary
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ Variants {
|
|||
}
|
||||
|
||||
// Main notification container
|
||||
Column {
|
||||
ColumnLayout {
|
||||
id: notificationStack
|
||||
// Position based on bar location
|
||||
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
|
||||
|
|
@ -92,8 +92,9 @@ Variants {
|
|||
Repeater {
|
||||
model: notificationModel
|
||||
delegate: Rectangle {
|
||||
width: 360 * scaling
|
||||
height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling))
|
||||
Layout.preferredWidth: 360 * scaling
|
||||
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
|
||||
Layout.maximumHeight: Layout.preferredHeight
|
||||
clip: true
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mOutline
|
||||
|
|
@ -105,6 +106,17 @@ Variants {
|
|||
property real opacityValue: 0.0
|
||||
property bool isRemoving: false
|
||||
|
||||
// Right-click to dismiss
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
animateOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scale and fade-in animation
|
||||
scale: scaleValue
|
||||
opacity: opacityValue
|
||||
|
|
@ -156,104 +168,139 @@ Variants {
|
|||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
ColumnLayout {
|
||||
id: notificationLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginL * scaling
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Right: header on top, then avatar + texts
|
||||
ColumnLayout {
|
||||
id: textColumn
|
||||
spacing: Style.marginS * scaling
|
||||
// Header section with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
id: appHeaderRow
|
||||
NText {
|
||||
text: `${(model.appName || model.desktopEntry)
|
||||
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
}
|
||||
Rectangle {
|
||||
width: 6 * scaling
|
||||
height: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NText {
|
||||
text: `${(model.appName || model.desktopEntry)
|
||||
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: bodyRow
|
||||
spacing: Style.marginM * scaling
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NImageCircled {
|
||||
id: appAvatar
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
// Start avatar aligned with body (below the summary)
|
||||
anchors.topMargin: textContent.childrenRect.y
|
||||
// Prefer notification-provided image (e.g., user avatar) then fall back to app icon
|
||||
imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName(
|
||||
model.appIcon, "application-x-executable")
|
||||
fallbackIcon: "apps"
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: (imagePath && imagePath !== "")
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Main content section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Avatar
|
||||
NImageCircled {
|
||||
id: appAvatar
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
imagePath: model.image && model.image !== "" ? model.image : ""
|
||||
fallbackIcon: ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: (model.image && model.image !== "")
|
||||
}
|
||||
|
||||
// Text content
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NText {
|
||||
text: model.summary || "No summary"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Column {
|
||||
id: textContent
|
||||
spacing: Style.marginS * scaling
|
||||
NText {
|
||||
text: model.body || ""
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
// Ensure a concrete width so text wraps
|
||||
width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0))
|
||||
|
||||
NText {
|
||||
text: model.summary || "No summary"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.body || ""
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
maximumLineCount: 5
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
maximumLineCount: 5
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions removed
|
||||
// Notification actions
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
visible: model.rawNotification && model.rawNotification.actions
|
||||
&& model.rawNotification.actions.length > 0
|
||||
|
||||
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
|
||||
|
||||
Repeater {
|
||||
model: parent.notificationActions
|
||||
|
||||
delegate: NButton {
|
||||
text: {
|
||||
var actionText = modelData.text || "Open"
|
||||
// If text contains comma, take the part after the comma (the display text)
|
||||
if (actionText.includes(",")) {
|
||||
return actionText.split(",")[1] || actionText
|
||||
}
|
||||
return actionText
|
||||
}
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
hoverColor: Color.mSecondary
|
||||
pressColor: Color.mTertiary
|
||||
outlined: false
|
||||
customHeight: 32 * scaling
|
||||
Layout.preferredHeight: 32 * scaling
|
||||
|
||||
onClicked: {
|
||||
if (modelData && modelData.invoke) {
|
||||
modelData.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push buttons to the left if needed
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button positioned absolutely
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
// Compact target (~24dp) and glyph (~16dp)
|
||||
sizeRatio: 0.75
|
||||
fontPointSize: 16
|
||||
sizeRatio: 0.6
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.marginM * scaling
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
|
||||
onClicked: {
|
||||
animateOut()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ NPanel {
|
|||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
|
@ -43,6 +44,13 @@ NPanel {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications_active"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
|
||||
sizeRatio: 0.8
|
||||
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "delete"
|
||||
tooltipText: "Clear history"
|
||||
|
|
@ -65,38 +73,44 @@ NPanel {
|
|||
}
|
||||
|
||||
// Empty state when no notifications
|
||||
Item {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: NotificationService.historyModel.count === 0
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginM * scaling
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "notifications_off"
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NIcon {
|
||||
text: "notifications_off"
|
||||
font.pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No notifications"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: "No notifications"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Your notifications will show up here as they arrive."
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: "Your notifications will show up here as they arrive."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Notification list
|
||||
ListView {
|
||||
id: notificationList
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -108,21 +122,21 @@ NPanel {
|
|||
visible: NotificationService.historyModel.count > 0
|
||||
|
||||
delegate: Rectangle {
|
||||
width: notificationList ? notificationList.width : 380 * scaling
|
||||
height: Math.max(80, notificationContent.height + 30)
|
||||
width: notificationList.width
|
||||
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
|
||||
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Style.marginM * scaling
|
||||
}
|
||||
id: notificationLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Notification content
|
||||
Column {
|
||||
id: notificationContent
|
||||
// Notification content column
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
|
@ -133,7 +147,8 @@ NPanel {
|
|||
font.weight: Font.Medium
|
||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width - 60
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: parent.width
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
|
@ -143,23 +158,27 @@ NPanel {
|
|||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width - 60
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: parent.width
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NotificationService.formatTimestamp(timestamp)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Trash icon button
|
||||
// Delete button
|
||||
NIconButton {
|
||||
icon: "delete"
|
||||
tooltipText: "Delete notification"
|
||||
sizeRatio: 0.7
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
onClicked: {
|
||||
Logger.log("NotificationHistory", "Removing notification:", summary)
|
||||
|
|
@ -172,7 +191,7 @@ NPanel {
|
|||
MouseArea {
|
||||
id: notificationMouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: Style.marginL * 3 * scaling
|
||||
anchors.rightMargin: Style.marginXL * scaling
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
|
@ -16,6 +17,7 @@ NPanel {
|
|||
panelHeight: 380 * scaling
|
||||
panelAnchorHorizontalCenter: true
|
||||
panelAnchorVerticalCenter: true
|
||||
panelKeyboardFocus: true
|
||||
|
||||
// Timer properties
|
||||
property int timerDuration: 9000 // 9 seconds
|
||||
|
|
@ -23,9 +25,44 @@ NPanel {
|
|||
property bool timerActive: false
|
||||
property int timeRemaining: 0
|
||||
|
||||
// Cancel timer when panel is closing
|
||||
// Navigation properties
|
||||
property int selectedIndex: 0
|
||||
readonly property var powerOptions: [{
|
||||
"action": "lock",
|
||||
"icon": "lock_outline",
|
||||
"title": "Lock",
|
||||
"subtitle": "Lock your session"
|
||||
}, {
|
||||
"action": "suspend",
|
||||
"icon": "bedtime",
|
||||
"title": "Suspend",
|
||||
"subtitle": "Put the system to sleep"
|
||||
}, {
|
||||
"action": "reboot",
|
||||
"icon": "refresh",
|
||||
"title": "Reboot",
|
||||
"subtitle": "Restart the system"
|
||||
}, {
|
||||
"action": "logout",
|
||||
"icon": "exit_to_app",
|
||||
"title": "Logout",
|
||||
"subtitle": "End your session"
|
||||
}, {
|
||||
"action": "shutdown",
|
||||
"icon": "power_settings_new",
|
||||
"title": "Shutdown",
|
||||
"subtitle": "Turn off the system",
|
||||
"isShutdown": true
|
||||
}]
|
||||
|
||||
// Lifecycle handlers
|
||||
onOpened: {
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
cancelTimer()
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
// Timer management
|
||||
|
|
@ -79,6 +116,38 @@ NPanel {
|
|||
root.close()
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
function selectNext() {
|
||||
if (powerOptions.length > 0) {
|
||||
selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (powerOptions.length > 0) {
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
function selectLast() {
|
||||
if (powerOptions.length > 0) {
|
||||
selectedIndex = powerOptions.length - 1
|
||||
} else {
|
||||
selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
function activate() {
|
||||
if (powerOptions.length > 0 && powerOptions[selectedIndex]) {
|
||||
const option = powerOptions[selectedIndex]
|
||||
startTimer(option.action)
|
||||
}
|
||||
}
|
||||
|
||||
// Countdown timer
|
||||
Timer {
|
||||
id: countdownTimer
|
||||
|
|
@ -93,8 +162,92 @@ NPanel {
|
|||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: ui
|
||||
color: Color.transparent
|
||||
|
||||
// Keyboard shortcuts
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: ui.selectPrevious()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+J"
|
||||
onActivated: ui.selectNext()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Up"
|
||||
onActivated: ui.selectPrevious()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Down"
|
||||
onActivated: ui.selectNext()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Home"
|
||||
onActivated: ui.selectFirst()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "End"
|
||||
onActivated: ui.selectLast()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Return"
|
||||
onActivated: ui.activate()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Enter"
|
||||
onActivated: ui.activate()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Escape"
|
||||
onActivated: {
|
||||
if (timerActive) {
|
||||
cancelTimer()
|
||||
} else {
|
||||
cancelTimer()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
function selectNext() {
|
||||
root.selectNext()
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
root.selectPrevious()
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
root.selectFirst()
|
||||
}
|
||||
|
||||
function selectLast() {
|
||||
root.selectLast()
|
||||
}
|
||||
|
||||
function activate() {
|
||||
root.activate()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginL * scaling
|
||||
|
|
@ -144,55 +297,21 @@ NPanel {
|
|||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Lock Screen
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "lock_outline"
|
||||
title: "Lock"
|
||||
subtitle: "Lock your session"
|
||||
onClicked: startTimer("lock")
|
||||
pending: timerActive && pendingAction === "lock"
|
||||
}
|
||||
|
||||
// Suspend
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "bedtime"
|
||||
title: "Suspend"
|
||||
subtitle: "Put the system to sleep"
|
||||
onClicked: startTimer("suspend")
|
||||
pending: timerActive && pendingAction === "suspend"
|
||||
}
|
||||
|
||||
// Reboot
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "refresh"
|
||||
title: "Reboot"
|
||||
subtitle: "Restart the system"
|
||||
onClicked: startTimer("reboot")
|
||||
pending: timerActive && pendingAction === "reboot"
|
||||
}
|
||||
|
||||
// Logout
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "exit_to_app"
|
||||
title: "Logout"
|
||||
subtitle: "End your session"
|
||||
onClicked: startTimer("logout")
|
||||
pending: timerActive && pendingAction === "logout"
|
||||
}
|
||||
|
||||
// Shutdown
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "power_settings_new"
|
||||
title: "Shutdown"
|
||||
subtitle: "Turn off the system"
|
||||
onClicked: startTimer("shutdown")
|
||||
pending: timerActive && pendingAction === "shutdown"
|
||||
isShutdown: true
|
||||
Repeater {
|
||||
model: powerOptions
|
||||
delegate: PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: modelData.icon
|
||||
title: modelData.title
|
||||
subtitle: modelData.subtitle
|
||||
isShutdown: modelData.isShutdown || false
|
||||
isSelected: index === selectedIndex
|
||||
onClicked: {
|
||||
selectedIndex = index
|
||||
startTimer(modelData.action)
|
||||
}
|
||||
pending: timerActive && pendingAction === modelData.action
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -207,6 +326,7 @@ NPanel {
|
|||
property string subtitle: ""
|
||||
property bool pending: false
|
||||
property bool isShutdown: false
|
||||
property bool isSelected: false
|
||||
|
||||
signal clicked
|
||||
|
||||
|
|
@ -216,7 +336,7 @@ NPanel {
|
|||
if (pending) {
|
||||
return Qt.alpha(Color.mPrimary, 0.08)
|
||||
}
|
||||
if (mouseArea.containsMouse) {
|
||||
if (isSelected || mouseArea.containsMouse) {
|
||||
return Color.mSecondary
|
||||
}
|
||||
return Color.transparent
|
||||
|
|
@ -242,13 +362,12 @@ NPanel {
|
|||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: buttonRoot.icon
|
||||
color: {
|
||||
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
||||
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||
return Color.mError
|
||||
if (mouseArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||
return Color.mOnSecondary
|
||||
return Color.mOnSurface
|
||||
}
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
|
|
@ -264,7 +383,7 @@ NPanel {
|
|||
}
|
||||
|
||||
// Text content in the middle
|
||||
Column {
|
||||
ColumnLayout {
|
||||
anchors.left: iconElement.right
|
||||
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
|
@ -279,10 +398,10 @@ NPanel {
|
|||
color: {
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
||||
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||
return Color.mError
|
||||
if (mouseArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||
return Color.mOnSecondary
|
||||
return Color.mOnSurface
|
||||
}
|
||||
|
||||
|
|
@ -304,10 +423,10 @@ NPanel {
|
|||
color: {
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
||||
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||
return Color.mError
|
||||
if (mouseArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||
return Color.mOnSecondary
|
||||
return Color.mOnSurfaceVariant
|
||||
}
|
||||
opacity: Style.opacityHeavy
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ Popup {
|
|||
sourceComponent: {
|
||||
if (settingsPopup.widgetId === "CustomButton") {
|
||||
return customButtonSettings
|
||||
} else if (settingsPopup.widgetId === "Spacer") {
|
||||
return spacerSettings
|
||||
}
|
||||
// Add more widget settings components here as needed
|
||||
return null
|
||||
|
|
@ -157,4 +159,28 @@ Popup {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer settings component
|
||||
Component {
|
||||
id: spacerSettings
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, settingsPopup.widgetData)
|
||||
settings.width = parseInt(widthInput.text) || 20
|
||||
return settings
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: "Width (pixels)"
|
||||
description: "Width of the spacer in pixels."
|
||||
text: settingsPopup.widgetData.width || "20"
|
||||
placeholderText: "Enter width in pixels"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,234 +267,269 @@ NPanel {
|
|||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
color: Color.transparent
|
||||
|
||||
// Scrolling via keyboard
|
||||
Shortcut {
|
||||
sequence: "Down"
|
||||
onActivated: root.scrollDown()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Up"
|
||||
onActivated: root.scrollUp()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+J"
|
||||
onActivated: root.scrollDown()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: root.scrollUp()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "PgDown"
|
||||
onActivated: root.scrollPageDown()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "PgUp"
|
||||
onActivated: root.scrollPageUp()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
// Changing tab via keyboard
|
||||
Shortcut {
|
||||
sequence: "Tab"
|
||||
onActivated: root.selectNextTab()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Shift+Tab"
|
||||
onActivated: root.selectPreviousTab()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
// Main layout container that fills the panel
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM * scaling
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
id: sidebar
|
||||
Layout.preferredWidth: 220 * scaling
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
// Keyboard shortcuts container
|
||||
Item {
|
||||
Layout.preferredWidth: 0
|
||||
Layout.preferredHeight: 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton // Don't interfere with clicks
|
||||
property int wheelAccumulator: 0
|
||||
onWheel: wheel => {
|
||||
wheelAccumulator += wheel.angleDelta.y
|
||||
if (wheelAccumulator >= 120) {
|
||||
root.selectPreviousTab()
|
||||
wheelAccumulator = 0
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
root.selectNextTab()
|
||||
wheelAccumulator = 0
|
||||
}
|
||||
wheel.accepted = true
|
||||
}
|
||||
// Scrolling via keyboard
|
||||
Shortcut {
|
||||
sequence: "Down"
|
||||
onActivated: root.scrollDown()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginXS * 1.5 * scaling
|
||||
Shortcut {
|
||||
sequence: "Up"
|
||||
onActivated: root.scrollUp()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sections
|
||||
model: root.tabsModel
|
||||
delegate: Rectangle {
|
||||
id: tabItem
|
||||
width: parent.width
|
||||
height: 32 * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
|
||||
readonly property bool selected: index === currentTabIndex
|
||||
property bool hovering: false
|
||||
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
|
||||
Shortcut {
|
||||
sequence: "Ctrl+J"
|
||||
onActivated: root.scrollDown()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: root.scrollUp()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Behavior on tabTextColor {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "PgDown"
|
||||
onActivated: root.scrollPageDown()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
// Tab icon on the left side
|
||||
NIcon {
|
||||
text: modelData.icon
|
||||
color: tabTextColor
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
// Tab label on the left side
|
||||
NText {
|
||||
text: modelData.label
|
||||
color: tabTextColor
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onEntered: tabItem.hovering = true
|
||||
onExited: tabItem.hovering = false
|
||||
onCanceled: tabItem.hovering = false
|
||||
onClicked: currentTabIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "PgUp"
|
||||
onActivated: root.scrollPageUp()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
// Changing tab via keyboard
|
||||
Shortcut {
|
||||
sequence: "Tab"
|
||||
onActivated: root.selectNextTab()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Shift+Tab"
|
||||
onActivated: root.selectPreviousTab()
|
||||
enabled: root.opened
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
Rectangle {
|
||||
id: contentPane
|
||||
// Main content area
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
clip: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
// Sidebar
|
||||
Rectangle {
|
||||
id: sidebar
|
||||
Layout.preferredWidth: 220 * scaling
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Tab label on the main right side
|
||||
NText {
|
||||
text: root.tabsModel[currentTabIndex].label
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: root.close()
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton // Don't interfere with clicks
|
||||
property int wheelAccumulator: 0
|
||||
onWheel: wheel => {
|
||||
wheelAccumulator += wheel.angleDelta.y
|
||||
if (wheelAccumulator >= 120) {
|
||||
root.selectPreviousTab()
|
||||
wheelAccumulator = 0
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
root.selectNextTab()
|
||||
wheelAccumulator = 0
|
||||
}
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginXS * 1.5 * scaling
|
||||
|
||||
Repeater {
|
||||
id: sections
|
||||
model: root.tabsModel
|
||||
delegate: Loader {
|
||||
anchors.fill: parent
|
||||
active: index === root.currentTabIndex
|
||||
delegate: Rectangle {
|
||||
id: tabItem
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginS * scaling * 2
|
||||
radius: Style.radiusS * scaling
|
||||
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
|
||||
readonly property bool selected: index === currentTabIndex
|
||||
property bool hovering: false
|
||||
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Ready && item) {
|
||||
// Find and store reference to the ScrollView
|
||||
const scrollView = item.children[0]
|
||||
if (scrollView && scrollView.toString().includes("ScrollView")) {
|
||||
root.activeScrollView = scrollView
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
Behavior on tabTextColor {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: tabEntryRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Tab icon
|
||||
NIcon {
|
||||
text: modelData.icon
|
||||
color: tabTextColor
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
|
||||
// Tab label
|
||||
NText {
|
||||
text: modelData.label
|
||||
color: tabTextColor
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
padding: Style.marginL * scaling
|
||||
clip: true
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.activeScrollView = scrollView
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onEntered: tabItem.hovering = true
|
||||
onExited: tabItem.hovering = false
|
||||
onCanceled: tabItem.hovering = false
|
||||
onClicked: currentTabIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content pane
|
||||
Rectangle {
|
||||
id: contentPane
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Header row
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Tab title
|
||||
NText {
|
||||
text: root.tabsModel[currentTabIndex]?.label || ""
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Close button
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Divider
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Tab content area
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.transparent
|
||||
|
||||
Repeater {
|
||||
model: root.tabsModel
|
||||
delegate: Loader {
|
||||
anchors.fill: parent
|
||||
active: index === root.currentTabIndex
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Ready && item) {
|
||||
// Find and store reference to the ScrollView
|
||||
const scrollView = item.children[0]
|
||||
if (scrollView && scrollView.toString().includes("ScrollView")) {
|
||||
root.activeScrollView = scrollView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: true
|
||||
sourceComponent: root.tabsModel[index].source
|
||||
width: scrollView.availableWidth
|
||||
sourceComponent: Flickable {
|
||||
// Using a Flickable here with a pressDelay to fix conflict between
|
||||
// ScrollView and NTextInput. This fixes the weird text selection issue.
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
pressDelay: 200
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
padding: Style.marginL * scaling
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: {
|
||||
root.activeScrollView = scrollView
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: true
|
||||
sourceComponent: root.tabsModel[index]?.source
|
||||
width: scrollView.availableWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ ColumnLayout {
|
|||
Rectangle {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: Style.marginS * scaling
|
||||
Layout.preferredWidth: updateText.implicitWidth + 46 * scaling
|
||||
Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2))
|
||||
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
|
||||
radius: Style.radiusL * scaling
|
||||
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
|
||||
|
|
@ -85,11 +85,12 @@ ColumnLayout {
|
|||
}
|
||||
|
||||
RowLayout {
|
||||
id: updateRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
text: "system_update"
|
||||
text: "download"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ ColumnLayout {
|
|||
fallbackIcon: "person"
|
||||
borderColor: Color.mPrimary
|
||||
borderWidth: Math.max(1, Style.borderM * scaling)
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: `${Quickshell.env("USER") || "user"}'s profile picture`
|
||||
description: "Your profile picture that appears throughout the interface."
|
||||
text: Settings.data.general.avatarImage
|
||||
|
|
@ -75,6 +77,45 @@ ColumnLayout {
|
|||
onToggled: checked => Settings.data.dock.autoHide = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: "Dock Background Opacity"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Adjust the background opacity of the dock."
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
NSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
value: Settings.data.dock.backgroundOpacity
|
||||
onMoved: Settings.data.dock.backgroundOpacity = value
|
||||
cutoutColor: Color.mSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
|
|
|||
|
|
@ -5,94 +5,85 @@ import qs.Commons
|
|||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ScrollView {
|
||||
id: root
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
spacing: Style.marginL * scaling
|
||||
width: root.width
|
||||
|
||||
property real scaling: 1.0
|
||||
|
||||
contentWidth: contentColumn.width
|
||||
contentHeight: contentColumn.height
|
||||
// Enable/Disable Toggle
|
||||
NToggle {
|
||||
label: "Enable Hooks"
|
||||
description: "Enable or disable all hook commands."
|
||||
checked: Settings.data.hooks.enabled
|
||||
onToggled: checked => Settings.data.hooks.enabled = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
visible: Settings.data.hooks.enabled
|
||||
spacing: Style.marginL * scaling
|
||||
width: root.width
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Enable/Disable Toggle
|
||||
NToggle {
|
||||
label: "Enable Hooks"
|
||||
description: "Enable or disable all hook commands."
|
||||
checked: Settings.data.hooks.enabled
|
||||
onToggled: checked => Settings.data.hooks.enabled = checked
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Wallpaper Hook Section
|
||||
NInputAction {
|
||||
id: wallpaperHookInput
|
||||
label: "Wallpaper Change Hook"
|
||||
description: "Command to be executed when wallpaper changes."
|
||||
placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\""
|
||||
text: Settings.data.hooks.wallpaperChange
|
||||
onEditingFinished: {
|
||||
Settings.data.hooks.wallpaperChange = wallpaperHookInput.text
|
||||
}
|
||||
onActionClicked: {
|
||||
if (wallpaperHookInput.text) {
|
||||
HooksService.executeWallpaperHook("test", "test-screen")
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Dark Mode Hook Section
|
||||
NInputAction {
|
||||
id: darkModeHookInput
|
||||
label: "Theme Toggle Hook"
|
||||
description: "Command to be executed when theme toggles between dark and light mode."
|
||||
placeholderText: "e.g., notify-send \"Theme\" \"Toggled\""
|
||||
text: Settings.data.hooks.darkModeChange
|
||||
onEditingFinished: {
|
||||
Settings.data.hooks.darkModeChange = darkModeHookInput.text
|
||||
}
|
||||
onActionClicked: {
|
||||
if (darkModeHookInput.text) {
|
||||
HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode)
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Info section
|
||||
ColumnLayout {
|
||||
visible: Settings.data.hooks.enabled
|
||||
spacing: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
NLabel {
|
||||
label: "Hook Command Information"
|
||||
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
|
||||
}
|
||||
|
||||
// Wallpaper Hook Section
|
||||
NInputAction {
|
||||
id: wallpaperHookInput
|
||||
label: "Wallpaper Change Hook"
|
||||
description: "Command to be executed when wallpaper changes."
|
||||
placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\""
|
||||
text: Settings.data.hooks.wallpaperChange
|
||||
onEditingFinished: {
|
||||
Settings.data.hooks.wallpaperChange = wallpaperHookInput.text
|
||||
}
|
||||
onActionClicked: {
|
||||
if (wallpaperHookInput.text) {
|
||||
HooksService.executeWallpaperHook("test", "test-screen")
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Dark Mode Hook Section
|
||||
NInputAction {
|
||||
id: darkModeHookInput
|
||||
label: "Theme Toggle Hook"
|
||||
description: "Command to be executed when theme toggles between dark and light mode."
|
||||
placeholderText: "e.g., notify-send \"Theme\" \"Toggled\""
|
||||
text: Settings.data.hooks.darkModeChange
|
||||
onEditingFinished: {
|
||||
Settings.data.hooks.darkModeChange = darkModeHookInput.text
|
||||
}
|
||||
onActionClicked: {
|
||||
if (darkModeHookInput.text) {
|
||||
HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode)
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Info section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Hook Command Information"
|
||||
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: "Available Parameters"
|
||||
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
|
||||
}
|
||||
NLabel {
|
||||
label: "Available Parameters"
|
||||
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@ ColumnLayout {
|
|||
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Use App2Unit for Launching"
|
||||
description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration."
|
||||
checked: Settings.data.appLauncher.useApp2Unit
|
||||
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
|
|
|||
|
|
@ -12,22 +12,14 @@ ColumnLayout {
|
|||
spacing: Style.marginL * scaling
|
||||
|
||||
NToggle {
|
||||
label: "WiFi Enabled"
|
||||
description: "Enable WiFi connectivity."
|
||||
label: "Enable Wi-Fi"
|
||||
description: "Enable Wi-Fi connectivity."
|
||||
checked: Settings.data.network.wifiEnabled
|
||||
onToggled: checked => {
|
||||
Settings.data.network.wifiEnabled = checked
|
||||
NetworkService.setWifiEnabled(checked)
|
||||
if (checked) {
|
||||
ToastService.showNotice("WiFi", "Enabled")
|
||||
} else {
|
||||
ToastService.showNotice("WiFi", "Disabled")
|
||||
}
|
||||
}
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Bluetooth Enabled"
|
||||
label: "Enable Bluetooth"
|
||||
description: "Enable Bluetooth connectivity."
|
||||
checked: Settings.data.network.bluetoothEnabled
|
||||
onToggled: checked => {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,6 @@ ColumnLayout {
|
|||
NColorPicker {
|
||||
selectedColor: Settings.data.wallpaper.fillColor
|
||||
onColorSelected: color => Settings.data.wallpaper.fillColor = color
|
||||
onColorCancelled: selectedColor = Settings.data.wallpaper.fillColor
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +277,6 @@ ColumnLayout {
|
|||
NTextInput {
|
||||
label: "Custom Interval"
|
||||
description: "Enter time as HH:MM (e.g., 01:30)."
|
||||
inputMaxWidth: 100 * scaling
|
||||
text: {
|
||||
const s = Settings.data.wallpaper.randomIntervalSec
|
||||
const h = Math.floor(s / 3600)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ NBox {
|
|||
// Performance
|
||||
NIconButton {
|
||||
icon: "speed"
|
||||
tooltipText: "Set performance power profile"
|
||||
tooltipText: "Set performance power profile."
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
|
|
@ -43,7 +43,7 @@ NBox {
|
|||
// Balanced
|
||||
NIconButton {
|
||||
icon: "balance"
|
||||
tooltipText: "Set balanced power profile"
|
||||
tooltipText: "Set balanced power profile."
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
|
|
@ -57,7 +57,7 @@ NBox {
|
|||
// Eco
|
||||
NIconButton {
|
||||
icon: "eco"
|
||||
tooltipText: "Set eco power profile"
|
||||
tooltipText: "Set eco power profile."
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ NBox {
|
|||
}
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: "Open settings"
|
||||
tooltipText: "Open settings."
|
||||
onClicked: {
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.General
|
||||
settingsPanel.open(screen)
|
||||
|
|
@ -69,7 +69,7 @@ NBox {
|
|||
NIconButton {
|
||||
id: powerButton
|
||||
icon: "power_settings_new"
|
||||
tooltipText: "Power menu"
|
||||
tooltipText: "Power menu."
|
||||
onClicked: {
|
||||
powerPanel.open(screen)
|
||||
sidePanel.close()
|
||||
|
|
@ -79,7 +79,7 @@ NBox {
|
|||
NIconButton {
|
||||
id: closeButton
|
||||
icon: "close"
|
||||
tooltipText: "Close side panel"
|
||||
tooltipText: "Close side panel."
|
||||
onClicked: {
|
||||
sidePanel.close()
|
||||
}
|
||||
|
|
@ -104,19 +104,7 @@ NBox {
|
|||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
|
||||
var minutes = Math.floor(uptimeSeconds / 60) % 60
|
||||
var hours = Math.floor(uptimeSeconds / 3600) % 24
|
||||
var days = Math.floor(uptimeSeconds / 86400)
|
||||
|
||||
// Format the output
|
||||
if (days > 0) {
|
||||
uptimeText = days + "d " + hours + "h"
|
||||
} else if (hours > 0) {
|
||||
uptimeText = hours + "h" + minutes + "m"
|
||||
} else {
|
||||
uptimeText = minutes + "m"
|
||||
}
|
||||
|
||||
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds)
|
||||
uptimeProcess.running = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ NBox {
|
|||
Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling
|
||||
implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling
|
||||
|
||||
Column {
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
|
@ -22,11 +22,6 @@ NBox {
|
|||
anchors.bottomMargin: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Slight top padding
|
||||
Item {
|
||||
height: Style.marginXS * scaling
|
||||
}
|
||||
|
||||
NCircleStat {
|
||||
value: SystemStatService.cpuUsage
|
||||
icon: "speed"
|
||||
|
|
@ -60,10 +55,5 @@ NBox {
|
|||
width: 72 * scaling
|
||||
height: 68 * scaling
|
||||
}
|
||||
|
||||
// Extra bottom padding to shift the perceived stack slightly upward
|
||||
Item {
|
||||
height: Style.marginM * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ NBox {
|
|||
// Screen Recorder
|
||||
NIconButton {
|
||||
icon: "videocam"
|
||||
tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording" : "Start screen recording"
|
||||
tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording."
|
||||
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
|
|
@ -42,7 +42,7 @@ NBox {
|
|||
// Idle Inhibitor
|
||||
NIconButton {
|
||||
icon: "coffee"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake."
|
||||
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
|
|
@ -54,7 +54,7 @@ NBox {
|
|||
NIconButton {
|
||||
visible: Settings.data.wallpaper.enabled
|
||||
icon: "image"
|
||||
tooltipText: "Left click: Open wallpaper selector\nRight click: Set random wallpaper"
|
||||
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
|
||||
onClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
||||
|
|
|
|||
|
|
@ -14,16 +14,11 @@ NPanel {
|
|||
panelHeight: 500 * scaling
|
||||
panelKeyboardFocus: true
|
||||
|
||||
property string passwordPromptSsid: ""
|
||||
property string passwordSsid: ""
|
||||
property string passwordInput: ""
|
||||
property bool showPasswordPrompt: false
|
||||
property string expandedNetwork: "" // Track which network shows options
|
||||
property string expandedSsid: ""
|
||||
|
||||
onOpened: {
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
NetworkService.refreshNetworks()
|
||||
}
|
||||
}
|
||||
onOpened: NetworkService.scan()
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: Color.transparent
|
||||
|
|
@ -39,35 +34,32 @@ NPanel {
|
|||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
text: "wifi"
|
||||
text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "WiFi"
|
||||
text: "Wi-Fi"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
}
|
||||
|
||||
// Connection status indicator
|
||||
Rectangle {
|
||||
visible: NetworkService.hasActiveConnection
|
||||
width: 8 * scaling
|
||||
height: 8 * scaling
|
||||
radius: 4 * scaling
|
||||
color: Color.mPrimary
|
||||
NToggle {
|
||||
id: wifiSwitch
|
||||
checked: Settings.data.network.wifiEnabled
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.65 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: "Refresh networks"
|
||||
tooltipText: "Refresh"
|
||||
sizeRatio: 0.8
|
||||
enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading
|
||||
onClicked: NetworkService.refreshNetworks()
|
||||
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
|
||||
onClicked: NetworkService.scan()
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
|
|
@ -82,17 +74,18 @@ NPanel {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Error banner
|
||||
// Error message
|
||||
Rectangle {
|
||||
visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
|
||||
visible: NetworkService.lastError.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: errorText.implicitHeight + (Style.marginM * scaling * 2)
|
||||
Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * scaling * 2)
|
||||
color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1)
|
||||
radius: Style.radiusS * scaling
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: Color.mError
|
||||
|
||||
RowLayout {
|
||||
id: errorRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
|
@ -104,8 +97,7 @@ NPanel {
|
|||
}
|
||||
|
||||
NText {
|
||||
id: errorText
|
||||
text: NetworkService.connectError
|
||||
text: NetworkService.lastError
|
||||
color: Color.mError
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
wrapMode: Text.Wrap
|
||||
|
|
@ -115,301 +107,364 @@ NPanel {
|
|||
NIconButton {
|
||||
icon: "close"
|
||||
sizeRatio: 0.6
|
||||
onClicked: {
|
||||
NetworkService.connectStatus = ""
|
||||
NetworkService.connectError = ""
|
||||
}
|
||||
onClicked: NetworkService.lastError = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
// Main content area
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
contentWidth: availableWidth
|
||||
color: Color.transparent
|
||||
|
||||
// WiFi disabled state
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
visible: !Settings.data.network.wifiEnabled
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Loading state
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys(
|
||||
NetworkService.networks).length === 0
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NBusyIndicator {
|
||||
running: true
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Scanning for networks..."
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
// WiFi disabled state
|
||||
NIcon {
|
||||
text: "wifi_off"
|
||||
font.pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Wi-Fi is disabled"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Enable Wi-Fi to see available networks."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Scanning state
|
||||
ColumnLayout {
|
||||
visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(
|
||||
NetworkService.networks).length === 0
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NBusyIndicator {
|
||||
running: true
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Searching for nearby networks..."
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Networks list container
|
||||
ScrollView {
|
||||
visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(
|
||||
NetworkService.networks).length > 0)
|
||||
anchors.fill: parent
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
visible: !Settings.data.network.wifiEnabled
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
text: "wifi_off"
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
// Network list
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Settings.data.network.wifiEnabled)
|
||||
return []
|
||||
|
||||
NText {
|
||||
text: "WiFi is disabled"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Enable WiFi"
|
||||
icon: "wifi"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
Settings.data.network.wifiEnabled = true
|
||||
Settings.save()
|
||||
NetworkService.setWifiEnabled(true)
|
||||
const nets = Object.values(NetworkService.networks)
|
||||
return nets.sort((a, b) => {
|
||||
if (a.connected !== b.connected)
|
||||
return b.connected - a.connected
|
||||
return b.signal - a.signal
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network list
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Settings.data.network.wifiEnabled || NetworkService.isLoading)
|
||||
return []
|
||||
|
||||
// Sort networks: connected first, then by signal strength
|
||||
const nets = Object.values(NetworkService.networks)
|
||||
return nets.sort((a, b) => {
|
||||
if (a.connected && !b.connected)
|
||||
return -1
|
||||
if (!a.connected && b.connected)
|
||||
return 1
|
||||
return b.signal - a.signal
|
||||
})
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: networkRect.implicitHeight
|
||||
|
||||
Rectangle {
|
||||
id: networkRect
|
||||
width: parent.width
|
||||
implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2)
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
// Add opacity for operations in progress
|
||||
opacity: (NetworkService.disconnectingFrom === modelData.ssid
|
||||
|| NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0
|
||||
|
||||
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b,
|
||||
0.05) : Color.mSurface
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
|
||||
clip: true
|
||||
|
||||
// Smooth opacity animation
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: networkContent
|
||||
id: netColumn
|
||||
width: parent.width - (Style.marginM * scaling * 2)
|
||||
x: Style.marginM * scaling
|
||||
y: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Main network row
|
||||
// Main row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Signal icon
|
||||
NIcon {
|
||||
text: NetworkService.signalIcon(modelData.signal)
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
// Network info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 0
|
||||
spacing: 2 * scaling
|
||||
|
||||
NText {
|
||||
text: modelData.ssid || "Unknown Network"
|
||||
text: modelData.ssid
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
const security = modelData.security
|
||||
&& modelData.security !== "--" ? modelData.security : "Open"
|
||||
const signal = `${modelData.signal}%`
|
||||
return `${signal} • ${security}`
|
||||
}
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Right-aligned items container
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Connected badge
|
||||
Rectangle {
|
||||
visible: modelData.connected
|
||||
color: Color.mPrimary
|
||||
radius: width * 0.5
|
||||
width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2)
|
||||
RowLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NText {
|
||||
id: connectedLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Connected"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
// Saved badge - clickable
|
||||
Rectangle {
|
||||
visible: modelData.cached && !modelData.connected
|
||||
color: Color.mSurfaceVariant
|
||||
radius: width * 0.5
|
||||
width: savedLabel.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: savedLabel.implicitHeight + (Style.marginXS * scaling * 2)
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: parent.color = Qt.darker(Color.mSurfaceVariant, 1.1)
|
||||
onExited: parent.color = Color.mSurfaceVariant
|
||||
onClicked: {
|
||||
expandedNetwork = expandedNetwork === modelData.ssid ? "" : modelData.ssid
|
||||
showPasswordPrompt = false
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: savedLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Saved"
|
||||
text: `${modelData.signal}%`
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
NBusyIndicator {
|
||||
visible: NetworkService.connectingSsid === modelData.ssid
|
||||
running: NetworkService.connectingSsid === modelData.ssid
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * 0.6 * scaling
|
||||
}
|
||||
NText {
|
||||
text: "•"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
RowLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
visible: NetworkService.connectingSsid !== modelData.ssid
|
||||
NText {
|
||||
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt)
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
text: modelData.existing ? "Connect" : (NetworkService.isSecured(
|
||||
modelData.security) ? "Password" : "Connect")
|
||||
onClicked: {
|
||||
if (modelData.existing || !NetworkService.isSecured(modelData.security)) {
|
||||
NetworkService.connectNetwork(modelData.ssid, modelData.security)
|
||||
} else {
|
||||
expandedNetwork = modelData.ssid
|
||||
passwordPromptSsid = modelData.ssid
|
||||
showPasswordPrompt = true
|
||||
passwordInput = ""
|
||||
Qt.callLater(() => passwordInputField.forceActiveFocus())
|
||||
}
|
||||
Item {
|
||||
Layout.preferredWidth: Style.marginXXS * scaling
|
||||
}
|
||||
|
||||
// Update the status badges area (around line 237)
|
||||
Rectangle {
|
||||
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
color: Color.mPrimary
|
||||
radius: height * 0.5
|
||||
width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: connectedText
|
||||
anchors.centerIn: parent
|
||||
text: "Connected"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: modelData.connected
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
text: "Disconnect"
|
||||
onClicked: NetworkService.disconnectNetwork(modelData.ssid)
|
||||
Rectangle {
|
||||
visible: NetworkService.disconnectingFrom === modelData.ssid
|
||||
color: Color.mError
|
||||
radius: height * 0.5
|
||||
width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: disconnectingText
|
||||
anchors.centerIn: parent
|
||||
text: "Disconnecting..."
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: NetworkService.forgettingNetwork === modelData.ssid
|
||||
color: Color.mError
|
||||
radius: height * 0.5
|
||||
width: forgettingText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: forgettingText
|
||||
anchors.centerIn: parent
|
||||
text: "Forgetting..."
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.cached && !modelData.connected
|
||||
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||
color: Color.transparent
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: height * 0.5
|
||||
width: savedText.implicitWidth + (Style.marginS * scaling * 2)
|
||||
height: savedText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||
|
||||
NText {
|
||||
id: savedText
|
||||
anchors.centerIn: parent
|
||||
text: "Saved"
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action area
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NBusyIndicator {
|
||||
visible: NetworkService.connectingTo === modelData.ssid
|
||||
|| NetworkService.disconnectingFrom === modelData.ssid
|
||||
|| NetworkService.forgettingNetwork === modelData.ssid
|
||||
running: visible
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * 0.5 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: (modelData.existing || modelData.cached) && !modelData.connected
|
||||
&& NetworkService.connectingTo !== modelData.ssid
|
||||
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||
icon: "delete"
|
||||
tooltipText: "Forget network"
|
||||
sizeRatio: 0.7
|
||||
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid
|
||||
&& passwordSsid !== modelData.ssid
|
||||
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||
text: {
|
||||
if (modelData.existing || modelData.cached)
|
||||
return "Connect"
|
||||
if (!NetworkService.isSecured(modelData.security))
|
||||
return "Connect"
|
||||
return "Password"
|
||||
}
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
onClicked: {
|
||||
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
|
||||
NetworkService.connect(modelData.ssid)
|
||||
} else {
|
||||
passwordSsid = modelData.ssid
|
||||
passwordInput = ""
|
||||
expandedSsid = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
text: "Disconnect"
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
onClicked: NetworkService.disconnect(modelData.ssid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password input section
|
||||
// Password input
|
||||
Rectangle {
|
||||
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
|
||||
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: visible ? 50 * scaling : 0
|
||||
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusS * scaling
|
||||
|
||||
RowLayout {
|
||||
id: passwordRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Style.radiusS * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: Color.mSurface
|
||||
border.color: passwordInputField.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
TextInput {
|
||||
id: passwordInputField
|
||||
id: pwdInput
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.marginM * scaling
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
height: parent.height
|
||||
anchors.margins: Style.marginS * scaling
|
||||
text: passwordInput
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurface
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
clip: true
|
||||
focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt
|
||||
selectByMouse: true
|
||||
echoMode: TextInput.Password
|
||||
selectByMouse: true
|
||||
focus: visible
|
||||
passwordCharacter: "●"
|
||||
onTextChanged: passwordInput = text
|
||||
onVisibleChanged: if (visible)
|
||||
forceActiveFocus()
|
||||
onAccepted: {
|
||||
if (passwordInput) {
|
||||
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
|
||||
showPasswordPrompt = false
|
||||
expandedNetwork = ""
|
||||
if (text) {
|
||||
NetworkService.connect(passwordSsid, text)
|
||||
passwordSsid = ""
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -425,56 +480,75 @@ NPanel {
|
|||
|
||||
NButton {
|
||||
text: "Connect"
|
||||
icon: "check"
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
enabled: passwordInput.length > 0
|
||||
outlined: !enabled
|
||||
outlined: true
|
||||
onClicked: {
|
||||
if (passwordInput) {
|
||||
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
|
||||
showPasswordPrompt = false
|
||||
expandedNetwork = ""
|
||||
}
|
||||
NetworkService.connect(passwordSsid, passwordInput)
|
||||
passwordSsid = ""
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Cancel"
|
||||
sizeRatio: 0.9
|
||||
sizeRatio: 0.8
|
||||
onClicked: {
|
||||
showPasswordPrompt = false
|
||||
expandedNetwork = ""
|
||||
passwordSsid = ""
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forget network option - appears when saved badge is clicked
|
||||
RowLayout {
|
||||
visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid
|
||||
&& !showPasswordPrompt
|
||||
// Forget network
|
||||
Rectangle {
|
||||
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusS * scaling
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: Color.mOutline
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
RowLayout {
|
||||
id: forgetRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NButton {
|
||||
id: forgetButton
|
||||
text: "Forget Network"
|
||||
icon: "delete_outline"
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary
|
||||
outlined: !forgetButton.hovered
|
||||
Layout.preferredHeight: 28 * scaling
|
||||
onClicked: {
|
||||
NetworkService.forgetNetwork(modelData.ssid)
|
||||
expandedNetwork = ""
|
||||
RowLayout {
|
||||
NIcon {
|
||||
text: "delete_outline"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mError
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Forget this network?"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mError
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: forgetButton
|
||||
text: "Forget"
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
outlined: forgetButton.hovered ? false : true
|
||||
onClicked: {
|
||||
NetworkService.forget(modelData.ssid)
|
||||
expandedSsid = ""
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
sizeRatio: 0.8
|
||||
onClicked: expandedSsid = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -482,35 +556,42 @@ NPanel {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No networks found
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys(
|
||||
NetworkService.networks).length === 0
|
||||
spacing: Style.marginM * scaling
|
||||
// Empty state when no networks
|
||||
ColumnLayout {
|
||||
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(
|
||||
NetworkService.networks).length === 0
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NIcon {
|
||||
text: "wifi_find"
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No networks found"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NIcon {
|
||||
text: "wifi_find"
|
||||
font.pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Refresh"
|
||||
icon: "refresh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: NetworkService.refreshNetworks()
|
||||
}
|
||||
NText {
|
||||
text: "No networks found"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Scan again"
|
||||
icon: "refresh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: NetworkService.scan()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ Alternatively, you can add it to your NixOS configuration or flake:
|
|||
| Toggle Settings Window | `qs -c noctalia-shell ipc call settings toggle` |
|
||||
| Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen toggle` |
|
||||
| Toggle Notification History | `qs -c noctalia-shell ipc call notifications toggleHistory` |
|
||||
| Toggle Notification DND | `qs -c noctalia-shell ipc call notifications toggleDND` |
|
||||
| Change Wallpaper | `qs -c noctalia-shell ipc call wallpaper set $path $monitor` |
|
||||
| Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` |
|
||||
| Toggle Dark Mode | `qs -c noctalia-shell ipc call darkMode toggle` |
|
||||
|
|
@ -265,6 +266,10 @@ The launcher supports special commands for enhanced functionality:
|
|||
For Niri:
|
||||
|
||||
```
|
||||
debug {
|
||||
honor-xdg-activation-with-invalid-serial
|
||||
}
|
||||
|
||||
window-rule {
|
||||
geometry-corner-radius 20
|
||||
clip-to-geometry true
|
||||
|
|
@ -279,6 +284,8 @@ layer-rule {
|
|||
place-within-backdrop true
|
||||
}
|
||||
```
|
||||
`honor-xdg-activation-with-invalid-serial` allows notification actions (like view etc) to work.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ Singleton {
|
|||
"PowerToggle": powerToggleComponent,
|
||||
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
|
||||
"SidePanelToggle": sidePanelToggleComponent,
|
||||
"Spacer": spacerComponent,
|
||||
"SystemMonitor": systemMonitorComponent,
|
||||
"Taskbar": taskbarComponent,
|
||||
"Tray": trayComponent,
|
||||
|
|
@ -43,6 +44,11 @@ Singleton {
|
|||
"leftClickExec": "",
|
||||
"rightClickExec": "",
|
||||
"middleClickExec": ""
|
||||
},
|
||||
"Spacer": {
|
||||
"allowUserSettings": true,
|
||||
"icon": "space_bar",
|
||||
"width": 20
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -101,6 +107,9 @@ Singleton {
|
|||
property Component sidePanelToggleComponent: Component {
|
||||
SidePanelToggle {}
|
||||
}
|
||||
property Component spacerComponent: Component {
|
||||
Spacer {}
|
||||
}
|
||||
property Component systemMonitorComponent: Component {
|
||||
SystemMonitor {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,215 +8,239 @@ import qs.Commons
|
|||
Singleton {
|
||||
id: root
|
||||
|
||||
// Core properties
|
||||
// Core state
|
||||
property var networks: ({})
|
||||
property string connectingSsid: ""
|
||||
property string connectStatus: ""
|
||||
property string connectStatusSsid: ""
|
||||
property string connectError: ""
|
||||
property bool isLoading: false
|
||||
property bool ethernet: false
|
||||
property int retryCount: 0
|
||||
property int maxRetries: 3
|
||||
property bool scanning: false
|
||||
property bool connecting: false
|
||||
property string connectingTo: ""
|
||||
property string lastError: ""
|
||||
property bool ethernetConnected: false
|
||||
property string disconnectingFrom: ""
|
||||
property string forgettingNetwork: ""
|
||||
|
||||
// File path for persistent storage
|
||||
// Persistent cache
|
||||
property string cacheFile: Settings.cacheDir + "network.json"
|
||||
readonly property string cachedLastConnected: cacheAdapter.lastConnected
|
||||
readonly property var cachedNetworks: cacheAdapter.knownNetworks
|
||||
|
||||
// Stable properties for UI
|
||||
readonly property alias cache: adapter
|
||||
readonly property string lastConnectedNetwork: adapter.lastConnected
|
||||
|
||||
// File-based persistent storage
|
||||
// Cache file handling
|
||||
FileView {
|
||||
id: cacheFileView
|
||||
path: root.cacheFile
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
onLoaded: {
|
||||
Logger.log("Network", "Loaded network cache from disk")
|
||||
// Try to auto-connect on startup if WiFi is enabled
|
||||
if (Settings.data.network.wifiEnabled && adapter.lastConnected) {
|
||||
autoConnectTimer.start()
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
Logger.log("Network", "No existing cache found, creating new one")
|
||||
// Initialize with empty data
|
||||
adapter.knownNetworks = ({})
|
||||
adapter.lastConnected = ""
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
id: cacheAdapter
|
||||
property var knownNetworks: ({})
|
||||
property string lastConnected: ""
|
||||
property int lastRefresh: 0
|
||||
}
|
||||
|
||||
onLoadFailed: {
|
||||
cacheAdapter.knownNetworks = ({})
|
||||
cacheAdapter.lastConnected = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Save timer to batch writes
|
||||
Connections {
|
||||
target: Settings.data.network
|
||||
function onWifiEnabledChanged() {
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
ToastService.showNotice("Wi-Fi", "Enabled")
|
||||
} else {
|
||||
ToastService.showNotice("Wi-Fi", "Disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Network", "Service initialized")
|
||||
syncWifiState()
|
||||
refresh()
|
||||
}
|
||||
|
||||
// Save cache with debounce
|
||||
Timer {
|
||||
id: saveTimer
|
||||
running: false
|
||||
id: saveDebounce
|
||||
interval: 1000
|
||||
onTriggered: cacheFileView.writeAdapter()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Network", "Service started")
|
||||
function saveCache() {
|
||||
saveDebounce.restart()
|
||||
}
|
||||
|
||||
// Delayed scan timer
|
||||
Timer {
|
||||
id: delayedScanTimer
|
||||
interval: 7000
|
||||
onTriggered: scan()
|
||||
}
|
||||
|
||||
// Core functions
|
||||
function syncWifiState() {
|
||||
wifiStateProcess.running = true
|
||||
}
|
||||
|
||||
function setWifiEnabled(enabled) {
|
||||
Settings.data.network.wifiEnabled = enabled
|
||||
|
||||
wifiToggleProcess.action = enabled ? "on" : "off"
|
||||
wifiToggleProcess.running = true
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
ethernetStateProcess.running = true
|
||||
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
refreshNetworks()
|
||||
scan()
|
||||
}
|
||||
}
|
||||
|
||||
// Signal strength icon mapping
|
||||
function signalIcon(signal) {
|
||||
const levels = [{
|
||||
"threshold": 80,
|
||||
"icon": "network_wifi"
|
||||
}, {
|
||||
"threshold": 60,
|
||||
"icon": "network_wifi_3_bar"
|
||||
}, {
|
||||
"threshold": 40,
|
||||
"icon": "network_wifi_2_bar"
|
||||
}, {
|
||||
"threshold": 20,
|
||||
"icon": "network_wifi_1_bar"
|
||||
}]
|
||||
|
||||
for (const level of levels) {
|
||||
if (signal >= level.threshold)
|
||||
return level.icon
|
||||
}
|
||||
return "signal_wifi_0_bar"
|
||||
}
|
||||
|
||||
function isSecured(security) {
|
||||
return security && security.trim() !== "" && security.trim() !== "--"
|
||||
}
|
||||
|
||||
// Enhanced refresh with retry logic
|
||||
function refreshNetworks() {
|
||||
if (isLoading)
|
||||
function scan() {
|
||||
if (scanning)
|
||||
return
|
||||
|
||||
isLoading = true
|
||||
retryCount = 0
|
||||
adapter.lastRefresh = Date.now()
|
||||
performRefresh()
|
||||
scanning = true
|
||||
lastError = ""
|
||||
scanProcess.running = true
|
||||
Logger.log("Network", "Wi-Fi scan in progress...")
|
||||
}
|
||||
|
||||
function performRefresh() {
|
||||
checkEthernet.running = true
|
||||
existingNetworkProcess.running = true
|
||||
}
|
||||
function connect(ssid, password = "") {
|
||||
if (connecting)
|
||||
return
|
||||
|
||||
// Retry mechanism for failed operations
|
||||
function retryRefresh() {
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++
|
||||
Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`)
|
||||
retryTimer.start()
|
||||
connecting = true
|
||||
connectingTo = ssid
|
||||
lastError = ""
|
||||
|
||||
// Check if we have a saved connection
|
||||
if (networks[ssid]?.existing || cachedNetworks[ssid]) {
|
||||
connectProcess.mode = "saved"
|
||||
connectProcess.ssid = ssid
|
||||
connectProcess.password = ""
|
||||
} else {
|
||||
isLoading = false
|
||||
connectError = "Failed to refresh networks after multiple attempts"
|
||||
connectProcess.mode = "new"
|
||||
connectProcess.ssid = ssid
|
||||
connectProcess.password = password
|
||||
}
|
||||
|
||||
connectProcess.running = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: 1000 * retryCount // Progressive backoff
|
||||
repeat: false
|
||||
onTriggered: performRefresh()
|
||||
function disconnect(ssid) {
|
||||
disconnectingFrom = ssid
|
||||
disconnectProcess.ssid = ssid
|
||||
disconnectProcess.running = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: autoConnectTimer
|
||||
interval: 3000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) {
|
||||
Logger.log("Network", `Auto-connecting to ${adapter.lastConnected}`)
|
||||
connectToExisting(adapter.lastConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forget network function
|
||||
function forgetNetwork(ssid) {
|
||||
Logger.log("Network", `Forgetting network: ${ssid}`)
|
||||
function forget(ssid) {
|
||||
forgettingNetwork = ssid
|
||||
|
||||
// Remove from cache
|
||||
let known = adapter.knownNetworks
|
||||
let known = cacheAdapter.knownNetworks
|
||||
delete known[ssid]
|
||||
adapter.knownNetworks = known
|
||||
cacheAdapter.knownNetworks = known
|
||||
|
||||
// Clear last connected if it's this network
|
||||
if (adapter.lastConnected === ssid) {
|
||||
adapter.lastConnected = ""
|
||||
if (cacheAdapter.lastConnected === ssid) {
|
||||
cacheAdapter.lastConnected = ""
|
||||
}
|
||||
|
||||
// Save changes
|
||||
saveTimer.restart()
|
||||
saveCache()
|
||||
|
||||
// Remove NetworkManager profile
|
||||
// Remove from system
|
||||
forgetProcess.ssid = ssid
|
||||
forgetProcess.running = true
|
||||
}
|
||||
|
||||
// Helper function to immediately update network status
|
||||
function updateNetworkStatus(ssid, connected) {
|
||||
let nets = networks
|
||||
|
||||
// Update all networks connected status
|
||||
for (let key in nets) {
|
||||
if (nets[key].connected && key !== ssid) {
|
||||
nets[key].connected = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update the target network if it exists
|
||||
if (nets[ssid]) {
|
||||
nets[ssid].connected = connected
|
||||
nets[ssid].existing = true
|
||||
nets[ssid].cached = true
|
||||
} else if (connected) {
|
||||
// Create a temporary entry if network doesn't exist yet
|
||||
nets[ssid] = {
|
||||
"ssid": ssid,
|
||||
"security": "--",
|
||||
"signal": 100,
|
||||
"connected"// Default to good signal until real scan
|
||||
: true,
|
||||
"existing": true,
|
||||
"cached": true
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger property change notification
|
||||
networks = ({})
|
||||
networks = nets
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function signalIcon(signal) {
|
||||
if (signal >= 80)
|
||||
return "network_wifi"
|
||||
if (signal >= 60)
|
||||
return "network_wifi_3_bar"
|
||||
if (signal >= 40)
|
||||
return "network_wifi_2_bar"
|
||||
if (signal >= 20)
|
||||
return "network_wifi_1_bar"
|
||||
return "signal_wifi_0_bar"
|
||||
}
|
||||
|
||||
function isSecured(security) {
|
||||
return security && security !== "--" && security.trim() !== ""
|
||||
}
|
||||
|
||||
// Processes
|
||||
Process {
|
||||
id: forgetProcess
|
||||
property string ssid: ""
|
||||
id: ethernetStateProcess
|
||||
running: false
|
||||
command: ["nmcli", "connection", "delete", "id", ssid]
|
||||
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`)
|
||||
refreshNetworks()
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
if (text.includes("no such connection profile")) {
|
||||
Logger.log("Network", `Network profile not found: ${forgetProcess.ssid}`)
|
||||
} else {
|
||||
Logger.warn("Network", `Error forgetting network: ${text}`)
|
||||
}
|
||||
refreshNetworks()
|
||||
const connected = text.split("\n").some(line => {
|
||||
const parts = line.split(":")
|
||||
return parts[1] === "ethernet" && parts[2] === "connected"
|
||||
})
|
||||
if (root.ethernetConnected !== connected) {
|
||||
root.ethernetConnected = connected
|
||||
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi enable/disable functions
|
||||
function setWifiEnabled(enabled) {
|
||||
if (enabled) {
|
||||
isLoading = true
|
||||
wifiRadioProcess.action = "on"
|
||||
wifiRadioProcess.running = true
|
||||
} else {
|
||||
// Save current connection for later
|
||||
for (const ssid in networks) {
|
||||
if (networks[ssid].connected) {
|
||||
adapter.lastConnected = ssid
|
||||
saveTimer.restart()
|
||||
disconnectNetwork(ssid)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
wifiRadioProcess.action = "off"
|
||||
wifiRadioProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// Unified WiFi radio control
|
||||
Process {
|
||||
id: wifiRadioProcess
|
||||
id: wifiStateProcess
|
||||
running: false
|
||||
command: ["nmcli", "radio", "wifi"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const enabled = text.trim() === "enabled"
|
||||
Logger.log("Network", "Wi-Fi enabled:", enabled)
|
||||
if (Settings.data.network.wifiEnabled !== enabled) {
|
||||
Settings.data.network.wifiEnabled = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wifiToggleProcess
|
||||
property string action: "on"
|
||||
running: false
|
||||
command: ["nmcli", "radio", "wifi", action]
|
||||
|
|
@ -224,10 +248,12 @@ Singleton {
|
|||
onRunningChanged: {
|
||||
if (!running) {
|
||||
if (action === "on") {
|
||||
wifiEnableTimer.start()
|
||||
// Clear networks immediately and start delayed scan
|
||||
root.networks = ({})
|
||||
delayedScanTimer.interval = 8000
|
||||
delayedScanTimer.restart()
|
||||
} else {
|
||||
root.networks = ({})
|
||||
root.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -235,137 +261,177 @@ Singleton {
|
|||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`)
|
||||
Logger.warn("Network", "WiFi toggle error: " + text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: wifiEnableTimer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
refreshNetworks()
|
||||
if (adapter.lastConnected) {
|
||||
reconnectTimer.start()
|
||||
Process {
|
||||
id: scanProcess
|
||||
running: false
|
||||
command: ["sh", "-c", `
|
||||
# Get list of saved connection profiles (just the names)
|
||||
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
|
||||
|
||||
# Get WiFi networks
|
||||
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do
|
||||
ssid=$(echo "$line" | cut -d: -f1)
|
||||
security=$(echo "$line" | cut -d: -f2)
|
||||
signal=$(echo "$line" | cut -d: -f3)
|
||||
in_use=$(echo "$line" | cut -d: -f4)
|
||||
|
||||
# Skip empty SSIDs
|
||||
if [ -z "$ssid" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if SSID matches any profile name (simple check)
|
||||
# This covers most cases where profile name equals or contains the SSID
|
||||
existing=false
|
||||
if echo "$profiles" | grep -qF "$ssid|"; then
|
||||
existing=true
|
||||
fi
|
||||
|
||||
echo "$ssid|$security|$signal|$in_use|$existing"
|
||||
done
|
||||
`]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const nets = {}
|
||||
const lines = text.split("\n").filter(l => l.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split("|")
|
||||
if (parts.length < 5)
|
||||
continue
|
||||
|
||||
const ssid = parts[0]
|
||||
if (!ssid || ssid.trim() === "")
|
||||
continue
|
||||
|
||||
const network = {
|
||||
"ssid": ssid,
|
||||
"security": parts[1] || "--",
|
||||
"signal": parseInt(parts[2]) || 0,
|
||||
"connected": parts[3] === "*",
|
||||
"existing": parts[4] === "true",
|
||||
"cached": ssid in cacheAdapter.knownNetworks
|
||||
}
|
||||
|
||||
// Track connected network
|
||||
if (network.connected && cacheAdapter.lastConnected !== ssid) {
|
||||
cacheAdapter.lastConnected = ssid
|
||||
saveCache()
|
||||
}
|
||||
|
||||
// Keep best signal for duplicate SSIDs
|
||||
if (!nets[ssid] || network.signal > nets[ssid].signal) {
|
||||
nets[ssid] = network
|
||||
}
|
||||
}
|
||||
|
||||
// For logging purpose only
|
||||
Logger.log("Network", "Wi-Fi scan completed")
|
||||
const oldSSIDs = Object.keys(root.networks)
|
||||
const newSSIDs = Object.keys(nets)
|
||||
const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid))
|
||||
const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid))
|
||||
if (newNetworks.length > 0 || lostNetworks.length > 0) {
|
||||
if (newNetworks.length > 0) {
|
||||
Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", "))
|
||||
}
|
||||
if (lostNetworks.length > 0) {
|
||||
Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", "))
|
||||
}
|
||||
Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length)
|
||||
}
|
||||
|
||||
// Assign the results
|
||||
root.networks = nets
|
||||
root.scanning = false
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.scanning = false
|
||||
if (text.trim()) {
|
||||
Logger.warn("Network", "Scan error: " + text)
|
||||
// If scan fails, set a short retry
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
delayedScanTimer.interval = 5000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reconnectTimer
|
||||
interval: 3000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) {
|
||||
connectToExisting(adapter.lastConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection management
|
||||
function connectNetwork(ssid, security) {
|
||||
connectingSsid = ssid
|
||||
connectStatus = ""
|
||||
connectStatusSsid = ssid
|
||||
connectError = ""
|
||||
|
||||
// Check if profile exists
|
||||
if (networks[ssid]?.existing) {
|
||||
connectToExisting(ssid)
|
||||
return
|
||||
}
|
||||
|
||||
// Check cache for known network
|
||||
const known = adapter.knownNetworks[ssid]
|
||||
if (known?.profileName) {
|
||||
connectToExisting(known.profileName)
|
||||
return
|
||||
}
|
||||
|
||||
// New connection - need password for secured networks
|
||||
if (isSecured(security)) {
|
||||
// Password will be provided through submitPassword
|
||||
return
|
||||
}
|
||||
|
||||
// Open network - connect directly
|
||||
createAndConnect(ssid, "", security)
|
||||
}
|
||||
|
||||
function submitPassword(ssid, password) {
|
||||
const security = networks[ssid]?.security || ""
|
||||
createAndConnect(ssid, password, security)
|
||||
}
|
||||
|
||||
function connectToExisting(ssid) {
|
||||
connectingSsid = ssid
|
||||
upConnectionProcess.profileName = ssid
|
||||
upConnectionProcess.running = true
|
||||
}
|
||||
|
||||
function createAndConnect(ssid, password, security) {
|
||||
connectingSsid = ssid
|
||||
|
||||
connectProcess.ssid = ssid
|
||||
connectProcess.password = password
|
||||
connectProcess.isSecured = isSecured(security)
|
||||
connectProcess.running = true
|
||||
}
|
||||
|
||||
function disconnectNetwork(ssid) {
|
||||
disconnectProcess.ssid = ssid
|
||||
disconnectProcess.running = true
|
||||
}
|
||||
|
||||
// Connection process
|
||||
Process {
|
||||
id: connectProcess
|
||||
property string mode: "new"
|
||||
property string ssid: ""
|
||||
property string password: ""
|
||||
property bool isSecured: false
|
||||
running: false
|
||||
|
||||
command: {
|
||||
const cmd = ["nmcli", "device", "wifi", "connect", ssid]
|
||||
if (isSecured && password) {
|
||||
cmd.push("password", password)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
handleConnectionSuccess(connectProcess.ssid)
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
handleConnectionError(connectProcess.ssid, text)
|
||||
if (mode === "saved") {
|
||||
return ["nmcli", "connection", "up", "id", ssid]
|
||||
} else {
|
||||
const cmd = ["nmcli", "device", "wifi", "connect", ssid]
|
||||
if (password) {
|
||||
cmd.push("password", password)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: upConnectionProcess
|
||||
property string profileName: ""
|
||||
running: false
|
||||
command: ["nmcli", "connection", "up", "id", profileName]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
handleConnectionSuccess(upConnectionProcess.profileName)
|
||||
// Success - update cache
|
||||
let known = cacheAdapter.knownNetworks
|
||||
known[connectProcess.ssid] = {
|
||||
"profileName": connectProcess.ssid,
|
||||
"lastConnected": Date.now()
|
||||
}
|
||||
cacheAdapter.knownNetworks = known
|
||||
cacheAdapter.lastConnected = connectProcess.ssid
|
||||
saveCache()
|
||||
|
||||
// Immediately update the UI before scanning
|
||||
root.updateNetworkStatus(connectProcess.ssid, true)
|
||||
|
||||
root.connecting = false
|
||||
root.connectingTo = ""
|
||||
Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`)
|
||||
|
||||
// Still do a scan to get accurate signal and security info
|
||||
delayedScanTimer.interval = 1000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.connecting = false
|
||||
root.connectingTo = ""
|
||||
|
||||
if (text.trim()) {
|
||||
handleConnectionError(upConnectionProcess.profileName, text)
|
||||
// Parse common errors
|
||||
if (text.includes("Secrets were required") || text.includes("no secrets provided")) {
|
||||
root.lastError = "Incorrect password"
|
||||
forget(connectProcess.ssid)
|
||||
} else if (text.includes("No network with SSID")) {
|
||||
root.lastError = "Network not found"
|
||||
} else if (text.includes("Timeout")) {
|
||||
root.lastError = "Connection timeout"
|
||||
} else {
|
||||
root.lastError = text.split("\n")[0].trim()
|
||||
}
|
||||
|
||||
Logger.warn("Network", "Connect error: " + text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -377,221 +443,101 @@ Singleton {
|
|||
running: false
|
||||
command: ["nmcli", "connection", "down", "id", ssid]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
connectingSsid = ""
|
||||
connectStatus = ""
|
||||
connectStatusSsid = ""
|
||||
connectError = ""
|
||||
refreshNetworks()
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`)
|
||||
|
||||
// Immediately update UI on successful disconnect
|
||||
root.updateNetworkStatus(disconnectProcess.ssid, false)
|
||||
root.disconnectingFrom = ""
|
||||
|
||||
// Do a scan to refresh the list
|
||||
delayedScanTimer.interval = 1000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.disconnectingFrom = ""
|
||||
if (text.trim()) {
|
||||
Logger.warn("Network", `Disconnect warning: ${text}`)
|
||||
Logger.warn("Network", "Disconnect error: " + text)
|
||||
}
|
||||
// Still trigger a scan even on error
|
||||
delayedScanTimer.interval = 1000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection result handlers
|
||||
function handleConnectionSuccess(ssid) {
|
||||
connectingSsid = ""
|
||||
connectStatus = "success"
|
||||
connectStatusSsid = ssid
|
||||
connectError = ""
|
||||
|
||||
// Update cache
|
||||
let known = adapter.knownNetworks
|
||||
known[ssid] = {
|
||||
"profileName": ssid,
|
||||
"lastConnected": Date.now(),
|
||||
"autoConnect": true
|
||||
}
|
||||
adapter.knownNetworks = known
|
||||
adapter.lastConnected = ssid
|
||||
saveTimer.restart()
|
||||
|
||||
Logger.log("Network", `Successfully connected to ${ssid}`)
|
||||
refreshNetworks()
|
||||
}
|
||||
|
||||
function handleConnectionError(ssid, error) {
|
||||
connectingSsid = ""
|
||||
connectStatus = "error"
|
||||
connectStatusSsid = ssid
|
||||
connectError = parseError(error)
|
||||
|
||||
Logger.warn("Network", `Failed to connect to ${ssid}: ${error}`)
|
||||
}
|
||||
|
||||
function parseError(error) {
|
||||
// Simplify common error messages
|
||||
if (error.includes("Secrets were required") || error.includes("no secrets provided")) {
|
||||
return "Incorrect password"
|
||||
}
|
||||
if (error.includes("No network with SSID")) {
|
||||
return "Network not found"
|
||||
}
|
||||
if (error.includes("Connection activation failed")) {
|
||||
return "Connection failed. Please try again."
|
||||
}
|
||||
if (error.includes("Timeout")) {
|
||||
return "Connection timeout. Network may be out of range."
|
||||
}
|
||||
// Return first line only
|
||||
return error.split("\n")[0].trim()
|
||||
}
|
||||
|
||||
// Network scanning processes
|
||||
Process {
|
||||
id: existingNetworkProcess
|
||||
id: forgetProcess
|
||||
property string ssid: ""
|
||||
running: false
|
||||
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
|
||||
|
||||
// Try multiple common profile name patterns
|
||||
command: ["sh", "-c", `
|
||||
ssid="$1"
|
||||
deleted=false
|
||||
|
||||
# Try exact SSID match first
|
||||
if nmcli connection delete id "$ssid" 2>/dev/null; then
|
||||
echo "Deleted profile: $ssid"
|
||||
deleted=true
|
||||
fi
|
||||
|
||||
# Try "Auto <SSID>" pattern
|
||||
if nmcli connection delete id "Auto $ssid" 2>/dev/null; then
|
||||
echo "Deleted profile: Auto $ssid"
|
||||
deleted=true
|
||||
fi
|
||||
|
||||
# Try "<SSID> 1", "<SSID> 2", etc. patterns
|
||||
for i in 1 2 3; do
|
||||
if nmcli connection delete id "$ssid $i" 2>/dev/null; then
|
||||
echo "Deleted profile: $ssid $i"
|
||||
deleted=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$deleted" = "false" ]; then
|
||||
echo "No profiles found for SSID: $ssid"
|
||||
fi
|
||||
`, "--", ssid]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const profiles = {}
|
||||
const lines = text.split("\n").filter(l => l.trim())
|
||||
Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`)
|
||||
Logger.log("Network", text.trim().replace(/[\r\n]/g, " "))
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(":")
|
||||
const name = parts[0]
|
||||
const type = parts[1]
|
||||
if (name && type === "802-11-wireless") {
|
||||
profiles[name] = {
|
||||
"ssid": name,
|
||||
"type": type
|
||||
}
|
||||
}
|
||||
// Update both cached and existing status immediately
|
||||
let nets = root.networks
|
||||
if (nets[forgetProcess.ssid]) {
|
||||
nets[forgetProcess.ssid].cached = false
|
||||
nets[forgetProcess.ssid].existing = false
|
||||
// Trigger property change
|
||||
root.networks = ({})
|
||||
root.networks = nets
|
||||
}
|
||||
|
||||
scanProcess.existingProfiles = profiles
|
||||
scanProcess.running = true
|
||||
root.forgettingNetwork = ""
|
||||
|
||||
// Quick scan to verify the profile is gone
|
||||
delayedScanTimer.interval = 500
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.warn("Network", "Error listing connections:", text)
|
||||
retryRefresh()
|
||||
root.forgettingNetwork = ""
|
||||
if (text.trim() && !text.includes("No profiles found")) {
|
||||
Logger.warn("Network", "Forget error: " + text)
|
||||
}
|
||||
// Still Trigger a scan even on error
|
||||
delayedScanTimer.interval = 500
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: scanProcess
|
||||
property var existingProfiles: ({})
|
||||
running: false
|
||||
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const networksMap = {}
|
||||
const lines = text.split("\n").filter(l => l.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(":")
|
||||
if (parts.length < 4)
|
||||
continue
|
||||
|
||||
const ssid = parts[0]
|
||||
const security = parts[1]
|
||||
const signalStr = parts[2]
|
||||
const inUse = parts[3]
|
||||
if (!ssid)
|
||||
continue
|
||||
|
||||
const signal = parseInt(signalStr) || 0
|
||||
const connected = inUse === "*"
|
||||
|
||||
// Update last connected if we find the connected network
|
||||
if (connected && adapter.lastConnected !== ssid) {
|
||||
adapter.lastConnected = ssid
|
||||
saveTimer.restart()
|
||||
}
|
||||
|
||||
// Merge with existing or create new
|
||||
if (!networksMap[ssid] || signal > networksMap[ssid].signal) {
|
||||
networksMap[ssid] = {
|
||||
"ssid": ssid,
|
||||
"security": security || "--",
|
||||
"signal": signal,
|
||||
"connected": connected,
|
||||
"existing": ssid in scanProcess.existingProfiles,
|
||||
"cached": ssid in adapter.knownNetworks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.networks = networksMap
|
||||
root.isLoading = false
|
||||
scanProcess.existingProfiles = {}
|
||||
|
||||
//Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`)
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.warn("Network", "Error scanning networks:", text)
|
||||
retryRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkEthernet
|
||||
running: false
|
||||
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.ethernet = text.split("\n").some(line => {
|
||||
const parts = line.split(":")
|
||||
return parts[1] === "ethernet" && parts[2] === "connected"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh timer
|
||||
Timer {
|
||||
interval: 30000 // 30 seconds
|
||||
running: Settings.data.network.wifiEnabled && !isLoading
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
// Only refresh if we should
|
||||
const now = Date.now()
|
||||
const timeSinceLastRefresh = now - adapter.lastRefresh
|
||||
|
||||
// Refresh if: connected, or it's been more than 30 seconds
|
||||
if (hasActiveConnection || timeSinceLastRefresh > 30000) {
|
||||
refreshNetworks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool hasActiveConnection: {
|
||||
return Object.values(networks).some(net => net.connected)
|
||||
}
|
||||
|
||||
// Menu state management
|
||||
function onMenuOpened() {
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
refreshNetworks()
|
||||
}
|
||||
}
|
||||
|
||||
function onMenuClosed() {
|
||||
// Clean up temporary states
|
||||
connectStatus = ""
|
||||
connectError = ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ Singleton {
|
|||
|
||||
// Signal when notification is received
|
||||
onNotification: function (notification) {
|
||||
// Always add notification to history
|
||||
root.addToHistory(notification)
|
||||
|
||||
// Check if notifications are suppressed
|
||||
if (Settings.data.notifications && Settings.data.notifications.suppressed) {
|
||||
// Still add to history but don't show notification
|
||||
root.addToHistory(notification)
|
||||
// Check if do-not-disturb is enabled
|
||||
if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -46,8 +46,6 @@ Singleton {
|
|||
|
||||
// Add to our model
|
||||
root.addNotification(notification)
|
||||
// Also add to history
|
||||
root.addToHistory(notification)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +107,15 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.notifications
|
||||
function onDoNotDisturbChanged() {
|
||||
const label = Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' enabled" : "'Do Not Disturb' disabled"
|
||||
const description = Settings.data.notifications.doNotDisturb ? "You'll find these notifications in your history." : "Showing all notifications."
|
||||
ToastService.showNotice(label, description)
|
||||
}
|
||||
}
|
||||
|
||||
// Function to add notification to model
|
||||
function addNotification(notification) {
|
||||
notificationModel.insert(0, {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Singleton {
|
|||
if (bytesPerSecond < 1024) {
|
||||
return bytesPerSecond.toFixed(0) + "B/s"
|
||||
} else if (bytesPerSecond < 1024 * 1024) {
|
||||
return (bytesPerSecond / 1024).toFixed(1) + "KB/s"
|
||||
return (bytesPerSecond / 1024).toFixed(0) + "KB/s"
|
||||
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s"
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -165,13 +165,21 @@ Singleton {
|
|||
"timestamp": Date.now()
|
||||
}
|
||||
|
||||
// If there's already a toast showing, instantly start hide animation and show new one
|
||||
if (isShowingToast) {
|
||||
// Instantly start hide animation of current toast
|
||||
for (var i = 0; i < allToasts.length; i++) {
|
||||
allToasts[i].hide()
|
||||
}
|
||||
// Clear the queue since we're showing the new toast immediately
|
||||
messageQueue = []
|
||||
}
|
||||
|
||||
// Add to queue
|
||||
messageQueue.push(toastData)
|
||||
|
||||
// Process queue if not currently showing a toast
|
||||
if (!isShowingToast) {
|
||||
processQueue()
|
||||
}
|
||||
// Always process immediately for instant display
|
||||
processQueue()
|
||||
}
|
||||
|
||||
// Process the message queue
|
||||
|
|
@ -181,11 +189,6 @@ Singleton {
|
|||
return
|
||||
}
|
||||
|
||||
if (isShowingToast) {
|
||||
// Wait for current toast to finish
|
||||
return
|
||||
}
|
||||
|
||||
var toastData = messageQueue.shift()
|
||||
isShowingToast = true
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Singleton {
|
|||
id: root
|
||||
|
||||
// Public properties
|
||||
property string baseVersion: "2.5.0"
|
||||
property string baseVersion: "2.6.0"
|
||||
property bool isDevelopment: true
|
||||
|
||||
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Compact circular statistic display used in the SidePanel
|
||||
// Compact circular statistic display using Layout management
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
|
|
@ -28,20 +29,20 @@ Rectangle {
|
|||
// Repaint gauge when the bound value changes
|
||||
onValueChanged: gauge.requestPaint()
|
||||
|
||||
Row {
|
||||
id: innerRow
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling * contentScale
|
||||
spacing: Style.marginS * scaling * contentScale
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
// Gauge with percentage label placed inside the open gap (right side)
|
||||
// Main gauge container
|
||||
Item {
|
||||
id: gaugeWrap
|
||||
anchors.verticalCenter: innerRow.verticalCenter
|
||||
width: 68 * scaling * contentScale
|
||||
height: 68 * scaling * contentScale
|
||||
id: gaugeContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: 68 * scaling * contentScale
|
||||
Layout.preferredHeight: 68 * scaling * contentScale
|
||||
|
||||
Canvas {
|
||||
id: gauge
|
||||
|
|
@ -84,15 +85,13 @@ Rectangle {
|
|||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Tiny circular badge for the icon, inside the right-side gap
|
||||
// Tiny circular badge for the icon, positioned using anchors within the gauge
|
||||
Rectangle {
|
||||
id: iconBadge
|
||||
width: 28 * scaling * contentScale
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: Color.mSurface
|
||||
// border.color: Color.mPrimary
|
||||
// border.width: Math.max(1, Style.borderS * scaling)
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: -6 * scaling * contentScale
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ Rectangle {
|
|||
id: textItem
|
||||
text: Time.time
|
||||
anchors.centerIn: parent
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,39 +8,34 @@ Rectangle {
|
|||
id: root
|
||||
|
||||
property color selectedColor: "#000000"
|
||||
property bool expanded: false
|
||||
|
||||
signal colorSelected(color color)
|
||||
signal colorCancelled
|
||||
|
||||
implicitWidth: expanded ? 320 * scaling : 150 * scaling
|
||||
implicitHeight: expanded ? 300 * scaling : 40 * scaling
|
||||
implicitWidth: 150 * scaling
|
||||
implicitHeight: 40 * scaling
|
||||
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
property var presetColors: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000", "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E"]
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsed view - just show current color
|
||||
// Minimized Look
|
||||
MouseArea {
|
||||
visible: !root.expanded
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expanded = true
|
||||
onClicked: {
|
||||
var dialog = Qt.createComponent("NColorPickerDialog.qml").createObject(root, {
|
||||
"selectedColor": selectedColor,
|
||||
"parent": Overlay.overlay
|
||||
})
|
||||
// Connect the dialog's signal to the picker's signal
|
||||
dialog.colorSelected.connect(function (color) {
|
||||
root.selectedColor = color
|
||||
root.colorSelected(color)
|
||||
})
|
||||
|
||||
dialog.open()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
|
@ -68,119 +63,4 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded view - color selection
|
||||
ColumnLayout {
|
||||
visible: root.expanded
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: "Select Color"
|
||||
font.weight: Style.fontWeightBold
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: root.expanded = false
|
||||
}
|
||||
}
|
||||
|
||||
// Preset colors grid
|
||||
Grid {
|
||||
columns: 9
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
model: root.presetColors
|
||||
|
||||
Rectangle {
|
||||
width: Math.round(29 * scaling)
|
||||
height: width
|
||||
radius: Style.radiusXS * scaling
|
||||
color: modelData
|
||||
border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline
|
||||
border.width: root.selectedColor === modelData ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedColor = modelData
|
||||
// root.colorSelected(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom color input
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NTextInput {
|
||||
id: hexInput
|
||||
label: "Hex Color"
|
||||
text: root.selectedColor.toString().toUpperCase()
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
Layout.minimumWidth: 100 * scaling
|
||||
onEditingFinished: {
|
||||
if (/^#[0-9A-F]{6}$/i.test(text)) {
|
||||
root.selectedColor = text
|
||||
root.colorSelected(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 32 * scaling
|
||||
Layout.preferredHeight: 32 * scaling
|
||||
radius: Layout.preferredWidth * 0.5
|
||||
color: root.selectedColor
|
||||
border.color: Color.mOutline
|
||||
border.width: 1
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.bottomMargin: 5 * scaling
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
} // Spacer
|
||||
|
||||
NButton {
|
||||
text: "Cancel"
|
||||
outlined: true
|
||||
customHeight: Style.baseWidgetSize * scaling
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
onClicked: {
|
||||
root.colorCancelled()
|
||||
root.expanded = false
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Apply"
|
||||
customHeight: Style.baseWidgetSize * scaling
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
onClicked: {
|
||||
root.colorSelected(root.selectedColor)
|
||||
root.expanded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
516
Widgets/NColorPickerDialog.qml
Normal file
516
Widgets/NColorPickerDialog.qml
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property color selectedColor: "#000000"
|
||||
property real currentHue: 0
|
||||
property real currentSaturation: 0
|
||||
|
||||
signal colorSelected(color color)
|
||||
|
||||
width: 580 * scaling
|
||||
height: {
|
||||
const h = scrollView.implicitHeight + padding * 2
|
||||
Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2)
|
||||
}
|
||||
padding: Style.marginXL * scaling
|
||||
|
||||
// Center popup in parent
|
||||
x: (parent.width - width) * 0.5
|
||||
y: (parent.height - height) * 0.5
|
||||
|
||||
modal: true
|
||||
clip: true
|
||||
|
||||
function rgbToHsv(r, g, b) {
|
||||
r /= 255
|
||||
g /= 255
|
||||
b /= 255
|
||||
var max = Math.max(r, g, b), min = Math.min(r, g, b)
|
||||
var h, s, v = max
|
||||
var d = max - min
|
||||
s = max === 0 ? 0 : d / max
|
||||
if (max === min) {
|
||||
h = 0
|
||||
} else {
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0)
|
||||
break
|
||||
case g:
|
||||
h = (b - r) / d + 2
|
||||
break
|
||||
case b:
|
||||
h = (r - g) / d + 4
|
||||
break
|
||||
}
|
||||
h /= 6
|
||||
}
|
||||
return [h * 360, s * 100, v * 100]
|
||||
}
|
||||
|
||||
function hsvToRgb(h, s, v) {
|
||||
h /= 360
|
||||
s /= 100
|
||||
v /= 100
|
||||
|
||||
var r, g, b
|
||||
var i = Math.floor(h * 6)
|
||||
var f = h * 6 - i
|
||||
var p = v * (1 - s)
|
||||
var q = v * (1 - f * s)
|
||||
var t = v * (1 - (1 - f) * s)
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
r = v
|
||||
g = t
|
||||
b = p
|
||||
break
|
||||
case 1:
|
||||
r = q
|
||||
g = v
|
||||
b = p
|
||||
break
|
||||
case 2:
|
||||
r = p
|
||||
g = v
|
||||
b = t
|
||||
break
|
||||
case 3:
|
||||
r = p
|
||||
g = q
|
||||
b = v
|
||||
break
|
||||
case 4:
|
||||
r = t
|
||||
g = p
|
||||
b = v
|
||||
break
|
||||
case 5:
|
||||
r = v
|
||||
g = p
|
||||
b = q
|
||||
break
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusS * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
width: scrollView.availableWidth
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
text: "palette"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Color Picker"
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Color preview section
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 80 * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: root.selectedColor
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: root.selectedColor.toString().toUpperCase()
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Font.Bold
|
||||
color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round(
|
||||
root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hex input
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: "Hex Color"
|
||||
description: "Enter a hexadecimal color code"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
text: root.selectedColor.toString().toUpperCase()
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
Layout.fillWidth: true
|
||||
onEditingFinished: {
|
||||
if (/^#[0-9A-F]{6}$/i.test(text)) {
|
||||
root.selectedColor = text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RGB sliders section
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: slidersSection.implicitHeight + Style.marginL * scaling * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: slidersSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: "RGB Values"
|
||||
description: "Adjust red, green, blue, and brightness values"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "R"
|
||||
font.weight: Font.Bold
|
||||
Layout.preferredWidth: 20 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
id: redSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 255
|
||||
value: Math.round(root.selectedColor.r * 255)
|
||||
onMoved: {
|
||||
root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1)
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(redSlider.value)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "G"
|
||||
font.weight: Font.Bold
|
||||
Layout.preferredWidth: 20 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
id: greenSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 255
|
||||
value: Math.round(root.selectedColor.g * 255)
|
||||
onMoved: {
|
||||
root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1)
|
||||
// Update stored hue and saturation when RGB changes
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(greenSlider.value)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "B"
|
||||
font.weight: Font.Bold
|
||||
Layout.preferredWidth: 20 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
id: blueSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 255
|
||||
value: Math.round(root.selectedColor.b * 255)
|
||||
onMoved: {
|
||||
root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1)
|
||||
// Update stored hue and saturation when RGB changes
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(blueSlider.value)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "Brightness"
|
||||
font.weight: Font.Bold
|
||||
Layout.preferredWidth: 80 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
id: brightnessSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
value: {
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
return hsv[2]
|
||||
}
|
||||
onMoved: {
|
||||
var hue = root.currentHue
|
||||
var saturation = root.currentSaturation
|
||||
|
||||
if (hue === 0 && saturation === 0) {
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
hue = hsv[0]
|
||||
saturation = hsv[1]
|
||||
root.currentHue = hue
|
||||
root.currentSaturation = saturation
|
||||
}
|
||||
|
||||
var rgb = root.hsvToRgb(hue, saturation, value)
|
||||
root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1)
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(brightnessSlider.value) + "%"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: themePalette.implicitHeight + Style.marginL * scaling * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: themePalette
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NLabel {
|
||||
label: "Theme Colors"
|
||||
description: "Quick access to your theme's color palette"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Flow {
|
||||
spacing: 6 * scaling
|
||||
Layout.fillWidth: true
|
||||
flow: Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000"]
|
||||
|
||||
Rectangle {
|
||||
width: 24 * scaling
|
||||
height: 24 * scaling
|
||||
radius: 4 * scaling
|
||||
color: modelData
|
||||
border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline
|
||||
border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedColor = modelData
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: genericPalette.implicitHeight + Style.marginL * scaling * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: genericPalette
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NLabel {
|
||||
label: "Colors Palette"
|
||||
description: "Choose from a wide range of predefined colors"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 6 * scaling
|
||||
flow: Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", "#E74C3C", "#E67E22", "#F1C40F", "#2ECC71", "#1ABC9C", "#3498DB", "#2980B9", "#9B59B6", "#34495E", "#2C3E50", "#95A5A6", "#7F8C8D", "#FFFFFF", "#000000"]
|
||||
|
||||
Rectangle {
|
||||
width: 24 * scaling
|
||||
height: 24 * scaling
|
||||
radius: Style.radiusXXS * scaling
|
||||
color: modelData
|
||||
border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline
|
||||
border.width: Math.max(
|
||||
1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedColor = modelData
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20 * scaling
|
||||
Layout.bottomMargin: 20 * scaling
|
||||
spacing: 10 * scaling
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: cancelButton
|
||||
text: "Cancel"
|
||||
icon: "close"
|
||||
outlined: cancelButton.hovered ? false : true
|
||||
customHeight: 36 * scaling
|
||||
customWidth: 100 * scaling
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Apply"
|
||||
icon: "check"
|
||||
customHeight: 36 * scaling
|
||||
customWidth: 100 * scaling
|
||||
onClicked: {
|
||||
root.colorSelected(root.selectedColor)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ Rectangle {
|
|||
property string tooltipText
|
||||
property bool enabled: true
|
||||
property bool hovering: false
|
||||
property real fontPointSize: Style.fontSizeM
|
||||
|
||||
property color colorBg: Color.mSurfaceVariant
|
||||
property color colorFg: Color.mPrimary
|
||||
|
|
@ -41,7 +40,7 @@ Rectangle {
|
|||
|
||||
NIcon {
|
||||
text: root.icon
|
||||
font.pointSize: root.fontPointSize * scaling
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: root.hovering ? colorFgHover : colorFg
|
||||
// Center horizontally
|
||||
x: (root.width - width) / 2
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import QtQuick.Layouts
|
|||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
// Input and button row
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
// Public properties
|
||||
|
|
@ -21,57 +22,35 @@ ColumnLayout {
|
|||
|
||||
// Internal properties
|
||||
property real scaling: 1.0
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Label
|
||||
NText {
|
||||
text: root.label
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
NTextInput {
|
||||
id: textInput
|
||||
label: root.label
|
||||
description: root.description
|
||||
placeholderText: root.placeholderText
|
||||
text: root.text
|
||||
onEditingFinished: {
|
||||
root.text = text
|
||||
root.editingFinished()
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Description
|
||||
NText {
|
||||
text: root.description
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NButton {
|
||||
Layout.fillWidth: false
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
// Input and button row
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.fillWidth: true
|
||||
text: root.actionButtonText
|
||||
icon: root.actionButtonIcon
|
||||
backgroundColor: Color.mSecondary
|
||||
textColor: Color.mOnSecondary
|
||||
hoverColor: Color.mTertiary
|
||||
pressColor: Color.mPrimary
|
||||
enabled: root.actionButtonEnabled
|
||||
|
||||
NTextInput {
|
||||
id: textInput
|
||||
placeholderText: root.placeholderText
|
||||
text: root.text
|
||||
onEditingFinished: {
|
||||
root.text = text
|
||||
root.editingFinished()
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: root.actionButtonText
|
||||
icon: root.actionButtonIcon
|
||||
backgroundColor: Color.mSecondary
|
||||
textColor: Color.mOnSecondary
|
||||
hoverColor: Color.mTertiary
|
||||
pressColor: Color.mPrimary
|
||||
enabled: root.actionButtonEnabled
|
||||
Layout.fillWidth: false
|
||||
onClicked: {
|
||||
root.actionClicked()
|
||||
}
|
||||
onClicked: {
|
||||
root.actionClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,8 +231,7 @@ Item {
|
|||
root.clicked()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
}
|
||||
else if (mouse.button === Qt.MiddleButton) {
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ ColumnLayout {
|
|||
property string description: ""
|
||||
property bool readOnly: false
|
||||
property bool enabled: true
|
||||
property int inputMaxWidth: Math.round(420 * scaling)
|
||||
property color labelColor: Color.mOnSurface
|
||||
property color descriptionColor: Color.mOnSurfaceVariant
|
||||
property string fontFamily: Settings.data.ui.fontDefault
|
||||
|
|
@ -26,7 +25,6 @@ ColumnLayout {
|
|||
signal editingFinished
|
||||
|
||||
spacing: Style.marginS * scaling
|
||||
implicitHeight: frame.height
|
||||
|
||||
NLabel {
|
||||
label: root.label
|
||||
|
|
@ -34,6 +32,7 @@ ColumnLayout {
|
|||
labelColor: root.labelColor
|
||||
descriptionColor: root.descriptionColor
|
||||
visible: root.label !== "" || root.description !== ""
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Container
|
||||
|
|
@ -42,50 +41,48 @@ ColumnLayout {
|
|||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 80 * scaling
|
||||
Layout.maximumWidth: root.inputMaxWidth
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
|
||||
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.color: input.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Focus ring
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: frame.radius
|
||||
color: Color.transparent
|
||||
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
|
||||
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
TextField {
|
||||
id: input
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM * scaling
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
TextField {
|
||||
id: input
|
||||
Layout.fillWidth: true
|
||||
echoMode: TextInput.Normal
|
||||
readOnly: root.readOnly
|
||||
enabled: root.enabled
|
||||
color: Color.mOnSurface
|
||||
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
|
||||
background: null
|
||||
font.family: fontFamily
|
||||
font.pointSize: fontSize
|
||||
font.weight: fontWeight
|
||||
onEditingFinished: root.editingFinished()
|
||||
}
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
echoMode: TextInput.Normal
|
||||
readOnly: root.readOnly
|
||||
enabled: root.enabled
|
||||
color: Color.mOnSurface
|
||||
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
|
||||
|
||||
selectByMouse: true
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
background: null
|
||||
|
||||
font.family: root.fontFamily
|
||||
font.pointSize: root.fontSize
|
||||
font.weight: root.fontWeight
|
||||
|
||||
onEditingFinished: root.editingFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,16 @@ Item {
|
|||
// NToast updates its scaling when showing.
|
||||
scaling = ScalingService.getScreenScale(screen)
|
||||
|
||||
// Stop any running animations and reset state
|
||||
showAnimation.stop()
|
||||
hideAnimation.stop()
|
||||
autoHideTimer.stop()
|
||||
|
||||
// Ensure we start from the hidden position
|
||||
y = hiddenY
|
||||
visible = true
|
||||
|
||||
// Start the show animation
|
||||
showAnimation.start()
|
||||
if (duration > 0 && !persistent) {
|
||||
autoHideTimer.start()
|
||||
|
|
@ -81,7 +90,6 @@ Item {
|
|||
|
||||
// Main toast container
|
||||
Rectangle {
|
||||
id: container
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusL * scaling
|
||||
|
||||
|
|
@ -137,43 +145,41 @@ Item {
|
|||
}
|
||||
|
||||
// Label and description
|
||||
Column {
|
||||
id: textColumn
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NText {
|
||||
id: labelText
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
id: descriptionText
|
||||
Layout.fillWidth: true
|
||||
text: root.description
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Close button (only if persistent or manual dismiss needed)
|
||||
NIconButton {
|
||||
id: closeButton
|
||||
icon: "close"
|
||||
visible: root.persistent || root.duration === 0
|
||||
|
||||
color: Color.mOnSurface
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.mOutline
|
||||
|
||||
fontPointSize: Style.fontSizeM * scaling
|
||||
sizeRatio: 0.8
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue