Finish SettingsWindow, still need Wallpaper Selector

This commit is contained in:
Ly-sec 2025-08-12 14:03:11 +02:00
parent 54b2673f9a
commit c616f5a46c
12 changed files with 466 additions and 303 deletions

View file

@ -70,8 +70,8 @@ NLoader {
border.color: Colors.backgroundTertiary
border.width: Math.max(1, Style.borderMedium * scaling)
layer.enabled: true
width: 1040 * scaling
height: 640 * scaling
width: (screen.width/2) * scaling
height: (screen.height/2) * scaling
anchors.centerIn: parent
MouseArea {
@ -151,7 +151,7 @@ NLoader {
}
}
// Content (unchanged)
// Content
Rectangle {
id: contentPane
Layout.fillWidth: true
@ -200,14 +200,15 @@ NLoader {
Layout.fillHeight: true
currentIndex: settingsPanel.currentTabIndex
Repeater {
model: settingsPanel.tabsModel
delegate: Loader {
active: index === settingsPanel.currentTabIndex
visible: active
source: modelData.source
}
}
Tabs.General {}
Tabs.Bar {}
Tabs.TimeWeather {}
Tabs.ScreenRecorder {}
Tabs.Network {}
Tabs.Display {}
Tabs.Wallpaper {}
Tabs.Misc {}
Tabs.About {}
}
}
}

View file

@ -1,280 +1,443 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Services
import qs.Widgets
Item {
id: root
property real scaling: 1
readonly property string tabIcon: "info"
readonly property string tabLabel: "About"
readonly property int tabIndex: 8
anchors.fill: parent
ColumnLayout {
id: root
property string latestVersion: "Unknown"
property string currentVersion: "Unknown"
property var contributors: []
property string githubDataPath: Settings.configDir + "github_data.json"
property string latestVersion: "Unknown"
property string currentVersion: "v1.2.1" // Fallback version
property var contributors: []
property string githubDataPath: Settings.configDir + "github_data.json"
function loadFromFile() {
const now = Date.now()
const data = githubData
if (!data.timestamp || (now - data.timestamp > 3600 * 1000)) {
// 1h cache
fetchFromGitHub()
return
}
if (data.version)
root.latestVersion = data.version
if (data.contributors)
root.contributors = data.contributors
}
function loadFromFile() {
const now = Date.now();
const data = githubData;
if (!data.timestamp || (now - data.timestamp > 3.6e+06)) {
console.log("[About] Cache expired or missing, fetching new data from GitHub...");
fetchFromGitHub();
return ;
}
console.log("[About] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)");
if (data.version)
root.latestVersion = data.version;
function fetchFromGitHub() {
versionProcess.running = true
contributorsProcess.running = true
}
if (data.contributors) {
root.contributors = data.contributors;
}
function saveData() {
githubData.timestamp = Date.now()
Qt.callLater(function () {
githubDataFile.writeAdapter()
})
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginMedium * scaling
// Header
NText {
text: "Noctalia: quiet by design"
font.weight: Style.fontWeightBold
color: Colors.textPrimary
}
NText {
text: "It may just be another quickshell setup but it won't get in your way."
color: Colors.textSecondary
}
// Versions grid
RowLayout {
spacing: Style.marginLarge * scaling
ColumnLayout {
NText {
text: "Latest Version:"
color: Colors.textSecondary
function fetchFromGitHub() {
versionProcess.running = true;
contributorsProcess.running = true;
}
function saveData() {
githubData.timestamp = Date.now();
Qt.callLater(() => {
githubDataFile.writeAdapter();
});
}
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;
}
NText {
text: root.latestVersion
font.weight: Style.fontWeightBold
color: Colors.textPrimary
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;
}
}
}
}
ColumnLayout {
NText {
text: "Installed Version:"
color: Colors.textSecondary
}
FileView {
id: githubDataFile
path: root.githubDataPath
blockLoading: true
printErrors: true
watchChanges: true
onFileChanged: githubDataFile.reload()
onLoaded: loadFromFile()
onLoadFailed: function(error) {
console.log("GitHub data file doesn't exist yet, creating it...");
githubData.version = "Unknown";
githubData.contributors = [];
githubData.timestamp = 0;
githubDataFile.writeAdapter();
fetchFromGitHub();
}
NText {
text: root.currentVersion
font.weight: Style.fontWeightBold
color: Colors.textPrimary
Component.onCompleted: {
if (path)
reload();
}
}
Item {
JsonAdapter {
id: githubData
property string version: "Unknown"
property var contributors: []
property double timestamp: 0
}
}
Process {
id: versionProcess
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text);
if (data.tag_name) {
const version = data.tag_name;
githubData.version = version;
root.latestVersion = version;
console.log("[About] Latest version fetched from GitHub:", version);
} else {
console.log("No tag_name in GitHub response");
}
saveData();
} catch (e) {
console.error("Failed to parse version:", e);
}
}
}
}
Process {
id: contributorsProcess
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text);
githubData.contributors = data || [];
root.contributors = githubData.contributors;
saveData();
} catch (e) {
console.error("Failed to parse contributors:", e);
root.contributors = [];
}
}
}
}
ScrollView {
id: scrollView
Layout.fillWidth: true
}
NIconButton {
icon: "system_update"
tooltipText: "Open latest release"
onClicked: Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"])
}
}
Layout.fillHeight: true
padding: 16
rightPadding: 12
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
NDivider {
Layout.fillWidth: true
}
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
// Contributors
RowLayout {
spacing: Style.marginSmall * scaling
NText {
text: "Contributors"
font.weight: Style.fontWeightBold
color: Colors.textPrimary
}
NText {
text: "(" + root.contributors.length + ")"
color: Colors.textSecondary
}
}
GridView {
id: contributorsGrid
Layout.fillWidth: true
Layout.fillHeight: true
cellWidth: 200 * scaling
cellHeight: 100 * scaling
model: root.contributors
delegate: Rectangle {
width: contributorsGrid.cellWidth - 8 * scaling
height: contributorsGrid.cellHeight - 4 * scaling
radius: Style.radiusLarge * scaling
color: contributorArea.containsMouse ? Colors.highlight : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginSmall * scaling
spacing: Style.marginSmall * scaling
Item {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Image {
id: avatarImage
anchors.fill: parent
source: modelData.avatar_url || ""
asynchronous: true
visible: false
fillMode: Image.PreserveAspectCrop
}
MultiEffect {
anchors.fill: parent
source: avatarImage
maskEnabled: true
maskSource: mask
}
Item {
id: mask
anchors.fill: parent
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
}
}
NText {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary
visible: !avatarImage.source || avatarImage.status !== Image.Ready
text: "Noctalia: quiet by design"
font.pointSize: 24 * Scaling.scale(screen)
font.weight: Style.fontWeightBold
color: Colors.textPrimary
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: 8 * Scaling.scale(screen)
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2 * scaling
NText {
text: modelData.login || "Unknown"
color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary
text: "It may just be another quickshell setup but it won't get in your way."
font.pointSize: 14 * Scaling.scale(screen)
color: Colors.textSecondary
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: 16 * Scaling.scale(screen)
}
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: 4
columnSpacing: 8
NText {
text: "Latest Version:"
font.pointSize: 16 * Scaling.scale(screen)
color: Colors.textSecondary
Layout.alignment: Qt.AlignRight
}
NText {
text: root.latestVersion
font.pointSize: 16 * Scaling.scale(screen)
color: Colors.textPrimary
font.weight: Style.fontWeightBold
}
NText {
text: "Installed Version:"
font.pointSize: 16 * Scaling.scale(screen)
color: Colors.textSecondary
Layout.alignment: Qt.AlignRight
}
NText {
text: root.currentVersion
font.pointSize: 16 * Scaling.scale(screen)
color: Colors.textPrimary
font.weight: Style.fontWeightBold
}
}
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 8
Layout.preferredWidth: updateText.implicitWidth + 46
Layout.preferredHeight: 32
radius: 20
color: updateArea.containsMouse ? Colors.accentPrimary : "transparent"
border.color: Colors.accentPrimary
border.width: 1
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 (let 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: 8
NText {
text: "system_update"
font.family: "Material Symbols Outlined"
font.pointSize: 18 * Scaling.scale(screen)
color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary
}
NText {
id: updateText
text: "Download latest release"
font.pointSize: 14 * Scaling.scale(screen)
color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary
}
}
MouseArea {
id: updateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]);
}
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 26
Layout.bottomMargin: 18
height: 1
color: Colors.outline
opacity: 0.3
}
NText {
text: (modelData.contributions || 0) + " commits"
color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
text: "Contributors"
font.pointSize: 18 * Scaling.scale(screen)
font.weight: Style.fontWeightBold
color: Colors.textPrimary
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 32
}
}
NText {
text: "(" + root.contributors.length + ")"
font.pointSize: 14 * Scaling.scale(screen)
color: Colors.textSecondary
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 4
}
ScrollView {
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: 200 * 4
Layout.fillHeight: true
Layout.topMargin: 16
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
GridView {
id: contributorsGrid
anchors.fill: parent
width: 200 * 4
height: Math.ceil(root.contributors.length / 4) * 100
cellWidth: 200
cellHeight: 100
model: root.contributors
delegate: Rectangle {
width: contributorsGrid.cellWidth - 16
height: contributorsGrid.cellHeight - 4
radius: 20
color: contributorArea.containsMouse ? Colors.hover : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
Item {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Image {
id: avatarImage
anchors.fill: parent
source: modelData.avatar_url || ""
sourceSize: Qt.size(80, 80)
visible: false
mipmap: true
smooth: true
asynchronous: true
fillMode: Image.PreserveAspectCrop
cache: true
onStatusChanged: {
if (status === Image.Error) {
console.log("[About] Failed to load avatar for", modelData.login, "URL:", modelData.avatar_url);
}
}
}
MultiEffect {
anchors.fill: parent
source: avatarImage
maskEnabled: true
maskSource: mask
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
radius: parent.width / 2
}
}
NText {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pointSize: 24 * Scaling.scale(screen)
color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary
visible: !avatarImage.source || avatarImage.status !== Image.Ready
}
}
ColumnLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NText {
text: modelData.login || "Unknown"
font.pointSize: 13 * Scaling.scale(screen)
color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: (modelData.contributions || 0) + " commits"
font.pointSize: 11 * Scaling.scale(screen)
color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.html_url)
Quickshell.execDetached(["xdg-open", modelData.html_url]);
}
}
}
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (modelData.html_url)
Quickshell.execDetached(["xdg-open", modelData.html_url])
}
}
}
Item {
Layout.fillHeight: true
}
}
// Processes and persistence
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
}
}
}
}
FileView {
id: githubDataFile
path: root.githubDataPath
blockLoading: true
printErrors: true
watchChanges: true
onFileChanged: githubDataFile.reload()
onLoaded: loadFromFile()
onLoadFailed: {
githubData.version = "Unknown"
githubData.contributors = []
githubData.timestamp = 0
githubDataFile.writeAdapter()
fetchFromGitHub()
}
Component.onCompleted: {
if (path)
reload()
}
JsonAdapter {
id: githubData
property string version: "Unknown"
property var contributors: []
property double timestamp: 0
}
}
Process {
id: versionProcess
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
if (data.tag_name) {
const version = data.tag_name
githubData.version = version
root.latestVersion = version
}
saveData()
} catch (e) {
console.error("Failed to parse version:", e)
}
}
}
}
Process {
id: contributorsProcess
command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
githubData.contributors = data || []
root.contributors = githubData.contributors
saveData()
} catch (e) {
console.error("Failed to parse contributors:", e)
root.contributors = []
}
}
}
}
}

View file

@ -10,7 +10,8 @@ Item {
readonly property string tabIcon: "web_asset"
readonly property string tabLabel: "Bar"
readonly property int tabIndex: 1
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent

View file

@ -9,7 +9,8 @@ Item {
readonly property string tabIcon: "monitor"
readonly property string tabLabel: "Display"
readonly property int tabIndex: 5
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
// Helper functions to update arrays immutably
function addMonitor(list, name) {

View file

@ -14,9 +14,8 @@ Item {
readonly property string tabLabel: "General"
readonly property int tabIndex: 0
anchors.fill: parent
implicitWidth: parent ? parent.width : 0
implicitHeight: parent ? parent.height : 0
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent

View file

@ -8,7 +8,8 @@ Item {
readonly property string tabIcon: "more_horiz"
readonly property string tabLabel: "Misc"
readonly property int tabIndex: 7
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent

View file

@ -10,7 +10,8 @@ Item {
readonly property string tabIcon: "wifi"
readonly property string tabLabel: "Network"
readonly property int tabIndex: 4
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent

View file

@ -8,7 +8,8 @@ Item {
readonly property string tabIcon: "videocam"
readonly property string tabLabel: "Screen Recorder"
readonly property int tabIndex: 3
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent

View file

@ -8,7 +8,8 @@ Item {
readonly property string tabIcon: "schedule"
readonly property string tabLabel: "Time & Weather"
readonly property int tabIndex: 2
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
@ -64,30 +65,22 @@ Item {
onEditingFinished: Settings.data.location.name = text
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginSmall * scaling
ColumnLayout {
Layout.fillWidth: true
spacing: 2 * scaling
NText {
text: "Temperature Unit"
color: Colors.textPrimary
font.weight: Style.fontWeightBold
}
NText {
text: "Choose between Celsius and Fahrenheit"
color: Colors.textSecondary
wrapMode: Text.WordWrap
}
}
NComboBox {
optionsKeys: ["c", "f"]
optionsLabels: ["Celsius", "Fahrenheit"]
currentKey: Settings.data.location.useFahrenheit ? "f" : "c"
onSelected: function (key) {
Settings.data.location.useFahrenheit = (key === "f")
}
NText {
text: "Temperature Unit"
color: Colors.textPrimary
font.weight: Style.fontWeightBold
}
NText {
text: "Choose between Celsius and Fahrenheit"
color: Colors.textSecondary
font.pointSize: Style.fontSizeSmall * scaling
}
NComboBox {
optionsKeys: ["c", "f"]
optionsLabels: ["Celsius", "Fahrenheit"]
currentKey: Settings.data.location.useFahrenheit ? "f" : "c"
onSelected: function (key) {
Settings.data.location.useFahrenheit = (key === "f")
}
}

View file

@ -8,7 +8,8 @@ Item {
readonly property string tabIcon: "image"
readonly property string tabLabel: "Wallpaper"
readonly property int tabIndex: 6
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent

View file

@ -119,9 +119,9 @@ Singleton {
property bool generateTheme: false
property JsonObject swww
onDirectoryChanged: WallpaperManager.loadWallpapers()
onIsRandomChanged: WallpaperManager.toggleRandomWallpaper()
onRandomIntervalChanged: WallpaperManager.restartRandomWallpaperTimer()
onDirectoryChanged: Wallpapers.loadWallpapers()
onIsRandomChanged: Wallpapers.toggleRandomWallpaper()
onRandomIntervalChanged: Wallpapers.restartRandomWallpaperTimer()
swww: JsonObject {
property bool enabled: false

View file

@ -22,10 +22,11 @@ Singleton {
property string transitionType: Settings.data.wallpaper.swww.transitionType
property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"]
function loadWallpapers() {
scanning = true
wallpaperList = []
folderModel.folder = ""
folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "")
}
@ -38,11 +39,11 @@ Singleton {
if (!isInitial) {
Settings.data.wallpaper.current = path
}
if (Settings.data.swww.enabled) {
if (Settings.data.swww.transitionType === "random") {
if (Settings.data.wallpaper.swww.enabled) {
if (Settings.data.wallpaper.swww.transitionType === "random") {
transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)]
} else {
transitionType = Settings.data.swww.transitionType
transitionType = Settings.data.wallpaper.swww.transitionType
}
changeWallpaperProcess.running = true
}
@ -105,7 +106,7 @@ Singleton {
var files = []
var filesSwww = []
for (var i = 0; i < count; i++) {
var filepath = (Settings.data.wallpaper.folder !== undefined ? Settings.data.wallpaper.folder : "") + "/" + get(
var filepath = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + "/" + get(
i, "fileName")
files.push(filepath)
}
@ -118,7 +119,7 @@ Singleton {
Process {
id: changeWallpaperProcess
command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString(
), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.transitionDuration.toString(
), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString(
), currentWallpaper]
running: false
}