Compare commits
10 Commits
b97ac0276c
...
d72adec2ef
| Author | SHA1 | Date | |
|---|---|---|---|
| d72adec2ef | |||
| 38173b9959 | |||
| d2a8d0b563 | |||
| 1b51d1a9b3 | |||
| c5198abcf9 | |||
| f15f6e3c82 | |||
| e06de04203 | |||
| 37a134f077 | |||
| a618427dae | |||
| 86474f0663 |
@ -2,6 +2,6 @@ pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
readonly property string font: "JetBrainsMono Nerd Font"
|
||||
readonly property string font: "Iosevka Nerd Font Propo"
|
||||
readonly property real fontSize: 14
|
||||
}
|
||||
29
Colors.qml
Normal file
29
Colors.qml
Normal file
@ -0,0 +1,29 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: customColors
|
||||
// Core Backgrounds
|
||||
readonly property color background: "#24283B"
|
||||
readonly property color foreground: "#C0CAF5"
|
||||
readonly property color cursor: "#C0CAF5"
|
||||
|
||||
// The 16 Colors of the Apocalypse
|
||||
readonly property color color0: "#414868"
|
||||
readonly property color color1: "#F7768E"
|
||||
readonly property color color2: "#9ECE6A"
|
||||
readonly property color color3: "#E0AF68"
|
||||
readonly property color color4: "#7AA2F7"
|
||||
readonly property color color5: "#BB9AF7"
|
||||
readonly property color color6: "#7DCFFF"
|
||||
readonly property color color7: "#C0CAF5"
|
||||
readonly property color color8: "#414868"
|
||||
readonly property color color9: "#F7768E"
|
||||
readonly property color color10: "#9ECE6A"
|
||||
readonly property color color11: "#E0AF68"
|
||||
readonly property color color12: "#7AA2F7"
|
||||
readonly property color color13: "#BB9AF7"
|
||||
readonly property color color14: "#7DCFFF"
|
||||
readonly property color color15: "#C0CAF5"
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.modules.bar
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
required property var modelData
|
||||
implicitHeight: 30
|
||||
//color: Colors.background
|
||||
color: Colors.background
|
||||
anchors {
|
||||
top: true
|
||||
@ -15,25 +17,31 @@ PanelWindow {
|
||||
|
||||
RowLayout {
|
||||
id: leftLayout
|
||||
spacing: 30
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Workspaces {}
|
||||
Clock {
|
||||
Layout.leftMargin: 30
|
||||
}
|
||||
Mpris {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: centerLayout
|
||||
anchors.centerIn: parent
|
||||
Workspaces {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rightLayout
|
||||
spacing: 0
|
||||
spacing: 20
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Volume {}
|
||||
Battery {}
|
||||
PowerProfiles {}
|
||||
SystemTray {
|
||||
Layout.rightMargin: 10
|
||||
Layout.rightMargin: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import Quickshell.Services.UPower
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import "."
|
||||
import qs
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@ -9,14 +11,17 @@ Item {
|
||||
Row {
|
||||
id: batRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
spacing: 5
|
||||
IconImage {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: "root:/icons/" + UPower.displayDevice.iconName + ".svg"
|
||||
implicitWidth: 16
|
||||
implicitHeight: 16
|
||||
width: 12
|
||||
height: 12
|
||||
}
|
||||
Text {
|
||||
id: batteryText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.weight: 900
|
||||
font.family: Appearance.font
|
||||
font.pixelSize: Appearance.fontSize
|
||||
color: Colors.foreground
|
||||
|
||||
@ -1,12 +1,26 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: clockText.text.length + 10
|
||||
// FIX: Real pixels please!
|
||||
implicitWidth: clockText.implicitWidth
|
||||
implicitHeight: 30
|
||||
|
||||
Text {
|
||||
id: clockText
|
||||
anchors.centerIn: parent
|
||||
text: "sigma balls"
|
||||
font.weight: 900
|
||||
font.family: Appearance.font
|
||||
font.pixelSize: Appearance.fontSize
|
||||
color: Colors.foreground
|
||||
|
||||
text: Qt.formatDateTime(clock.date, "hh:mm")
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: customColors
|
||||
// Core Backgrounds
|
||||
readonly property color background: "#1F1F28"
|
||||
readonly property color foreground: "#DCD7BA"
|
||||
readonly property color cursor: "#DCD7BA"
|
||||
|
||||
// The 16 Colors of the Apocalypse
|
||||
readonly property color color0: "#090618"
|
||||
readonly property color color1: "#C34043"
|
||||
readonly property color color2: "#76946A"
|
||||
readonly property color color3: "#C0A36E"
|
||||
readonly property color color4: "#7E9CD8"
|
||||
readonly property color color5: "#957FB8"
|
||||
readonly property color color6: "#6A9589"
|
||||
readonly property color color7: "#C8C093"
|
||||
readonly property color color8: "#727169"
|
||||
readonly property color color9: "#E82424"
|
||||
readonly property color color10: "#98BB6C"
|
||||
readonly property color color11: "#E6C384"
|
||||
readonly property color color12: "#7FB4CA"
|
||||
readonly property color color13: "#938AA9"
|
||||
readonly property color color14: "#7AA89F"
|
||||
readonly property color color15: "#DCD7BA"
|
||||
}
|
||||
134
modules/bar/Mpris.qml
Normal file
134
modules/bar/Mpris.qml
Normal file
@ -0,0 +1,134 @@
|
||||
// ⚠️ Ensure Colors is imported
|
||||
// import "../../"
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
// 1. Let Repeater loop through the ObjectModel for us
|
||||
Repeater {
|
||||
id: mprisRepeater
|
||||
model: Mpris.players
|
||||
|
||||
delegate: RowLayout {
|
||||
required property var modelData
|
||||
// 2. 🕵️♀️ FILTER LOGIC
|
||||
// Check if this specific player is Spotify.
|
||||
// We verify 'modelData' exists and check the name.
|
||||
property bool isSpotify: modelData && modelData.identity.toLowerCase().includes("spotify")
|
||||
|
||||
// 3. 👻 HIDE NON-SPOTIFY PLAYERS
|
||||
visible: isSpotify
|
||||
|
||||
// If hidden, take up ZERO space
|
||||
Layout.preferredWidth: isSpotify ? Math.min(implicitWidth, 400) : 0
|
||||
Layout.fillHeight: true
|
||||
|
||||
// 4. 🎵 USE 'modelData' DIRECTLY
|
||||
// property string title: modelData.metadata["xesam:title"] || "No Title"
|
||||
// property string artist: modelData.metadata["xesam:artist"] || "Unknown"
|
||||
// property string artUrl: modelData.metadata["mpris:artUrl"] || ""
|
||||
// property bool isPlaying: modelData.playbackStatus === MprisPlaybackStatus.Playing
|
||||
property string title: modelData.trackTitle
|
||||
property string artist: modelData.trackArtist
|
||||
property string artUrl: modelData.trackArtUrl
|
||||
property bool isPlaying: modelData.isPlaying
|
||||
|
||||
spacing: 8
|
||||
|
||||
// 🖼️ ALBUM ART
|
||||
Rectangle {
|
||||
Layout.preferredHeight: parent.height * 0.8
|
||||
Layout.preferredWidth: Layout.preferredHeight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
radius: 4
|
||||
color: Colors.background
|
||||
clip: true
|
||||
visible: parent.visible // Optimization
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: parent.parent.artUrl // Access property from delegate
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
}
|
||||
}
|
||||
|
||||
// 📝 TEXT INFO
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 0
|
||||
visible: parent.visible
|
||||
|
||||
Text {
|
||||
text: parent.parent.title
|
||||
color: Colors.foreground
|
||||
font.bold: true
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.preferredWidth: implicitWidth
|
||||
}
|
||||
|
||||
Text {
|
||||
text: parent.parent.artist
|
||||
color: Colors.foreground
|
||||
opacity: 0.7
|
||||
font.pixelSize: 10
|
||||
Layout.preferredWidth: implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
// ⏯️ CONTROLS
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 8
|
||||
visible: parent.visible
|
||||
|
||||
// PREV
|
||||
Text {
|
||||
text: ""
|
||||
color: Colors.foreground
|
||||
font.pixelSize: 24
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
// Use modelData to control THIS player
|
||||
onClicked: modelData.previous()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
|
||||
// PLAY / PAUSE
|
||||
Text {
|
||||
text: parent.parent.isPlaying ? "" : ""
|
||||
color: Colors.foreground
|
||||
font.pixelSize: 24
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: modelData.playPause()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
|
||||
// NEXT
|
||||
Text {
|
||||
text: ""
|
||||
color: Colors.foreground
|
||||
font.pixelSize: 24
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: modelData.next()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
modules/bar/PowerProfiles.qml
Normal file
33
modules/bar/PowerProfiles.qml
Normal file
@ -0,0 +1,33 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import qs
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: 80
|
||||
Text {
|
||||
id: powerProfile
|
||||
text: PowerProfile.toString(PowerProfiles.profile)
|
||||
font.weight: 900
|
||||
color: Colors.foreground
|
||||
font.family: Appearance.font
|
||||
font.pixelSize: Appearance.fontSize
|
||||
anchors.centerIn: parent
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.OpenHandCursor
|
||||
anchors.fill: parent
|
||||
onClicked: mouse => {
|
||||
const modes = [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance];
|
||||
let current = PowerProfiles.profile;
|
||||
let currentIndex = modes.indexOf(current);
|
||||
let nextIndex = (currentIndex + 1) % modes.length;
|
||||
let prevIndex = (currentIndex - 1) % modes.length;
|
||||
if (mouse.button == Qt.LeftButton)
|
||||
PowerProfiles.profile = modes[nextIndex];
|
||||
if (mouse.button == Qt.RightButton)
|
||||
PowerProfiles.profile = modes[prevIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ Item {
|
||||
clip: true
|
||||
|
||||
// This was already correct in your last file, but keep it this way!
|
||||
implicitWidth: layout.implicitWidth + 10
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: 30
|
||||
|
||||
// Hide if empty so we don't have a 50px gap for nothing
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Io
|
||||
import qs
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@ -8,7 +10,23 @@ Item {
|
||||
implicitHeight: volRow.implicitHeight
|
||||
// grab the default speaker (Sink)
|
||||
property var sink: Pipewire.defaultAudioSink
|
||||
Process {
|
||||
id: pavu
|
||||
command: ["pavucontrol"] // The command and args list
|
||||
|
||||
}
|
||||
MouseArea {
|
||||
cursorShape: Qt.OpenHandCursor
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Left Click: Summon the Mixer!
|
||||
console.log("Summoning Pavucontrol... Nya!");
|
||||
pavu.startDetached();
|
||||
}
|
||||
}
|
||||
anchors.fill: parent
|
||||
// Scroll to change volume (The fancy stuff!)
|
||||
}
|
||||
// Logic to pick the correct icon name
|
||||
function getVolumeIcon() {
|
||||
// Safety check: if Pipewire is dead or sink is missing
|
||||
@ -39,8 +57,9 @@ Item {
|
||||
spacing: 5
|
||||
|
||||
IconImage {
|
||||
width: 16
|
||||
height: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 12
|
||||
height: 12
|
||||
|
||||
// The magic: 'image://theme/' pulls from your system icon theme (Papirus, Adwaita, etc.)
|
||||
source: "root:/icons/" + root.getVolumeIcon() + "-symbolic.svg"
|
||||
@ -51,30 +70,16 @@ Item {
|
||||
|
||||
Text {
|
||||
PwObjectTracker {
|
||||
|
||||
objects: Pipewire.defaultAudioSink
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 20
|
||||
font.weight: 900
|
||||
color: Colors.foreground
|
||||
font.family: Appearance.font
|
||||
font.pixelSize: Appearance.fontSize
|
||||
text: Math.round(Pipewire.defaultAudioSink.audio.volume * 100) + "%"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (root.sink) {
|
||||
root.sink.audio.muted = !root.sink.audio.muted;
|
||||
}
|
||||
}
|
||||
// Scroll to change volume (The fancy stuff!)
|
||||
onWheel: wheel => {
|
||||
if (root.sink) {
|
||||
if (wheel.angleDelta.y > 0) {
|
||||
root.sink.audio.volume += 0.05; // Up 5%
|
||||
} else {
|
||||
root.sink.audio.volume -= 0.05; // Down 5%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click to toggle mute! (Bonus feature)
|
||||
|
||||
@ -1,38 +1,37 @@
|
||||
|
||||
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: workspaceRepeater.count * 30
|
||||
property var modelData
|
||||
implicitWidth: workspaceRow.implicitWidth
|
||||
height: 30
|
||||
Row {
|
||||
id: workspaceRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 10 // Slightly increase spacing between workspace buttons
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeater
|
||||
model: Hyprland.workspaces
|
||||
|
||||
anchors.centerIn: parent
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
id: workspaceNumber
|
||||
width: 16
|
||||
height: 16
|
||||
radius: 20
|
||||
//color: modelData.active ? myPallete.accent : myPallete.window
|
||||
color: modelData.active ? Colors.foreground : "transparent"
|
||||
|
||||
Text {
|
||||
id: workspaceNumber
|
||||
font.weight: 900
|
||||
font.family: Appearance.font
|
||||
font.pixelSize: Appearance.fontSize
|
||||
anchors.centerIn: parent
|
||||
anchors.centerIn: workspaceNumber
|
||||
text: modelData.id
|
||||
color: modelData.active ? Colors.background : Colors.foreground // Set contrasting color for workspace number
|
||||
}
|
||||
}
|
||||
model: Hyprland.workspaces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
modules/notifications/NotiPopup.qml
Normal file
142
modules/notifications/NotiPopup.qml
Normal file
@ -0,0 +1,142 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs
|
||||
import "."
|
||||
import QtQuick.Layouts
|
||||
|
||||
WlrLayershell {
|
||||
id: root
|
||||
|
||||
// 1. Position: Top Right Corner, covering the full height
|
||||
// We make it a fixed width (e.g., 400px) so it doesn't block the whole screen
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
}
|
||||
margins {
|
||||
top: 30
|
||||
}
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: notifList.contentHeight + 10
|
||||
|
||||
// 2. Layer: Put it ABOVE normal windows
|
||||
layer: WlrLayer.Overlay
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// 3. CRITICAL: Make the window itself invisible!
|
||||
// We only want to see the notifications, not the container.
|
||||
color: "transparent"
|
||||
|
||||
// 4. Input: Let clicks pass through empty areas
|
||||
// (This is default behavior if the background is transparent in some compositors,
|
||||
// but usually you need to be careful with handling mouse events here)
|
||||
|
||||
// THE SPAWNER
|
||||
ListView {
|
||||
id: notifList
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
// Use 'spacing' to put gaps between notifications
|
||||
spacing: 10
|
||||
|
||||
// Align to the bottom (like Windows) or Top (like GNOME)?
|
||||
// verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
// 🔗 CONNECT TO THE SERVER
|
||||
// Assuming your NotificationServer is a singleton or globally accessible
|
||||
// ... other imports
|
||||
|
||||
// Inside your ListView...
|
||||
model: NotifServer.trackedNotifications
|
||||
delegate: Item {
|
||||
id: notifyItem
|
||||
implicitWidth: ListView.view.width
|
||||
implicitHeight: 60 // Fixed height is usually better for icon layouts
|
||||
|
||||
required property var modelData
|
||||
Timer {
|
||||
id: timout
|
||||
interval: 30000
|
||||
running: true
|
||||
onRunningChanged: notifyItem.modelData.dismiss()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.background
|
||||
radius: 10
|
||||
border.color: Colors.color5
|
||||
|
||||
// 2. Use RowLayout to put Image | Text side-by-side
|
||||
Row {
|
||||
anchors.margins: 10
|
||||
anchors.fill: parent
|
||||
anchors.centerIn: parent
|
||||
spacing: 15
|
||||
|
||||
// 🖼️ THE IMAGE ON THE LEFT
|
||||
Image {
|
||||
|
||||
// Use the image if available, otherwise hide this space?
|
||||
// Or you could use an icon fallback.
|
||||
source: notifyItem.modelData.image
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Hide if no image exists so text takes full width
|
||||
visible: notifyItem.modelData.image !== ""
|
||||
|
||||
// Fixed size for consistency
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
// Crop it nicely so it doesn't stretch
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
// Optional: Cache it for performance
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// 📝 THE TEXT ON THE RIGHT
|
||||
ColumnLayout {
|
||||
// Take up all remaining width
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignTop // Center vertically
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: notifyItem.modelData.summary
|
||||
color: Colors.foreground
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: notifyItem.modelData.body
|
||||
color: Colors.foreground
|
||||
|
||||
// Limit to 2 lines
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (Your MouseArea for closing can still go here covering the whole thing)
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: notifyItem.modelData.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
modules/notifications/NotifServer.qml
Normal file
15
modules/notifications/NotifServer.qml
Normal file
@ -0,0 +1,15 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
pragma Singleton
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
NotificationServer {
|
||||
bodyMarkupSupported: true
|
||||
persistenceSupported: true
|
||||
imageSupported: true
|
||||
onNotification: notification => {
|
||||
notification.tracked = true;
|
||||
console.log("got notification!!! arf woof");
|
||||
}
|
||||
}
|
||||
2
modules/notifications/qmldir
Normal file
2
modules/notifications/qmldir
Normal file
@ -0,0 +1,2 @@
|
||||
singleton NotifServer 1.0 NotifServer.qml
|
||||
NotiPopup 1.0 NotiPopup.qml
|
||||
39
modules/wallpaper/Overlay.qml
Normal file
39
modules/wallpaper/Overlay.qml
Normal file
@ -0,0 +1,39 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "../../"
|
||||
import "."
|
||||
|
||||
WlrLayershell {
|
||||
id: overlayRoot
|
||||
|
||||
// 1. Fill the entire screen
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
// 2. Sit on top of EVERYTHING (even fullscreen apps if compositor allows)
|
||||
layer: WlrLayer.Top
|
||||
|
||||
// 3. Invisible background
|
||||
color: "transparent"
|
||||
|
||||
// 4. 👻 GHOST MODE ENABLED 👻
|
||||
// An empty Region means "I accept mouse events nowhere".
|
||||
// This guarantees you can click through the black corners.
|
||||
mask: Region {}
|
||||
|
||||
// 5. Load the corners!
|
||||
ScreenCorners {
|
||||
// Adjust these to match your screen's aesthetic
|
||||
cornerRadius: 25
|
||||
cornerColor: Colors.background
|
||||
shouldShow: true
|
||||
|
||||
// Ensure it stays on top of any other items in this window
|
||||
z: 999
|
||||
}
|
||||
}
|
||||
175
modules/wallpaper/ScreenCorners.qml
Normal file
175
modules/wallpaper/ScreenCorners.qml
Normal file
@ -0,0 +1,175 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 🛠️ CONFIGURATION (Tweaked to match your setup)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// How round do you want the screen?
|
||||
property real cornerRadius: 20
|
||||
|
||||
// What color should the corners be? (Usually black to match the bezel)
|
||||
// You can change this to "transparent" or a theme color if you want.
|
||||
property color cornerColor
|
||||
|
||||
// Enable/Disable toggle
|
||||
property bool shouldShow: true
|
||||
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// Wrapper with layer caching to reduce GPU usage
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
|
||||
Shape {
|
||||
id: cornersShape
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
enabled: false // Click-through
|
||||
|
||||
ShapePath {
|
||||
id: cornersPath
|
||||
|
||||
// Map our local properties to the variables the code expects
|
||||
readonly property real cornerRadius: root.cornerRadius
|
||||
readonly property real cornerSize: root.cornerRadius // Usually same as radius
|
||||
|
||||
// Margins (Leave 0 unless your bar overlaps)
|
||||
readonly property real topMargin: 0
|
||||
readonly property real bottomMargin: 0
|
||||
readonly property real leftMargin: 0
|
||||
readonly property real rightMargin: 0
|
||||
|
||||
readonly property real screenWidth: cornersShape.width
|
||||
readonly property real screenHeight: cornersShape.height
|
||||
|
||||
strokeWidth: -1 // No outline
|
||||
fillColor: cornerColor
|
||||
|
||||
// Smooth fade if you toggle it
|
||||
|
||||
// ==========================================
|
||||
// 📐 GEOMETRY LOGIC (Untouched)
|
||||
// ==========================================
|
||||
|
||||
// Top-Left
|
||||
startX: leftMargin
|
||||
startY: topMargin
|
||||
PathLine {
|
||||
relativeX: cornersPath.cornerSize
|
||||
relativeY: 0
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: cornersPath.cornerSize - cornersPath.cornerRadius
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -cornersPath.cornerRadius
|
||||
relativeY: cornersPath.cornerRadius
|
||||
radiusX: cornersPath.cornerRadius
|
||||
radiusY: cornersPath.cornerRadius
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius)
|
||||
relativeY: 0
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -cornersPath.cornerSize
|
||||
}
|
||||
|
||||
// Top-Right
|
||||
PathMove {
|
||||
x: cornersPath.screenWidth - cornersPath.rightMargin - cornersPath.cornerSize
|
||||
y: cornersPath.topMargin
|
||||
}
|
||||
PathLine {
|
||||
relativeX: cornersPath.cornerSize
|
||||
relativeY: 0
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: cornersPath.cornerSize
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius)
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -cornersPath.cornerRadius
|
||||
relativeY: -cornersPath.cornerRadius
|
||||
radiusX: cornersPath.cornerRadius
|
||||
radiusY: cornersPath.cornerRadius
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius)
|
||||
}
|
||||
// Bottom-Left
|
||||
PathMove {
|
||||
x: cornersPath.leftMargin
|
||||
y: cornersPath.screenHeight - cornersPath.bottomMargin - cornersPath.cornerSize
|
||||
}
|
||||
PathLine {
|
||||
relativeX: cornersPath.cornerSize - cornersPath.cornerRadius
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: cornersPath.cornerRadius
|
||||
relativeY: cornersPath.cornerRadius
|
||||
radiusX: cornersPath.cornerRadius
|
||||
radiusY: cornersPath.cornerRadius
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: cornersPath.cornerSize - cornersPath.cornerRadius
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -cornersPath.cornerSize
|
||||
relativeY: 0
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -cornersPath.cornerSize
|
||||
}
|
||||
|
||||
// Bottom-Right
|
||||
PathMove {
|
||||
x: cornersPath.screenWidth - cornersPath.rightMargin
|
||||
y: cornersPath.screenHeight - cornersPath.bottomMargin
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -cornersPath.cornerSize
|
||||
relativeY: 0
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius)
|
||||
}
|
||||
PathArc {
|
||||
relativeX: cornersPath.cornerRadius
|
||||
relativeY: -cornersPath.cornerRadius
|
||||
radiusX: cornersPath.cornerRadius
|
||||
radiusY: cornersPath.cornerRadius
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: cornersPath.cornerSize - cornersPath.cornerRadius
|
||||
relativeY: 0
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: cornersPath.cornerSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
modules/wallpaper/WallSwitcher.qml
Normal file
102
modules/wallpaper/WallSwitcher.qml
Normal file
@ -0,0 +1,102 @@
|
||||
import QtQuick
|
||||
import Qt.labs.folderlistmodel 2.15 // <--- The magic file scanner!
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import "."
|
||||
import qs.modules.bar
|
||||
import qs
|
||||
|
||||
FloatingWindow {
|
||||
id: root
|
||||
title: "quickshell-WallSwitcher"
|
||||
visible: false
|
||||
implicitWidth: 840
|
||||
implicitHeight: 640
|
||||
|
||||
GlobalShortcut {
|
||||
// This is the "Secret Password" Hyprland will use
|
||||
name: "toggle-walls"
|
||||
|
||||
onPressed: {
|
||||
// Toggle visibility!
|
||||
root.visible = !root.visible;
|
||||
|
||||
console.log("Shortcut pressed! Switcher is now: " + (root.visible ? "Visible" : "Hidden"));
|
||||
}
|
||||
}
|
||||
|
||||
// Make it float above everything else
|
||||
Text {
|
||||
id: titleText
|
||||
text: "Wallpapers in " + WallpaperStore.wallDir
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
font.pixelSize: 20
|
||||
topPadding: 20
|
||||
bottomPadding: 10
|
||||
font.family: Appearance.font
|
||||
color: Colors.foreground
|
||||
}
|
||||
|
||||
color: Colors.background // Dark background
|
||||
|
||||
// 1. The File Scanner
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
folder: "file:///home/lucy/.walls/" // <--- Your stash!
|
||||
nameFilters: ["*.png", "*.jpg", "*.jpeg"]
|
||||
showDirs: false
|
||||
}
|
||||
|
||||
// 2. The Grid Display
|
||||
GridView {
|
||||
anchors.top: titleText.bottom // Sit below the title!
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 20
|
||||
cellWidth: 200
|
||||
cellHeight: 100
|
||||
clip: true
|
||||
|
||||
model: folderModel
|
||||
|
||||
delegate: Item {
|
||||
width: 200
|
||||
height: 100
|
||||
|
||||
Image {
|
||||
width: 180
|
||||
height: 90
|
||||
anchors.centerIn: parent
|
||||
// "fileUrl" is provided by FolderListModel
|
||||
source: fileUrl
|
||||
|
||||
// IMPORTANT: Downscale the image for the thumbnail!
|
||||
// If you don't do this, loading 50 4K images will eat your RAM
|
||||
// faster than Chrome eats memory! 🙀
|
||||
sourceSize.width: 140
|
||||
sourceSize.height: 90
|
||||
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
Process {
|
||||
id: generateScheme
|
||||
property string cleanPath: fileUrl.toString().replace("file://", "")
|
||||
command: ["wallust", "run", cleanPath]
|
||||
}
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
let cleanPath = fileUrl.toString().replace("file://", "");
|
||||
// Update the Singleton!
|
||||
WallpaperStore.currentWall = fileUrl.toString();
|
||||
//generateScheme.startDetached();
|
||||
console.log(generateScheme.stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
modules/wallpaper/Wallpaper.qml
Normal file
81
modules/wallpaper/Wallpaper.qml
Normal file
@ -0,0 +1,81 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls // <--- Needed for StackView
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
WlrLayershell {
|
||||
id: root
|
||||
layer: WlrLayer.Background
|
||||
keyboardFocus: WlrKeyboardFocus.None
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
// We need to accept the screen from Variants
|
||||
property var screen
|
||||
property var modelData
|
||||
|
||||
// 1. The StackView manages the images
|
||||
StackView {
|
||||
id: wallStack
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
// 2. Define what a "Wallpaper" looks like
|
||||
Component {
|
||||
id: wallComponent
|
||||
Image {
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
width: StackView.view.width
|
||||
height: StackView.view.height
|
||||
asynchronous: true // ⚡ VERY IMPORTANT: Prevents lag while loading!
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Load the initial wallpaper immediately (No animation on boot)
|
||||
initialItem: wallComponent.createObject(wallStack, {
|
||||
"source": WallpaperStore.currentWall
|
||||
})
|
||||
|
||||
// 4. THE ANIMATIONS 🎬
|
||||
// When a new wall replaces the old one:
|
||||
|
||||
// New One: Fades In (0 -> 1)
|
||||
replaceEnter: Transition {
|
||||
NumberAnimation {
|
||||
property: "x"
|
||||
from: wallStack.width
|
||||
to: 0
|
||||
duration: 800 // Slower = Smoother
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Old One: Fades Out (1 -> 0)
|
||||
replaceExit: Transition {
|
||||
NumberAnimation {
|
||||
property: "x"
|
||||
from: 0
|
||||
to: -wallStack.width
|
||||
duration: 800
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. The Trigger 🔫
|
||||
// We listen for the singleton to change, then tell the Stack to update
|
||||
Connections {
|
||||
target: WallpaperStore
|
||||
|
||||
function onCurrentWallChanged() {
|
||||
// "Replace the current item with a new wallComponent using the new source"
|
||||
wallStack.replace(wallComponent, {
|
||||
"source": WallpaperStore.currentWall
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
31
modules/wallpaper/WallpaperStore.qml
Normal file
31
modules/wallpaper/WallpaperStore.qml
Normal file
@ -0,0 +1,31 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell.Io // <--- Import for FileView and JsonAdapter
|
||||
|
||||
QtObject {
|
||||
id: store
|
||||
|
||||
// 1. The File Manager
|
||||
property string wallDir: "~/.walls/"
|
||||
property var settings: FileView {
|
||||
path: "/home/lucy/.cache/quickshell_settings.json"
|
||||
|
||||
// Auto-save when properties change
|
||||
onAdapterUpdated: writeAdapter()
|
||||
|
||||
// Auto-load when the file changes on disk
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
|
||||
// 2. The Magic Adapter
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
// This property corresponds to a key in your JSON file!
|
||||
property string lastWallpaper: "/home/lucy/.walls/frieren_river.jpg"
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create a helper property for the rest of your app to use
|
||||
// This keeps the "WallpaperStore.currentWall" name working!
|
||||
property alias currentWall: adapter.lastWallpaper
|
||||
}
|
||||
5
modules/wallpaper/qmldir
Normal file
5
modules/wallpaper/qmldir
Normal file
@ -0,0 +1,5 @@
|
||||
singleton WallpaperStore 1.0 WallpaperStore.qml
|
||||
Wallpaper 1.0 Wallpaper.qml
|
||||
WallSwitcher 1.0 WallSwitcher.qml
|
||||
Overlay 1.0 Overlay.qml
|
||||
ScreenCorners 1.0 ScreenCorners.qml
|
||||
23
shell.qml
23
shell.qml
@ -1,4 +1,23 @@
|
||||
//@ pragma UseQApplication
|
||||
import qs.modules.bar
|
||||
//pragma ComponentBehavior: Bound
|
||||
pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import "./modules/bar/"
|
||||
import "./modules/wallpaper/"
|
||||
import "./modules/notifications/"
|
||||
|
||||
Bar {}
|
||||
Scope {
|
||||
WallSwitcher {}
|
||||
Variants {
|
||||
id: wallVariants
|
||||
model: Quickshell.screens
|
||||
delegate: Wallpaper {}
|
||||
}
|
||||
Variants {
|
||||
id: barVariants
|
||||
model: Quickshell.screens
|
||||
delegate: Bar {}
|
||||
}
|
||||
NotiPopup {}
|
||||
Overlay {}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user