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 {
|
.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;
|
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -278,12 +278,14 @@ Singleton {
|
||||||
property string position: "center"
|
property string position: "center"
|
||||||
property real backgroundOpacity: 1.0
|
property real backgroundOpacity: 1.0
|
||||||
property list<string> pinnedExecs: []
|
property list<string> pinnedExecs: []
|
||||||
|
property bool useApp2Unit: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// dock
|
// dock
|
||||||
property JsonObject dock: JsonObject {
|
property JsonObject dock: JsonObject {
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
property bool exclusive: false
|
property bool exclusive: false
|
||||||
|
property real backgroundOpacity: 1.0
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,6 +297,7 @@ Singleton {
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
property JsonObject notifications: JsonObject {
|
property JsonObject notifications: JsonObject {
|
||||||
|
property bool doNotDisturb: false
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ Singleton {
|
||||||
property int fontWeightBold: 700
|
property int fontWeightBold: 700
|
||||||
|
|
||||||
// Radii
|
// Radii
|
||||||
|
property int radiusXXS: 4 * Settings.data.general.radiusRatio
|
||||||
property int radiusXS: 8 * Settings.data.general.radiusRatio
|
property int radiusXS: 8 * Settings.data.general.radiusRatio
|
||||||
property int radiusS: 12 * Settings.data.general.radiusRatio
|
property int radiusS: 12 * Settings.data.general.radiusRatio
|
||||||
property int radiusM: 16 * 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
|
// 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) {
|
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||||
const hours = Math.floor(totalSeconds / 3600)
|
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
|
||||||
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
|
return '0s'
|
||||||
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
|
}
|
||||||
|
|
||||||
var str = ""
|
// Floor the input to handle decimal seconds
|
||||||
if (hours) {
|
totalSeconds = Math.floor(totalSeconds)
|
||||||
str += hours.toString() + "h"
|
|
||||||
}
|
const days = Math.floor(totalSeconds / 86400)
|
||||||
if (minutes) {
|
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||||
str += minutes.toString() + "m"
|
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) {
|
if (!hours && !minutes) {
|
||||||
str += seconds.toString() + "s"
|
parts.push(`${seconds}s`)
|
||||||
}
|
}
|
||||||
return str
|
|
||||||
|
return parts.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ Variants {
|
||||||
transitionProgress = 0.0
|
transitionProgress = 0.0
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
currentWallpaper.asynchronous = true
|
currentWallpaper.asynchronous = true
|
||||||
}, 100)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,45 @@ import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
readonly property real minWidth: 160
|
readonly property real minWidth: 160
|
||||||
readonly property real maxWidth: 400
|
readonly property real maxWidth: 400
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
visible: getTitle() !== ""
|
visible: getTitle() !== ""
|
||||||
|
|
||||||
function 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 : ""
|
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppIcon() {
|
function getAppIcon() {
|
||||||
|
// Try CompositorService first
|
||||||
const focusedWindow = CompositorService.getFocusedWindow()
|
const focusedWindow = CompositorService.getFocusedWindow()
|
||||||
if (!focusedWindow || !focusedWindow.appId)
|
if (focusedWindow && focusedWindow.appId) {
|
||||||
return ""
|
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 {
|
NText {
|
||||||
id: fullTitleMetrics
|
id: fullTitleMetrics
|
||||||
visible: false
|
visible: false
|
||||||
|
|
@ -43,15 +50,13 @@ Row {
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
// Let the Rectangle size itself based on its content (the Row)
|
id: windowTitleRect
|
||||||
visible: root.visible
|
visible: root.visible
|
||||||
width: row.width + Style.marginM * 2 * scaling
|
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: mainContainer
|
id: mainContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -59,16 +64,16 @@ Row {
|
||||||
anchors.rightMargin: Style.marginS * scaling
|
anchors.rightMargin: Style.marginS * scaling
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: row
|
id: contentLayout
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.centerIn: parent
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
// Window icon
|
// Window icon
|
||||||
Item {
|
Item {
|
||||||
width: Style.fontSizeL * scaling * 1.2
|
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
|
||||||
height: Style.fontSizeL * scaling * 1.2
|
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
|
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
|
|
@ -83,26 +88,24 @@ Row {
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
Layout.preferredWidth: {
|
||||||
// For short titles, show full. For long titles, truncate and expand on hover
|
|
||||||
width: {
|
|
||||||
if (mouseArea.containsMouse) {
|
if (mouseArea.containsMouse) {
|
||||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||||
} else {
|
} else {
|
||||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
text: getTitle()
|
text: getTitle()
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
font.weight: Style.fontWeightMedium
|
font.weight: Style.fontWeightMedium
|
||||||
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
|
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
color: Color.mSecondary
|
color: Color.mSecondary
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Behavior on width {
|
Behavior on Layout.preferredWidth {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Style.animationSlow
|
duration: Style.animationSlow
|
||||||
easing.type: Easing.InOutCubic
|
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 {
|
NTooltip {
|
||||||
id: tooltip
|
id: tooltip
|
||||||
text: Time.dateString
|
text: `${Time.dateString}.`
|
||||||
target: clock
|
target: clock
|
||||||
positionAbove: Settings.data.bar.position === "bottom"
|
positionAbove: Settings.data.bar.position === "bottom"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,13 @@ NIconButton {
|
||||||
} else {
|
} else {
|
||||||
var lines = []
|
var lines = []
|
||||||
if (userLeftClickExec !== "") {
|
if (userLeftClickExec !== "") {
|
||||||
lines.push(`Left click: <i>${userLeftClickExec}</i>`)
|
lines.push(`Left click: <i>${userLeftClickExec}</i>.`)
|
||||||
}
|
}
|
||||||
if (userRightClickExec !== "") {
|
if (userRightClickExec !== "") {
|
||||||
lines.push(`Right click: <i>${userRightClickExec}</i>`)
|
lines.push(`Right click: <i>${userRightClickExec}</i>.`)
|
||||||
}
|
}
|
||||||
if (userMiddleClickExec !== "") {
|
if (userMiddleClickExec !== "") {
|
||||||
lines.push(`Middle click: <i>${userMiddleClickExec}</i>`)
|
lines.push(`Middle click: <i>${userMiddleClickExec}</i>.`)
|
||||||
}
|
}
|
||||||
return lines.join("<br/>")
|
return lines.join("<br/>")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
|
@ -6,7 +7,7 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Row {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
|
|
@ -18,12 +19,13 @@ Row {
|
||||||
// Use the shared service for keyboard layout
|
// Use the shared service for keyboard layout
|
||||||
property string currentLayout: KeyboardLayoutService.currentLayout
|
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||||
|
|
||||||
width: pill.width
|
implicitWidth: pill.width
|
||||||
height: pill.height
|
implicitHeight: pill.height
|
||||||
|
|
||||||
NPill {
|
NPill {
|
||||||
id: pill
|
id: pill
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||||
icon: "keyboard_alt"
|
icon: "keyboard_alt"
|
||||||
iconCircleColor: Color.mPrimary
|
iconCircleColor: Color.mPrimary
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
|
|
@ -15,10 +15,10 @@ Row {
|
||||||
readonly property real minWidth: 160
|
readonly property real minWidth: 160
|
||||||
readonly property real maxWidth: 400
|
readonly property real maxWidth: 400
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
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() {
|
function getTitle() {
|
||||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||||
|
|
@ -35,15 +35,13 @@ Row {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: mediaMini
|
id: mediaMini
|
||||||
|
|
||||||
// Let the Rectangle size itself based on its content (the Row)
|
Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling
|
||||||
width: row.width + 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)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
||||||
Item {
|
Item {
|
||||||
id: anchor
|
id: anchor
|
||||||
|
|
@ -61,7 +59,7 @@ Row {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
|
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
|
||||||
&& MediaService.isPlaying && MediaService.trackLength > 0
|
&& MediaService.isPlaying
|
||||||
z: 0
|
z: 0
|
||||||
|
|
||||||
sourceComponent: LinearSpectrum {
|
sourceComponent: LinearSpectrum {
|
||||||
|
|
@ -71,42 +69,42 @@ Row {
|
||||||
fillColor: Color.mOnSurfaceVariant
|
fillColor: Color.mOnSurfaceVariant
|
||||||
opacity: 0.4
|
opacity: 0.4
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
|
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
|
||||||
&& MediaService.isPlaying && MediaService.trackLength > 0
|
&& MediaService.isPlaying
|
||||||
z: 0
|
z: 0
|
||||||
|
|
||||||
sourceComponent: MirroredSpectrum {
|
sourceComponent: MirroredSpectrum {
|
||||||
width: mainContainer.width - Style.marginS * scaling
|
width: mainContainer.width - Style.marginS * scaling
|
||||||
height: mainContainer.height - Style.marginS * scaling
|
height: mainContainer.height - Style.marginS * scaling
|
||||||
values: CavaService.values
|
values: CavaService.values
|
||||||
fillColor: Color.mOnSurfaceVariant
|
fillColor: Color.mOnSurfaceVariant
|
||||||
opacity: 0.4
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Loader {
|
||||||
id: row
|
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
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
z: 1 // Above the visualizer
|
z: 1 // Above the visualizer
|
||||||
|
|
@ -116,17 +114,18 @@ Row {
|
||||||
text: MediaService.isPlaying ? "pause" : "play_arrow"
|
text: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
|
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
ColumnLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
visible: Settings.data.audio.showMiniplayerAlbumArt
|
visible: Settings.data.audio.showMiniplayerAlbumArt
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: Math.round(18 * scaling)
|
Layout.preferredWidth: Math.round(18 * scaling)
|
||||||
height: Math.round(18 * scaling)
|
Layout.preferredHeight: Math.round(18 * scaling)
|
||||||
|
|
||||||
NImageCircled {
|
NImageCircled {
|
||||||
id: trackArt
|
id: trackArt
|
||||||
|
|
@ -142,23 +141,23 @@ Row {
|
||||||
NText {
|
NText {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
|
||||||
// For short titles, show full. For long titles, truncate and expand on hover
|
Layout.preferredWidth: {
|
||||||
width: {
|
|
||||||
if (mouseArea.containsMouse) {
|
if (mouseArea.containsMouse) {
|
||||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||||
} else {
|
} else {
|
||||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
text: getTitle()
|
text: getTitle()
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
font.weight: Style.fontWeightMedium
|
font.weight: Style.fontWeightMedium
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
color: Color.mTertiary
|
color: Color.mTertiary
|
||||||
|
|
||||||
Behavior on width {
|
Behavior on Layout.preferredWidth {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Style.animationSlow
|
duration: Style.animationSlow
|
||||||
easing.type: Easing.InOutCubic
|
easing.type: Easing.InOutCubic
|
||||||
|
|
@ -205,10 +204,10 @@ Row {
|
||||||
text: {
|
text: {
|
||||||
var str = ""
|
var str = ""
|
||||||
if (MediaService.canGoNext) {
|
if (MediaService.canGoNext) {
|
||||||
str += "Right click for next\n"
|
str += "Right click for next.\n"
|
||||||
}
|
}
|
||||||
if (MediaService.canGoPrevious) {
|
if (MediaService.canGoPrevious) {
|
||||||
str += "Middle click for previous\n"
|
str += "Middle click for previous."
|
||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ Item {
|
||||||
AudioService.setInputMuted(!AudioService.inputMuted)
|
AudioService.setInputMuted(!AudioService.inputMuted)
|
||||||
}
|
}
|
||||||
onMiddleClicked: {
|
onMiddleClicked: {
|
||||||
Quickshell.execDetached(["pwvucontrol"]);
|
Quickshell.execDetached(["pwvucontrol"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ NIconButton {
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
|
||||||
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
|
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
|
onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
|
||||||
|
|
||||||
onRightClicked: {
|
onRightClicked: {
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,14 @@ NIconButton {
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
|
|
||||||
sizeRatio: 0.8
|
sizeRatio: 0.8
|
||||||
icon: "notifications"
|
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications"
|
||||||
tooltipText: "Notification history"
|
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
|
colorBg: Color.mSurfaceVariant
|
||||||
colorFg: Color.mOnSurface
|
colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface
|
||||||
colorBorder: Color.transparent
|
colorBorder: Color.transparent
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
|
||||||
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this)
|
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
|
property real scaling: 1.0
|
||||||
|
|
||||||
icon: Settings.data.bar.useDistroLogo ? "" : "widgets"
|
icon: Settings.data.bar.useDistroLogo ? "" : "widgets"
|
||||||
tooltipText: "Open side panel"
|
tooltipText: "Open side panel."
|
||||||
sizeRatio: 0.8
|
sizeRatio: 0.8
|
||||||
|
|
||||||
colorBg: Color.mSurfaceVariant
|
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
|
||||||
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
// Let the Rectangle size itself based on its content (the Row)
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
width: row.width + Style.marginM * scaling * 2
|
Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
RowLayout {
|
||||||
|
id: mainLayout
|
||||||
Item {
|
|
||||||
id: mainContainer
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: Style.marginS * scaling
|
anchors.leftMargin: Style.marginS * scaling
|
||||||
anchors.rightMargin: Style.marginS * scaling
|
anchors.rightMargin: Style.marginS * scaling
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
Row {
|
// CPU Usage Component
|
||||||
id: row
|
RowLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
id: cpuUsageLayout
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginXS * scaling
|
||||||
Row {
|
Layout.alignment: Qt.AlignVCenter
|
||||||
id: cpuUsageLayout
|
|
||||||
spacing: Style.marginXS * scaling
|
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
id: cpuUsageIcon
|
id: cpuUsageIcon
|
||||||
text: "speed"
|
text: "speed"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU Temperature Component
|
NText {
|
||||||
Row {
|
id: cpuUsageText
|
||||||
id: cpuTempLayout
|
text: `${SystemStatService.cpuUsage}%`
|
||||||
// spacing is thin here to compensate for the vertical thermometer icon
|
font.family: Settings.data.ui.fontFixed
|
||||||
spacing: Style.marginXXS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NIcon {
|
// CPU Temperature Component
|
||||||
text: "thermometer"
|
RowLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
id: cpuTempLayout
|
||||||
}
|
// spacing is thin here to compensate for the vertical thermometer icon
|
||||||
|
spacing: Style.marginXXS * scaling
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
NText {
|
NIcon {
|
||||||
text: `${SystemStatService.cpuTemp}°C`
|
text: "thermometer"
|
||||||
font.family: Settings.data.ui.fontFixed
|
Layout.alignment: Qt.AlignVCenter
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
|
||||||
font.weight: Style.fontWeightMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
color: Color.mPrimary
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory Usage Component
|
NText {
|
||||||
Row {
|
text: `${SystemStatService.cpuTemp}°C`
|
||||||
id: memoryUsageLayout
|
font.family: Settings.data.ui.fontFixed
|
||||||
spacing: Style.marginXS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NIcon {
|
// Memory Usage Component
|
||||||
text: "memory"
|
RowLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
id: memoryUsageLayout
|
||||||
}
|
spacing: Style.marginXS * scaling
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
NText {
|
NIcon {
|
||||||
text: `${SystemStatService.memoryUsageGb}G`
|
text: "memory"
|
||||||
font.family: Settings.data.ui.fontFixed
|
Layout.alignment: Qt.AlignVCenter
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
|
||||||
font.weight: Style.fontWeightMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
color: Color.mPrimary
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network Download Speed Component
|
NText {
|
||||||
Row {
|
text: `${SystemStatService.memoryUsageGb}G`
|
||||||
id: networkDownloadLayout
|
font.family: Settings.data.ui.fontFixed
|
||||||
spacing: Style.marginXS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
visible: Settings.data.bar.showNetworkStats
|
font.weight: Style.fontWeightMedium
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NIcon {
|
// Network Download Speed Component
|
||||||
text: "download"
|
RowLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
id: networkDownloadLayout
|
||||||
}
|
spacing: Style.marginXS * scaling
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
visible: Settings.data.bar.showNetworkStats
|
||||||
|
|
||||||
NText {
|
NIcon {
|
||||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
text: "download"
|
||||||
font.family: Settings.data.ui.fontFixed
|
Layout.alignment: Qt.AlignVCenter
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
|
||||||
font.weight: Style.fontWeightMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
color: Color.mPrimary
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network Upload Speed Component
|
NText {
|
||||||
Row {
|
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||||
id: networkUploadLayout
|
font.family: Settings.data.ui.fontFixed
|
||||||
spacing: Style.marginXS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
visible: Settings.data.bar.showNetworkStats
|
font.weight: Style.fontWeightMedium
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NIcon {
|
// Network Upload Speed Component
|
||||||
text: "upload"
|
RowLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
id: networkUploadLayout
|
||||||
}
|
spacing: Style.marginXS * scaling
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
visible: Settings.data.bar.showNetworkStats
|
||||||
|
|
||||||
NText {
|
NIcon {
|
||||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
text: "upload"
|
||||||
font.family: Settings.data.ui.fontFixed
|
Layout.alignment: Qt.AlignVCenter
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
}
|
||||||
font.weight: Style.fontWeightMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
NText {
|
||||||
verticalAlignment: Text.AlignVCenter
|
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||||
color: Color.mPrimary
|
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
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
|
@ -17,15 +18,14 @@ Rectangle {
|
||||||
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
|
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
|
||||||
|
|
||||||
// Always visible when there are toplevels
|
// 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)
|
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: taskbarRow
|
id: taskbarLayout
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.centerIn: parent
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Style.marginXXS * root.scaling
|
spacing: Style.marginXXS * root.scaling
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|
@ -35,8 +35,10 @@ Rectangle {
|
||||||
required property Toplevel modelData
|
required property Toplevel modelData
|
||||||
property Toplevel toplevel: modelData
|
property Toplevel toplevel: modelData
|
||||||
property bool isActive: ToplevelManager.activeToplevel === 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 {
|
Rectangle {
|
||||||
id: iconBackground
|
id: iconBackground
|
||||||
|
|
@ -89,7 +91,7 @@ Rectangle {
|
||||||
|
|
||||||
NTooltip {
|
NTooltip {
|
||||||
id: taskbarTooltip
|
id: taskbarTooltip
|
||||||
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App"
|
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
|
||||||
target: taskbarItem
|
target: taskbarItem
|
||||||
positionAbove: Settings.data.bar.position === "bottom"
|
positionAbove: Settings.data.bar.position === "bottom"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,26 +26,26 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: SystemTray.items.values.length > 0
|
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)
|
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: tray
|
id: trayLayout
|
||||||
|
anchors.centerIn: parent
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: repeater
|
id: repeater
|
||||||
model: SystemTray.items
|
model: SystemTray.items
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: itemSize
|
Layout.preferredWidth: itemSize
|
||||||
height: itemSize
|
Layout.preferredHeight: itemSize
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
visible: modelData
|
visible: modelData
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
|
|
@ -146,13 +146,14 @@ Rectangle {
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
PanelService.willOpenPanel(trayPanel)
|
PanelService.willOpenPanel(trayPanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
visible = false
|
visible = false
|
||||||
trayMenu.item.hideMenu()
|
if (trayMenu.item) {
|
||||||
|
trayMenu.item.hideMenu()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clicking outside of the rectangle to close
|
// Clicking outside of the rectangle to close
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ Item {
|
||||||
collapsedIconColor: Color.mOnSurface
|
collapsedIconColor: Color.mOnSurface
|
||||||
autoHide: false // Important to be false so we can hover as long as we want
|
autoHide: false // Important to be false so we can hover as long as we want
|
||||||
text: Math.floor(AudioService.volume * 100) + "%"
|
text: Math.floor(AudioService.volume * 100) + "%"
|
||||||
tooltipText: "Volume: " + Math.round(
|
tooltipText: "Volume: " + Math.round(AudioService.volume * 100)
|
||||||
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
|
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
|
||||||
|
|
||||||
onWheel: function (delta) {
|
onWheel: function (delta) {
|
||||||
wheelAccumulator += delta
|
wheelAccumulator += delta
|
||||||
|
|
@ -85,7 +85,7 @@ Item {
|
||||||
AudioService.setMuted(!AudioService.muted)
|
AudioService.setMuted(!AudioService.muted)
|
||||||
}
|
}
|
||||||
onMiddleClicked: {
|
onMiddleClicked: {
|
||||||
Quickshell.execDetached(["pwvucontrol"]);
|
Quickshell.execDetached(["pwvucontrol"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,8 @@ NIconButton {
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
|
|
||||||
visible: Settings.data.network.wifiEnabled
|
|
||||||
|
|
||||||
sizeRatio: 0.8
|
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
|
colorBg: Color.mSurfaceVariant
|
||||||
colorFg: Color.mOnSurface
|
colorFg: Color.mOnSurface
|
||||||
colorBorder: Color.transparent
|
colorBorder: Color.transparent
|
||||||
|
|
@ -32,7 +22,7 @@ NIconButton {
|
||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
try {
|
try {
|
||||||
if (NetworkService.ethernet) {
|
if (NetworkService.ethernetConnected) {
|
||||||
return "lan"
|
return "lan"
|
||||||
}
|
}
|
||||||
let connected = false
|
let connected = false
|
||||||
|
|
@ -46,10 +36,10 @@ NIconButton {
|
||||||
}
|
}
|
||||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error("WiFi", "Error getting icon:", error)
|
Logger.error("Wi-Fi", "Error getting icon:", error)
|
||||||
return "signal_wifi_bad"
|
return "signal_wifi_bad"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltipText: "Network / Wi-Fi"
|
tooltipText: "Network / Wi-Fi."
|
||||||
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
|
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import qs.Services
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ShellScreen screen: null
|
property ShellScreen screen
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
|
|
||||||
property bool isDestroying: false
|
property bool isDestroying: false
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ Variants {
|
||||||
|
|
||||||
screen: modelData
|
screen: modelData
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "noctalia-dock"
|
||||||
|
|
||||||
property bool autoHide: Settings.data.dock.autoHide
|
property bool autoHide: Settings.data.dock.autoHide
|
||||||
property bool hidden: autoHide
|
property bool hidden: autoHide
|
||||||
property int hideDelay: 500
|
property int hideDelay: 500
|
||||||
|
|
@ -128,9 +130,9 @@ Variants {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: dockContainer
|
id: dockContainer
|
||||||
width: dock.width + 48 * scaling
|
width: dockLayout.implicitWidth + 48 * scaling
|
||||||
height: iconSize * 1.4 * scaling
|
height: iconSize * 1.4 * scaling
|
||||||
color: Color.mSurface
|
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.bottomMargin: dockSpacing
|
anchors.bottomMargin: dockSpacing
|
||||||
|
|
@ -176,7 +178,7 @@ Variants {
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: dock
|
id: dock
|
||||||
width: runningAppsRow.width
|
width: dockLayout.implicitWidth
|
||||||
height: parent.height - (20 * scaling)
|
height: parent.height - (20 * scaling)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
|
@ -192,10 +194,10 @@ Variants {
|
||||||
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
|
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: runningAppsRow
|
id: dockLayout
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
height: parent.height
|
Layout.preferredHeight: parent.height
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|
@ -203,8 +205,10 @@ Variants {
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
id: appButton
|
id: appButton
|
||||||
width: iconSize * scaling
|
Layout.preferredWidth: iconSize * scaling
|
||||||
height: iconSize * scaling
|
Layout.preferredHeight: iconSize * scaling
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
radius: Style.radiusM * scaling
|
radius: Style.radiusM * scaling
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ Item {
|
||||||
function toggleHistory() {
|
function toggleHistory() {
|
||||||
notificationHistoryPanel.toggle(getActiveScreen())
|
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
|
anchors.margins: Style.marginL * scaling
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
Item {
|
NTextInput {
|
||||||
id: searchInputWrap
|
id: searchInput
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
|
|
||||||
|
|
||||||
NTextInput {
|
fontSize: Style.fontSizeL * scaling
|
||||||
id: searchInput
|
fontWeight: Style.fontWeightSemiBold
|
||||||
anchors.fill: parent
|
|
||||||
inputMaxWidth: Number.MAX_SAFE_INTEGER
|
|
||||||
|
|
||||||
fontSize: Style.fontSizeL * scaling
|
text: searchText
|
||||||
fontWeight: Style.fontWeightSemiBold
|
placeholderText: "Search entries... or use > for commands"
|
||||||
|
|
||||||
text: searchText
|
onTextChanged: searchText = text
|
||||||
placeholderText: "Search entries... or use > for commands"
|
|
||||||
|
|
||||||
onTextChanged: searchText = text
|
Component.onCompleted: {
|
||||||
|
if (searchInput.inputItem && searchInput.inputItem.visible) {
|
||||||
|
searchInput.inputItem.forceActiveFocus()
|
||||||
|
|
||||||
Component.onCompleted: {
|
// Override the TextField's default Home/End behavior
|
||||||
if (searchInput.inputItem && searchInput.inputItem.visible) {
|
searchInput.inputItem.Keys.priority = Keys.BeforeItem
|
||||||
searchInput.inputItem.forceActiveFocus()
|
searchInput.inputItem.Keys.onPressed.connect(function (event) {
|
||||||
|
// Intercept Home and End BEFORE the TextField handles them
|
||||||
// Override the TextField's default Home/End behavior
|
if (event.key === Qt.Key_Home) {
|
||||||
searchInput.inputItem.Keys.priority = Keys.BeforeItem
|
ui.selectFirst()
|
||||||
searchInput.inputItem.Keys.onPressed.connect(function (event) {
|
event.accepted = true
|
||||||
// Intercept Home and End BEFORE the TextField handles them
|
return
|
||||||
if (event.key === Qt.Key_Home) {
|
} else if (event.key === Qt.Key_End) {
|
||||||
ui.selectFirst()
|
ui.selectLast()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
return
|
return
|
||||||
} else if (event.key === Qt.Key_End) {
|
}
|
||||||
ui.selectLast()
|
})
|
||||||
event.accepted = true
|
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
|
||||||
return
|
ui.selectNext()
|
||||||
}
|
})
|
||||||
})
|
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
|
||||||
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
|
ui.selectPrevious()
|
||||||
ui.selectNext()
|
})
|
||||||
})
|
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
|
||||||
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
|
ui.activate()
|
||||||
ui.selectPrevious()
|
})
|
||||||
})
|
|
||||||
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
|
|
||||||
ui.activate()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,11 @@ Item {
|
||||||
"isImage": false,
|
"isImage": false,
|
||||||
"onActivate": function () {
|
"onActivate": function () {
|
||||||
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
|
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()
|
app.execute()
|
||||||
} else if (app.exec) {
|
} else if (app.exec) {
|
||||||
// Fallback to manual execution
|
// Fallback to manual execution
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ Loader {
|
||||||
anchors.topMargin: 80 * scaling
|
anchors.topMargin: 80 * scaling
|
||||||
spacing: 40 * scaling
|
spacing: 40 * scaling
|
||||||
|
|
||||||
Column {
|
ColumnLayout {
|
||||||
spacing: Style.marginXS * scaling
|
spacing: Style.marginXS * scaling
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
|
|
@ -168,6 +168,7 @@ Loader {
|
||||||
font.letterSpacing: -2 * scaling
|
font.letterSpacing: -2 * scaling
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
SequentialAnimation on scale {
|
SequentialAnimation on scale {
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
|
|
@ -192,22 +193,23 @@ Loader {
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
width: timeText.width
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.preferredWidth: timeText.implicitWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
ColumnLayout {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 108 * scaling
|
Layout.preferredWidth: 108 * scaling
|
||||||
height: 108 * scaling
|
Layout.preferredHeight: 108 * scaling
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
border.color: Color.mPrimary
|
border.color: Color.mPrimary
|
||||||
border.width: Math.max(1, Style.borderL * scaling)
|
border.width: Math.max(1, Style.borderL * scaling)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
z: 10
|
z: 10
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|
@ -375,377 +377,371 @@ Loader {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
anchors.verticalCenterOffset: 50 * scaling
|
anchors.verticalCenterOffset: 50 * scaling
|
||||||
|
|
||||||
Item {
|
Rectangle {
|
||||||
width: parent.width
|
id: terminalBackground
|
||||||
height: 280 * scaling
|
anchors.fill: parent
|
||||||
Layout.fillWidth: true
|
radius: Style.radiusM * scaling
|
||||||
|
color: Qt.alpha(Color.mSurface, 0.9)
|
||||||
Rectangle {
|
border.color: Color.mPrimary
|
||||||
id: terminalBackground
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 20
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 40 * scaling
|
height: 1
|
||||||
color: Qt.alpha(Color.mPrimary, 0.2)
|
color: Qt.alpha(Color.mPrimary, 0.1)
|
||||||
topLeftRadius: Style.radiusS * scaling
|
y: index * 10 * scaling
|
||||||
topRightRadius: Style.radiusS * scaling
|
opacity: Style.opacityMedium
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
SequentialAnimation on opacity {
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
to: 0.6
|
to: 0.6
|
||||||
duration: 2000
|
duration: 2000 + Math.random() * 1000
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
}
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
to: 0.2
|
to: 0.1
|
||||||
duration: 2000
|
duration: 2000 + Math.random() * 1000
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Power buttons at bottom right
|
||||||
Row {
|
RowLayout {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: 50 * scaling
|
anchors.margins: 50 * scaling
|
||||||
spacing: 20 * scaling
|
spacing: 20 * scaling
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 60 * scaling
|
Layout.preferredWidth: 60 * scaling
|
||||||
height: 60 * scaling
|
Layout.preferredHeight: 60 * scaling
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
|
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
|
||||||
border.color: Color.mError
|
border.color: Color.mError
|
||||||
|
|
@ -769,8 +765,8 @@ Loader {
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 60 * scaling
|
Layout.preferredWidth: 60 * scaling
|
||||||
height: 60 * scaling
|
Layout.preferredHeight: 60 * scaling
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
|
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
|
||||||
border.color: Color.mPrimary
|
border.color: Color.mPrimary
|
||||||
|
|
@ -794,8 +790,8 @@ Loader {
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 60 * scaling
|
Layout.preferredWidth: 60 * scaling
|
||||||
height: 60 * scaling
|
Layout.preferredHeight: 60 * scaling
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
|
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
|
||||||
border.color: Color.mSecondary
|
border.color: Color.mSecondary
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ Variants {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main notification container
|
// Main notification container
|
||||||
Column {
|
ColumnLayout {
|
||||||
id: notificationStack
|
id: notificationStack
|
||||||
// Position based on bar location
|
// Position based on bar location
|
||||||
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
|
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
|
||||||
|
|
@ -92,8 +92,9 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: notificationModel
|
model: notificationModel
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: 360 * scaling
|
Layout.preferredWidth: 360 * scaling
|
||||||
height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling))
|
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
|
||||||
|
Layout.maximumHeight: Layout.preferredHeight
|
||||||
clip: true
|
clip: true
|
||||||
radius: Style.radiusL * scaling
|
radius: Style.radiusL * scaling
|
||||||
border.color: Color.mOutline
|
border.color: Color.mOutline
|
||||||
|
|
@ -105,6 +106,17 @@ Variants {
|
||||||
property real opacityValue: 0.0
|
property real opacityValue: 0.0
|
||||||
property bool isRemoving: false
|
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 and fade-in animation
|
||||||
scale: scaleValue
|
scale: scaleValue
|
||||||
opacity: opacityValue
|
opacity: opacityValue
|
||||||
|
|
@ -156,104 +168,139 @@ Variants {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
ColumnLayout {
|
||||||
id: contentRow
|
id: notificationLayout
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.marginL * scaling
|
anchors.margins: Style.marginM * scaling
|
||||||
spacing: Style.marginL * scaling
|
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Right: header on top, then avatar + texts
|
// Header section with app name and timestamp
|
||||||
ColumnLayout {
|
RowLayout {
|
||||||
id: textColumn
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
RowLayout {
|
NText {
|
||||||
spacing: Style.marginS * scaling
|
text: `${(model.appName || model.desktopEntry)
|
||||||
id: appHeaderRow
|
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
|
||||||
NText {
|
color: Color.mSecondary
|
||||||
text: `${(model.appName || model.desktopEntry)
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
|| "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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Rectangle {
|
||||||
id: bodyRow
|
Layout.preferredWidth: 6 * scaling
|
||||||
spacing: Style.marginM * 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 {
|
Item {
|
||||||
id: appAvatar
|
Layout.fillWidth: true
|
||||||
Layout.preferredWidth: 40 * scaling
|
}
|
||||||
Layout.preferredHeight: 40 * scaling
|
}
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
// Start avatar aligned with body (below the summary)
|
// Main content section
|
||||||
anchors.topMargin: textContent.childrenRect.y
|
RowLayout {
|
||||||
// Prefer notification-provided image (e.g., user avatar) then fall back to app icon
|
Layout.fillWidth: true
|
||||||
imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName(
|
spacing: Style.marginM * scaling
|
||||||
model.appIcon, "application-x-executable")
|
|
||||||
fallbackIcon: "apps"
|
// Avatar
|
||||||
borderColor: Color.transparent
|
NImageCircled {
|
||||||
borderWidth: 0
|
id: appAvatar
|
||||||
visible: (imagePath && imagePath !== "")
|
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 {
|
NText {
|
||||||
id: textContent
|
text: model.body || ""
|
||||||
spacing: Style.marginS * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: Color.mOnSurface
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
// Ensure a concrete width so text wraps
|
maximumLineCount: 5
|
||||||
width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0))
|
elide: Text.ElideRight
|
||||||
|
visible: text.length > 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Close"
|
tooltipText: "Close"
|
||||||
// Compact target (~24dp) and glyph (~16dp)
|
sizeRatio: 0.6
|
||||||
sizeRatio: 0.75
|
|
||||||
fontPointSize: 16
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Style.marginM * scaling
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.margins: Style.marginS * scaling
|
anchors.rightMargin: Style.marginM * scaling
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
animateOut()
|
animateOut()
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ NPanel {
|
||||||
anchors.margins: Style.marginL * scaling
|
anchors.margins: Style.marginL * scaling
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
// Header section
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
@ -43,6 +44,13 @@ NPanel {
|
||||||
Layout.fillWidth: true
|
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 {
|
NIconButton {
|
||||||
icon: "delete"
|
icon: "delete"
|
||||||
tooltipText: "Clear history"
|
tooltipText: "Clear history"
|
||||||
|
|
@ -65,38 +73,44 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty state when no notifications
|
// Empty state when no notifications
|
||||||
Item {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
visible: NotificationService.historyModel.count === 0
|
visible: NotificationService.historyModel.count === 0
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
ColumnLayout {
|
Item {
|
||||||
anchors.centerIn: parent
|
Layout.fillHeight: true
|
||||||
spacing: Style.marginM * scaling
|
}
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: "notifications_off"
|
text: "notifications_off"
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
font.pointSize: 64 * scaling
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurfaceVariant
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "No notifications"
|
text: "No notifications"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurfaceVariant
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "Your notifications will show up here as they arrive."
|
text: "Your notifications will show up here as they arrive."
|
||||||
font.pointSize: Style.fontSizeNormal * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification list
|
||||||
ListView {
|
ListView {
|
||||||
id: notificationList
|
id: notificationList
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -108,21 +122,21 @@ NPanel {
|
||||||
visible: NotificationService.historyModel.count > 0
|
visible: NotificationService.historyModel.count > 0
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: notificationList ? notificationList.width : 380 * scaling
|
width: notificationList.width
|
||||||
height: Math.max(80, notificationContent.height + 30)
|
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
|
||||||
radius: Style.radiusM * scaling
|
radius: Style.radiusM * scaling
|
||||||
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
|
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
|
||||||
|
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors {
|
id: notificationLayout
|
||||||
fill: parent
|
anchors.fill: parent
|
||||||
margins: Style.marginM * scaling
|
anchors.margins: Style.marginM * scaling
|
||||||
}
|
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Notification content
|
// Notification content column
|
||||||
Column {
|
ColumnLayout {
|
||||||
id: notificationContent
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
|
|
@ -133,7 +147,8 @@ NPanel {
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
|
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
width: parent.width - 60
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: parent.width
|
||||||
maximumLineCount: 2
|
maximumLineCount: 2
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
@ -143,23 +158,27 @@ NPanel {
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
width: parent.width - 60
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: parent.width
|
||||||
maximumLineCount: 3
|
maximumLineCount: 3
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: NotificationService.formatTimestamp(timestamp)
|
text: NotificationService.formatTimestamp(timestamp)
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trash icon button
|
// Delete button
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "delete"
|
icon: "delete"
|
||||||
tooltipText: "Delete notification"
|
tooltipText: "Delete notification"
|
||||||
sizeRatio: 0.7
|
sizeRatio: 0.7
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Logger.log("NotificationHistory", "Removing notification:", summary)
|
Logger.log("NotificationHistory", "Removing notification:", summary)
|
||||||
|
|
@ -172,7 +191,7 @@ NPanel {
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: notificationMouseArea
|
id: notificationMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.rightMargin: Style.marginL * 3 * scaling
|
anchors.rightMargin: Style.marginXL * scaling
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
|
@ -16,6 +17,7 @@ NPanel {
|
||||||
panelHeight: 380 * scaling
|
panelHeight: 380 * scaling
|
||||||
panelAnchorHorizontalCenter: true
|
panelAnchorHorizontalCenter: true
|
||||||
panelAnchorVerticalCenter: true
|
panelAnchorVerticalCenter: true
|
||||||
|
panelKeyboardFocus: true
|
||||||
|
|
||||||
// Timer properties
|
// Timer properties
|
||||||
property int timerDuration: 9000 // 9 seconds
|
property int timerDuration: 9000 // 9 seconds
|
||||||
|
|
@ -23,9 +25,44 @@ NPanel {
|
||||||
property bool timerActive: false
|
property bool timerActive: false
|
||||||
property int timeRemaining: 0
|
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: {
|
onClosed: {
|
||||||
cancelTimer()
|
cancelTimer()
|
||||||
|
selectedIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer management
|
// Timer management
|
||||||
|
|
@ -79,6 +116,38 @@ NPanel {
|
||||||
root.close()
|
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
|
// Countdown timer
|
||||||
Timer {
|
Timer {
|
||||||
id: countdownTimer
|
id: countdownTimer
|
||||||
|
|
@ -93,8 +162,92 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
panelContent: Rectangle {
|
panelContent: Rectangle {
|
||||||
|
id: ui
|
||||||
color: Color.transparent
|
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 {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: Style.marginL * scaling
|
anchors.topMargin: Style.marginL * scaling
|
||||||
|
|
@ -144,55 +297,21 @@ NPanel {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Lock Screen
|
Repeater {
|
||||||
PowerButton {
|
model: powerOptions
|
||||||
Layout.fillWidth: true
|
delegate: PowerButton {
|
||||||
icon: "lock_outline"
|
Layout.fillWidth: true
|
||||||
title: "Lock"
|
icon: modelData.icon
|
||||||
subtitle: "Lock your session"
|
title: modelData.title
|
||||||
onClicked: startTimer("lock")
|
subtitle: modelData.subtitle
|
||||||
pending: timerActive && pendingAction === "lock"
|
isShutdown: modelData.isShutdown || false
|
||||||
}
|
isSelected: index === selectedIndex
|
||||||
|
onClicked: {
|
||||||
// Suspend
|
selectedIndex = index
|
||||||
PowerButton {
|
startTimer(modelData.action)
|
||||||
Layout.fillWidth: true
|
}
|
||||||
icon: "bedtime"
|
pending: timerActive && pendingAction === modelData.action
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +326,7 @@ NPanel {
|
||||||
property string subtitle: ""
|
property string subtitle: ""
|
||||||
property bool pending: false
|
property bool pending: false
|
||||||
property bool isShutdown: false
|
property bool isShutdown: false
|
||||||
|
property bool isSelected: false
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|
||||||
|
|
@ -216,7 +336,7 @@ NPanel {
|
||||||
if (pending) {
|
if (pending) {
|
||||||
return Qt.alpha(Color.mPrimary, 0.08)
|
return Qt.alpha(Color.mPrimary, 0.08)
|
||||||
}
|
}
|
||||||
if (mouseArea.containsMouse) {
|
if (isSelected || mouseArea.containsMouse) {
|
||||||
return Color.mSecondary
|
return Color.mSecondary
|
||||||
}
|
}
|
||||||
return Color.transparent
|
return Color.transparent
|
||||||
|
|
@ -242,13 +362,12 @@ NPanel {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: buttonRoot.icon
|
text: buttonRoot.icon
|
||||||
color: {
|
color: {
|
||||||
|
|
||||||
if (buttonRoot.pending)
|
if (buttonRoot.pending)
|
||||||
return Color.mPrimary
|
return Color.mPrimary
|
||||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||||
return Color.mError
|
return Color.mError
|
||||||
if (mouseArea.containsMouse)
|
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||||
return Color.mOnTertiary
|
return Color.mOnSecondary
|
||||||
return Color.mOnSurface
|
return Color.mOnSurface
|
||||||
}
|
}
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
font.pointSize: Style.fontSizeXXXL * scaling
|
||||||
|
|
@ -264,7 +383,7 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text content in the middle
|
// Text content in the middle
|
||||||
Column {
|
ColumnLayout {
|
||||||
anchors.left: iconElement.right
|
anchors.left: iconElement.right
|
||||||
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
|
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -279,10 +398,10 @@ NPanel {
|
||||||
color: {
|
color: {
|
||||||
if (buttonRoot.pending)
|
if (buttonRoot.pending)
|
||||||
return Color.mPrimary
|
return Color.mPrimary
|
||||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||||
return Color.mError
|
return Color.mError
|
||||||
if (mouseArea.containsMouse)
|
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||||
return Color.mOnTertiary
|
return Color.mOnSecondary
|
||||||
return Color.mOnSurface
|
return Color.mOnSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,10 +423,10 @@ NPanel {
|
||||||
color: {
|
color: {
|
||||||
if (buttonRoot.pending)
|
if (buttonRoot.pending)
|
||||||
return Color.mPrimary
|
return Color.mPrimary
|
||||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||||
return Color.mError
|
return Color.mError
|
||||||
if (mouseArea.containsMouse)
|
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||||
return Color.mOnTertiary
|
return Color.mOnSecondary
|
||||||
return Color.mOnSurfaceVariant
|
return Color.mOnSurfaceVariant
|
||||||
}
|
}
|
||||||
opacity: Style.opacityHeavy
|
opacity: Style.opacityHeavy
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ Popup {
|
||||||
sourceComponent: {
|
sourceComponent: {
|
||||||
if (settingsPopup.widgetId === "CustomButton") {
|
if (settingsPopup.widgetId === "CustomButton") {
|
||||||
return customButtonSettings
|
return customButtonSettings
|
||||||
|
} else if (settingsPopup.widgetId === "Spacer") {
|
||||||
|
return spacerSettings
|
||||||
}
|
}
|
||||||
// Add more widget settings components here as needed
|
// Add more widget settings components here as needed
|
||||||
return null
|
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 {
|
panelContent: Rectangle {
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Style.marginL * scaling
|
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
|
|
||||||
// Scrolling via keyboard
|
// Main layout container that fills the panel
|
||||||
Shortcut {
|
ColumnLayout {
|
||||||
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 {
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Style.marginM * scaling
|
anchors.margins: Style.marginL * scaling
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
Rectangle {
|
// Keyboard shortcuts container
|
||||||
id: sidebar
|
Item {
|
||||||
Layout.preferredWidth: 220 * scaling
|
Layout.preferredWidth: 0
|
||||||
Layout.fillHeight: true
|
Layout.preferredHeight: 0
|
||||||
color: Color.mSurfaceVariant
|
|
||||||
border.color: Color.mOutline
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
radius: Style.radiusM * scaling
|
|
||||||
|
|
||||||
MouseArea {
|
// Scrolling via keyboard
|
||||||
anchors.fill: parent
|
Shortcut {
|
||||||
acceptedButtons: Qt.NoButton // Don't interfere with clicks
|
sequence: "Down"
|
||||||
property int wheelAccumulator: 0
|
onActivated: root.scrollDown()
|
||||||
onWheel: wheel => {
|
enabled: root.opened
|
||||||
wheelAccumulator += wheel.angleDelta.y
|
|
||||||
if (wheelAccumulator >= 120) {
|
|
||||||
root.selectPreviousTab()
|
|
||||||
wheelAccumulator = 0
|
|
||||||
} else if (wheelAccumulator <= -120) {
|
|
||||||
root.selectNextTab()
|
|
||||||
wheelAccumulator = 0
|
|
||||||
}
|
|
||||||
wheel.accepted = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Shortcut {
|
||||||
anchors.fill: parent
|
sequence: "Up"
|
||||||
anchors.margins: Style.marginS * scaling
|
onActivated: root.scrollUp()
|
||||||
spacing: Style.marginXS * 1.5 * scaling
|
enabled: root.opened
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Shortcut {
|
||||||
id: sections
|
sequence: "Ctrl+J"
|
||||||
model: root.tabsModel
|
onActivated: root.scrollDown()
|
||||||
delegate: Rectangle {
|
enabled: root.opened
|
||||||
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)
|
|
||||||
|
|
||||||
Behavior on color {
|
Shortcut {
|
||||||
ColorAnimation {
|
sequence: "Ctrl+K"
|
||||||
duration: Style.animationFast
|
onActivated: root.scrollUp()
|
||||||
}
|
enabled: root.opened
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on tabTextColor {
|
Shortcut {
|
||||||
ColorAnimation {
|
sequence: "PgDown"
|
||||||
duration: Style.animationFast
|
onActivated: root.scrollPageDown()
|
||||||
}
|
enabled: root.opened
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Shortcut {
|
||||||
anchors.fill: parent
|
sequence: "PgUp"
|
||||||
anchors.leftMargin: Style.marginS * scaling
|
onActivated: root.scrollPageUp()
|
||||||
anchors.rightMargin: Style.marginS * scaling
|
enabled: root.opened
|
||||||
spacing: Style.marginS * scaling
|
}
|
||||||
// Tab icon on the left side
|
|
||||||
NIcon {
|
// Changing tab via keyboard
|
||||||
text: modelData.icon
|
Shortcut {
|
||||||
color: tabTextColor
|
sequence: "Tab"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
onActivated: root.selectNextTab()
|
||||||
}
|
enabled: root.opened
|
||||||
// Tab label on the left side
|
}
|
||||||
NText {
|
|
||||||
text: modelData.label
|
Shortcut {
|
||||||
color: tabTextColor
|
sequence: "Shift+Tab"
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
onActivated: root.selectPreviousTab()
|
||||||
font.weight: Style.fontWeightBold
|
enabled: root.opened
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content
|
// Main content area
|
||||||
Rectangle {
|
RowLayout {
|
||||||
id: contentPane
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
radius: Style.radiusM * scaling
|
spacing: Style.marginM * scaling
|
||||||
color: Color.mSurfaceVariant
|
|
||||||
border.color: Color.mOutline
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
ColumnLayout {
|
// Sidebar
|
||||||
id: contentLayout
|
Rectangle {
|
||||||
anchors.fill: parent
|
id: sidebar
|
||||||
anchors.margins: Style.marginL * scaling
|
Layout.preferredWidth: 220 * scaling
|
||||||
spacing: Style.marginS * 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 {
|
MouseArea {
|
||||||
id: headerRow
|
anchors.fill: parent
|
||||||
Layout.fillWidth: true
|
acceptedButtons: Qt.NoButton // Don't interfere with clicks
|
||||||
spacing: Style.marginS * scaling
|
property int wheelAccumulator: 0
|
||||||
|
onWheel: wheel => {
|
||||||
// Tab label on the main right side
|
wheelAccumulator += wheel.angleDelta.y
|
||||||
NText {
|
if (wheelAccumulator >= 120) {
|
||||||
text: root.tabsModel[currentTabIndex].label
|
root.selectPreviousTab()
|
||||||
font.pointSize: Style.fontSizeXL * scaling
|
wheelAccumulator = 0
|
||||||
font.weight: Style.fontWeightBold
|
} else if (wheelAccumulator <= -120) {
|
||||||
color: Color.mPrimary
|
root.selectNextTab()
|
||||||
Layout.fillWidth: true
|
wheelAccumulator = 0
|
||||||
}
|
}
|
||||||
NIconButton {
|
wheel.accepted = true
|
||||||
icon: "close"
|
}
|
||||||
tooltipText: "Close"
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NDivider {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
anchors.fill: parent
|
||||||
}
|
anchors.margins: Style.marginS * scaling
|
||||||
|
spacing: Style.marginXS * 1.5 * scaling
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
id: sections
|
||||||
model: root.tabsModel
|
model: root.tabsModel
|
||||||
delegate: Loader {
|
delegate: Rectangle {
|
||||||
anchors.fill: parent
|
id: tabItem
|
||||||
active: index === root.currentTabIndex
|
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: {
|
Behavior on color {
|
||||||
if (status === Loader.Ready && item) {
|
ColorAnimation {
|
||||||
// Find and store reference to the ScrollView
|
duration: Style.animationFast
|
||||||
const scrollView = item.children[0]
|
|
||||||
if (scrollView && scrollView.toString().includes("ScrollView")) {
|
|
||||||
root.activeScrollView = scrollView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceComponent: ColumnLayout {
|
Behavior on tabTextColor {
|
||||||
ScrollView {
|
ColorAnimation {
|
||||||
id: scrollView
|
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.fillWidth: true
|
||||||
Layout.fillHeight: true
|
}
|
||||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
}
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
|
||||||
padding: Style.marginL * scaling
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
MouseArea {
|
||||||
root.activeScrollView = scrollView
|
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 {
|
sourceComponent: Flickable {
|
||||||
active: true
|
// Using a Flickable here with a pressDelay to fix conflict between
|
||||||
sourceComponent: root.tabsModel[index].source
|
// ScrollView and NTextInput. This fixes the weird text selection issue.
|
||||||
width: scrollView.availableWidth
|
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 {
|
Rectangle {
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
Layout.topMargin: Style.marginS * scaling
|
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)
|
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
|
||||||
radius: Style.radiusL * scaling
|
radius: Style.radiusL * scaling
|
||||||
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
|
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
|
||||||
|
|
@ -85,11 +85,12 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: updateRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: "system_update"
|
text: "download"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
|
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@ ColumnLayout {
|
||||||
fallbackIcon: "person"
|
fallbackIcon: "person"
|
||||||
borderColor: Color.mPrimary
|
borderColor: Color.mPrimary
|
||||||
borderWidth: Math.max(1, Style.borderM * scaling)
|
borderWidth: Math.max(1, Style.borderM * scaling)
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
}
|
}
|
||||||
|
|
||||||
NTextInput {
|
NTextInput {
|
||||||
|
Layout.fillWidth: true
|
||||||
label: `${Quickshell.env("USER") || "user"}'s profile picture`
|
label: `${Quickshell.env("USER") || "user"}'s profile picture`
|
||||||
description: "Your profile picture that appears throughout the interface."
|
description: "Your profile picture that appears throughout the interface."
|
||||||
text: Settings.data.general.avatarImage
|
text: Settings.data.general.avatarImage
|
||||||
|
|
@ -75,6 +77,45 @@ ColumnLayout {
|
||||||
onToggled: checked => Settings.data.dock.autoHide = checked
|
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 {
|
ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
||||||
|
|
@ -5,94 +5,85 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ScrollView {
|
ColumnLayout {
|
||||||
id: root
|
id: contentColumn
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
width: root.width
|
||||||
|
|
||||||
property real scaling: 1.0
|
// Enable/Disable Toggle
|
||||||
|
NToggle {
|
||||||
contentWidth: contentColumn.width
|
label: "Enable Hooks"
|
||||||
contentHeight: contentColumn.height
|
description: "Enable or disable all hook commands."
|
||||||
|
checked: Settings.data.hooks.enabled
|
||||||
|
onToggled: checked => Settings.data.hooks.enabled = checked
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: contentColumn
|
visible: Settings.data.hooks.enabled
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
width: root.width
|
Layout.fillWidth: true
|
||||||
|
|
||||||
// Enable/Disable Toggle
|
NDivider {
|
||||||
NToggle {
|
Layout.fillWidth: true
|
||||||
label: "Enable Hooks"
|
|
||||||
description: "Enable or disable all hook commands."
|
|
||||||
checked: Settings.data.hooks.enabled
|
|
||||||
onToggled: checked => Settings.data.hooks.enabled = checked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
ColumnLayout {
|
||||||
visible: Settings.data.hooks.enabled
|
spacing: Style.marginM * scaling
|
||||||
spacing: Style.marginL * scaling
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NDivider {
|
NLabel {
|
||||||
Layout.fillWidth: true
|
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
|
NLabel {
|
||||||
NInputAction {
|
label: "Available Parameters"
|
||||||
id: wallpaperHookInput
|
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
|
||||||
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)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,13 @@ ColumnLayout {
|
||||||
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
|
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 {
|
ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,14 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "WiFi Enabled"
|
label: "Enable Wi-Fi"
|
||||||
description: "Enable WiFi connectivity."
|
description: "Enable Wi-Fi connectivity."
|
||||||
checked: Settings.data.network.wifiEnabled
|
checked: Settings.data.network.wifiEnabled
|
||||||
onToggled: checked => {
|
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||||
Settings.data.network.wifiEnabled = checked
|
|
||||||
NetworkService.setWifiEnabled(checked)
|
|
||||||
if (checked) {
|
|
||||||
ToastService.showNotice("WiFi", "Enabled")
|
|
||||||
} else {
|
|
||||||
ToastService.showNotice("WiFi", "Disabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Bluetooth Enabled"
|
label: "Enable Bluetooth"
|
||||||
description: "Enable Bluetooth connectivity."
|
description: "Enable Bluetooth connectivity."
|
||||||
checked: Settings.data.network.bluetoothEnabled
|
checked: Settings.data.network.bluetoothEnabled
|
||||||
onToggled: checked => {
|
onToggled: checked => {
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,6 @@ ColumnLayout {
|
||||||
NColorPicker {
|
NColorPicker {
|
||||||
selectedColor: Settings.data.wallpaper.fillColor
|
selectedColor: Settings.data.wallpaper.fillColor
|
||||||
onColorSelected: color => Settings.data.wallpaper.fillColor = color
|
onColorSelected: color => Settings.data.wallpaper.fillColor = color
|
||||||
onColorCancelled: selectedColor = Settings.data.wallpaper.fillColor
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,7 +277,6 @@ ColumnLayout {
|
||||||
NTextInput {
|
NTextInput {
|
||||||
label: "Custom Interval"
|
label: "Custom Interval"
|
||||||
description: "Enter time as HH:MM (e.g., 01:30)."
|
description: "Enter time as HH:MM (e.g., 01:30)."
|
||||||
inputMaxWidth: 100 * scaling
|
|
||||||
text: {
|
text: {
|
||||||
const s = Settings.data.wallpaper.randomIntervalSec
|
const s = Settings.data.wallpaper.randomIntervalSec
|
||||||
const h = Math.floor(s / 3600)
|
const h = Math.floor(s / 3600)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ NBox {
|
||||||
// Performance
|
// Performance
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "speed"
|
icon: "speed"
|
||||||
tooltipText: "Set performance power profile"
|
tooltipText: "Set performance power profile."
|
||||||
enabled: hasPP
|
enabled: hasPP
|
||||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||||
colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
|
colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
|
||||||
|
|
@ -43,7 +43,7 @@ NBox {
|
||||||
// Balanced
|
// Balanced
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "balance"
|
icon: "balance"
|
||||||
tooltipText: "Set balanced power profile"
|
tooltipText: "Set balanced power profile."
|
||||||
enabled: hasPP
|
enabled: hasPP
|
||||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||||
colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
|
colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
|
||||||
|
|
@ -57,7 +57,7 @@ NBox {
|
||||||
// Eco
|
// Eco
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "eco"
|
icon: "eco"
|
||||||
tooltipText: "Set eco power profile"
|
tooltipText: "Set eco power profile."
|
||||||
enabled: hasPP
|
enabled: hasPP
|
||||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||||
colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
|
colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ NBox {
|
||||||
}
|
}
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "settings"
|
icon: "settings"
|
||||||
tooltipText: "Open settings"
|
tooltipText: "Open settings."
|
||||||
onClicked: {
|
onClicked: {
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.General
|
settingsPanel.requestedTab = SettingsPanel.Tab.General
|
||||||
settingsPanel.open(screen)
|
settingsPanel.open(screen)
|
||||||
|
|
@ -69,7 +69,7 @@ NBox {
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: powerButton
|
id: powerButton
|
||||||
icon: "power_settings_new"
|
icon: "power_settings_new"
|
||||||
tooltipText: "Power menu"
|
tooltipText: "Power menu."
|
||||||
onClicked: {
|
onClicked: {
|
||||||
powerPanel.open(screen)
|
powerPanel.open(screen)
|
||||||
sidePanel.close()
|
sidePanel.close()
|
||||||
|
|
@ -79,7 +79,7 @@ NBox {
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: closeButton
|
id: closeButton
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Close side panel"
|
tooltipText: "Close side panel."
|
||||||
onClicked: {
|
onClicked: {
|
||||||
sidePanel.close()
|
sidePanel.close()
|
||||||
}
|
}
|
||||||
|
|
@ -104,19 +104,7 @@ NBox {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
|
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
|
||||||
var minutes = Math.floor(uptimeSeconds / 60) % 60
|
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds)
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
uptimeProcess.running = false
|
uptimeProcess.running = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ NBox {
|
||||||
Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling
|
Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling
|
||||||
implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling
|
implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling
|
||||||
|
|
||||||
Column {
|
ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
@ -22,11 +22,6 @@ NBox {
|
||||||
anchors.bottomMargin: Style.marginM * scaling
|
anchors.bottomMargin: Style.marginM * scaling
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
// Slight top padding
|
|
||||||
Item {
|
|
||||||
height: Style.marginXS * scaling
|
|
||||||
}
|
|
||||||
|
|
||||||
NCircleStat {
|
NCircleStat {
|
||||||
value: SystemStatService.cpuUsage
|
value: SystemStatService.cpuUsage
|
||||||
icon: "speed"
|
icon: "speed"
|
||||||
|
|
@ -60,10 +55,5 @@ NBox {
|
||||||
width: 72 * scaling
|
width: 72 * scaling
|
||||||
height: 68 * 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
|
// Screen Recorder
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "videocam"
|
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
|
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
|
||||||
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
|
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
@ -42,7 +42,7 @@ NBox {
|
||||||
// Idle Inhibitor
|
// Idle Inhibitor
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "coffee"
|
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
|
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
|
||||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
|
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
@ -54,7 +54,7 @@ NBox {
|
||||||
NIconButton {
|
NIconButton {
|
||||||
visible: Settings.data.wallpaper.enabled
|
visible: Settings.data.wallpaper.enabled
|
||||||
icon: "image"
|
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: {
|
onClicked: {
|
||||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,11 @@ NPanel {
|
||||||
panelHeight: 500 * scaling
|
panelHeight: 500 * scaling
|
||||||
panelKeyboardFocus: true
|
panelKeyboardFocus: true
|
||||||
|
|
||||||
property string passwordPromptSsid: ""
|
property string passwordSsid: ""
|
||||||
property string passwordInput: ""
|
property string passwordInput: ""
|
||||||
property bool showPasswordPrompt: false
|
property string expandedSsid: ""
|
||||||
property string expandedNetwork: "" // Track which network shows options
|
|
||||||
|
|
||||||
onOpened: {
|
onOpened: NetworkService.scan()
|
||||||
if (Settings.data.network.wifiEnabled) {
|
|
||||||
NetworkService.refreshNetworks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panelContent: Rectangle {
|
panelContent: Rectangle {
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
|
|
@ -39,35 +34,32 @@ NPanel {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: "wifi"
|
text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
color: Color.mPrimary
|
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "WiFi"
|
text: "Wi-Fi"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection status indicator
|
NToggle {
|
||||||
Rectangle {
|
id: wifiSwitch
|
||||||
visible: NetworkService.hasActiveConnection
|
checked: Settings.data.network.wifiEnabled
|
||||||
width: 8 * scaling
|
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||||
height: 8 * scaling
|
baseSize: Style.baseWidgetSize * 0.65 * scaling
|
||||||
radius: 4 * scaling
|
|
||||||
color: Color.mPrimary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "refresh"
|
icon: "refresh"
|
||||||
tooltipText: "Refresh networks"
|
tooltipText: "Refresh"
|
||||||
sizeRatio: 0.8
|
sizeRatio: 0.8
|
||||||
enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading
|
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
|
||||||
onClicked: NetworkService.refreshNetworks()
|
onClicked: NetworkService.scan()
|
||||||
}
|
}
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
|
|
@ -82,17 +74,18 @@ NPanel {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error banner
|
// Error message
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
|
visible: NetworkService.lastError.length > 0
|
||||||
Layout.fillWidth: true
|
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)
|
color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1)
|
||||||
radius: Style.radiusS * scaling
|
radius: Style.radiusS * scaling
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
border.color: Color.mError
|
border.color: Color.mError
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: errorRow
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.marginM * scaling
|
anchors.margins: Style.marginM * scaling
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
@ -104,8 +97,7 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
id: errorText
|
text: NetworkService.lastError
|
||||||
text: NetworkService.connectError
|
|
||||||
color: Color.mError
|
color: Color.mError
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
|
|
@ -115,301 +107,364 @@ NPanel {
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
sizeRatio: 0.6
|
sizeRatio: 0.6
|
||||||
onClicked: {
|
onClicked: NetworkService.lastError = ""
|
||||||
NetworkService.connectStatus = ""
|
|
||||||
NetworkService.connectError = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
// Main content area
|
||||||
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
color: Color.transparent
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
|
||||||
clip: true
|
|
||||||
contentWidth: availableWidth
|
|
||||||
|
|
||||||
|
// WiFi disabled state
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
width: parent.width
|
visible: !Settings.data.network.wifiEnabled
|
||||||
|
anchors.fill: parent
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Loading state
|
Item {
|
||||||
ColumnLayout {
|
Layout.fillHeight: true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
width: parent.width
|
||||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
|
||||||
visible: !Settings.data.network.wifiEnabled
|
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
NIcon {
|
// Network list
|
||||||
text: "wifi_off"
|
Repeater {
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
model: {
|
||||||
color: Color.mOnSurfaceVariant
|
if (!Settings.data.network.wifiEnabled)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
return []
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
const nets = Object.values(NetworkService.networks)
|
||||||
text: "WiFi is disabled"
|
return nets.sort((a, b) => {
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
if (a.connected !== b.connected)
|
||||||
color: Color.mOnSurfaceVariant
|
return b.connected - a.connected
|
||||||
Layout.alignment: Qt.AlignHCenter
|
return b.signal - a.signal
|
||||||
}
|
})
|
||||||
|
|
||||||
NButton {
|
|
||||||
text: "Enable WiFi"
|
|
||||||
icon: "wifi"
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
onClicked: {
|
|
||||||
Settings.data.network.wifiEnabled = true
|
|
||||||
Settings.save()
|
|
||||||
NetworkService.setWifiEnabled(true)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
Rectangle {
|
||||||
id: networkRect
|
Layout.fillWidth: true
|
||||||
width: parent.width
|
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
|
||||||
implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2)
|
|
||||||
radius: Style.radiusM * scaling
|
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,
|
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b,
|
||||||
0.05) : Color.mSurface
|
0.05) : Color.mSurface
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
|
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
|
||||||
clip: true
|
|
||||||
|
// Smooth opacity animation
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: networkContent
|
id: netColumn
|
||||||
width: parent.width - (Style.marginM * scaling * 2)
|
width: parent.width - (Style.marginM * scaling * 2)
|
||||||
x: Style.marginM * scaling
|
x: Style.marginM * scaling
|
||||||
y: Style.marginM * scaling
|
y: Style.marginM * scaling
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
// Main network row
|
// Main row
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
// Signal icon
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: NetworkService.signalIcon(modelData.signal)
|
text: NetworkService.signalIcon(modelData.signal)
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
|
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network info
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignVCenter
|
spacing: 2 * scaling
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: modelData.ssid || "Unknown Network"
|
text: modelData.ssid
|
||||||
font.pointSize: Style.fontSizeNormal * scaling
|
font.pointSize: Style.fontSizeNormal * scaling
|
||||||
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
|
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
|
||||||
elide: Text.ElideRight
|
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
RowLayout {
|
||||||
text: {
|
spacing: Style.marginXS * scaling
|
||||||
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)
|
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
id: connectedLabel
|
text: `${modelData.signal}%`
|
||||||
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"
|
|
||||||
font.pointSize: Style.fontSizeXXS * scaling
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Loading indicator
|
NText {
|
||||||
NBusyIndicator {
|
text: "•"
|
||||||
visible: NetworkService.connectingSsid === modelData.ssid
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
running: NetworkService.connectingSsid === modelData.ssid
|
color: Color.mOnSurfaceVariant
|
||||||
color: Color.mPrimary
|
}
|
||||||
size: Style.baseWidgetSize * 0.6 * scaling
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action buttons
|
NText {
|
||||||
RowLayout {
|
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
|
||||||
spacing: Style.marginXS * scaling
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
visible: NetworkService.connectingSsid !== modelData.ssid
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
NButton {
|
Item {
|
||||||
visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt)
|
Layout.preferredWidth: Style.marginXXS * scaling
|
||||||
outlined: !hovered
|
}
|
||||||
fontSize: Style.fontSizeXS * scaling
|
|
||||||
text: modelData.existing ? "Connect" : (NetworkService.isSecured(
|
// Update the status badges area (around line 237)
|
||||||
modelData.security) ? "Password" : "Connect")
|
Rectangle {
|
||||||
onClicked: {
|
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
if (modelData.existing || !NetworkService.isSecured(modelData.security)) {
|
color: Color.mPrimary
|
||||||
NetworkService.connectNetwork(modelData.ssid, modelData.security)
|
radius: height * 0.5
|
||||||
} else {
|
width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
|
||||||
expandedNetwork = modelData.ssid
|
height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||||
passwordPromptSsid = modelData.ssid
|
|
||||||
showPasswordPrompt = true
|
NText {
|
||||||
passwordInput = ""
|
id: connectedText
|
||||||
Qt.callLater(() => passwordInputField.forceActiveFocus())
|
anchors.centerIn: parent
|
||||||
}
|
text: "Connected"
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
|
color: Color.mOnPrimary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NButton {
|
Rectangle {
|
||||||
visible: modelData.connected
|
visible: NetworkService.disconnectingFrom === modelData.ssid
|
||||||
outlined: !hovered
|
color: Color.mError
|
||||||
fontSize: Style.fontSizeXS * scaling
|
radius: height * 0.5
|
||||||
backgroundColor: Color.mError
|
width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2)
|
||||||
text: "Disconnect"
|
height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||||
onClicked: NetworkService.disconnectNetwork(modelData.ssid)
|
|
||||||
|
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 {
|
Rectangle {
|
||||||
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
|
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: visible ? 50 * scaling : 0
|
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
radius: Style.radiusS * scaling
|
radius: Style.radiusS * scaling
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: passwordRow
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.marginS * scaling
|
anchors.margins: Style.marginS * scaling
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
radius: Style.radiusS * scaling
|
radius: Style.radiusXS * scaling
|
||||||
color: Color.mSurface
|
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)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
TextInput {
|
TextInput {
|
||||||
id: passwordInputField
|
id: pwdInput
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: Style.marginM * scaling
|
anchors.margins: Style.marginS * scaling
|
||||||
anchors.rightMargin: Style.marginM * scaling
|
|
||||||
height: parent.height
|
|
||||||
text: passwordInput
|
text: passwordInput
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
clip: true
|
|
||||||
focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt
|
|
||||||
selectByMouse: true
|
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
|
selectByMouse: true
|
||||||
|
focus: visible
|
||||||
passwordCharacter: "●"
|
passwordCharacter: "●"
|
||||||
onTextChanged: passwordInput = text
|
onTextChanged: passwordInput = text
|
||||||
|
onVisibleChanged: if (visible)
|
||||||
|
forceActiveFocus()
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (passwordInput) {
|
if (text) {
|
||||||
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
|
NetworkService.connect(passwordSsid, text)
|
||||||
showPasswordPrompt = false
|
passwordSsid = ""
|
||||||
expandedNetwork = ""
|
passwordInput = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,56 +480,75 @@ NPanel {
|
||||||
|
|
||||||
NButton {
|
NButton {
|
||||||
text: "Connect"
|
text: "Connect"
|
||||||
icon: "check"
|
fontSize: Style.fontSizeXXS * scaling
|
||||||
fontSize: Style.fontSizeXS * scaling
|
|
||||||
enabled: passwordInput.length > 0
|
enabled: passwordInput.length > 0
|
||||||
outlined: !enabled
|
outlined: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (passwordInput) {
|
NetworkService.connect(passwordSsid, passwordInput)
|
||||||
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
|
passwordSsid = ""
|
||||||
showPasswordPrompt = false
|
passwordInput = ""
|
||||||
expandedNetwork = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Cancel"
|
sizeRatio: 0.8
|
||||||
sizeRatio: 0.9
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
showPasswordPrompt = false
|
passwordSsid = ""
|
||||||
expandedNetwork = ""
|
|
||||||
passwordInput = ""
|
passwordInput = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forget network option - appears when saved badge is clicked
|
// Forget network
|
||||||
RowLayout {
|
Rectangle {
|
||||||
visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid
|
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
&& !showPasswordPrompt
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginXS * scaling
|
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
|
||||||
spacing: Style.marginS * scaling
|
color: Color.mSurfaceVariant
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
border.color: Color.mOutline
|
||||||
|
|
||||||
Item {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
id: forgetRow
|
||||||
}
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginS * scaling
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
NButton {
|
RowLayout {
|
||||||
id: forgetButton
|
NIcon {
|
||||||
text: "Forget Network"
|
text: "delete_outline"
|
||||||
icon: "delete_outline"
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
fontSize: Style.fontSizeXXS * scaling
|
color: Color.mError
|
||||||
backgroundColor: Color.mError
|
}
|
||||||
textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary
|
|
||||||
outlined: !forgetButton.hovered
|
NText {
|
||||||
Layout.preferredHeight: 28 * scaling
|
text: "Forget this network?"
|
||||||
onClicked: {
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
NetworkService.forgetNetwork(modelData.ssid)
|
color: Color.mError
|
||||||
expandedNetwork = ""
|
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
|
// Empty state when no networks
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(
|
||||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
NetworkService.networks).length === 0
|
||||||
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys(
|
anchors.fill: parent
|
||||||
NetworkService.networks).length === 0
|
spacing: Style.marginL * scaling
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
|
|
||||||
NIcon {
|
Item {
|
||||||
text: "wifi_find"
|
Layout.fillHeight: true
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
}
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
NIcon {
|
||||||
text: "No networks found"
|
text: "wifi_find"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: 64 * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
NButton {
|
NText {
|
||||||
text: "Refresh"
|
text: "No networks found"
|
||||||
icon: "refresh"
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
Layout.alignment: Qt.AlignHCenter
|
color: Color.mOnSurfaceVariant
|
||||||
onClicked: NetworkService.refreshNetworks()
|
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 Settings Window | `qs -c noctalia-shell ipc call settings toggle` |
|
||||||
| Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen 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 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` |
|
| Change Wallpaper | `qs -c noctalia-shell ipc call wallpaper set $path $monitor` |
|
||||||
| Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` |
|
| Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` |
|
||||||
| Toggle Dark Mode | `qs -c noctalia-shell ipc call darkMode toggle` |
|
| 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:
|
For Niri:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
debug {
|
||||||
|
honor-xdg-activation-with-invalid-serial
|
||||||
|
}
|
||||||
|
|
||||||
window-rule {
|
window-rule {
|
||||||
geometry-corner-radius 20
|
geometry-corner-radius 20
|
||||||
clip-to-geometry true
|
clip-to-geometry true
|
||||||
|
|
@ -279,6 +284,8 @@ layer-rule {
|
||||||
place-within-backdrop true
|
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,
|
"PowerToggle": powerToggleComponent,
|
||||||
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
|
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
|
||||||
"SidePanelToggle": sidePanelToggleComponent,
|
"SidePanelToggle": sidePanelToggleComponent,
|
||||||
|
"Spacer": spacerComponent,
|
||||||
"SystemMonitor": systemMonitorComponent,
|
"SystemMonitor": systemMonitorComponent,
|
||||||
"Taskbar": taskbarComponent,
|
"Taskbar": taskbarComponent,
|
||||||
"Tray": trayComponent,
|
"Tray": trayComponent,
|
||||||
|
|
@ -43,6 +44,11 @@ Singleton {
|
||||||
"leftClickExec": "",
|
"leftClickExec": "",
|
||||||
"rightClickExec": "",
|
"rightClickExec": "",
|
||||||
"middleClickExec": ""
|
"middleClickExec": ""
|
||||||
|
},
|
||||||
|
"Spacer": {
|
||||||
|
"allowUserSettings": true,
|
||||||
|
"icon": "space_bar",
|
||||||
|
"width": 20
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -101,6 +107,9 @@ Singleton {
|
||||||
property Component sidePanelToggleComponent: Component {
|
property Component sidePanelToggleComponent: Component {
|
||||||
SidePanelToggle {}
|
SidePanelToggle {}
|
||||||
}
|
}
|
||||||
|
property Component spacerComponent: Component {
|
||||||
|
Spacer {}
|
||||||
|
}
|
||||||
property Component systemMonitorComponent: Component {
|
property Component systemMonitorComponent: Component {
|
||||||
SystemMonitor {}
|
SystemMonitor {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,215 +8,239 @@ import qs.Commons
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Core properties
|
// Core state
|
||||||
property var networks: ({})
|
property var networks: ({})
|
||||||
property string connectingSsid: ""
|
property bool scanning: false
|
||||||
property string connectStatus: ""
|
property bool connecting: false
|
||||||
property string connectStatusSsid: ""
|
property string connectingTo: ""
|
||||||
property string connectError: ""
|
property string lastError: ""
|
||||||
property bool isLoading: false
|
property bool ethernetConnected: false
|
||||||
property bool ethernet: false
|
property string disconnectingFrom: ""
|
||||||
property int retryCount: 0
|
property string forgettingNetwork: ""
|
||||||
property int maxRetries: 3
|
|
||||||
|
|
||||||
// File path for persistent storage
|
// Persistent cache
|
||||||
property string cacheFile: Settings.cacheDir + "network.json"
|
property string cacheFile: Settings.cacheDir + "network.json"
|
||||||
|
readonly property string cachedLastConnected: cacheAdapter.lastConnected
|
||||||
|
readonly property var cachedNetworks: cacheAdapter.knownNetworks
|
||||||
|
|
||||||
// Stable properties for UI
|
// Cache file handling
|
||||||
readonly property alias cache: adapter
|
|
||||||
readonly property string lastConnectedNetwork: adapter.lastConnected
|
|
||||||
|
|
||||||
// File-based persistent storage
|
|
||||||
FileView {
|
FileView {
|
||||||
id: cacheFileView
|
id: cacheFileView
|
||||||
path: root.cacheFile
|
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 {
|
JsonAdapter {
|
||||||
id: adapter
|
id: cacheAdapter
|
||||||
property var knownNetworks: ({})
|
property var knownNetworks: ({})
|
||||||
property string lastConnected: ""
|
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 {
|
Timer {
|
||||||
id: saveTimer
|
id: saveDebounce
|
||||||
running: false
|
|
||||||
interval: 1000
|
interval: 1000
|
||||||
onTriggered: cacheFileView.writeAdapter()
|
onTriggered: cacheFileView.writeAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
function saveCache() {
|
||||||
Logger.log("Network", "Service started")
|
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) {
|
if (Settings.data.network.wifiEnabled) {
|
||||||
refreshNetworks()
|
scan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal strength icon mapping
|
function scan() {
|
||||||
function signalIcon(signal) {
|
if (scanning)
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
isLoading = true
|
scanning = true
|
||||||
retryCount = 0
|
lastError = ""
|
||||||
adapter.lastRefresh = Date.now()
|
scanProcess.running = true
|
||||||
performRefresh()
|
Logger.log("Network", "Wi-Fi scan in progress...")
|
||||||
}
|
}
|
||||||
|
|
||||||
function performRefresh() {
|
function connect(ssid, password = "") {
|
||||||
checkEthernet.running = true
|
if (connecting)
|
||||||
existingNetworkProcess.running = true
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// Retry mechanism for failed operations
|
connecting = true
|
||||||
function retryRefresh() {
|
connectingTo = ssid
|
||||||
if (retryCount < maxRetries) {
|
lastError = ""
|
||||||
retryCount++
|
|
||||||
Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`)
|
// Check if we have a saved connection
|
||||||
retryTimer.start()
|
if (networks[ssid]?.existing || cachedNetworks[ssid]) {
|
||||||
|
connectProcess.mode = "saved"
|
||||||
|
connectProcess.ssid = ssid
|
||||||
|
connectProcess.password = ""
|
||||||
} else {
|
} else {
|
||||||
isLoading = false
|
connectProcess.mode = "new"
|
||||||
connectError = "Failed to refresh networks after multiple attempts"
|
connectProcess.ssid = ssid
|
||||||
|
connectProcess.password = password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
function disconnect(ssid) {
|
||||||
id: retryTimer
|
disconnectingFrom = ssid
|
||||||
interval: 1000 * retryCount // Progressive backoff
|
disconnectProcess.ssid = ssid
|
||||||
repeat: false
|
disconnectProcess.running = true
|
||||||
onTriggered: performRefresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
function forget(ssid) {
|
||||||
id: autoConnectTimer
|
forgettingNetwork = ssid
|
||||||
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}`)
|
|
||||||
|
|
||||||
// Remove from cache
|
// Remove from cache
|
||||||
let known = adapter.knownNetworks
|
let known = cacheAdapter.knownNetworks
|
||||||
delete known[ssid]
|
delete known[ssid]
|
||||||
adapter.knownNetworks = known
|
cacheAdapter.knownNetworks = known
|
||||||
|
|
||||||
// Clear last connected if it's this network
|
if (cacheAdapter.lastConnected === ssid) {
|
||||||
if (adapter.lastConnected === ssid) {
|
cacheAdapter.lastConnected = ""
|
||||||
adapter.lastConnected = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save changes
|
saveCache()
|
||||||
saveTimer.restart()
|
|
||||||
|
|
||||||
// Remove NetworkManager profile
|
// Remove from system
|
||||||
forgetProcess.ssid = ssid
|
forgetProcess.ssid = ssid
|
||||||
forgetProcess.running = true
|
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 {
|
Process {
|
||||||
id: forgetProcess
|
id: ethernetStateProcess
|
||||||
property string ssid: ""
|
|
||||||
running: false
|
running: false
|
||||||
command: ["nmcli", "connection", "delete", "id", ssid]
|
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`)
|
const connected = text.split("\n").some(line => {
|
||||||
refreshNetworks()
|
const parts = line.split(":")
|
||||||
}
|
return parts[1] === "ethernet" && parts[2] === "connected"
|
||||||
}
|
})
|
||||||
|
if (root.ethernetConnected !== connected) {
|
||||||
stderr: StdioCollector {
|
root.ethernetConnected = connected
|
||||||
onStreamFinished: {
|
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
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"
|
property string action: "on"
|
||||||
running: false
|
running: false
|
||||||
command: ["nmcli", "radio", "wifi", action]
|
command: ["nmcli", "radio", "wifi", action]
|
||||||
|
|
@ -224,10 +248,12 @@ Singleton {
|
||||||
onRunningChanged: {
|
onRunningChanged: {
|
||||||
if (!running) {
|
if (!running) {
|
||||||
if (action === "on") {
|
if (action === "on") {
|
||||||
wifiEnableTimer.start()
|
// Clear networks immediately and start delayed scan
|
||||||
|
root.networks = ({})
|
||||||
|
delayedScanTimer.interval = 8000
|
||||||
|
delayedScanTimer.restart()
|
||||||
} else {
|
} else {
|
||||||
root.networks = ({})
|
root.networks = ({})
|
||||||
root.isLoading = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -235,137 +261,177 @@ Singleton {
|
||||||
stderr: StdioCollector {
|
stderr: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`)
|
Logger.warn("Network", "WiFi toggle error: " + text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Process {
|
||||||
id: wifiEnableTimer
|
id: scanProcess
|
||||||
interval: 2000
|
running: false
|
||||||
repeat: false
|
command: ["sh", "-c", `
|
||||||
onTriggered: {
|
# Get list of saved connection profiles (just the names)
|
||||||
refreshNetworks()
|
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
|
||||||
if (adapter.lastConnected) {
|
|
||||||
reconnectTimer.start()
|
# 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 {
|
Process {
|
||||||
id: connectProcess
|
id: connectProcess
|
||||||
|
property string mode: "new"
|
||||||
property string ssid: ""
|
property string ssid: ""
|
||||||
property string password: ""
|
property string password: ""
|
||||||
property bool isSecured: false
|
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
command: {
|
command: {
|
||||||
const cmd = ["nmcli", "device", "wifi", "connect", ssid]
|
if (mode === "saved") {
|
||||||
if (isSecured && password) {
|
return ["nmcli", "connection", "up", "id", ssid]
|
||||||
cmd.push("password", password)
|
} else {
|
||||||
}
|
const cmd = ["nmcli", "device", "wifi", "connect", ssid]
|
||||||
return cmd
|
if (password) {
|
||||||
}
|
cmd.push("password", password)
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
handleConnectionSuccess(connectProcess.ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text.trim()) {
|
|
||||||
handleConnectionError(connectProcess.ssid, text)
|
|
||||||
}
|
}
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: upConnectionProcess
|
|
||||||
property string profileName: ""
|
|
||||||
running: false
|
|
||||||
command: ["nmcli", "connection", "up", "id", profileName]
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
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 {
|
stderr: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
|
root.connecting = false
|
||||||
|
root.connectingTo = ""
|
||||||
|
|
||||||
if (text.trim()) {
|
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
|
running: false
|
||||||
command: ["nmcli", "connection", "down", "id", ssid]
|
command: ["nmcli", "connection", "down", "id", ssid]
|
||||||
|
|
||||||
onRunningChanged: {
|
stdout: StdioCollector {
|
||||||
if (!running) {
|
onStreamFinished: {
|
||||||
connectingSsid = ""
|
Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`)
|
||||||
connectStatus = ""
|
|
||||||
connectStatusSsid = ""
|
// Immediately update UI on successful disconnect
|
||||||
connectError = ""
|
root.updateNetworkStatus(disconnectProcess.ssid, false)
|
||||||
refreshNetworks()
|
root.disconnectingFrom = ""
|
||||||
|
|
||||||
|
// Do a scan to refresh the list
|
||||||
|
delayedScanTimer.interval = 1000
|
||||||
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr: StdioCollector {
|
stderr: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
|
root.disconnectingFrom = ""
|
||||||
if (text.trim()) {
|
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 {
|
Process {
|
||||||
id: existingNetworkProcess
|
id: forgetProcess
|
||||||
|
property string ssid: ""
|
||||||
running: false
|
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 {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
const profiles = {}
|
Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`)
|
||||||
const lines = text.split("\n").filter(l => l.trim())
|
Logger.log("Network", text.trim().replace(/[\r\n]/g, " "))
|
||||||
|
|
||||||
for (const line of lines) {
|
// Update both cached and existing status immediately
|
||||||
const parts = line.split(":")
|
let nets = root.networks
|
||||||
const name = parts[0]
|
if (nets[forgetProcess.ssid]) {
|
||||||
const type = parts[1]
|
nets[forgetProcess.ssid].cached = false
|
||||||
if (name && type === "802-11-wireless") {
|
nets[forgetProcess.ssid].existing = false
|
||||||
profiles[name] = {
|
// Trigger property change
|
||||||
"ssid": name,
|
root.networks = ({})
|
||||||
"type": type
|
root.networks = nets
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scanProcess.existingProfiles = profiles
|
root.forgettingNetwork = ""
|
||||||
scanProcess.running = true
|
|
||||||
|
// Quick scan to verify the profile is gone
|
||||||
|
delayedScanTimer.interval = 500
|
||||||
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr: StdioCollector {
|
stderr: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (text.trim()) {
|
root.forgettingNetwork = ""
|
||||||
Logger.warn("Network", "Error listing connections:", text)
|
if (text.trim() && !text.includes("No profiles found")) {
|
||||||
retryRefresh()
|
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
|
// Signal when notification is received
|
||||||
onNotification: function (notification) {
|
onNotification: function (notification) {
|
||||||
|
// Always add notification to history
|
||||||
|
root.addToHistory(notification)
|
||||||
|
|
||||||
// Check if notifications are suppressed
|
// Check if do-not-disturb is enabled
|
||||||
if (Settings.data.notifications && Settings.data.notifications.suppressed) {
|
if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) {
|
||||||
// Still add to history but don't show notification
|
|
||||||
root.addToHistory(notification)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,8 +46,6 @@ Singleton {
|
||||||
|
|
||||||
// Add to our model
|
// Add to our model
|
||||||
root.addNotification(notification)
|
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 to add notification to model
|
||||||
function addNotification(notification) {
|
function addNotification(notification) {
|
||||||
notificationModel.insert(0, {
|
notificationModel.insert(0, {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ Singleton {
|
||||||
if (bytesPerSecond < 1024) {
|
if (bytesPerSecond < 1024) {
|
||||||
return bytesPerSecond.toFixed(0) + "B/s"
|
return bytesPerSecond.toFixed(0) + "B/s"
|
||||||
} else if (bytesPerSecond < 1024 * 1024) {
|
} 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) {
|
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
|
||||||
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s"
|
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s"
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -165,13 +165,21 @@ Singleton {
|
||||||
"timestamp": Date.now()
|
"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
|
// Add to queue
|
||||||
messageQueue.push(toastData)
|
messageQueue.push(toastData)
|
||||||
|
|
||||||
// Process queue if not currently showing a toast
|
// Always process immediately for instant display
|
||||||
if (!isShowingToast) {
|
processQueue()
|
||||||
processQueue()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the message queue
|
// Process the message queue
|
||||||
|
|
@ -181,11 +189,6 @@ Singleton {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isShowingToast) {
|
|
||||||
// Wait for current toast to finish
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var toastData = messageQueue.shift()
|
var toastData = messageQueue.shift()
|
||||||
isShowingToast = true
|
isShowingToast = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Public properties
|
// Public properties
|
||||||
property string baseVersion: "2.5.0"
|
property string baseVersion: "2.6.0"
|
||||||
property bool isDevelopment: true
|
property bool isDevelopment: true
|
||||||
|
|
||||||
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`
|
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
// Compact circular statistic display used in the SidePanel
|
// Compact circular statistic display using Layout management
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
|
@ -28,20 +29,20 @@ Rectangle {
|
||||||
// Repaint gauge when the bound value changes
|
// Repaint gauge when the bound value changes
|
||||||
onValueChanged: gauge.requestPaint()
|
onValueChanged: gauge.requestPaint()
|
||||||
|
|
||||||
Row {
|
ColumnLayout {
|
||||||
id: innerRow
|
id: mainLayout
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.marginS * scaling * contentScale
|
anchors.margins: Style.marginS * scaling * contentScale
|
||||||
spacing: Style.marginS * scaling * contentScale
|
spacing: 0
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
// Gauge with percentage label placed inside the open gap (right side)
|
// Main gauge container
|
||||||
Item {
|
Item {
|
||||||
id: gaugeWrap
|
id: gaugeContainer
|
||||||
anchors.verticalCenter: innerRow.verticalCenter
|
Layout.fillWidth: true
|
||||||
width: 68 * scaling * contentScale
|
Layout.fillHeight: true
|
||||||
height: 68 * scaling * contentScale
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
Layout.preferredWidth: 68 * scaling * contentScale
|
||||||
|
Layout.preferredHeight: 68 * scaling * contentScale
|
||||||
|
|
||||||
Canvas {
|
Canvas {
|
||||||
id: gauge
|
id: gauge
|
||||||
|
|
@ -84,15 +85,13 @@ Rectangle {
|
||||||
horizontalAlignment: Text.AlignHCenter
|
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 {
|
Rectangle {
|
||||||
id: iconBadge
|
id: iconBadge
|
||||||
width: 28 * scaling * contentScale
|
width: 28 * scaling * contentScale
|
||||||
height: width
|
height: width
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: Color.mSurface
|
color: Color.mSurface
|
||||||
// border.color: Color.mPrimary
|
|
||||||
// border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.rightMargin: -6 * scaling * contentScale
|
anchors.rightMargin: -6 * scaling * contentScale
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ Rectangle {
|
||||||
id: textItem
|
id: textItem
|
||||||
text: Time.time
|
text: Time.time
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,39 +8,34 @@ Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property color selectedColor: "#000000"
|
property color selectedColor: "#000000"
|
||||||
property bool expanded: false
|
|
||||||
|
|
||||||
signal colorSelected(color color)
|
signal colorSelected(color color)
|
||||||
signal colorCancelled
|
|
||||||
|
|
||||||
implicitWidth: expanded ? 320 * scaling : 150 * scaling
|
implicitWidth: 150 * scaling
|
||||||
implicitHeight: expanded ? 300 * scaling : 40 * scaling
|
implicitHeight: 40 * scaling
|
||||||
|
|
||||||
radius: Style.radiusM * scaling
|
radius: Style.radiusM * scaling
|
||||||
color: Color.mSurface
|
color: Color.mSurface
|
||||||
border.color: Color.mOutline
|
border.color: Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
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"]
|
// Minimized Look
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Style.animationFast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Style.animationFast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapsed view - just show current color
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
visible: !root.expanded
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
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 {
|
RowLayout {
|
||||||
anchors.fill: parent
|
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 string tooltipText
|
||||||
property bool enabled: true
|
property bool enabled: true
|
||||||
property bool hovering: false
|
property bool hovering: false
|
||||||
property real fontPointSize: Style.fontSizeM
|
|
||||||
|
|
||||||
property color colorBg: Color.mSurfaceVariant
|
property color colorBg: Color.mSurfaceVariant
|
||||||
property color colorFg: Color.mPrimary
|
property color colorFg: Color.mPrimary
|
||||||
|
|
@ -41,7 +40,7 @@ Rectangle {
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: root.icon
|
text: root.icon
|
||||||
font.pointSize: root.fontPointSize * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
color: root.hovering ? colorFgHover : colorFg
|
color: root.hovering ? colorFgHover : colorFg
|
||||||
// Center horizontally
|
// Center horizontally
|
||||||
x: (root.width - width) / 2
|
x: (root.width - width) / 2
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import QtQuick.Layouts
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ColumnLayout {
|
// Input and button row
|
||||||
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Public properties
|
// Public properties
|
||||||
|
|
@ -21,57 +22,35 @@ ColumnLayout {
|
||||||
|
|
||||||
// Internal properties
|
// Internal properties
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Label
|
NTextInput {
|
||||||
NText {
|
id: textInput
|
||||||
text: root.label
|
label: root.label
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
description: root.description
|
||||||
font.weight: Style.fontWeightBold
|
placeholderText: root.placeholderText
|
||||||
color: Color.mOnSurface
|
text: root.text
|
||||||
|
onEditingFinished: {
|
||||||
|
root.text = text
|
||||||
|
root.editingFinished()
|
||||||
|
}
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Description
|
NButton {
|
||||||
NText {
|
Layout.fillWidth: false
|
||||||
text: root.description
|
Layout.alignment: Qt.AlignBottom
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input and button row
|
text: root.actionButtonText
|
||||||
RowLayout {
|
icon: root.actionButtonIcon
|
||||||
spacing: Style.marginM * scaling
|
backgroundColor: Color.mSecondary
|
||||||
Layout.fillWidth: true
|
textColor: Color.mOnSecondary
|
||||||
|
hoverColor: Color.mTertiary
|
||||||
|
pressColor: Color.mPrimary
|
||||||
|
enabled: root.actionButtonEnabled
|
||||||
|
|
||||||
NTextInput {
|
onClicked: {
|
||||||
id: textInput
|
root.actionClicked()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,8 +231,7 @@ Item {
|
||||||
root.clicked()
|
root.clicked()
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
root.rightClicked()
|
root.rightClicked()
|
||||||
}
|
} else if (mouse.button === Qt.MiddleButton) {
|
||||||
else if (mouse.button === Qt.MiddleButton) {
|
|
||||||
root.middleClicked()
|
root.middleClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ ColumnLayout {
|
||||||
property string description: ""
|
property string description: ""
|
||||||
property bool readOnly: false
|
property bool readOnly: false
|
||||||
property bool enabled: true
|
property bool enabled: true
|
||||||
property int inputMaxWidth: Math.round(420 * scaling)
|
|
||||||
property color labelColor: Color.mOnSurface
|
property color labelColor: Color.mOnSurface
|
||||||
property color descriptionColor: Color.mOnSurfaceVariant
|
property color descriptionColor: Color.mOnSurfaceVariant
|
||||||
property string fontFamily: Settings.data.ui.fontDefault
|
property string fontFamily: Settings.data.ui.fontDefault
|
||||||
|
|
@ -26,7 +25,6 @@ ColumnLayout {
|
||||||
signal editingFinished
|
signal editingFinished
|
||||||
|
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
implicitHeight: frame.height
|
|
||||||
|
|
||||||
NLabel {
|
NLabel {
|
||||||
label: root.label
|
label: root.label
|
||||||
|
|
@ -34,6 +32,7 @@ ColumnLayout {
|
||||||
labelColor: root.labelColor
|
labelColor: root.labelColor
|
||||||
descriptionColor: root.descriptionColor
|
descriptionColor: root.descriptionColor
|
||||||
visible: root.label !== "" || root.description !== ""
|
visible: root.label !== "" || root.description !== ""
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container
|
// Container
|
||||||
|
|
@ -42,50 +41,48 @@ ColumnLayout {
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumWidth: 80 * scaling
|
Layout.minimumWidth: 80 * scaling
|
||||||
Layout.maximumWidth: root.inputMaxWidth
|
|
||||||
|
|
||||||
implicitWidth: parent.width
|
|
||||||
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
|
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
|
||||||
|
|
||||||
radius: Style.radiusM * scaling
|
radius: Style.radiusM * scaling
|
||||||
color: Color.mSurface
|
color: Color.mSurface
|
||||||
border.color: Color.mOutline
|
border.color: input.activeFocus ? Color.mSecondary : Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
// Focus ring
|
Behavior on border.color {
|
||||||
Rectangle {
|
ColorAnimation {
|
||||||
anchors.fill: parent
|
duration: Style.animationFast
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
TextField {
|
||||||
|
id: input
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: Style.marginM * scaling
|
anchors.leftMargin: Style.marginM * scaling
|
||||||
anchors.rightMargin: Style.marginM * scaling
|
anchors.rightMargin: Style.marginM * scaling
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
|
|
||||||
TextField {
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
id: input
|
|
||||||
Layout.fillWidth: true
|
echoMode: TextInput.Normal
|
||||||
echoMode: TextInput.Normal
|
readOnly: root.readOnly
|
||||||
readOnly: root.readOnly
|
enabled: root.enabled
|
||||||
enabled: root.enabled
|
color: Color.mOnSurface
|
||||||
color: Color.mOnSurface
|
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
|
||||||
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
|
|
||||||
background: null
|
selectByMouse: true
|
||||||
font.family: fontFamily
|
|
||||||
font.pointSize: fontSize
|
topPadding: 0
|
||||||
font.weight: fontWeight
|
bottomPadding: 0
|
||||||
onEditingFinished: root.editingFinished()
|
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.
|
// NToast updates its scaling when showing.
|
||||||
scaling = ScalingService.getScreenScale(screen)
|
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
|
visible = true
|
||||||
|
|
||||||
|
// Start the show animation
|
||||||
showAnimation.start()
|
showAnimation.start()
|
||||||
if (duration > 0 && !persistent) {
|
if (duration > 0 && !persistent) {
|
||||||
autoHideTimer.start()
|
autoHideTimer.start()
|
||||||
|
|
@ -81,7 +90,6 @@ Item {
|
||||||
|
|
||||||
// Main toast container
|
// Main toast container
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: container
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Style.radiusL * scaling
|
radius: Style.radiusL * scaling
|
||||||
|
|
||||||
|
|
@ -137,43 +145,41 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label and description
|
// Label and description
|
||||||
Column {
|
ColumnLayout {
|
||||||
id: textColumn
|
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
id: labelText
|
Layout.fillWidth: true
|
||||||
text: root.label
|
text: root.label
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width
|
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
id: descriptionText
|
Layout.fillWidth: true
|
||||||
text: root.description
|
text: root.description
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width
|
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close button (only if persistent or manual dismiss needed)
|
// Close button (only if persistent or manual dismiss needed)
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: closeButton
|
|
||||||
icon: "close"
|
icon: "close"
|
||||||
visible: root.persistent || root.duration === 0
|
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
|
sizeRatio: 0.8
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue