Merge branch 'rebuild' of github.com:Ly-sec/Noctalia into rebuild

This commit is contained in:
quadbyte 2025-08-13 08:04:44 -04:00
commit c49e59220a
10 changed files with 569 additions and 214 deletions

View file

@ -15,6 +15,8 @@ Variants {
required property ShellScreen modelData
readonly property real scaling: Scaling.scale(screen)
property var settingsPanel: null
screen: modelData
implicitHeight: Style.barHeight * scaling
color: "transparent"

View file

@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Services
import qs.Widgets
NIconButton {
id: root
readonly property real scaling: Scaling.scale(screen)
sizeMultiplier: 0.8
showBorder: false
icon: "notifications"
tooltipText: "Notification History"
onClicked: {
if (!notificationHistoryPanelLoader.active) {
notificationHistoryPanelLoader.isLoaded = true
}
if (notificationHistoryPanelLoader.item) {
notificationHistoryPanelLoader.item.visible = !notificationHistoryPanelLoader.item.visible
}
}
NotificationHistoryPanel {
id: notificationHistoryPanelLoader
}
}

View file

@ -0,0 +1,162 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import qs.Services
import qs.Widgets
// Loader for Notification History panel
NLoader {
id: root
content: Component {
NPanel {
id: notificationPanel
Connections {
target: notificationPanel
ignoreUnknownSignals: true
function onDismissed() {
notificationPanel.visible = false
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Rectangle {
color: Colors.backgroundSecondary
radius: Style.radiusMedium * scaling
border.color: Colors.backgroundTertiary
border.width: Math.max(1, Style.borderMedium * scaling)
width: 400 * scaling
height: 500 * scaling
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Style.marginTiny * scaling
anchors.rightMargin: Style.marginTiny * scaling
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginMedium * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginMedium * scaling
NText {
text: "notifications"
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeXL * scaling
color: Colors.accentPrimary
}
NText {
text: "Notification History"
font.pointSize: Style.fontSizeLarge * scaling
font.bold: true
color: Colors.textPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "delete"
sizeMultiplier: 0.8
tooltipText: "Clear history"
onClicked: NotificationService.clearHistory()
}
NIconButton {
icon: "close"
sizeMultiplier: 0.8
onClicked: {
notificationPanel.visible = false
}
}
}
NDivider {}
ListView {
id: notificationList
Layout.fillWidth: true
Layout.fillHeight: true
model: NotificationService.historyModel
spacing: Style.marginMedium * scaling
clip: true
boundsBehavior: Flickable.StopAtBounds
delegate: Rectangle {
width: notificationList ? (notificationList.width - 20) : 380 * scaling
height: 80
radius: Style.radiusMedium * scaling
color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent"
RowLayout {
anchors {
fill: parent
margins: 15
}
spacing: 15
// Notification content
Column {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 5
NText {
text: (summary || "No summary").substring(0, 100)
font.pointSize: Style.fontSizeMedium * scaling
font.weight: Font.Medium
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary
wrapMode: Text.Wrap
width: parent.width - 30
maximumLineCount: 2
elide: Text.ElideRight
}
NText {
text: (body || "").substring(0, 150)
font.pointSize: Style.fontSizeSmall * scaling
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
wrapMode: Text.Wrap
width: parent.width - 30
maximumLineCount: 3
elide: Text.ElideRight
}
NText {
text: NotificationService.formatTimestamp(timestamp)
font.pointSize: Style.fontSizeSmall * scaling
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
}
}
}
MouseArea {
id: notificationMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("[NotificationHistory] Removing notification:", summary)
NotificationService.historyModel.remove(index)
NotificationService.saveHistory()
}
}
}
ScrollBar.vertical: ScrollBar {
active: true
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
}
}
}
}
}
}
}

View file

@ -10,6 +10,13 @@ Item {
width: pill.width
height: pill.height
// Reference to settings panel
property var settingsPanel: null
Component.onCompleted: {
console.log("[Volume] settingsPanel received:", !!settingsPanel)
}
// Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false
@ -21,17 +28,17 @@ Item {
}
function getIconColor() {
return (Audio.volume <= 1.0) ? Colors.textPrimary : getVolumeColor()
return (getDisplayVolume() <= 1.0) ? Colors.textPrimary : getVolumeColor()
}
function getVolumeColor() {
if (Audio.volume <= 1.0) {
if (getDisplayVolume() <= 1.0) {
return Colors.accentPrimary
}
// Indicate that the volume is over 100%
// Calculate interpolation factor (0 at 100%, 1.0 at 200%)
let factor = (Audio.volume - 1.0)
let factor = (getDisplayVolume() - 1.0)
// Blend between accent and warning colors
return Qt.rgba(Colors.accentPrimary.r + (Colors.error.r - Colors.accentPrimary.r) * factor,
@ -39,6 +46,15 @@ Item {
Colors.accentPrimary.b + (Colors.error.b - Colors.accentPrimary.b) * factor, 1)
}
function getDisplayVolume() {
// If volumeOverdrive is false, clamp to 100%
if (!Settings.data.audio || !Settings.data.audio.volumeOverdrive) {
return Math.min(Audio.volume, 1.0)
}
// If volumeOverdrive is true, allow up to 200%
return Math.min(Audio.volume, 2.0)
}
// Connection used to open the pill when volume changes
Connections {
target: Audio.sink?.audio ? Audio.sink?.audio : null
@ -59,9 +75,9 @@ Item {
iconCircleColor: getVolumeColor()
collapsedIconColor: getIconColor()
autoHide: true
text: Math.round(Audio.volume * 100) + "%"
text: Math.round(getDisplayVolume() * 100) + "%"
tooltipText: "Volume: " + Math.round(
Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
getDisplayVolume() * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
onWheel: function (angle) {
if (angle > 0) {
@ -71,7 +87,15 @@ Item {
}
}
onClicked: {
audioDeviceSelector.isLoaded = !audioDeviceSelector.isLoaded
// Open settings panel and navigate to Audio tab
console.log("[Volume] Attempting to open settings panel...")
try {
settingsPanel.isLoaded = true
settingsPanel.content.currentTabIndex = 5 // Audio tab index
console.log("[Volume] Settings panel opened successfully")
} catch (error) {
console.log("[Volume] Error opening settings panel:", error)
}
}
}
}

View file

@ -1,180 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import qs.Services
import qs.Widgets
NIconButton {
id: root
readonly property real scaling: Scaling.scale(screen)
sizeMultiplier: 0.8
showBorder: false
icon: "notifications"
tooltipText: "Notification History"
onClicked: {
notificationHistoryLoader.active = !notificationHistoryLoader.active
}
// Loader for Notification History menu
NLoader {
id: notificationHistoryLoader
active: false
content: Component {
NPanel {
id: notificationPanel
Connections {
target: notificationPanel
ignoreUnknownSignals: true
function onDismissed() {
notificationHistoryLoader.active = false
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Rectangle {
color: Colors.backgroundSecondary
radius: Style.radiusMedium * scaling
border.color: Colors.backgroundTertiary
border.width: Math.max(1, Style.borderMedium * scaling)
width: 400 * scaling
height: 500 * scaling
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Style.marginTiny * scaling
anchors.rightMargin: Style.marginTiny * scaling
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginMedium * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginMedium * scaling
NText {
text: "notifications"
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeXL * scaling
color: Colors.accentPrimary
}
NText {
text: "Notification History"
font.pointSize: Style.fontSizeLarge * scaling
font.bold: true
color: Colors.textPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "delete"
sizeMultiplier: 0.8
tooltipText: "Clear history"
onClicked: NotificationService.clearHistory()
}
NIconButton {
icon: "close"
sizeMultiplier: 0.8
onClicked: {
notificationHistoryLoader.active = false
}
}
}
NDivider {}
ListView {
id: notificationList
Layout.fillWidth: true
Layout.fillHeight: true
model: NotificationService.historyModel
spacing: Style.marginMedium * scaling
clip: true
boundsBehavior: Flickable.StopAtBounds
delegate: Rectangle {
width: notificationList ? (notificationList.width - 20) : 380 * scaling
height: 80
radius: Style.radiusMedium * scaling
color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent"
RowLayout {
anchors {
fill: parent
margins: 15
}
spacing: 15
// Notification content
Column {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 5
NText {
text: (summary || "No summary").substring(0, 100)
font.pointSize: Style.fontSizeMedium * scaling
font.weight: Font.Medium
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary
wrapMode: Text.Wrap
width: parent.width - 30
maximumLineCount: 2
elide: Text.ElideRight
}
NText {
text: (body || "").substring(0, 150)
font.pointSize: Style.fontSizeSmall * scaling
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
wrapMode: Text.Wrap
width: parent.width - 30
maximumLineCount: 3
elide: Text.ElideRight
}
NText {
text: NotificationService.formatTimestamp(timestamp)
font.pointSize: Style.fontSizeSmall * scaling
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
}
}
}
MouseArea {
id: notificationMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("[NotificationHistory] Removing notification:", summary)
NotificationService.historyModel.remove(index)
NotificationService.saveHistory()
}
}
}
ScrollBar.vertical: ScrollBar {
active: true
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
}
}
}
}
}
}
}
}

View file

@ -39,6 +39,10 @@ NLoader {
"label": "Network",
"icon": "wifi",
"source": "Tabs/Network.qml"
}, {
"label": "Audio",
"icon": "volume_up",
"source": "Tabs/Audio.qml"
}, {
"label": "Display",
"icon": "monitor",
@ -212,6 +216,7 @@ NLoader {
Tabs.TimeWeather {}
Tabs.ScreenRecorder {}
Tabs.Network {}
Tabs.Audio {}
Tabs.Display {}
Tabs.Wallpaper {}
Tabs.WallpaperSelector {}

View file

@ -0,0 +1,308 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import qs.Modules.Settings
import qs.Widgets
import qs.Services
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: "Audio"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.textPrimary
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
NText {
text: "Master Volume"
font.weight: Style.fontWeightBold
color: Colors.textPrimary
}
NText {
text: "System-wide volume level"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
id: masterVolumeSlider
Layout.fillWidth: true
from: 0
to: allowOverdrive.value ? 200 : 100
value: (Audio.volume || 0) * 100
stepSize: 5
onValueChanged: {
Audio.volumeSet(value / 100)
}
}
NText {
text: Math.round(masterVolumeSlider.value) + "%"
Layout.alignment: Qt.AlignVCenter
color: Colors.textSecondary
}
}
NToggle {
id: allowOverdrive
label: "Allow Volume Overdrive"
description: "Enable volume levels above 100% (up to 200%)"
value: Settings.data.audio ? Settings.data.audio.volumeOverdrive : false
onToggled: function (checked) {
Settings.data.audio.volumeOverdrive = checked
// If overdrive is disabled and current volume is above 100%, cap it
if (!checked && Audio.volume > 1.0) {
Audio.volumeSet(1.0)
}
}
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginMedium * scaling
NToggle {
label: "Mute Audio"
description: "Mute or unmute the default audio output"
value: Audio.muted
onToggled: function (newValue) {
if (Audio.sink && Audio.sink.audio) {
Audio.sink.audio.muted = newValue
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
// Audio Devices
ColumnLayout {
spacing: Style.marginLarge * scaling
Layout.fillWidth: true
NText {
text: "Audio Devices"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.textPrimary
Layout.bottomMargin: Style.marginSmall * scaling
}
// Output Device
NComboBox {
id: outputDeviceCombo
label: "Output Device"
description: "Default audio output device"
optionsKeys: outputDeviceKeys
optionsLabels: outputDeviceLabels
currentKey: Audio.sink ? Audio.sink.id.toString() : ""
onSelected: function (key) {
// Find the node by ID and set it as preferred
for (let i = 0; i < Pipewire.nodes.count; i++) {
let node = Pipewire.nodes.get(i)
if (node.id.toString() === key && node.isSink) {
Pipewire.preferredDefaultAudioSink = node
break
}
}
}
}
// Input Device
NComboBox {
id: inputDeviceCombo
label: "Input Device"
description: "Default audio input device"
optionsKeys: inputDeviceKeys
optionsLabels: inputDeviceLabels
currentKey: Audio.source ? Audio.source.id.toString() : ""
onSelected: function (key) {
// Find the node by ID and set it as preferred
for (let i = 0; i < Pipewire.nodes.count; i++) {
let node = Pipewire.nodes.get(i)
if (node.id.toString() === key && !node.isSink) {
Pipewire.preferredDefaultAudioSource = node
break
}
}
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * scaling
Layout.bottomMargin: Style.marginMedium * scaling
}
// Audio Visualizer Category
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
NText {
text: "Audio Visualizer"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Colors.textPrimary
Layout.bottomMargin: Style.marginSmall * scaling
}
// Audio Visualizer section
NComboBox {
id: audioVisualizerCombo
label: "Visualization Type"
description: "Choose a visualization type for media playback"
optionsKeys: ["radial", "bars", "wave"]
optionsLabels: ["Radial", "Bars", "Wave"]
currentKey: Settings.data.audio ? Settings.data.audio.audioVisualizer.type : "radial"
onSelected: function (key) {
if (!Settings.data.audio) {
Settings.data.audio = {}
}
if (!Settings.data.audio.audioVisualizer) {
Settings.data.audio.audioVisualizer = {}
}
Settings.data.audio.audioVisualizer.type = key
}
}
}
}
}
}
// Device list properties
property var outputDeviceKeys: ["default"]
property var outputDeviceLabels: ["Default Output"]
property var inputDeviceKeys: ["default"]
property var inputDeviceLabels: ["Default Input"]
// Bind Pipewire nodes
PwObjectTracker {
id: nodeTracker
objects: [Pipewire.nodes]
}
// Update device lists when component is completed
Component.onCompleted: {
updateDeviceLists()
}
// Timer to check if pipewire is ready and update device lists
Timer {
id: deviceUpdateTimer
interval: 100
repeat: true
running: !(Pipewire && Pipewire.ready)
onTriggered: {
if (Pipewire && Pipewire.ready) {
updateDeviceLists()
running = false
}
}
}
// Update device lists when nodes change
Connections {
target: nodeTracker
function onObjectsChanged() {
updateDeviceLists()
}
}
Repeater {
id: nodesRepeater
model: Pipewire.nodes
delegate: Item {
Component.onCompleted: {
if (modelData && modelData.isSink && modelData.audio) {
// Add to output devices
let key = modelData.id.toString()
if (!outputDeviceKeys.includes(key)) {
outputDeviceKeys.push(key)
outputDeviceLabels.push(modelData.description || modelData.name || "Unknown Device")
}
} else if (modelData && !modelData.isSink && modelData.audio) {
// Add to input devices
let key = modelData.id.toString()
if (!inputDeviceKeys.includes(key)) {
inputDeviceKeys.push(key)
inputDeviceLabels.push(modelData.description || modelData.name || "Unknown Device")
}
}
}
}
}
function updateDeviceLists() {
if (Pipewire && Pipewire.ready) {
// Update comboboxes
if (outputDeviceCombo) {
outputDeviceCombo.optionsKeys = outputDeviceKeys
outputDeviceCombo.optionsLabels = outputDeviceLabels
}
if (inputDeviceCombo) {
inputDeviceCombo.optionsKeys = inputDeviceKeys
inputDeviceCombo.optionsLabels = inputDeviceLabels
}
}
}
}

View file

@ -40,17 +40,7 @@ ColumnLayout {
Layout.bottomMargin: Style.marginSmall * scaling
}
// Audio Visualizer section
NComboBox {
label: "Audio Visualizer"
description: "Choose a visualization type"
optionsKeys: ["radial", "bars", "wave"]
optionsLabels: ["Radial", "Bars", "Wave"]
currentKey: Settings.data.audioVisualizer.type
onSelected: function (key) {
Settings.data.audioVisualizer.type = key
}
}
}
}
}

View file

@ -27,6 +27,13 @@ QtObject {
// Signal when notification is received
onNotification: function (notification) {
// Check if notifications are suppressed
if (Settings.data.notifications && Settings.data.notifications.suppressed) {
// Still add to history but don't show notification
root.addToHistory(notification)
return
}
// Track the notification
notification.tracked = true

View file

@ -26,6 +26,8 @@ Singleton {
// Used to access via Settings.data.xxx.yyy
property var data: adapter
// Needed to only have one NPanel loaded at a time. <--- VERY BROKEN
//property var openPanel: null
Item {
Component.onCompleted: {
@ -36,17 +38,22 @@ Singleton {
}
FileView {
// TBC ? needed for SWWW only ?
// Qt.callLater(function () {
// WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true);
// })
path: settingsFile
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
Component.onCompleted: {
Component.onCompleted: function () {
reload()
}
onLoaded: {
onLoaded: function () {
Qt.callLater(function () {
if (adapter.wallpaper.current !== "") {
console.log("Settings: Initializing wallpaper to:", adapter.wallpaper.current)
Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true)
}
})
@ -80,6 +87,7 @@ Singleton {
property string avatarImage: defaultAvatar
property bool dimDesktop: true
property bool showScreenCorners: false
property bool showDock: false
}
// location
@ -102,8 +110,9 @@ Singleton {
property string videoCodec: "h264"
property string quality: "very_high"
property string colorRange: "limited"
property string audioSource: "default_output"
property bool showCursor: true
// New: optional audio source selection (default: system output)
property string audioSource: "default_output"
}
// wallpaper
@ -160,11 +169,16 @@ Singleton {
property list<string> monitors: []
}
// audioVisualizer
property JsonObject audioVisualizer
// audio
property JsonObject audio
audioVisualizer: JsonObject {
property string type: "radial"
audio: JsonObject {
property bool volumeOverdrive: false
property JsonObject audioVisualizer
audioVisualizer: JsonObject {
property string type: "radial"
}
}
// ui
@ -179,14 +193,8 @@ Singleton {
Connections {
target: adapter.wallpaper
function onIsRandomChanged() {
Wallpapers.toggleRandomWallpaper()
}
function onRandomIntervalChanged() {
Wallpapers.restartRandomWallpaperTimer()
}
function onDirectoryChanged() {
Wallpapers.loadWallpapers()
}
function onIsRandomChanged() { Wallpapers.toggleRandomWallpaper() }
function onRandomIntervalChanged() { Wallpapers.restartRandomWallpaperTimer() }
function onDirectoryChanged() { Wallpapers.loadWallpapers() }
}
}