Post refactoring fixes 1/??

This commit is contained in:
quadbyte 2025-08-15 21:55:32 -04:00
parent 605e3610d2
commit 4631239b92
19 changed files with 8 additions and 11 deletions

View file

@ -0,0 +1,369 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Modules.SettingsPanel.Tabs as Tabs
import qs.Commons
import qs.Services
import qs.Widgets
NLoader {
id: root
// Tabs enumeration, order is NOT relevant
enum Tab {
About,
AudioService,
Bar,
ColorScheme,
Display,
General,
Network,
ScreenRecorder,
TimeWeather,
Wallpaper,
WallpaperSelector
}
property int requestedTab: SettingsPanel.Tab.General
content: Component {
NPanel {
id: panel
property int currentTabIndex: 0
// Override hide function to animate first
function hide() {
// Start hide animation
bgRect.scaleValue = 0.8
bgRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: panel
function onDismissed() {
hide()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
panel.visible = false
panel.dismissed()
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Component {
id: generalTab
Tabs.GeneralTab {}
}
Component {
id: barTab
Tabs.BarTab {}
}
Component {
id: audioTab
Tabs.AudioTab {}
}
Component {
id: displayTab
Tabs.DisplayTab {}
}
Component {
id: networkTab
Tabs.NetworkTab {}
}
Component {
id: timeWeatherTab
Tabs.TimeWeatherTab {}
}
Component {
id: colorSchemeTab
Tabs.ColorSchemeTab {}
}
Component {
id: wallpaperTab
Tabs.WallpaperTab {}
}
Component {
id: wallpaperSelectorTab
Tabs.WallpaperSelectorTab {}
}
Component {
id: screenRecorderTab
Tabs.ScreenRecorderTab {}
}
Component {
id: aboutTab
Tabs.AboutTab {}
}
property var tabsModel: [{
"id": SettingsPanel.Tab.General,
"label": "General",
"icon": "tune",
"source": generalTab
}, {
"id": SettingsPanel.Tab.Bar,
"label": "Bar",
"icon": "web_asset",
"source": barTab
}, {
"id": SettingsPanel.Tab.AudioService,
"label": "AudioService",
"icon": "volume_up",
"source": audioTab
}, {
"id": SettingsPanel.Tab.Display,
"label": "Display",
"icon": "monitor",
"source": displayTab
}, {
"id": SettingsPanel.Tab.Network,
"label": "Network",
"icon": "lan",
"source": networkTab
}, {
"id": SettingsPanel.Tab.TimeWeather,
"label": "Time & Weather",
"icon": "schedule",
"source": timeWeatherTab
}, {
"id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme",
"icon": "palette",
"source": colorSchemeTab
}, {
"id": SettingsPanel.Tab.Wallpaper,
"label": "Wallpaper",
"icon": "image",
"source": wallpaperTab
}, {
"id": SettingsPanel.Tab.WallpaperSelector,
"label": "Wallpaper Selector",
"icon": "wallpaper_slideshow",
"source": wallpaperSelectorTab
}, {
"id": SettingsPanel.Tab.ScreenRecorder,
"label": "Screen Recorder",
"icon": "videocam",
"source": screenRecorderTab
}, {
"id": SettingsPanel.Tab.About,
"label": "About",
"icon": "info",
"source": aboutTab
}]
Component.onCompleted: {
var initialIndex = 0
if (root.requestedTab !== null) {
for (var i = 0; i < panel.tabsModel.length; i++) {
if (panel.tabsModel[i].id === root.requestedTab) {
initialIndex = i
break
}
}
}
// Now that the UI is settled, set the current tab index.
panel.currentTabIndex = initialIndex
show()
}
onVisibleChanged: {
if (!visible && (bgRect.opacityValue > 0)) {
hide()
}
}
Rectangle {
id: bgRect
color: Colors.mSurface
radius: Style.radiusLarge * scaling
border.color: Colors.mOutlineVariant
border.width: Math.max(1, Style.borderThin * scaling)
layer.enabled: true
width: Math.max(screen.width * 0.5, 1280) * scaling
height: Math.max(screen.height * 0.5, 720) * scaling
anchors.centerIn: parent
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
MouseArea {
anchors.fill: parent
}
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginLarge * scaling
Rectangle {
id: sidebar
Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling
Layout.fillHeight: true
color: Colors.mSurfaceVariant
border.color: Colors.mOutlineVariant
border.width: Math.max(1, Style.borderThin * scaling)
radius: Style.radiusMedium * scaling
Column {
anchors.fill: parent
anchors.margins: Style.marginSmall * scaling
spacing: Style.marginTiny * 1.5 * scaling
Repeater {
id: sections
model: panel.tabsModel
delegate: Rectangle {
id: tabItem
width: parent.width
height: 32 * scaling
radius: Style.radiusSmall * scaling
color: selected ? Colors.mPrimary : (tabItem.hovering ? Colors.mTertiary : "transparent")
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Colors.mOnPrimary : (tabItem.hovering ? Colors.mOnTertiary : Colors.mOnSurface)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginSmall * scaling
anchors.rightMargin: Style.marginSmall * scaling
spacing: Style.marginSmall * scaling
// Tab icon on the left side
NText {
text: modelData.icon
color: tabTextColor
font.family: "Material Symbols Outlined"
font.variableAxes: {
"wght": (Font.Normal + Font.Bold) / 2.0
}
font.pointSize: Style.fontSizeLarge * scaling
}
// Tab label on the left side
NText {
text: modelData.label
color: tabTextColor
font.pointSize: Style.fontSizeMedium * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
}
}
}
}
// Content
Rectangle {
id: contentPane
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusMedium * scaling
color: Colors.mSurfaceVariant
border.color: Colors.mOutlineVariant
border.width: Math.max(1, Style.borderThin * scaling)
clip: true
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginSmall * scaling
RowLayout {
id: headerRow
Layout.fillWidth: true
spacing: Style.marginSmall * scaling
// Tab label on the main right side
NText {
text: panel.tabsModel[currentTabIndex].label
font.pointSize: Style.fontSizeLarge * scaling
font.weight: Style.fontWeightBold
color: Colors.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter
onClicked: panel.hide()
}
}
NDivider {
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Repeater {
model: panel.tabsModel
onItemAdded: function (index, item) {
item.sourceComponent = panel.tabsModel[index].source
}
delegate: Loader {
// All loaders will occupy the same space, stacked on top of each other.
anchors.fill: parent
visible: index === panel.currentTabIndex
// The loader is only active (and uses memory) when its page is visible.
active: visible
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,264 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
property string latestVersion: GitHubService.latestVersion
property string currentVersion: "Unknown" // Fallback version
property var contributors: GitHubService.contributors
spacing: 0
Layout.fillWidth: true
Layout.fillHeight: true
Process {
id: currentVersionProcess
command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"]
Component.onCompleted: {
running = true
}
stdout: StdioCollector {
onStreamFinished: {
const version = text.trim()
if (version && version !== "Unknown") {
root.currentVersion = version
} else {
currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir
+ " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"]
currentVersionProcess.running = true
}
}
}
}
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginLarge * scaling
rightPadding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
NText {
text: "Noctalia: quiet by design"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginSmall * scaling
}
NText {
text: "It may just be another quickshell setup but it won't get in your way."
font.pointSize: Style.fontSizeMedium * scaling
color: Colors.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginLarge * scaling
}
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: Style.marginTiny * scaling
columnSpacing: Style.marginSmall * scaling
NText {
text: "Latest Version:"
color: Colors.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
text: root.latestVersion
color: Colors.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
text: "Installed Version:"
color: Colors.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
text: root.currentVersion
color: Colors.mOnSurface
font.weight: Style.fontWeightBold
}
}
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginSmall * scaling
Layout.preferredWidth: updateText.implicitWidth + 46 * scaling
Layout.preferredHeight: Style.barHeight * scaling
radius: Style.radiusLarge * scaling
color: updateArea.containsMouse ? Colors.mPrimary : "transparent"
border.color: Colors.mPrimary
border.width: Math.max(1, Style.borderThin * scaling)
visible: {
if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown")
return false
const latest = root.latestVersion.replace("v", "").split(".")
const current = root.currentVersion.replace("v", "").split(".")
for (var i = 0; i < Math.max(latest.length, current.length); i++) {
const l = parseInt(latest[i] || "0")
const c = parseInt(current[i] || "0")
if (l > c)
return true
if (l < c)
return false
}
return false
}
RowLayout {
anchors.centerIn: parent
spacing: Style.marginSmall * scaling
NText {
text: "system_update"
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeXL * scaling
color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary
}
NText {
id: updateText
text: "Download latest release"
font.pointSize: Style.fontSizeLarge * scaling
color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary
}
}
MouseArea {
id: updateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"])
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
NText {
text: `Shout-out to our ${root.contributors.length} awesome contributors!`
font.pointSize: Style.fontSizeLarge * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginLarge * 2
}
ScrollView {
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: 200 * Style.marginTiny * scaling
Layout.fillHeight: true
Layout.topMargin: Style.marginLarge * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
GridView {
id: contributorsGrid
anchors.fill: parent
width: 200 * 4 * scaling
height: Math.ceil(root.contributors.length / 4) * 100
cellWidth: Style.baseWidgetSize * 6.25 * scaling
cellHeight: Style.baseWidgetSize * 3.125 * scaling
model: root.contributors
delegate: Rectangle {
width: contributorsGrid.cellWidth - Style.marginLarge * scaling
height: contributorsGrid.cellHeight - Style.marginTiny * scaling
radius: Style.radiusLarge * scaling
color: contributorArea.containsMouse ? Colors.mTertiary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginSmall * scaling
spacing: Style.marginMedium * scaling
Item {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling
NImageRounded {
imagePath: modelData.avatar_url || ""
anchors.fill: parent
anchors.margins: Style.marginTiny * scaling
fallbackIcon: "person"
borderColor: Colors.mPrimary
borderWidth: Math.max(1, Style.borderMedium * scaling)
imageRadius: width * 0.5
}
}
ColumnLayout {
spacing: Style.marginTiny * scaling
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NText {
text: modelData.login || "Unknown"
font.weight: Style.fontWeightBold
color: contributorArea.containsMouse ? Colors.mSurface : Colors.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: (modelData.contributions || 0) + " " + ((modelData.contributions
|| 0) === 1 ? "commit" : "commits")
font.pointSize: Style.fontSizeSmall * scaling
color: contributorArea.containsMouse ? Colors.mSurface : Colors.mOnSurface
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.html_url)
Quickshell.execDetached(["xdg-open", modelData.html_url])
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,276 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import qs.Widgets
import qs.Commons
import qs.Services
ColumnLayout {
id: root
property real localVolume: AudioService.volume
// Connection used to open the pill when volume changes
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
localVolume = AudioService.volume
}
}
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginTiny * scaling
Layout.fillWidth: true
NText {
text: "AudioService"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// Volume Controls
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
// Master Volume
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
ColumnLayout {
spacing: Style.marginTiniest * scaling
NText {
text: "Master Volume"
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "System-wide volume level"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
RowLayout {
// Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily
// Probably because they have some quick fades in and out to avoid clipping
// We use a timer to space out the updates, to avoid lock up
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (Math.abs(localVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localVolume)
}
}
}
NSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0
value: localVolume
stepSize: 0.01
onMoved: {
localVolume = value
}
}
NText {
text: Math.floor(AudioService.volume * 100) + "%"
Layout.alignment: Qt.AlignVCenter
color: Colors.mOnSurface
}
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginMedium * scaling
NToggle {
label: "Mute AudioService"
description: "Mute or unmute the default audio output"
value: AudioService.muted
onToggled: function (newValue) {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = newValue
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// AudioService Devices
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "AudioService Devices"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// -------------------------------
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
spacing: Style.marginTiniest * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginLarge * scaling
NText {
text: "Output Device"
font.pointSize: Style.fontSizeMedium * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "Select the desired audio output device"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: AudioService.sinks
NRadioButton {
required property PwNode modelData
ButtonGroup.group: sinks
checked: AudioService.sink?.id === modelData.id
onClicked: AudioService.setAudioSink(modelData)
text: modelData.description
}
}
}
}
// -------------------------------
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: Style.marginTiniest * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginLarge * scaling
NText {
text: "Input Device"
font.pointSize: Style.fontSizeMedium * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "Select desired audio input device"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: AudioService.sources
NRadioButton {
required property PwNode modelData
ButtonGroup.group: sources
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
text: modelData.description
}
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * scaling
Layout.bottomMargin: Style.marginMedium * scaling
}
// AudioService Visualizer Category
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
NText {
text: "AudioService Visualizer"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// AudioService Visualizer section
NComboBox {
id: audioVisualizerCombo
label: "Visualization Type"
description: "Choose a visualization type for media playback"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "linear"
name: "Linear"
}
}
currentKey: Settings.data.audio.visualizerType
onSelected: function (key) {
Settings.data.audio.visualizerType = key
}
}
}
}
}
}

View file

@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Components"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NToggle {
label: "Show Active Window"
description: "Display the title of the currently focused window below the bar"
value: Settings.data.bar.showActiveWindow
onToggled: function (newValue) {
Settings.data.bar.showActiveWindow = newValue
}
}
NToggle {
label: "Show System Info"
description: "Display system information (CPU, RAM, Temperature)"
value: Settings.data.bar.showSystemInfo
onToggled: function (newValue) {
Settings.data.bar.showSystemInfo = newValue
}
}
NToggle {
label: "Show Taskbar"
description: "Display a taskbar showing currently open windows"
value: Settings.data.bar.showTaskbar
onToggled: function (newValue) {
Settings.data.bar.showTaskbar = newValue
}
}
NToggle {
label: "Show Media"
description: "Display media controls and information"
value: Settings.data.bar.showMedia
onToggled: function (newValue) {
Settings.data.bar.showMedia = newValue
}
}
}
}
}
}

View file

@ -0,0 +1,325 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
import Quickshell.Io
ColumnLayout {
id: root
spacing: 0
// Helper function to get color from scheme file
function getSchemeColor(schemePath, colorKey) {
// Extract scheme name from path
var schemeName = schemePath.split("/").pop().replace(".json", "")
// Try to get from cached data first
if (schemeColorsCache[schemeName] && schemeColorsCache[schemeName][colorKey]) {
return schemeColorsCache[schemeName][colorKey]
}
// Return a default color if not cached yet
return "#000000"
}
// Cache for scheme colors
property var schemeColorsCache: ({})
// Scale properties for card animations
property real cardScaleLow: 0.95
property real cardScaleHigh: 1.0
// This function is called by the FileView Repeater when a scheme file is loaded
function schemeLoaded(schemeName, jsonData) {
var colors = {}
// Extract colors from JSON data
if (jsonData && typeof jsonData === 'object') {
colors.mPrimary = jsonData.mPrimary || jsonData.primary || "#000000"
colors.mSecondary = jsonData.mSecondary || jsonData.secondary || "#000000"
colors.mTertiary = jsonData.mTertiary || jsonData.tertiary || "#000000"
colors.mError = jsonData.mError || jsonData.error || "#ff0000"
colors.mSurface = jsonData.mSurface || jsonData.surface || "#ffffff"
colors.mOnSurface = jsonData.mOnSurface || jsonData.onSurface || "#000000"
colors.mOutline = jsonData.mOutline || jsonData.outline || "#666666"
} else {
// Default colors on failure
colors = {
"mPrimary": "#000000",
"mSecondary": "#000000",
"mTertiary": "#000000",
"mError": "#ff0000",
"mSurface": "#ffffff",
"mOnSurface": "#000000",
"mOutline": "#666666"
}
}
// Update the cache. This must be done by re-assigning the whole object to trigger updates.
var newCache = schemeColorsCache
newCache[schemeName] = colors
schemeColorsCache = newCache
}
// When the list of available schemes changes, clear the cache.
// The Repeater below will automatically re-create the FileViews.
Connections {
target: ColorSchemesService
function onSchemesChanged() {
schemeColorsCache = {}
}
}
// A non-visual Item to host the Repeater that loads the color scheme files.
Item {
visible: false
id: fileLoaders
Repeater {
model: ColorSchemesService.schemes
// The delegate is a Component, which correctly wraps the non-visual FileView
delegate: Item {
FileView {
path: modelData
blockLoading: true
onLoaded: {
var schemeName = path.split("/").pop().replace(".json", "")
try {
var jsonData = JSON.parse(text())
root.schemeLoaded(schemeName, jsonData)
} catch (e) {
console.warn("Failed to parse JSON for scheme:", schemeName, e)
root.schemeLoaded(schemeName, null) // Load defaults on parse error
}
}
}
}
}
}
// UI Code
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
// Use Matugen
NToggle {
label: "Use Matugen"
description: "Automatically generate colors based on your active wallpaper using Matugen"
value: Settings.data.colorSchemes.useWallpaperColors
onToggled: function (newValue) {
Settings.data.colorSchemes.useWallpaperColors = newValue
if (Settings.data.colorSchemes.useWallpaperColors) {
ColorSchemesService.changedWallpaper()
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
ColumnLayout {
spacing: Style.marginTiniest * scaling
Layout.fillWidth: true
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeLarge * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.fillWidth: true
}
NText {
text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead."
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
ColumnLayout {
spacing: Style.marginTiny * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * scaling
// Color Schemes Grid
GridLayout {
columns: 4
rowSpacing: Style.marginLarge * scaling
columnSpacing: Style.marginLarge * scaling
Layout.fillWidth: true
Repeater {
model: ColorSchemesService.schemes
Rectangle {
id: schemeCard
property string schemePath: modelData
Layout.fillWidth: true
Layout.preferredHeight: 120 * scaling
radius: Style.radiusMedium * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderThick * scaling)
border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline
scale: root.cardScaleLow
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
// TBC: broken uncheck useWallpaperColors
Settings.data.colorSchemes.useWallpaperColors = false
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemesService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
onExited: {
schemeCard.scale = root.cardScaleLow
}
}
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginSmall * scaling
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeMedium * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
// Color swatches
RowLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Primary color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mPrimary")
}
// Secondary color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
// Tertiary color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
// Error color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
}
}
// Selection indicator
Rectangle {
visible: Settings.data.colorSchemes.predefinedScheme === schemePath
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginSmall * scaling
width: 24 * scaling
height: 24 * scaling
radius: width * 0.5
color: Colors.mPrimary
NText {
anchors.centerIn: parent
text: "✓"
font.pointSize: Style.fontSizeSmall * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnPrimary
}
}
// Smooth animations
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,191 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
Item {
property real scaling: 1
readonly property string tabIcon: "monitor"
readonly property string tabLabel: "Display"
readonly property int tabIndex: 5
Layout.fillWidth: true
Layout.fillHeight: true
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice()
if (!arr.includes(name))
arr.push(name)
return arr
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name
})
}
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
contentWidth: parent.width
ColumnLayout {
width: parent.width
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.margins: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Permonitor configuration"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "By default, bars and notifications are shown on all displays. Select one or more below to narrow your view."
font.pointSize: Style.fontSize * scaling
color: Colors.mOnSurfaceVariant
}
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
Layout.fillWidth: true
radius: Style.radiusMedium * scaling
color: Colors.mSurface
border.color: Colors.mOutline
border.width: Math.max(1, Style.borderThin * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginTiniest * scaling
NText {
text: (modelData.name || "Unknown")
font.pointSize: Style.fontSizeLarge * scaling
font.weight: Style.fontWeightBold
color: Colors.mSecondary
}
NText {
text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})`
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
}
ColumnLayout {
spacing: Style.marginLarge * scaling
NToggle {
label: "Bar"
description: "Enable the top bar on this monitor"
value: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: function (newValue) {
if (newValue) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
}
}
}
NToggle {
label: "Notifications"
description: "Enable notifications on this monitor"
value: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: function (newValue) {
if (newValue) {
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors,
modelData.name)
} else {
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors,
modelData.name)
}
}
}
NToggle {
label: "Dock"
description: "Enable the dock on this monitor"
value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: function (newValue) {
if (newValue) {
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name)
} else {
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name)
}
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// Brightness Section
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
NText {
text: "Brightness Step Size"
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "Adjust the step size for brightness changes (scroll wheel, ipc bind)"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginMedium * scaling
NSlider {
Layout.fillWidth: true
from: 1
to: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
onPressedChanged: {
if (!pressed) {
Settings.data.brightness.brightnessStep = value
}
}
}
NText {
text: Settings.data.brightness.brightnessStep + "%"
Layout.alignment: Qt.AlignVCenter
color: Colors.mOnSurface
}
}
}
Item {
Layout.fillHeight: true
}
}
}
}
}

View file

@ -0,0 +1,124 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "General Settings"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
// Profile section
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginLarge * scaling
// Avatar preview
NImageRounded {
width: 64 * scaling
height: 64 * scaling
imagePath: Settings.data.general.avatarImage
fallbackIcon: "person"
borderColor: Colors.mPrimary
borderWidth: Math.max(1, Style.borderMedium)
}
NTextInput {
label: "Profile Picture"
description: "Your profile picture displayed in various places throughout the shell"
text: Settings.data.general.avatarImage
placeholderText: "/home/user/.face"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.general.avatarImage = text
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "User Interface"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
NToggle {
label: "Show Corners"
description: "Display rounded corners on the edge of the screen"
value: Settings.data.general.showScreenCorners
onToggled: function (v) {
Settings.data.general.showScreenCorners = v
}
}
NToggle {
label: "Dim Desktop"
description: "Dim the desktop when panels or menus are open"
value: Settings.data.general.dimDesktop
onToggled: function (v) {
Settings.data.general.dimDesktop = v
}
}
NToggle {
label: "Auto-hide Dock"
description: "Automatically hide the dock when not in use"
value: Settings.data.dock.autoHide
onToggled: function (v) {
Settings.data.dock.autoHide = v
}
}
}
}
}
}

View file

@ -0,0 +1,65 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Interfaces"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NToggle {
label: "WiFi Enabled"
description: "Enable WiFi connectivity"
value: Settings.data.network.wifiEnabled
onToggled: function (newValue) {
Settings.data.network.wifiEnabled = newValue
}
}
NToggle {
label: "Bluetooth Enabled"
description: "Enable Bluetooth connectivity"
value: Settings.data.network.bluetoothEnabled
onToggled: function (newValue) {
Settings.data.network.bluetoothEnabled = newValue
}
}
}
}
}
}

View file

@ -0,0 +1,270 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginTiny * scaling
Layout.fillWidth: true
NText {
text: "Recording"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// Output Directory
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
NTextInput {
label: "Output Directory"
description: "Directory where screen recordings will be saved"
placeholderText: "/home/xxx/Videos"
text: Settings.data.screenRecorder.directory
onEditingFinished: {
Settings.data.screenRecorder.directory = text
}
}
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginMedium * scaling
// Show Cursor
NToggle {
label: "Show Cursor"
description: "Record mouse cursor in the video"
value: Settings.data.screenRecorder.showCursor
onToggled: function (newValue) {
Settings.data.screenRecorder.showCursor = newValue
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// Video Settings
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Video Settings"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// Frame Rate
NComboBox {
label: "Frame Rate"
description: "Target frame rate for screen recordings (default: 60)"
model: ListModel {
ListElement {
key: "30"
name: "30 FPS"
}
ListElement {
key: "60"
name: "60 FPS"
}
ListElement {
key: "120"
name: "120 FPS"
}
ListElement {
key: "240"
name: "240 FPS"
}
}
currentKey: Settings.data.screenRecorder.frameRate
onSelected: function (key) {
Settings.data.screenRecorder.frameRate = key
}
}
// Video Quality
NComboBox {
label: "Video Quality"
description: "Higher quality results in larger file sizes"
model: ListModel {
ListElement {
key: "medium"
name: "Medium"
}
ListElement {
key: "high"
name: "High"
}
ListElement {
key: "very_high"
name: "Very High"
}
ListElement {
key: "ultra"
name: "Ultra"
}
}
currentKey: Settings.data.screenRecorder.quality
onSelected: function (key) {
Settings.data.screenRecorder.quality = key
}
}
// Video Codec
NComboBox {
label: "Video Codec"
description: "Different codecs offer different compression and compatibility"
model: ListModel {
ListElement {
key: "h264"
name: "H264"
}
ListElement {
key: "hevc"
name: "HEVC"
}
ListElement {
key: "av1"
name: "AV1"
}
ListElement {
key: "vp8"
name: "VP8"
}
ListElement {
key: "vp9"
name: "VP9"
}
}
currentKey: Settings.data.screenRecorder.videoCodec
onSelected: function (key) {
Settings.data.screenRecorder.videoCodec = key
}
}
// Color Range
NComboBox {
label: "Color Range"
description: "Limited is recommended for better compatibility"
model: ListModel {
ListElement {
key: "limited"
name: "Limited"
}
ListElement {
key: "full"
name: "Full"
}
}
currentKey: Settings.data.screenRecorder.colorRange
onSelected: function (key) {
Settings.data.screenRecorder.colorRange = key
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// AudioService Settings
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "AudioService Settings"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// AudioService Source
NComboBox {
label: "AudioService Source"
description: "AudioService source to capture during recording"
model: ListModel {
ListElement {
key: "default_output"
name: "System Output"
}
ListElement {
key: "default_input"
name: "Microphone Input"
}
ListElement {
key: "both"
name: "System Output + Microphone Input"
}
}
currentKey: Settings.data.screenRecorder.audioSource
onSelected: function (key) {
Settings.data.screenRecorder.audioSource = key
}
}
// AudioService Codec
NComboBox {
label: "AudioService Codec"
description: "Opus is recommended for best performance and smallest audio size"
model: ListModel {
ListElement {
key: "opus"
name: "Opus"
}
ListElement {
key: "aac"
name: "AAC"
}
}
currentKey: Settings.data.screenRecorder.audioCodec
onSelected: function (key) {
Settings.data.screenRecorder.audioCodec = key
}
}
}
}
}
}
}

View file

@ -0,0 +1,131 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginTiny * scaling
Layout.fillWidth: true
NText {
text: "Location"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// Location section
ColumnLayout {
spacing: Style.marginMedium * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
NTextInput {
label: "Location name"
description: "Choose a known location near you"
text: Settings.data.location.name
placeholderText: "Enter the location name"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.location.name = text
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// Time section
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Time Format"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
NToggle {
label: "Use 12-Hour Clock"
description: "Display time in 12-hour format (AM/PM) instead of 24-hour"
value: Settings.data.location.use12HourClock
onToggled: function (newValue) {
Settings.data.location.use12HourClock = newValue
}
}
NToggle {
label: "Reverse Day/Month"
description: "Display date as DD/MM instead of MM/DD"
value: Settings.data.location.reverseDayMonth
onToggled: function (newValue) {
Settings.data.location.reverseDayMonth = newValue
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// Weather section
ColumnLayout {
spacing: Style.marginMedium * scaling
Layout.fillWidth: true
NText {
text: "Weather"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
NToggle {
label: "Use Fahrenheit"
description: "Display temperature in Fahrenheit instead of Celsius"
value: Settings.data.location.useFahrenheit
onToggled: function (newValue) {
Settings.data.location.useFahrenheit = newValue
}
}
}
}
}
}
}

View file

@ -0,0 +1,246 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt.labs.folderlistmodel
import qs.Commons
import qs.Services
import qs.Widgets
Item {
property real scaling: 1
readonly property string tabIcon: "photo_library"
readonly property string tabLabel: "Wallpaper Selector"
readonly property int tabIndex: 7
Layout.fillWidth: true
Layout.fillHeight: true
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
contentWidth: parent.width
ColumnLayout {
width: parent.width
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.margins: Style.marginLarge * scaling
Layout.fillWidth: true
// Current wallpaper display
NText {
text: "Current Wallpaper"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 120 * scaling
radius: Style.radiusMedium * scaling
color: Colors.mSurface
border.color: Colors.mOutline
border.width: Math.max(1, Style.borderThin * scaling)
clip: true
NImageRounded {
id: currentWallpaperImage
anchors.fill: parent
anchors.margins: Style.marginSmall * scaling
imagePath: WallpapersService.currentWallpaper
fallbackIcon: "image"
borderColor: Colors.mOutline
borderWidth: Math.max(1, Style.borderThin * scaling)
imageRadius: Style.radiusMedium * scaling
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Layout.fillWidth: true
// Wallpaper grid
NText {
text: "Wallpaper Selector"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "Click on a wallpaper to set it as your current wallpaper"
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
NText {
text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType
+ " transition" : "Wallpapers will change instantly"
color: Colors.mOnSurface
font.pointSize: Style.fontSizeSmall * scaling
visible: Settings.data.wallpaper.swww.enabled
}
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh wallpaper list"
onClicked: {
WallpapersService.loadWallpapers()
}
Layout.alignment: Qt.AlignTop | Qt.AlignRight
}
}
// Wallpaper grid container
Item {
Layout.fillWidth: true
Layout.preferredHeight: {
return Math.ceil(WallpapersService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight
}
GridView {
id: wallpaperGridView
anchors.fill: parent
clip: true
model: WallpapersService.wallpaperList
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.AutoFlickDirection
interactive: false
property int columns: 5
property int itemSize: Math.floor(
(width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling
leftMargin: Style.marginSmall * scaling
rightMargin: Style.marginSmall * scaling
topMargin: Style.marginSmall * scaling
bottomMargin: Style.marginSmall * scaling
delegate: Rectangle {
id: wallpaperItem
property string wallpaperPath: modelData
property bool isSelected: wallpaperPath === WallpapersService.currentWallpaper
width: wallpaperGridView.itemSize
height: Math.floor(wallpaperGridView.itemSize * 0.67)
radius: Style.radiusMedium * scaling
color: isSelected ? Colors.mPrimary : Colors.mSurface
border.color: isSelected ? Colors.mSecondary : Colors.mOutline
border.width: Math.max(1, Style.borderThin * scaling)
clip: true
NImageRounded {
anchors.fill: parent
anchors.margins: Style.marginTiny * scaling
imagePath: wallpaperPath
fallbackIcon: "image"
imageRadius: Style.radiusMedium * scaling
}
// Selection indicator
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginTiny * scaling
width: 20 * scaling
height: 20 * scaling
radius: width / 2
color: Colors.mPrimary
border.color: Colors.mOutline
border.width: Math.max(1, Style.borderThin * scaling)
visible: isSelected
NText {
anchors.centerIn: parent
text: "check"
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnPrimary
}
}
// Hover effect
Rectangle {
anchors.fill: parent
color: Colors.mOnSurface
opacity: mouseArea.containsMouse ? 0.1 : 0
radius: parent.radius
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onClicked: {
WallpapersService.changeWallpaper(wallpaperPath)
}
}
}
}
// Empty state
Rectangle {
anchors.fill: parent
color: Colors.mSurface
radius: Style.radiusMedium * scaling
border.color: Colors.mOutline
border.width: Math.max(1, Style.borderThin * scaling)
visible: WallpapersService.wallpaperList.length === 0 && !WallpapersService.scanning
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginMedium * scaling
NText {
text: "folder_open"
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeLarge * scaling
color: Colors.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No wallpapers found"
color: Colors.mOnSurface
font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Make sure your wallpaper directory is configured and contains image files"
color: Colors.mOnSurface
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,350 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginMedium * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Directory"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// Wallpaper Settings Category
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
// Wallpaper Folder
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
NTextInput {
label: "Wallpaper Directory"
description: "Path to your wallpaper directory"
text: Settings.data.wallpaper.directory
Layout.fillWidth: true
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Automation"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// Random Wallpaper
NToggle {
label: "Random Wallpaper"
description: "Automatically select random wallpapers from the folder"
value: Settings.data.wallpaper.isRandom
onToggled: function (newValue) {
Settings.data.wallpaper.isRandom = newValue
}
}
// Interval
ColumnLayout {
RowLayout {
Layout.fillWidth: true
ColumnLayout {
NText {
text: "Wallpaper Interval"
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "How often to change wallpapers automatically (in seconds)"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: sliderWpInterval.value + " seconds"
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
NSlider {
id: sliderWpInterval
Layout.fillWidth: true
from: 10
to: 900
stepSize: 10
value: Settings.data.wallpaper.randomInterval
onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value)
cutoutColor: Colors.mSurface
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// -------------------------------
// SWWW
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "SWWW"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.bottomMargin: Style.marginSmall * scaling
}
// Use SWWW
NToggle {
label: "Use SWWW"
description: "Use SWWW daemon for advanced wallpaper management"
value: Settings.data.wallpaper.swww.enabled
onToggled: function (newValue) {
Settings.data.wallpaper.swww.enabled = newValue
}
}
// SWWW Settings (only visible when useSWWW is enabled)
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
visible: Settings.data.wallpaper.swww.enabled
// Resize Mode
NComboBox {
label: "Resize Mode"
description: "How SWWW should resize wallpapers to fit the screen"
model: ListModel {
ListElement {
key: "no"
name: "No"
}
ListElement {
key: "crop"
name: "Crop"
}
ListElement {
key: "fit"
name: "Fit"
}
ListElement {
key: "stretch"
name: "Stretch"
}
}
currentKey: Settings.data.wallpaper.swww.resizeMethod
onSelected: function (key) {
Settings.data.wallpaper.swww.resizeMethod = key
}
}
// Transition Type
NComboBox {
label: "Transition Type"
description: "Animation type when switching between wallpapers"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "simple"
name: "Simple"
}
ListElement {
key: "fade"
name: "Fade"
}
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
ListElement {
key: "wipe"
name: "Wipe"
}
ListElement {
key: "wave"
name: "Wave"
}
ListElement {
key: "grow"
name: "Grow"
}
ListElement {
key: "center"
name: "Center"
}
ListElement {
key: "any"
name: "Any"
}
ListElement {
key: "outer"
name: "Outer"
}
ListElement {
key: "random"
name: "Random"
}
}
currentKey: Settings.data.wallpaper.swww.transitionType
onSelected: function (key) {
Settings.data.wallpaper.swww.transitionType = key
}
}
// Transition FPS
ColumnLayout {
RowLayout {
Layout.fillWidth: true
ColumnLayout {
NText {
text: "Transition FPS"
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "Frames per second for transition animations"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: sliderWpTransitionFps.value + " FPS"
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
NSlider {
id: sliderWpTransitionFps
Layout.fillWidth: true
from: 30
to: 500
stepSize: 5
value: Settings.data.wallpaper.swww.transitionFps
onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value)
cutoutColor: Colors.mSurface
}
}
// Transition Duration
ColumnLayout {
RowLayout {
Layout.fillWidth: true
ColumnLayout {
NText {
text: "Transition Duration"
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "Duration of transition animations in seconds"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: sliderWpTransitionDuration.value.toFixed(2) + "s"
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
NSlider {
id: sliderWpTransitionDuration
Layout.fillWidth: true
from: 0.25
to: 10
stepSize: 0.05
value: Settings.data.wallpaper.swww.transitionDuration
onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value
cutoutColor: Colors.mSurface
}
}
}
}
}
}
}