mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: backgroundContainer
|
||||
|
||||
required property var modelData
|
||||
property string displayName: modelData.name
|
||||
|
||||
property url wallpaperPath: {
|
||||
const displays = Config.runtime.monitors
|
||||
const fallback = Config.runtime.appearance.background.defaultPath
|
||||
|
||||
if (!displays)
|
||||
return fallback
|
||||
|
||||
const monitor = displays?.[displayName]
|
||||
return monitor?.wallpaper ?? fallback
|
||||
}
|
||||
|
||||
// parallax config
|
||||
property bool parallaxEnabled: Config.runtime.appearance.background.parallax.enabled
|
||||
property real parallaxZoom: Config.runtime.appearance.background.parallax.zoom
|
||||
property int workspaceRange: Config.runtime.bar.modules.workspaces.workspaceIndicators
|
||||
|
||||
// hyprland
|
||||
property int activeWorkspaceId: Hyprland.focusedWorkspace?.id ?? 1
|
||||
|
||||
// wallpaper geometry
|
||||
property real wallpaperWidth: bgImg.implicitWidth
|
||||
property real wallpaperHeight: bgImg.implicitHeight
|
||||
|
||||
property real wallpaperToScreenRatio: {
|
||||
if (wallpaperWidth <= 0 || wallpaperHeight <= 0)
|
||||
return 1
|
||||
return Math.min(
|
||||
wallpaperWidth / width,
|
||||
wallpaperHeight / height
|
||||
)
|
||||
}
|
||||
|
||||
property real effectiveScale: parallaxEnabled ? parallaxZoom : 1
|
||||
|
||||
property real movableXSpace: Math.max(
|
||||
0,
|
||||
((wallpaperWidth / wallpaperToScreenRatio * effectiveScale) - width) / 2
|
||||
)
|
||||
|
||||
// workspace mapping
|
||||
property int lowerWorkspace: Math.floor((activeWorkspaceId - 1) / workspaceRange) * workspaceRange + 1
|
||||
property int upperWorkspace: lowerWorkspace + workspaceRange
|
||||
property int workspaceSpan: Math.max(1, upperWorkspace - lowerWorkspace)
|
||||
|
||||
property real valueX: {
|
||||
if (!parallaxEnabled)
|
||||
return 0.5
|
||||
return (activeWorkspaceId - lowerWorkspace) / workspaceSpan
|
||||
}
|
||||
|
||||
// sidebar globals
|
||||
property bool sidebarLeftOpen: Globals.visiblility.sidebarLeft
|
||||
&& Config.runtime.appearance.background.parallax.enableSidebarLeft
|
||||
|
||||
property bool sidebarRightOpen: Globals.visiblility.sidebarRight
|
||||
&& Config.runtime.appearance.background.parallax.enableSidebarRight
|
||||
|
||||
property real sidebarOffset: {
|
||||
if (sidebarLeftOpen && !sidebarRightOpen)
|
||||
if (Config.runtime.bar.position === "right")
|
||||
return 0.15
|
||||
else return -0.15
|
||||
if (sidebarRightOpen && !sidebarLeftOpen)
|
||||
if (Config.runtime.bar.position === "left")
|
||||
return -0.15
|
||||
else return 0.15
|
||||
return 0
|
||||
}
|
||||
|
||||
property real effectiveValueX: Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
1,
|
||||
valueX + sidebarOffset
|
||||
)
|
||||
)
|
||||
|
||||
// window
|
||||
color: (bgImg.status === Image.Error) ? Appearance.colors.colLayer2 : "transparent"
|
||||
WlrLayershell.namespace: "nucleus:background"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
screen: modelData
|
||||
visible: Config.initialized && Config.runtime.appearance.background.enabled
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// wallpaper picker
|
||||
Process {
|
||||
id: wallpaperProc
|
||||
|
||||
command: ["bash", "-c", Directories.scriptsPath + "/interface/changebg.sh"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const out = text.trim()
|
||||
|
||||
if (out !== "null" && out.length > 0) {
|
||||
const parts = out.split("|")
|
||||
|
||||
if (parts.length === 2) {
|
||||
const monitor = parts[0]
|
||||
const wallpaper = parts[1]
|
||||
|
||||
Config.updateKey(
|
||||
"monitors." + monitor + ".wallpaper",
|
||||
wallpaper
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "clock", "changePosition"
|
||||
])
|
||||
if (Config.runtime.appearance.colors.autogenerated) {
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wallpaper
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
StyledImage {
|
||||
id: bgImg
|
||||
|
||||
visible: status === Image.Ready
|
||||
smooth: false
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: wallpaperPath + "?t=" + Date.now()
|
||||
|
||||
width: wallpaperWidth / wallpaperToScreenRatio * effectiveScale
|
||||
height: wallpaperHeight / wallpaperToScreenRatio * effectiveScale
|
||||
|
||||
x: -movableXSpace - (effectiveValueX - 0.5) * 2 * movableXSpace
|
||||
y: 0
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(600)
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready) {
|
||||
backgroundContainer.wallpaperWidth = implicitWidth
|
||||
backgroundContainer.wallpaperHeight = implicitHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: widgetCanvas
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
// error ui
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
visible: bgImg.status === Image.Error
|
||||
|
||||
Rectangle {
|
||||
width: 550
|
||||
height: 400
|
||||
radius: Appearance.rounding.windowRounding
|
||||
color: "transparent"
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: Metrics.margin("normal")
|
||||
spacing: Metrics.margin("small")
|
||||
|
||||
MaterialSymbol {
|
||||
text: "wallpaper"
|
||||
font.pixelSize: Metrics.fontSize("wildass")
|
||||
color: Appearance.colors.colOnLayer2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Wallpaper Missing"
|
||||
font.pixelSize: Metrics.fontSize("hugeass")
|
||||
font.bold: true
|
||||
color: Appearance.colors.colOnLayer2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Seems like you haven't set a wallpaper yet."
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
StyledButton {
|
||||
text: "Set wallpaper"
|
||||
icon: "wallpaper"
|
||||
secondary: true
|
||||
radius: Metrics.radius("large")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: wallpaperProc.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "background"
|
||||
|
||||
function change() {
|
||||
wallpaperProc.running = true
|
||||
}
|
||||
|
||||
function next() {
|
||||
WallpaperSlideshow.nextWallpaper()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Clock {
|
||||
id: clock
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import "../../components/morphedPolygons/geometry/offset.js" as Offset
|
||||
import "../../components/morphedPolygons/material-shapes.js" as MaterialShapes // For polygons
|
||||
import "../../components/morphedPolygons/shapes/corner-rounding.js" as CornerRounding
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.components.morphedPolygons
|
||||
import qs.services
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property bool imageFailed: false
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: clock
|
||||
|
||||
required property var modelData
|
||||
property int padding: Config.runtime.appearance.background.clock.edgeSpacing
|
||||
property int clockHeight: Config.runtime.appearance.background.clock.isAnalog ? 250 : 160
|
||||
property int clockWidth: Config.runtime.appearance.background.clock.isAnalog ? 250 : 360
|
||||
|
||||
function setRandomPosition() {
|
||||
const x = Math.floor(Math.random() * (width - clockWidth));
|
||||
const y = Math.floor(Math.random() * (height - clockHeight));
|
||||
animX.to = x;
|
||||
animY.to = y;
|
||||
moveAnim.start();
|
||||
Config.updateKey("appearance.background.clock.xPos", x);
|
||||
Config.updateKey("appearance.background.clock.yPos", y);
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
visible: (Config.runtime.appearance.background.clock.enabled && Config.initialized && !imageFailed)
|
||||
exclusiveZone: 0
|
||||
WlrLayershell.layer: WlrLayer.Bottom
|
||||
screen: modelData
|
||||
|
||||
ParallelAnimation {
|
||||
id: moveAnim
|
||||
|
||||
NumberAnimation {
|
||||
id: animX
|
||||
|
||||
target: rootContentContainer
|
||||
property: "x"
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: animY
|
||||
|
||||
target: rootContentContainer
|
||||
property: "y"
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
margins {
|
||||
top: padding
|
||||
bottom: padding
|
||||
left: padding
|
||||
right: padding
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rootContentContainer
|
||||
|
||||
property real releasedX: 0
|
||||
property real releasedY: 0
|
||||
|
||||
height: clockHeight
|
||||
width: clockWidth
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
x = Config.runtime.appearance.background.clock.xPos;
|
||||
y = Config.runtime.appearance.background.clock.yPos;
|
||||
});
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
drag.target: rootContentContainer
|
||||
drag.axis: Drag.XAndYAxis
|
||||
acceptedButtons: Qt.RightButton
|
||||
onReleased: {
|
||||
if (ma.button === Qt.RightButton)
|
||||
return
|
||||
Config.updateKey("appearance.background.clock.xPos", rootContentContainer.x);
|
||||
Config.updateKey("appearance.background.clock.yPos", rootContentContainer.y);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: digitalClockContainer
|
||||
|
||||
visible: !Config.runtime.appearance.background.clock.isAnalog
|
||||
|
||||
Column {
|
||||
spacing: Metrics.spacing(-40)
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: Time.format("hh:mm")
|
||||
font.pixelSize: Metrics.fontSize(Appearance.font.size.wildass * 3)
|
||||
font.family: Metrics.fontFamily("main")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(8)
|
||||
animate: false
|
||||
text: Time.format("dddd, dd/MM")
|
||||
font.pixelSize: Metrics.fontSize(32)
|
||||
font.family: Metrics.fontFamily("main")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: analogClockContainer
|
||||
|
||||
property int hours: parseInt(Time.format("hh"))
|
||||
property int minutes: parseInt(Time.format("mm"))
|
||||
property int seconds: parseInt(Time.format("ss"))
|
||||
readonly property real cx: width / 2
|
||||
readonly property real cy: height / 2
|
||||
property var shapes: [MaterialShapes.getCookie7Sided, MaterialShapes.getCookie9Sided, MaterialShapes.getCookie12Sided, MaterialShapes.getPixelCircle, MaterialShapes.getCircle, MaterialShapes.getGhostish]
|
||||
|
||||
anchors.fill: parent
|
||||
visible: Config.runtime.appearance.background.clock.isAnalog
|
||||
width: clock.width / 1.1
|
||||
height: clock.height / 1.1
|
||||
|
||||
// Polygon
|
||||
MorphedPolygon {
|
||||
id: shapeCanvas
|
||||
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3secondaryContainer
|
||||
roundedPolygon: analogClockContainer.shapes[Config.runtime.appearance.background.clock.shape]()
|
||||
|
||||
transform: Rotation {
|
||||
origin.x: shapeCanvas.width / 2
|
||||
origin.y: shapeCanvas.height / 2
|
||||
angle: shapeCanvas.rotation
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
running: Config.runtime.appearance.animations.enabled && Config.runtime.appearance.background.clock.rotatePolygonBg
|
||||
duration: Config.runtime.appearance.background.clock.rotationDuration * 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
ClockDial {
|
||||
id: dial
|
||||
anchors.fill: parent
|
||||
anchors.margins: parent.width * 0.12
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
z: 0
|
||||
}
|
||||
|
||||
// Hour hand
|
||||
StyledRect {
|
||||
z: 2
|
||||
width: 10
|
||||
height: parent.height * 0.3
|
||||
radius: Metrics.radius("full")
|
||||
color: Qt.darker(Appearance.m3colors.m3secondary, 0.8)
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: (analogClockContainer.hours % 12 + analogClockContainer.minutes / 60) * 30
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: 16
|
||||
height: 16
|
||||
radius: width / 2
|
||||
color: Appearance.m3colors.m3secondary
|
||||
z: 99 // Ensures its on top of everthing
|
||||
|
||||
// Inner dot
|
||||
StyledRect {
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
radius: width / 2
|
||||
anchors.centerIn: parent
|
||||
z: 100
|
||||
color: Appearance.m3colors.m3primaryContainer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Minute hand
|
||||
StyledRect {
|
||||
width: 18
|
||||
height: parent.height * 0.35
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3secondary
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.minutes * 6
|
||||
z: 10 // On top of all hands
|
||||
}
|
||||
|
||||
// Second hand
|
||||
StyledRect {
|
||||
visible: true
|
||||
width: 4
|
||||
height: parent.height * 0.28
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3error
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.seconds * 6
|
||||
z: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("hh")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(30)
|
||||
font.pixelSize: Metrics.fontSize(80)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("mm")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(110)
|
||||
font.pixelSize: Metrics.fontSize(80)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function changePosition() {
|
||||
clock.setRandomPosition();
|
||||
}
|
||||
|
||||
target: "clock"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: "white"
|
||||
readonly property real cx: width / 2
|
||||
readonly property real cy: height / 2
|
||||
readonly property real radius: Math.min(width, height) / 2
|
||||
opacity: 0.4
|
||||
|
||||
// Hour marks (12 ticks)
|
||||
Repeater {
|
||||
model: 12
|
||||
|
||||
Item {
|
||||
width: root.width
|
||||
height: root.height
|
||||
anchors.centerIn: parent
|
||||
rotation: index * 30
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Rectangle {
|
||||
width: 3 // thickness of tick
|
||||
height: 15 // length of tick
|
||||
color: root.color
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: -root.radius * 0.15 / 2
|
||||
radius: width / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minute marks (60 ticks)
|
||||
Repeater {
|
||||
model: 60
|
||||
|
||||
Item {
|
||||
width: root.width
|
||||
height: root.height
|
||||
anchors.centerIn: parent
|
||||
rotation: index * 6
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Rectangle {
|
||||
width: index % 5 === 0 ? 3 : 2 // thicker for 5-minute marks
|
||||
height: index % 5 === 0 ? 15 : 8 // longer for 5-minute marks
|
||||
color: root.color
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: -root.radius * 0.15 / 2
|
||||
radius: width / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
.config/quickshell/nucleus-shell/modules/interface/bar/Bar.qml
Normal file
163
.config/quickshell/nucleus-shell/modules/interface/bar/Bar.qml
Normal file
@@ -0,0 +1,163 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
GothCorners {
|
||||
opacity: ConfigResolver.bar(bar.displayName).gothCorners && !ConfigResolver.bar(bar.displayName).floating && ConfigResolver.bar(bar.displayName).enabled && !ConfigResolver.bar(bar.displayName).merged ? 1 : 0
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
// some exclusiveSpacing so it won't look like its sticking into the window when floating
|
||||
|
||||
id: bar
|
||||
|
||||
required property var modelData
|
||||
property string displayName: modelData.name
|
||||
property int rd: ConfigResolver.bar(displayName).radius * Config.runtime.appearance.rounding.factor // So it won't be modified when factor is 0
|
||||
property int margin: ConfigResolver.bar(displayName).margins
|
||||
property bool floating: ConfigResolver.bar(displayName).floating
|
||||
property bool merged: ConfigResolver.bar(displayName).merged
|
||||
property string pos: ConfigResolver.bar(displayName).position
|
||||
property bool vertical: pos === "left" || pos === "right"
|
||||
// Simple position properties
|
||||
property bool attachedTop: pos === "top"
|
||||
property bool attachedBottom: pos === "bottom"
|
||||
property bool attachedLeft: pos === "left"
|
||||
property bool attachedRight: pos === "right"
|
||||
|
||||
screen: modelData // Show bar on all screens
|
||||
visible: ConfigResolver.bar(displayName).enabled && Config.initialized
|
||||
WlrLayershell.namespace: "nucleus:bar"
|
||||
exclusiveZone: ConfigResolver.bar(displayName).floating ? ConfigResolver.bar(displayName).density + Metrics.margin("tiny") : ConfigResolver.bar(displayName).density
|
||||
implicitHeight: ConfigResolver.bar(displayName).density // density === height. (horizontal orientation)
|
||||
implicitWidth: ConfigResolver.bar(displayName).density // density === width. (vertical orientation)
|
||||
color: "transparent" // Keep panel window's color transparent, so that it can be modified by background rect
|
||||
|
||||
// This is probably a little weird way to set anchors but I think it's the best way. (and it works)
|
||||
anchors {
|
||||
top: ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
|
||||
bottom: ConfigResolver.bar(displayName).position === "bottom" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
|
||||
left: ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
|
||||
right: ConfigResolver.bar(displayName).position === "right" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
|
||||
}
|
||||
|
||||
margins {
|
||||
top: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
bottom: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
left: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && !vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
right: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && !vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: background
|
||||
color: Appearance.m3colors.m3background
|
||||
anchors.fill: parent
|
||||
topLeftRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedBottom || attachedRight ? rd : 0;
|
||||
}
|
||||
topRightRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedBottom || attachedLeft ? rd : 0;
|
||||
}
|
||||
bottomLeftRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedTop || attachedRight ? rd : 0;
|
||||
}
|
||||
bottomRightRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedTop || attachedLeft ? rd : 0;
|
||||
}
|
||||
|
||||
BarContent {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on topLeftRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on bottomRightRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on topRightRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "content/"
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
property string displayName: screen?.name ?? ""
|
||||
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
|
||||
|
||||
Row {
|
||||
id: hCenterRow
|
||||
visible: isHorizontal
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
SystemUsageModule {}
|
||||
MediaPlayerModule {}
|
||||
ActiveWindowModule {}
|
||||
ClockModule {}
|
||||
BatteryIndicatorModule {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: hLeftRow
|
||||
|
||||
visible: isHorizontal
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Metrics.spacing(4)
|
||||
anchors.leftMargin: ConfigResolver.bar(displayName).density * 0.3
|
||||
|
||||
ToggleModule {
|
||||
icon: "menu"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.sidebarLeft
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.sidebarLeft = value
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceModule {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: hRightRow
|
||||
|
||||
visible: isHorizontal
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Metrics.spacing(4)
|
||||
anchors.rightMargin: ConfigResolver.bar(displayName).density * 0.3
|
||||
|
||||
SystemTray {
|
||||
id: sysTray
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: seperator
|
||||
visible: (sysTray.items.count > 0) && ConfigResolver.bar(displayName).modules.statusIcons.enabled
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
font.pixelSize: Metrics.fontSize("hugeass")
|
||||
text: "·"
|
||||
}
|
||||
|
||||
StatusIconsModule {}
|
||||
|
||||
StyledText {
|
||||
id: seperator2
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
font.pixelSize: Metrics.fontSize("hugeass")
|
||||
text: "·"
|
||||
}
|
||||
|
||||
ToggleModule {
|
||||
icon: "power_settings_new"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.powermenu
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.powermenu = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical Layout
|
||||
Item {
|
||||
visible: !isHorizontal
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: ConfigResolver.bar(displayName).density * 0.1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitWidth: vRow.implicitHeight
|
||||
implicitHeight: vRow.implicitWidth
|
||||
|
||||
Row {
|
||||
id: vRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(8)
|
||||
rotation: 90
|
||||
|
||||
ToggleModule {
|
||||
icon: "menu"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.sidebarLeft
|
||||
rotation: 270
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.sidebarLeft = value
|
||||
}
|
||||
}
|
||||
|
||||
SystemUsageModule {}
|
||||
MediaPlayerModule {}
|
||||
|
||||
SystemTray {
|
||||
rotation: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !isHorizontal
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 35
|
||||
implicitWidth: centerRow.implicitHeight
|
||||
implicitHeight: centerRow.implicitWidth
|
||||
|
||||
Row {
|
||||
id: centerRow
|
||||
anchors.centerIn: parent
|
||||
|
||||
WorkspaceModule {
|
||||
rotation: 90
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !isHorizontal
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: ConfigResolver.bar(displayName).density * 0.1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitWidth: row.implicitHeight
|
||||
implicitHeight: row.implicitWidth
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(6)
|
||||
rotation: 90
|
||||
|
||||
ClockModule {
|
||||
rotation: 270
|
||||
}
|
||||
|
||||
StatusIconsModule {}
|
||||
BatteryIndicatorModule {}
|
||||
|
||||
ToggleModule {
|
||||
icon: "power_settings_new"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.powermenu
|
||||
rotation: 270
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.powermenu = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property int opacity: 0
|
||||
|
||||
color: "transparent"
|
||||
visible: Config.initialized
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
bottom: true
|
||||
right: true
|
||||
}
|
||||
|
||||
Item {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3background
|
||||
layer.enabled: true
|
||||
opacity: root.opacity
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
maskSource: mask
|
||||
maskEnabled: true
|
||||
maskInverted: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("large")
|
||||
easing.type: Easing.InOutExpo
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Config.runtime.bar.position === "bottom" ? -15 : 0
|
||||
anchors.bottomMargin: Config.runtime.bar.position === "top" ? -15 : 0
|
||||
anchors.leftMargin: Config.runtime.bar.position === "right" ? -15 : 0
|
||||
anchors.rightMargin: Config.runtime.bar.position === "left" ? -15 : 0
|
||||
radius: Metrics.radius("normal")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: container
|
||||
intersection: Intersection.Xor
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: container
|
||||
property string displayName: screen?.name ?? ""
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
property Toplevel activeToplevel: Compositor.isWorkspaceOccupied(Compositor.focusedWorkspaceId)
|
||||
? Compositor.activeToplevel
|
||||
: null
|
||||
|
||||
implicitWidth: row.implicitWidth + 30
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
|
||||
function simplifyTitle(title) {
|
||||
if (!title)
|
||||
return ""
|
||||
|
||||
title = title.replace(/[●⬤○◉◌◎]/g, "") // Symbols to remove
|
||||
|
||||
// Normalize separators
|
||||
title = title
|
||||
.replace(/\s*[|—]\s*/g, " - ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
|
||||
const parts = title.split(" - ").map(p => p.trim()).filter(Boolean)
|
||||
|
||||
if (parts.length === 1)
|
||||
return parts[0]
|
||||
|
||||
// Known app names (extend freely my fellow contributors)
|
||||
const apps = [
|
||||
"Firefox", "Mozilla Firefox",
|
||||
"Chromium", "Google Chrome",
|
||||
"Neovim", "VS Code", "Code",
|
||||
"Kitty", "Alacritty", "Terminal",
|
||||
"Discord", "Spotify", "Steam",
|
||||
"Settings - Nucleus", "Settings"
|
||||
]
|
||||
|
||||
let app = ""
|
||||
for (let i = parts.length - 1; i >= 0; i--) { // loop over
|
||||
for (let a of apps) {
|
||||
if (parts[i].includes(a)) {
|
||||
app = a
|
||||
break
|
||||
}
|
||||
}
|
||||
if (app) break
|
||||
}
|
||||
|
||||
if (!app)
|
||||
app = parts[parts.length - 1]
|
||||
|
||||
const context = parts.find(p => p !== app)
|
||||
|
||||
return context ? `${app} · ${context}` : app
|
||||
}
|
||||
|
||||
|
||||
function formatAppId(appId) { // Random ass function to make it look good
|
||||
if (!appId || appId.length === 0)
|
||||
return "";
|
||||
|
||||
// split on dashes/underscores
|
||||
const parts = appId.split(/[-_]/);
|
||||
// capitalize each segment
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
parts[i] = p.charAt(0).toUpperCase() + p.slice(1);
|
||||
}
|
||||
return parts.join("-");
|
||||
}
|
||||
|
||||
/* Column {
|
||||
id: col
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
id: workspaceText
|
||||
font.pixelSize: Metrics.fontSize("smallie")
|
||||
text: {
|
||||
if (!activeToplevel)
|
||||
return "Desktop"
|
||||
|
||||
const id = activeToplevel.appId || ""
|
||||
|
||||
return id // Just for aesthetics
|
||||
}
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: titleText
|
||||
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title, 24) || `Workspace ${Hyprland.focusedWorkspaceId}`)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Metrics.fontSize("smalle")
|
||||
}
|
||||
} */
|
||||
|
||||
Rectangle {
|
||||
visible: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
anchors.fill: parent
|
||||
height: 34
|
||||
width: row.height + 30
|
||||
radius: ConfigResolver.bar(displayName).modules.radius
|
||||
}
|
||||
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
spacing: 12
|
||||
anchors.centerIn: parent
|
||||
|
||||
MaterialSymbol {
|
||||
icon: "desktop_windows"
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title), 24) || `Workspace ${Hyprland.focusedWorkspaceId}`
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: batteryIndicatorModuleContainer
|
||||
|
||||
visible: UPower.batteryPresent
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Determine if bar is isVertical
|
||||
property bool isVertical: ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right"
|
||||
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
color: isVertical ? Appearance.m3colors.m3primary : Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor // No need to use metrics here...
|
||||
|
||||
implicitWidth: child.implicitWidth + Appearance.margin.large - (isVertical ? 10 : 0)
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: child
|
||||
anchors.centerIn: parent
|
||||
spacing: isVertical ? 0 : Metrics.spacing(8)
|
||||
|
||||
// Icon for isVertical bars
|
||||
MaterialSymbol {
|
||||
visible: isVertical
|
||||
icon: UPower.battIcon
|
||||
iconSize: Metrics.iconSize(20)
|
||||
}
|
||||
|
||||
// Battery percentage text
|
||||
StyledText {
|
||||
animate: false
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
rotation: isVertical ? 270 : 0
|
||||
text: (isVertical ? UPower.percentage : UPower.percentage + "%")
|
||||
}
|
||||
|
||||
// Circular progress for horizontal bars
|
||||
CircularProgressBar {
|
||||
visible: !isVertical
|
||||
value: UPower.percentage / 100
|
||||
icon: UPower.battIcon
|
||||
iconSize: Metrics.iconSize(18)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
|
||||
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
|
||||
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: 37
|
||||
implicitHeight: 30
|
||||
|
||||
AnimatedImage {
|
||||
id: art
|
||||
anchors.fill: parent
|
||||
source: Directories.assetsPath + "/gifs/bongo-cat.gif"
|
||||
cache: false // this is important
|
||||
smooth: true // smooooooth
|
||||
rotation: isVertical ? 270 : 0
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
|
||||
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
|
||||
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
// Let the layout compute size automatically
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
|
||||
color: isVertical ? "transparent" : Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
|
||||
// Padding around the text
|
||||
implicitWidth: isVertical ? textItem.implicitWidth + 40 : textItem.implicitWidth + Metrics.margin("large")
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
animate: false
|
||||
text: Time.format(clockContainer.format)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Io
|
||||
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: mediaPlayer
|
||||
|
||||
property bool isVertical: (
|
||||
ConfigResolver.bar(screen?.name ?? "").position === "left" ||
|
||||
ConfigResolver.bar(screen?.name ?? "").position === "right"
|
||||
)
|
||||
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius *
|
||||
Config.runtime.appearance.rounding.factor
|
||||
|
||||
implicitWidth: isVertical
|
||||
? row.implicitWidth + Metrics.margin("large") - 10
|
||||
: row.implicitWidth + Metrics.margin("large")
|
||||
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
id: row
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.margin("small")
|
||||
|
||||
|
||||
ClippingRectangle {
|
||||
id: iconButton
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius / 1.2
|
||||
|
||||
color: Appearance.colors.colLayer1Hover
|
||||
opacity: 0.9
|
||||
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
|
||||
Image {
|
||||
id: art
|
||||
|
||||
anchors.fill: parent
|
||||
visible: Mpris.artUrl !== ""
|
||||
|
||||
source: Mpris.artUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: Mpris.artUrl === ""
|
||||
icon: "music_note"
|
||||
|
||||
iconSize: 18
|
||||
color: Config.runtime.appearance.theme === "dark"
|
||||
? "#b1a4a4"
|
||||
: "grey"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: Mpris.playPause()
|
||||
|
||||
onEntered: iconButton.opacity = 1
|
||||
onExited: iconButton.opacity = 0.9
|
||||
}
|
||||
|
||||
|
||||
RotationAnimation on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
|
||||
duration: Metrics.chronoDuration(4000)
|
||||
loops: Animation.Infinite
|
||||
|
||||
running: Mpris.isPlaying &&
|
||||
Config.runtime.appearance.animations.enabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StyledText {
|
||||
id: textItem
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
text: StringUtils.shortText(Mpris.title, 16)
|
||||
|
||||
visible: !mediaPlayer.isVertical
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: statusIconsContainer
|
||||
|
||||
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.enabled
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
StyledRect {
|
||||
id: bgRect
|
||||
|
||||
color: Globals.visiblility.sidebarRight ? Appearance.m3colors.m3paddingContainer : "transparent"
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
|
||||
implicitWidth: isVertical ? contentRow.implicitWidth + Metrics.margin("large") - 8 : contentRow.implicitWidth + Metrics.margin("large")
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: isVertical ? Metrics.spacing(8) : Metrics.spacing(16)
|
||||
|
||||
|
||||
MaterialSymbol {
|
||||
id: wifi
|
||||
animate: false
|
||||
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.networkStatusEnabled
|
||||
rotation: isVertical ? 270 : 0
|
||||
icon: Network.icon
|
||||
iconSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: btIcon
|
||||
animate: false
|
||||
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.bluetoothStatusEnabled
|
||||
rotation: isVertical ? 270 : 0
|
||||
icon: Bluetooth.icon
|
||||
iconSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (Globals.visiblility.sidebarLeft)
|
||||
return
|
||||
Globals.visiblility.sidebarRight = !Globals.visiblility.sidebarRight
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import qs.modules.components
|
||||
import qs.config
|
||||
import qs.services
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property Repeater items: items
|
||||
property bool horizontalMode: (ConfigResolver.bar(screen?.name ?? "").position === "top" || ConfigResolver.bar(screen?.name ?? "").position === "bottom")
|
||||
clip: true
|
||||
implicitWidth: layout.implicitWidth + Metrics.margin("verylarge")
|
||||
implicitHeight: 34
|
||||
|
||||
Rectangle {
|
||||
visible: (items.count > 0) ? 1 : 0
|
||||
id: padding
|
||||
implicitHeight: padding.height
|
||||
anchors.fill: parent
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: layout
|
||||
anchors.centerIn: parent
|
||||
rows: 1
|
||||
columns: items.count
|
||||
rowSpacing: Metrics.spacing(10)
|
||||
columnSpacing: Metrics.spacing(10)
|
||||
|
||||
Repeater {
|
||||
id: items
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
id: trayItemRoot
|
||||
required property SystemTrayItem modelData
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
|
||||
IconImage {
|
||||
visible: trayItemRoot.modelData.icon !== ""
|
||||
source: trayItemRoot.modelData.icon
|
||||
asynchronous: true
|
||||
anchors.fill: parent
|
||||
rotation: root.horizontalMode ? 0 : 270
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
menu: trayItemRoot.modelData.menu
|
||||
}
|
||||
|
||||
StyledPopout {
|
||||
id: popout
|
||||
hoverTarget: hover
|
||||
interactable: true
|
||||
hCenterOnItem: true
|
||||
requiresHover: false
|
||||
|
||||
Component {
|
||||
Item {
|
||||
width: childColumn.implicitWidth
|
||||
height: childColumn.height
|
||||
|
||||
ColumnLayout {
|
||||
id: childColumn
|
||||
spacing: Metrics.spacing(5)
|
||||
|
||||
Repeater {
|
||||
model: menuOpener.children
|
||||
delegate: TrayMenuItem {
|
||||
parentColumn: childColumn
|
||||
Layout.preferredWidth: childColumn.width > 0 ? childColumn.width : implicitWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (popout.isVisible)
|
||||
popout.hide();
|
||||
else
|
||||
popout.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TrayMenuItem: Item {
|
||||
id: itemRoot
|
||||
required property QsMenuEntry modelData
|
||||
required property ColumnLayout parentColumn
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: rowLayout.implicitWidth + 10
|
||||
implicitHeight: !itemRoot.modelData.isSeparator ? rowLayout.implicitHeight + 10 : 1
|
||||
|
||||
MouseArea {
|
||||
id: hover
|
||||
hoverEnabled: itemRoot.modelData.enabled
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (!itemRoot.modelData.hasChildren)
|
||||
itemRoot.modelData.triggered();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: itemBg
|
||||
anchors.fill: parent
|
||||
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
|
||||
color: itemRoot.modelData.isSeparator ? Appearance.m3colors.m3outline : hover.containsMouse ? Appearance.m3colors.m3surfaceContainer : Appearance.m3colors.m3surface
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
visible: !itemRoot.modelData.isSeparator
|
||||
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
|
||||
spacing: Metrics.spacing(5)
|
||||
anchors {
|
||||
left: itemBg.left
|
||||
leftMargin: Metrics.margin(5)
|
||||
top: itemBg.top
|
||||
topMargin:Metrics.margin(5)
|
||||
}
|
||||
|
||||
IconImage {
|
||||
visible: itemRoot.modelData.icon !== ""
|
||||
source: itemRoot.modelData.icon
|
||||
width: 15
|
||||
height: 15
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: itemRoot.modelData.text
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
visible: itemRoot.modelData.hasChildren
|
||||
icon: "chevron_right"
|
||||
iconSize: Metrics.iconSize(16)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: systemUsageContainer
|
||||
property string displayName: screen?.name ?? ""
|
||||
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
|
||||
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.enabled && haveWidth
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
property bool haveWidth:
|
||||
ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled ||
|
||||
ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled ||
|
||||
ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
|
||||
|
||||
|
||||
// Normalize values so UI always receives correct ranges
|
||||
function normalize(v) {
|
||||
if (v > 1) return v / 100
|
||||
return v
|
||||
}
|
||||
|
||||
function percent(v) {
|
||||
if (v <= 1) return Math.round(v * 100)
|
||||
return Math.round(v)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
|
||||
implicitWidth: child.implicitWidth + Metrics.margin("large")
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: child
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
// CPU
|
||||
CircularProgressBar {
|
||||
rotation: !isHorizontal ? 270 : 0
|
||||
icon: "developer_board"
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled
|
||||
iconSize: Metrics.iconSize(14)
|
||||
value: normalize(SystemDetails.cpuPercent)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled && isHorizontal
|
||||
animate: false
|
||||
text: percent(SystemDetails.cpuPercent) + "%"
|
||||
}
|
||||
|
||||
// RAM
|
||||
CircularProgressBar {
|
||||
rotation: !isHorizontal ? 270 : 0
|
||||
Layout.leftMargin: Metrics.margin(4)
|
||||
icon: "memory_alt"
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
|
||||
iconSize: Metrics.iconSize(14)
|
||||
value: normalize(SystemDetails.ramPercent)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled && isHorizontal
|
||||
animate: false
|
||||
text: percent(SystemDetails.ramPercent) + "%"
|
||||
}
|
||||
|
||||
// Temperature
|
||||
CircularProgressBar {
|
||||
rotation: !isHorizontal ? 270 : 0
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled
|
||||
Layout.leftMargin: Metrics.margin(4)
|
||||
icon: "device_thermostat"
|
||||
iconSize: Metrics.iconSize(14)
|
||||
value: normalize(SystemDetails.cpuTempPercent)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled && isHorizontal
|
||||
animate: false
|
||||
text: percent(SystemDetails.cpuTempPercent) + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: bg
|
||||
|
||||
property string icon
|
||||
property color iconColor: Appearance.syntaxHighlightingTheme
|
||||
property int iconSize
|
||||
property bool toggle
|
||||
property bool transparentBg: false
|
||||
|
||||
signal toggled(bool value)
|
||||
|
||||
color: (ma.containsMouse && !transparentBg)
|
||||
? Appearance.m3colors.m3paddingContainer
|
||||
: "transparent"
|
||||
|
||||
radius: Metrics.radius("childish")
|
||||
|
||||
implicitWidth: textItem.implicitWidth + 12
|
||||
implicitHeight: textItem.implicitHeight + 6
|
||||
|
||||
MaterialSymbol {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 0.4
|
||||
anchors.horizontalCenterOffset: 0.499
|
||||
iconSize: bg.iconSize
|
||||
icon: bg.icon
|
||||
color: bg.iconColor
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: bg.toggled(!bg.toggle)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: workspaceContainer
|
||||
property string displayName: screen?.name ?? ""
|
||||
property int numWorkspaces: ConfigResolver.bar(displayName).modules.workspaces.workspaceIndicators
|
||||
property var workspaceOccupied: []
|
||||
property var occupiedRanges: []
|
||||
|
||||
function japaneseNumber(num) {
|
||||
var kanjiMap = {
|
||||
"0": "零",
|
||||
"1": "一",
|
||||
"2": "二",
|
||||
"3": "三",
|
||||
"4": "四",
|
||||
"5": "五",
|
||||
"6": "六",
|
||||
"7": "七",
|
||||
"8": "八",
|
||||
"9": "九",
|
||||
"10": "十"
|
||||
};
|
||||
return kanjiMap[num] !== undefined ? kanjiMap[num] : "Number out of range";
|
||||
}
|
||||
|
||||
function updateWorkspaceOccupied() {
|
||||
const offset = 1;
|
||||
workspaceOccupied = Array.from({
|
||||
"length": numWorkspaces
|
||||
}, (_, i) => {
|
||||
return Compositor.isWorkspaceOccupied(i + 1);
|
||||
});
|
||||
const ranges = [];
|
||||
let start = -1;
|
||||
for (let i = 0; i < workspaceOccupied.length; i++) {
|
||||
if (workspaceOccupied[i]) {
|
||||
if (start === -1)
|
||||
start = i;
|
||||
|
||||
} else if (start !== -1) {
|
||||
ranges.push({
|
||||
"start": start,
|
||||
"end": i - 1
|
||||
});
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
if (start !== -1)
|
||||
ranges.push({
|
||||
"start": start,
|
||||
"end": workspaceOccupied.length - 1
|
||||
});
|
||||
|
||||
occupiedRanges = ranges;
|
||||
}
|
||||
|
||||
visible: ConfigResolver.bar(displayName).modules.workspaces.enabled
|
||||
implicitWidth: bg.implicitWidth
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
Component.onCompleted: updateWorkspaceOccupied()
|
||||
|
||||
Connections {
|
||||
function onStateChanged() {
|
||||
updateWorkspaceOccupied();
|
||||
}
|
||||
|
||||
target: Compositor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
implicitWidth: workspaceRow.implicitWidth + Metrics.margin("large") - 8
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
|
||||
// occupied background highlight
|
||||
Item {
|
||||
id: occupiedStretchLayer
|
||||
|
||||
anchors.centerIn: workspaceRow
|
||||
width: workspaceRow.width
|
||||
height: 26
|
||||
z: 0
|
||||
visible: Compositor.require("hyprland") // Hyprland only
|
||||
|
||||
Repeater {
|
||||
model: occupiedRanges
|
||||
|
||||
Rectangle {
|
||||
height: 26
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
color: ColorUtils.mix(Appearance.m3colors.m3tertiary, Appearance.m3colors.m3surfaceContainerLowest)
|
||||
opacity: 0.8
|
||||
x: modelData.start * (26 + workspaceRow.spacing)
|
||||
width: (modelData.end - modelData.start + 1) * 26 + (modelData.end - modelData.start) * workspaceRow.spacing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// workspace highlight
|
||||
Rectangle {
|
||||
id: highlight
|
||||
|
||||
property int offset: Compositor.require("hyprland") ? 1 : 0
|
||||
property int index: Math.max(0, Compositor.focusedWorkspaceId - 1 - offset)
|
||||
property real itemWidth: 26
|
||||
property real spacing: workspaceRow.spacing
|
||||
property int highlightIndex: {
|
||||
if (!Compositor.focusedWorkspaceId)
|
||||
return 0;
|
||||
|
||||
if (Compositor.require("hyprland"))
|
||||
return Compositor.focusedWorkspaceId - 1;
|
||||
// Hyprland starts at 2 internally
|
||||
return Compositor.focusedWorkspaceId - 2; // Niri or default
|
||||
}
|
||||
property real targetX: Math.min(highlightIndex, numWorkspaces - 1) * (itemWidth + spacing) + 7.3
|
||||
property real animatedX1: targetX
|
||||
property real animatedX2: targetX
|
||||
|
||||
x: Math.min(animatedX1, animatedX2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.abs(animatedX2 - animatedX1) + itemWidth - 1
|
||||
height: 24
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
color: Appearance.m3colors.m3tertiary
|
||||
onTargetXChanged: {
|
||||
animatedX1 = targetX;
|
||||
animatedX2 = targetX;
|
||||
}
|
||||
|
||||
Behavior on animatedX1 {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on animatedX2 {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(133)
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspaceRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
Repeater {
|
||||
model: numWorkspaces
|
||||
|
||||
Item {
|
||||
property int wsIndex: index + 1
|
||||
property bool occupied: Compositor.isWorkspaceOccupied(wsIndex)
|
||||
property bool focused: wsIndex === Compositor.focusedWorkspaceId
|
||||
|
||||
width: 26
|
||||
height: 26
|
||||
|
||||
// Icon container — only used on Hyprland
|
||||
ClippingRectangle {
|
||||
id: iconContainer
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 20
|
||||
height: 20
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.small
|
||||
clip: true
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
|
||||
anchors.fill: parent
|
||||
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && occupied
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
source: {
|
||||
const win = Compositor.focusedWindowForWorkspace(wsIndex);
|
||||
return win ? AppRegistry.iconForClass(win.class) : "";
|
||||
}
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Kanji mode — only if not Hyprland
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
|
||||
text: japaneseNumber(index + 1)
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
}
|
||||
|
||||
// Numbers mode — only if not Hyprland
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: !ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
|
||||
text: index + 1
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
}
|
||||
|
||||
// Symbols for unoccupied workspaces — only for Hyprland icons
|
||||
MaterialSymbol {
|
||||
property string displayText: Config.runtime.appearance.rounding.factor === 0 ? "crop_square" : "fiber_manual_record"
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && !occupied
|
||||
text: displayText
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
font.pixelSize: Metrics.iconSize(10)
|
||||
fill: 1
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Compositor.changeWorkspace(wsIndex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
FloatingWindow {
|
||||
id: appWin
|
||||
color: Appearance.m3colors.m3background
|
||||
property bool initialChatSelected: false
|
||||
property bool chatsInitialized: false
|
||||
|
||||
function appendMessage(sender, message) {
|
||||
messageModel.append({
|
||||
"sender": sender,
|
||||
"message": message
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function updateChatsList(files) {
|
||||
let existing = {
|
||||
};
|
||||
for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true
|
||||
for (let file of files) {
|
||||
let name = file.trim();
|
||||
if (!name.length)
|
||||
continue;
|
||||
|
||||
if (name.endsWith(".txt"))
|
||||
name = name.slice(0, -4);
|
||||
|
||||
if (!existing[name])
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
|
||||
delete existing[name];
|
||||
}
|
||||
// remove chats that no longer exist
|
||||
for (let name in existing) {
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === name) {
|
||||
chatListModel.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ensure default exists
|
||||
let hasDefault = false;
|
||||
for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") {
|
||||
hasDefault = true;
|
||||
}
|
||||
if (!hasDefault) {
|
||||
chatListModel.insert(0, {
|
||||
"name": "default"
|
||||
});
|
||||
FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt");
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
// Always scroll to end after appending
|
||||
chatView.forceLayout();
|
||||
chatView.positionViewAtEnd();
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (userInput.text === "" || Zenith.loading)
|
||||
return ;
|
||||
|
||||
Zenith.pendingInput = userInput.text;
|
||||
appendMessage("You", userInput.text);
|
||||
userInput.text = "";
|
||||
Zenith.loading = true;
|
||||
Zenith.send();
|
||||
}
|
||||
|
||||
function loadChatHistory(chatName) {
|
||||
messageModel.clear();
|
||||
Zenith.loadChat(chatName);
|
||||
}
|
||||
|
||||
function selectDefaultChat() {
|
||||
let defaultIndex = -1;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === "default") {
|
||||
defaultIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultIndex !== -1) {
|
||||
chatSelector.currentIndex = defaultIndex;
|
||||
Zenith.currentChat = "default";
|
||||
loadChatHistory("default");
|
||||
} else if (chatListModel.count > 0) {
|
||||
chatSelector.currentIndex = 0;
|
||||
Zenith.currentChat = chatListModel.get(0).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
}
|
||||
|
||||
visible: Globals.states.intelligenceWindowOpen
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
return ;
|
||||
|
||||
chatsInitialized = false;
|
||||
messageModel.clear();
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openWindow() {
|
||||
Globals.states.intelligenceWindowOpen = true;
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
Globals.states.intelligenceWindowOpen = false;
|
||||
}
|
||||
|
||||
target: "intelligence"
|
||||
}
|
||||
|
||||
ListModel {
|
||||
// { sender: "You" | "AI", message: string }
|
||||
|
||||
id: messageModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: chatListModel
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Intelligence is disabled!"
|
||||
Layout.leftMargin: Metrics.margin(24)
|
||||
font.pixelSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Go to the settings to enable intelligence"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: Config.runtime.misc.intelligence.enabled
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: chatSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: chatListModel
|
||||
textRole: "name"
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex < 0)
|
||||
return ;
|
||||
|
||||
let name = chatListModel.get(currentIndex).name;
|
||||
if (name === Zenith.currentChat)
|
||||
return ;
|
||||
|
||||
Zenith.currentChat = name;
|
||||
loadChatHistory(name);
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "add"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
let name = "new-chat-" + chatListModel.count;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.createFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
chatSelector.currentIndex = chatListModel.count - 1;
|
||||
Zenith.currentChat = name;
|
||||
messageModel.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "edit"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0
|
||||
onClicked: renameDialog.open()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "delete"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default"
|
||||
onClicked: {
|
||||
let name = chatSelector.currentText;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.removeFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.remove(chatSelector.currentIndex);
|
||||
selectDefaultChat();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: modelSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"]
|
||||
currentIndex: 0
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentTextChanged: Zenith.currentModel = currentText
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "close_fullscreen"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "closeWindow"]);
|
||||
Globals.visiblility.sidebarLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: chatView
|
||||
|
||||
model: messageModel
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(12)
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic
|
||||
|
||||
width: chatView.width
|
||||
height: bubble.implicitHeight + 6
|
||||
Component.onCompleted: {
|
||||
chatView.forceLayout();
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
Item {
|
||||
width: sender === "AI" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: bubble
|
||||
|
||||
radius: Metrics.radius("normal")
|
||||
color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh
|
||||
implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8)
|
||||
implicitHeight: textItem.implicitHeight
|
||||
anchors.right: sender === "You" ? parent.right : undefined
|
||||
anchors.left: sender === "AI" ? parent.left : undefined
|
||||
anchors.topMargin: Metrics.margin(2)
|
||||
|
||||
TextEdit {
|
||||
id: textItem
|
||||
|
||||
text: StringUtils.markdownToHtml(message)
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true // make it selectable but not editable
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
color: Appearance.syntaxHighlightingTheme
|
||||
padding: Metrics.padding(8)
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent);
|
||||
p.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
width: sender === "You" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
height: 50
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(6)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledTextField {
|
||||
// Shift+Enter → insert newline
|
||||
// Enter → send message
|
||||
|
||||
id: userInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Type your message..."
|
||||
font.pixelSize: Metrics.iconSize(14)
|
||||
padding: Metrics.spacing(8)
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (event.modifiers & Qt.ShiftModifier)
|
||||
insert("\n");
|
||||
else
|
||||
sendMessage();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Send"
|
||||
enabled: userInput.text.trim().length > 0 && !Zenith.loading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: sendMessage()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: renameDialog
|
||||
|
||||
title: "Rename Chat"
|
||||
modal: true
|
||||
visible: false
|
||||
standardButtons: Dialog.NoButton
|
||||
x: (appWin.width - 360) / 2 // center horizontally
|
||||
y: (appWin.height - 160) / 2 // center vertically
|
||||
width: 360
|
||||
height: 200
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
StyledText {
|
||||
text: "Enter a new name for the chat"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: renameInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "New name"
|
||||
filled: false
|
||||
highlight: false
|
||||
text: chatSelector.currentText
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
Layout.preferredHeight: 45
|
||||
padding: Metrics.padding(8)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
StyledButton {
|
||||
text: "Cancel"
|
||||
Layout.preferredWidth: 80
|
||||
onClicked: renameDialog.close()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Rename"
|
||||
Layout.preferredWidth: 100
|
||||
enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText
|
||||
onClicked: {
|
||||
let oldName = chatSelector.currentText;
|
||||
let newName = renameInput.text.trim();
|
||||
let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt";
|
||||
let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt";
|
||||
FileUtils.renameFile(oldPath, newPath, function(success) {
|
||||
if (success) {
|
||||
chatListModel.set(chatSelector.currentIndex, {
|
||||
"name": newName
|
||||
});
|
||||
Zenith.currentChat = newName;
|
||||
renameDialog.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
header: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Thinking…"
|
||||
visible: Zenith.loading
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
leftMargin: Metrics.margin(22)
|
||||
bottomMargin: Metrics.margin(76)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
// only auto-select once
|
||||
function onChatsListed(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
let previousChat = Zenith.currentChat;
|
||||
updateChatsList(lines);
|
||||
// select & load once
|
||||
if (!chatsInitialized) {
|
||||
chatsInitialized = true;
|
||||
let index = -1;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === previousChat) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1 && chatListModel.count > 0)
|
||||
index = 0;
|
||||
|
||||
if (index !== -1) {
|
||||
chatSelector.currentIndex = index;
|
||||
Zenith.currentChat = chatListModel.get(index).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
return ;
|
||||
}
|
||||
// AFTER init: only react if current chat vanished
|
||||
let stillExists = false;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === Zenith.currentChat) {
|
||||
stillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stillExists && chatListModel.count > 0) {
|
||||
chatSelector.currentIndex = 0;
|
||||
Zenith.currentChat = chatListModel.get(0).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
}
|
||||
|
||||
function onAiReply(text) {
|
||||
appendMessage("AI", text.slice(5));
|
||||
Zenith.loading = false;
|
||||
}
|
||||
|
||||
function onChatLoaded(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
let batch = [];
|
||||
for (let l of lines) {
|
||||
let line = l.trim();
|
||||
if (!line.length)
|
||||
continue;
|
||||
|
||||
let u = line.match(/^\[\d{4}-.*\] User: (.*)$/);
|
||||
let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/);
|
||||
if (u)
|
||||
batch.push({
|
||||
"sender": "You",
|
||||
"message": u[1]
|
||||
});
|
||||
else if (a)
|
||||
batch.push({
|
||||
"sender": "AI",
|
||||
"message": a[1]
|
||||
});
|
||||
else if (batch.length)
|
||||
batch[batch.length - 1].message += "\n" + line;
|
||||
}
|
||||
messageModel.clear();
|
||||
for (let m of batch) messageModel.append(m)
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
target: Zenith
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls
|
||||
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
property bool hovered: false
|
||||
property bool selected: false
|
||||
|
||||
required property int parentWidth
|
||||
|
||||
width: parentWidth
|
||||
height: 50
|
||||
color: {
|
||||
if (selected || hovered)
|
||||
return Appearance.m3colors.m3surfaceContainerHigh
|
||||
else
|
||||
return Appearance.m3colors.m3surface
|
||||
}
|
||||
radius: Metrics.radius(15)
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation {
|
||||
duration: Metrics.chronoDuration(200)
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
}
|
||||
|
||||
ClippingWrapperRectangle {
|
||||
id: entryIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - (size / 2)
|
||||
|
||||
property int size: 25
|
||||
height: size
|
||||
width: size
|
||||
radius: Metrics.radius(1000)
|
||||
|
||||
color: "transparent"
|
||||
|
||||
child: Image {
|
||||
source: Quickshell.iconPath(modelData.icon, "application-x-executable")
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect { // Tint if needed, ngl this looks fucking cool when you use monochrome
|
||||
saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: entryIcon.right
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - (height / 2)
|
||||
|
||||
height: 40
|
||||
spacing: Metrics.spacing(-5)
|
||||
|
||||
StyledText {
|
||||
font.weight: 400
|
||||
text: modelData.name
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: {
|
||||
if (root.hovered || root.selected)
|
||||
return Appearance.m3colors.m3onSurface
|
||||
else
|
||||
return Appearance.colors.colOutline
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation {
|
||||
duration: Metrics.chronoDuration(200)
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.weight: 400
|
||||
text: StringUtils.shortText(modelData.comment, 65) // Limit maximum chars to 65
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: {
|
||||
if (root.hovered || root.selected)
|
||||
return Qt.alpha(Appearance.m3colors.m3onSurface, 0.7)
|
||||
else
|
||||
return Qt.alpha(Appearance.colors.colOutline, 0.7)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation {
|
||||
duration: Metrics.chronoDuration(200)
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onEntered: root.hovered = true
|
||||
onExited: root.hovered = false
|
||||
onClicked: {
|
||||
modelData.execute()
|
||||
IPCLoader.toggleLauncher()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls
|
||||
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
|
||||
readonly property bool launcherOpen: Globals.visiblility.launcher
|
||||
|
||||
visible: launcherOpen
|
||||
focusable: true
|
||||
aboveWindows: true // btw I never knew this was a property (read docs)
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore // why this? idk but it works atleast
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
ScrollView {
|
||||
id: maskId
|
||||
|
||||
implicitHeight: DisplayMetrics.scaledHeight(0.623)
|
||||
implicitWidth: DisplayMetrics.scaledWidth(0.3)
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (parent.width / 2) - (implicitWidth / 2)
|
||||
anchors.topMargin: (parent.height / 2) - (implicitHeight / 2)
|
||||
|
||||
clip: true
|
||||
focus: true
|
||||
|
||||
Rectangle {
|
||||
id: launcher
|
||||
property string currentSearch: ""
|
||||
property int entryIndex: 0
|
||||
property list<DesktopEntry> appList: Apps.list
|
||||
|
||||
Connections {
|
||||
target: launcherWindow
|
||||
function onLauncherOpenChanged() {
|
||||
if (!launcherWindow.launcherOpen) {
|
||||
launcher.currentSearch = ""
|
||||
launcher.entryIndex = 0
|
||||
launcher.appList = Apps.list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius(21)
|
||||
|
||||
StyledRect {
|
||||
id: searchBox
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
width: parent.width - 20
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (parent.width / 2) - (width / 2)
|
||||
height: 45
|
||||
radius: Metrics.radius(15)
|
||||
z: 2
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onDownPressed: launcher.entryIndex += 1
|
||||
Keys.onUpPressed: {
|
||||
if (launcher.entryIndex != 0)
|
||||
launcher.entryIndex -= 1
|
||||
}
|
||||
Keys.onEscapePressed: Globals.visiblility.launcher = false
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
launcher.appList[launcher.entryIndex].execute()
|
||||
Globals.visiblility.launcher = false
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
launcher.currentSearch = launcher.currentSearch.slice(0, -1)
|
||||
} else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
|
||||
launcher.currentSearch += event.text
|
||||
}
|
||||
|
||||
launcher.appList = Apps.fuzzyQuery(launcher.currentSearch)
|
||||
launcher.entryIndex = 0
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: iconText
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
icon: "search"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.weight: 600
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - ((font.pixelSize + 5) / 2)
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeHolderText
|
||||
anchors.left: iconText.right
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
color: (launcher.currentSearch != "") ? Appearance.m3colors.m3onSurface : Appearance.colors.colOutline
|
||||
text: (launcher.currentSearch != "") ? launcher.currentSearch : "Start typing to search ..."
|
||||
font.pixelSize: Metrics.fontSize(13)
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - ((font.pixelSize + 5) / 2)
|
||||
animate: false
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.top: searchBox.bottom
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (parent.width / 2) - (width / 2)
|
||||
width: parent.width - 20
|
||||
height: parent.height - searchBox.height - 20
|
||||
|
||||
ListView {
|
||||
id: appList
|
||||
anchors.fill: parent
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors.bottomMargin: Metrics.margin(4)
|
||||
|
||||
model: launcher.appList
|
||||
currentIndex: launcher.entryIndex
|
||||
|
||||
delegate: AppItem {
|
||||
required property int index
|
||||
required property DesktopEntry modelData
|
||||
selected: index === launcher.entryIndex
|
||||
parentWidth: appList.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle() {
|
||||
Globals.visiblility.launcher = !Globals.visiblility.launcher;
|
||||
}
|
||||
|
||||
target: "launcher"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
/*
|
||||
|
||||
This LauncherContent has been depricated.
|
||||
And yet not used. (4/3/26)
|
||||
|
||||
*/
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
property int selectedIndex: -1
|
||||
property string searchQuery: ""
|
||||
property var calcVars: ({})
|
||||
|
||||
property alias listView: listView
|
||||
property alias filteredModel: filteredModel
|
||||
|
||||
function launchCurrent() {
|
||||
launchApp(listView.currentIndex)
|
||||
}
|
||||
|
||||
function webSearchUrl(query) {
|
||||
const engine = (Config.runtime.launcher.webSearchEngine || "").toLowerCase()
|
||||
if (engine.startsWith("http"))
|
||||
return engine.replace("%s", encodeURIComponent(query))
|
||||
|
||||
const engines = {
|
||||
"google": "https://www.google.com/search?q=%s",
|
||||
"duckduckgo": "https://duckduckgo.com/?q=%s",
|
||||
"brave": "https://search.brave.com/search?q=%s",
|
||||
"bing": "https://www.bing.com/search?q=%s",
|
||||
"startpage": "https://www.startpage.com/search?q=%s"
|
||||
}
|
||||
const template = engines[engine] || engines["duckduckgo"]
|
||||
return template.replace("%s", encodeURIComponent(query))
|
||||
}
|
||||
|
||||
function moveSelection(delta) {
|
||||
if (filteredModel.count === 0) return
|
||||
|
||||
selectedIndex = Math.max(0, Math.min(selectedIndex + delta, filteredModel.count - 1))
|
||||
listView.currentIndex = selectedIndex
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
function fuzzyMatch(text, pattern) {
|
||||
text = text.toLowerCase()
|
||||
pattern = pattern.toLowerCase()
|
||||
let ti = 0, pi = 0
|
||||
while (ti < text.length && pi < pattern.length) {
|
||||
if (text[ti] === pattern[pi]) pi++
|
||||
ti++
|
||||
}
|
||||
return pi === pattern.length
|
||||
}
|
||||
|
||||
function evalExpression(expr) {
|
||||
try {
|
||||
const fn = new Function("vars", `
|
||||
with (vars) { with (Math) { return (${expr}); } }
|
||||
`)
|
||||
const res = fn(calcVars)
|
||||
if (res === undefined || Number.isNaN(res)) return null
|
||||
return res
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilter() {
|
||||
filteredModel.clear()
|
||||
const query = searchQuery.toLowerCase().trim()
|
||||
|
||||
const calcVal = evalExpression(query)
|
||||
if (calcVal !== null && query !== "") {
|
||||
filteredModel.append({
|
||||
name: String(calcVal),
|
||||
displayName: String(calcVal),
|
||||
comment: "Calculation",
|
||||
icon: "",
|
||||
exec: "",
|
||||
isCalc: true,
|
||||
isWeb: false
|
||||
})
|
||||
}
|
||||
|
||||
const sourceApps = AppRegistry.apps
|
||||
|
||||
if (query === "") {
|
||||
for (let app of sourceApps) {
|
||||
filteredModel.append({
|
||||
name: app.name,
|
||||
displayName: app.name,
|
||||
comment: app.comment,
|
||||
icon: AppRegistry.iconForDesktopIcon(app.icon),
|
||||
exec: app.exec,
|
||||
isCalc: false,
|
||||
isWeb: false
|
||||
})
|
||||
}
|
||||
selectedIndex = filteredModel.count > 0 ? 0 : -1
|
||||
listView.currentIndex = selectedIndex
|
||||
return
|
||||
}
|
||||
|
||||
let exactMatches = []
|
||||
let startsWithMatches = []
|
||||
let containsMatches = []
|
||||
let fuzzyMatches = []
|
||||
|
||||
for (let app of sourceApps) {
|
||||
const name = app.name ? app.name.toLowerCase() : ""
|
||||
const comment = app.comment ? app.comment.toLowerCase() : ""
|
||||
|
||||
if (name === query) exactMatches.push(app)
|
||||
else if (name.startsWith(query)) startsWithMatches.push(app)
|
||||
else if (name.includes(query) || comment.includes(query)) containsMatches.push(app)
|
||||
else if (Config.runtime.launcher.fuzzySearchEnabled && fuzzyMatch(name, query)) fuzzyMatches.push(app)
|
||||
}
|
||||
|
||||
const sortedResults = [
|
||||
...exactMatches,
|
||||
...startsWithMatches,
|
||||
...containsMatches,
|
||||
...fuzzyMatches
|
||||
]
|
||||
|
||||
for (let app of sortedResults) {
|
||||
filteredModel.append({
|
||||
name: app.name,
|
||||
displayName: app.name,
|
||||
comment: app.comment,
|
||||
icon: AppRegistry.iconForDesktopIcon(app.icon),
|
||||
exec: app.exec,
|
||||
isCalc: false,
|
||||
isWeb: false
|
||||
})
|
||||
}
|
||||
|
||||
if (filteredModel.count === 0 && query !== "") {
|
||||
filteredModel.append({
|
||||
name: query,
|
||||
displayName: "Search the web for \"" + query + "\"",
|
||||
comment: "Web search",
|
||||
icon: "public",
|
||||
exec: webSearchUrl(query),
|
||||
isCalc: false,
|
||||
isWeb: true
|
||||
})
|
||||
}
|
||||
|
||||
selectedIndex = filteredModel.count > 0 ? 0 : -1
|
||||
listView.currentIndex = selectedIndex
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
|
||||
function launchApp(idx) {
|
||||
if (idx < 0 || idx >= filteredModel.count) return
|
||||
|
||||
const app = filteredModel.get(idx)
|
||||
if (app.isCalc) return
|
||||
if (app.isWeb)
|
||||
Quickshell.execDetached(["xdg-open", app.exec])
|
||||
else
|
||||
Quickshell.execDetached(["bash", "-c", app.exec + " &"])
|
||||
|
||||
closeLauncher()
|
||||
}
|
||||
|
||||
function closeLauncher() {
|
||||
Globals.visiblility.launcher = false
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
searchQuery = ""
|
||||
updateFilter()
|
||||
selectedIndex = -1
|
||||
listView.currentIndex = -1
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AppRegistry
|
||||
function onReady() {
|
||||
updateFilter()
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
opacity: Globals.visiblility.launcher ? 1 : 0
|
||||
anchors.margins: Metrics.margin(10)
|
||||
|
||||
ListModel { id: filteredModel }
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
model: filteredModel
|
||||
spacing: Metrics.spacing(8)
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
highlightRangeMode: ListView.StrictlyEnforceRange
|
||||
preferredHighlightBegin: 0
|
||||
preferredHighlightEnd: height
|
||||
highlightMoveDuration: 120
|
||||
currentIndex: selectedIndex
|
||||
|
||||
delegate: Rectangle {
|
||||
property bool isSelected: listView.currentIndex === index
|
||||
|
||||
width: listView.width
|
||||
height: 60
|
||||
radius: Appearance.rounding.normal
|
||||
color: isSelected ? Appearance.m3colors.m3surfaceContainerHighest : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(10)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
Item {
|
||||
width: 32
|
||||
height: 32
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: !model.isCalc && !model.isWeb
|
||||
smooth: true
|
||||
mipmap: true
|
||||
antialiasing: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
source: model.icon
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
visible: model.isCalc
|
||||
icon: "calculate"
|
||||
iconSize: Metrics.iconSize(28)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
visible: model.isWeb
|
||||
icon: "public"
|
||||
iconSize: Metrics.iconSize(28)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: listView.width - 120
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
Text {
|
||||
text: model.displayName
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.comment
|
||||
font.pixelSize: Metrics.fontSize(11)
|
||||
elide: Text.ElideRight
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: launchApp(index)
|
||||
onEntered: listView.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pam
|
||||
|
||||
// I just copied the default example and modified it. lol
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
signal unlocked()
|
||||
signal failed()
|
||||
|
||||
// These properties are in the context and not individual lock surfaces
|
||||
// so all surfaces can share the same state.
|
||||
property string currentText: ""
|
||||
property bool unlockInProgress: false
|
||||
property bool showFailure: false
|
||||
|
||||
// Clear the failure text once the user starts typing.
|
||||
onCurrentTextChanged: showFailure = false;
|
||||
|
||||
function tryUnlock() {
|
||||
if (currentText === "") return;
|
||||
|
||||
root.unlockInProgress = true;
|
||||
pam.start();
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
|
||||
// Its best to have a custom pam config for quickshell, as the system one
|
||||
// might not be what your interface expects, and break in some way.
|
||||
// This particular example only supports passwords.
|
||||
configDirectory: "pam"
|
||||
config: "password.conf"
|
||||
|
||||
// pam_unix will ask for a response for the password prompt
|
||||
onPamMessage: {
|
||||
if (this.responseRequired) {
|
||||
this.respond(root.currentText);
|
||||
}
|
||||
}
|
||||
|
||||
// pam_unix won't send any important messages so all we need is the completion status.
|
||||
onCompleted: result => {
|
||||
if (result == PamResult.Success) {
|
||||
root.unlocked();
|
||||
} else {
|
||||
root.currentText = "";
|
||||
root.showFailure = true;
|
||||
}
|
||||
|
||||
root.unlockInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
|
||||
Scope {
|
||||
// This stores all the information shared between the lock surfaces on each screen.
|
||||
LockContext {
|
||||
id: lockContext
|
||||
|
||||
onUnlocked: {
|
||||
// Unlock the screen before exiting, or the compositor will display a
|
||||
// fallback lock you can't interact with.
|
||||
lock.locked = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
|
||||
// Lock the session immediately when quickshell starts.
|
||||
locked: false
|
||||
|
||||
WlSessionLockSurface {
|
||||
LockSurface {
|
||||
anchors.fill: parent
|
||||
context: lockContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "lockscreen"
|
||||
function lock() {
|
||||
lock.locked = true;
|
||||
}
|
||||
function unlock() {
|
||||
lock.locked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import "../../components/morphedPolygons/geometry/offset.js" as Offset
|
||||
import "../../components/morphedPolygons/material-shapes.js" as MaterialShapes // For polygons
|
||||
import "../../components/morphedPolygons/shapes/corner-rounding.js" as CornerRounding
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Fusion
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.interface.background
|
||||
import qs.modules.components
|
||||
import qs.modules.components.morphedPolygons
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property LockContext context
|
||||
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
source: Config.runtime.appearance.background.path
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(20)
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
topMargin: Metrics.spacing(20)
|
||||
rightMargin: Metrics.spacing(30)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: themeIcon
|
||||
|
||||
fill: 1
|
||||
icon: Config.runtime.appearance.theme === "light" ? "light_mode" : "dark_mode"
|
||||
iconSize: Metrics.fontSize("hugeass")
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: wifi
|
||||
|
||||
icon: Network.icon
|
||||
iconSize: Metrics.fontSize("hugeass")
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: btIcon
|
||||
|
||||
icon: Bluetooth.icon
|
||||
iconSize: Metrics.fontSize("hugeass")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: keyboardLayoutIcon
|
||||
|
||||
text: SystemDetails.keyboardLayout
|
||||
font.pixelSize: Metrics.fontSize(Appearance.font.size.huge - 4)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
topMargin: Metrics.margin(150)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: clock
|
||||
|
||||
visible: !Config.runtime.appearance.background.clock.isAnalog
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
animate: false
|
||||
renderType: Text.NativeRendering
|
||||
font.pixelSize: Metrics.fontSize(180)
|
||||
text: Time.format("hh:mm")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: date
|
||||
|
||||
visible: !Config.runtime.appearance.background.clock.isAnalog
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
animate: false
|
||||
renderType: Text.NativeRendering
|
||||
font.pixelSize: Metrics.fontSize(50)
|
||||
text: Time.format("dddd, dd/MM")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: analogClockContainer
|
||||
|
||||
property int hours: parseInt(Time.format("hh"))
|
||||
property int minutes: parseInt(Time.format("mm"))
|
||||
property int seconds: parseInt(Time.format("ss"))
|
||||
readonly property real cx: width / 2
|
||||
readonly property real cy: height / 2
|
||||
property var shapes: [MaterialShapes.getCookie7Sided, MaterialShapes.getCookie9Sided, MaterialShapes.getCookie12Sided, MaterialShapes.getPixelCircle, MaterialShapes.getCircle, MaterialShapes.getGhostish]
|
||||
|
||||
visible: Config.runtime.appearance.background.clock.isAnalog
|
||||
width: 350
|
||||
height: 350
|
||||
|
||||
// Polygon
|
||||
MorphedPolygon {
|
||||
id: shapeCanvas
|
||||
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3secondaryContainer
|
||||
roundedPolygon: analogClockContainer.shapes[Config.runtime.appearance.background.clock.shape]()
|
||||
}
|
||||
|
||||
ClockDial {
|
||||
anchors.fill: parent
|
||||
anchors.margins: parent.width * 0.12
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
z: 0
|
||||
}
|
||||
|
||||
// Hour hand
|
||||
StyledRect {
|
||||
z: 2
|
||||
width: 10
|
||||
height: parent.height * 0.3
|
||||
radius: Metrics.radius("full")
|
||||
color: Qt.darker(Appearance.m3colors.m3secondary, 0.8)
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: (analogClockContainer.hours % 12 + analogClockContainer.minutes / 60) * 30
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: 16
|
||||
height: 16
|
||||
radius: width / 2
|
||||
color: Appearance.m3colors.m3secondary
|
||||
z: 99 // Ensures its on top of everthing
|
||||
|
||||
// Inner dot
|
||||
StyledRect {
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
radius: width / 2
|
||||
anchors.centerIn: parent
|
||||
z: 100
|
||||
color: Appearance.m3colors.m3primaryContainer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Minute hand
|
||||
StyledRect {
|
||||
width: 14
|
||||
height: parent.height * 0.35
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3secondary
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.minutes * 6
|
||||
z: 10 // On top of all hands
|
||||
}
|
||||
|
||||
// Second hand
|
||||
StyledRect {
|
||||
visible: true
|
||||
width: 4
|
||||
height: parent.height * 0.28
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3error
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.seconds * 6
|
||||
z: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("hh")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(60)
|
||||
font.pixelSize: Metrics.fontSize(100)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("mm")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(150)
|
||||
font.pixelSize: Metrics.fontSize(100)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
// Commenting this will make the password entry visible on all monitors.
|
||||
visible: Window.active
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: Metrics.margin(20)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
StyledTextField {
|
||||
id: passwordBox
|
||||
|
||||
implicitWidth: 300
|
||||
padding: Metrics.padding(10)
|
||||
placeholder: root.context.showFailure ? "Incorrect Password" : "Enter Password"
|
||||
focus: true
|
||||
enabled: !root.context.unlockInProgress
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
// Update the text in the context when the text in the box changes.
|
||||
onTextChanged: root.context.currentText = this.text
|
||||
// Try to unlock when enter is pressed.
|
||||
onAccepted: root.context.tryUnlock()
|
||||
|
||||
// Update the text in the box to match the text in the context.
|
||||
// This makes sure multiple monitors have the same text.
|
||||
Connections {
|
||||
function onCurrentTextChanged() {
|
||||
passwordBox.text = root.context.currentText;
|
||||
}
|
||||
|
||||
target: root.context
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "chevron_right"
|
||||
padding: Metrics.padding(10)
|
||||
radius: Metrics.radius("unsharpenmore")
|
||||
// don't steal focus from the text box
|
||||
focusPolicy: Qt.NoFocus
|
||||
enabled: !root.context.unlockInProgress && root.context.currentText !== ""
|
||||
onClicked: root.context.tryUnlock()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
auth required pam_unix.so
|
||||
@@ -0,0 +1,144 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool startAnim: false
|
||||
property string title: "No Title"
|
||||
property string body: "No content"
|
||||
property var rawNotif: null
|
||||
property bool tracked: false
|
||||
property string image: ""
|
||||
property var buttons: [
|
||||
{ label: "Okay!", onClick: () => console.log("Okay") }
|
||||
]
|
||||
|
||||
opacity: tracked ? 1 : (startAnim ? 1 : 0)
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
radius: Metrics.radius("large")
|
||||
|
||||
property bool hovered: mouseHandler.containsMouse
|
||||
property bool clicked: mouseHandler.containsPress
|
||||
color: hovered ? (clicked ? Appearance.m3colors.m3surfaceContainerHigh : Appearance.m3colors.m3surfaceContainerLow) : Appearance.m3colors.m3surface
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
}
|
||||
implicitHeight: Math.max(content.implicitHeight + 30, 80)
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(10)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
ClippingRectangle {
|
||||
width: 50
|
||||
height: 50
|
||||
radius: Metrics.radius("large")
|
||||
clip: true
|
||||
color: root.image === "" ? Appearance.m3colors.m3surfaceContainer : "transparent"
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.image
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
}
|
||||
MaterialSymbol {
|
||||
icon: "chat"
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
anchors.centerIn: parent
|
||||
visible: root.image === ""
|
||||
iconSize: Metrics.iconSize(22)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.bold: true
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
wrapMode: Text.Wrap
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.body.length > 123 ? root.body.substr(0, 120) + "..." : root.body
|
||||
visible: root.body.length > 0
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.buttons.length > 1
|
||||
Layout.preferredHeight: 40
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
Repeater {
|
||||
model: buttons
|
||||
|
||||
StyledButton {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
implicitWidth: 0
|
||||
text: modelData.label
|
||||
base_bg: index !== 0
|
||||
? Appearance.m3colors.m3secondaryContainer
|
||||
: Appearance.m3colors.m3primary
|
||||
|
||||
base_fg: index !== 0
|
||||
? Appearance.m3colors.m3onSecondaryContainer
|
||||
: Appearance.m3colors.m3onPrimary
|
||||
onClicked: modelData.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: mouseHandler
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
visible: root.buttons.length === 0 || root.buttons.length === 1
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.buttons.length === 1 && root.buttons[0].onClick) {
|
||||
root.buttons[0].onClick()
|
||||
root.rawNotif?.notification.dismiss()
|
||||
} else if (root.buttons.length === 0) {
|
||||
console.log("[Notification] Dismissed a notification with no action.")
|
||||
root.rawNotif.notification.tracked = false
|
||||
root.rawNotif.popup = false
|
||||
root.rawNotif?.notification.dismiss()
|
||||
} else {
|
||||
console.log("[Notification] Dismissed a notification with multiple actions.")
|
||||
root.rawNotif?.notification.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
startAnim = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property int innerSpacing: Metrics.spacing(10)
|
||||
|
||||
PanelWindow {
|
||||
id: window
|
||||
|
||||
implicitWidth: 520
|
||||
visible: true
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Normal
|
||||
WlrLayershell.namespace: "nucleus:notification"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: Config.runtime.notifications.position.endsWith("left")
|
||||
bottom: true
|
||||
right: Config.runtime.notifications.position.endsWith("right")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: notificationList
|
||||
|
||||
anchors.leftMargin: 0
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
anchors.rightMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Rectangle {
|
||||
id: bgRectangle
|
||||
|
||||
layer.enabled: true
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(20)
|
||||
anchors.rightMargin: Metrics.margin(20)
|
||||
anchors.right: parent.right
|
||||
height: window.mask.height > 0 ? window.mask.height + 40 : 0
|
||||
color: Appearance.m3colors.m3background
|
||||
radius: Metrics.radius("large")
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowOpacity: 1
|
||||
shadowColor: Appearance.m3colors.m3shadow
|
||||
shadowBlur: 1
|
||||
shadowScale: 1
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: notificationColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Repeater {
|
||||
id: rep
|
||||
|
||||
model: (!Config.runtime.notifications.doNotDisturb && Config.runtime.notifications.enabled) ? NotifServer.popups : []
|
||||
|
||||
NotificationChild {
|
||||
id: child
|
||||
|
||||
width: notificationColumn.width - 80
|
||||
anchors.horizontalCenter: notificationColumn.horizontalCenter
|
||||
y: {
|
||||
var pos = 0;
|
||||
for (let i = 0; i < index; i++) {
|
||||
var prev = rep.itemAt(i);
|
||||
if (prev)
|
||||
pos += prev.height + root.innerSpacing;
|
||||
|
||||
}
|
||||
return pos + 20;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (!modelData.shown)
|
||||
modelData.shown = true;
|
||||
|
||||
}
|
||||
title: modelData.summary
|
||||
body: modelData.body
|
||||
image: modelData.image || modelData.appIcon
|
||||
rawNotif: modelData
|
||||
tracked: modelData.shown
|
||||
buttons: modelData.actions.map((action) => {
|
||||
return ({
|
||||
"label": action.text,
|
||||
"onClick": () => {
|
||||
return action.invoke();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
Behavior on y {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
width: window.width
|
||||
height: {
|
||||
var total = 0;
|
||||
for (let i = 0; i < rep.count; i++) {
|
||||
var child = rep.itemAt(i);
|
||||
if (child)
|
||||
total += child.height + (i < rep.count - 1 ? root.innerSpacing : 0);
|
||||
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Wayland
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Connections {
|
||||
target: Brightness
|
||||
|
||||
function onBrightnessChanged() {
|
||||
root.shouldShowOsd = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
property var monitor: Brightness.monitors.length > 0 ? Brightness.monitors[0] : null
|
||||
|
||||
property bool shouldShowOsd: false
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 3000
|
||||
onTriggered: root.shouldShowOsd = false
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.shouldShowOsd
|
||||
|
||||
PanelWindow {
|
||||
visible: Config.runtime.overlays.brightnessOverlayEnabled && Config.runtime.overlays.enabled
|
||||
WlrLayershell.namespace: "nucleus:brightnessOsd"
|
||||
exclusiveZone: 0
|
||||
anchors.top: Config.runtime.overlays.brightnessOverlayPosition.startsWith("top")
|
||||
anchors.bottom: Config.runtime.overlays.brightnessOverlayPosition.startsWith("bottom")
|
||||
anchors.right: Config.runtime.overlays.brightnessOverlayPosition.endsWith("right")
|
||||
anchors.left: Config.runtime.overlays.brightnessOverlayPosition.endsWith("left")
|
||||
margins {
|
||||
top: Metrics.margin(10)
|
||||
bottom: Metrics.margin(10)
|
||||
left: Metrics.margin(10)
|
||||
right: Metrics.margin(10)
|
||||
}
|
||||
|
||||
implicitWidth: 460
|
||||
implicitHeight: 105
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.childish
|
||||
color: Appearance.m3colors.m3background
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: Metrics.margin(15)
|
||||
rightMargin: Metrics.margin(25)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
property real brightnessLevel: Math.floor(Brightness.getMonitorForScreen(Hyprland.focusedMonitor)?.multipliedBrightness*100)
|
||||
icon: {
|
||||
if (brightnessLevel > 66) return "brightness_high"
|
||||
else if (brightnessLevel > 33) return "brightness_medium"
|
||||
else return "brightness_low"
|
||||
}
|
||||
iconSize: Metrics.iconSize(30)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
implicitHeight: 40
|
||||
spacing: Metrics.spacing(5)
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: "Brightness - " + Math.round(monitor.brightness * 100) + '%'
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
implicitHeight: 35
|
||||
from: 0
|
||||
to: 100
|
||||
value: Math.round(monitor.brightness * 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
VolumeOverlay{}
|
||||
BrightnessOverlay{}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [ Pipewire.defaultAudioSink ]
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Pipewire.defaultAudioSink?.audio ?? null
|
||||
|
||||
function onVolumeChanged() {
|
||||
root.shouldShowOsd = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root.shouldShowOsd = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property bool shouldShowOsd: false
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 3000
|
||||
onTriggered: root.shouldShowOsd = false
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.shouldShowOsd
|
||||
|
||||
PanelWindow {
|
||||
visible: Config.runtime.overlays.volumeOverlayEnabled && Config.runtime.overlays.enabled
|
||||
WlrLayershell.namespace: "nucleus:brightnessOsd"
|
||||
exclusiveZone: 0
|
||||
anchors.top: Config.runtime.overlays.volumeOverlayPosition.startsWith("top")
|
||||
anchors.bottom: Config.runtime.overlays.volumeOverlayPosition.startsWith("bottom")
|
||||
anchors.right: Config.runtime.overlays.volumeOverlayPosition.endsWith("right")
|
||||
anchors.left: Config.runtime.overlays.volumeOverlayPosition.endsWith("left")
|
||||
margins {
|
||||
top: Metrics.margin(10)
|
||||
bottom: Metrics.margin(10)
|
||||
left: Metrics.margin(10)
|
||||
right: Metrics.margin(10)
|
||||
}
|
||||
implicitWidth: 460
|
||||
implicitHeight: 105
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {}
|
||||
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.childish
|
||||
color: Appearance.m3colors.m3background
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: Metrics.margin(15)
|
||||
rightMargin: Metrics.margin(25)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
property real volume: Pipewire.defaultAudioSink?.audio.muted ? 0 : Pipewire.defaultAudioSink?.audio.volume * 100
|
||||
icon: volume > 50 ? "volume_up" : volume > 0 ? "volume_down" : 'volume_off'
|
||||
iconSize: Metrics.iconSize(34);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
|
||||
animate: false
|
||||
text: (Pipewire.defaultAudioSink?.description ?? "Unknown") + " - " +
|
||||
(Pipewire.defaultAudioSink?.audio.muted ? 'Muted' : Math.floor(Pipewire.defaultAudioSink?.audio.volume * 100) + '%')
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 35
|
||||
value: (Pipewire.defaultAudioSink?.audio.muted ? 0 : Pipewire.defaultAudioSink?.audio.volume) * 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
property bool active: false
|
||||
property var window: null
|
||||
|
||||
Connections {
|
||||
target: Polkit
|
||||
function onIsActiveChanged() {
|
||||
if (Polkit.isActive) {
|
||||
root.active = true;
|
||||
} else if (root.active && window) {
|
||||
window.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.active
|
||||
component: Prompt {
|
||||
id: window
|
||||
|
||||
Component.onCompleted: root.window = window
|
||||
Component.onDestruction: root.window = null
|
||||
|
||||
onFadeOutFinished: root.active = false
|
||||
|
||||
Item {
|
||||
id: promptContainer
|
||||
property bool showPassword: false
|
||||
property bool authenticating: false
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: promptBg.width
|
||||
height: promptBg.height
|
||||
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
parent.layer.enabled = true;
|
||||
parent.layer.effect = effectComponent;
|
||||
}
|
||||
|
||||
Component {
|
||||
id: effectComponent
|
||||
MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowOpacity: 1
|
||||
shadowColor: Appearance.colors.m3shadow
|
||||
shadowBlur: 1
|
||||
shadowScale: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: promptBg
|
||||
width: promptLayout.width + 40
|
||||
height: promptLayout.height + 40
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius(20)
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: promptLayout
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors {
|
||||
left: promptBg.left
|
||||
leftMargin: Metrics.margin(20)
|
||||
top: promptBg.top
|
||||
topMargin: Metrics.margin(20)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(5)
|
||||
MaterialSymbol {
|
||||
icon: "security"
|
||||
color: Appearance.m3colors.m3primary
|
||||
font.pixelSize: Metrics.fontSize(22)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
text: "Authentication required"
|
||||
font.family: "Outfit SemiBold"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
text: Polkit.flow.message
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(5)
|
||||
StyledTextField {
|
||||
id: textfield
|
||||
Layout.fillWidth: true
|
||||
leftPadding: undefined
|
||||
padding: Metrics.padding(10)
|
||||
filled: false
|
||||
enabled: !promptContainer.authenticating
|
||||
placeholder: Polkit.flow.inputPrompt.substring(0, Polkit.flow.inputPrompt.length - 2)
|
||||
echoMode: promptContainer.showPassword ? TextInput.Normal : TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
focus: true
|
||||
Keys.onReturnPressed: okButton.clicked()
|
||||
}
|
||||
StyledButton {
|
||||
Layout.fillHeight: true
|
||||
width: height
|
||||
radius: Metrics.radius(10)
|
||||
topLeftRadius: Metrics.radius(5)
|
||||
bottomLeftRadius: Metrics.radius(5)
|
||||
enabled: !promptContainer.authenticating
|
||||
checkable: true
|
||||
checked: promptContainer.showPassword
|
||||
icon: promptContainer.showPassword ? 'visibility' : 'visibility_off'
|
||||
onToggled: promptContainer.showPassword = !promptContainer.showPassword
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RowLayout {
|
||||
RowLayout {
|
||||
visible: Polkit.flow.failed && !Polkit.flow.isSuccessful && !promptContainer.authenticating
|
||||
MaterialSymbol {
|
||||
icon: "warning"
|
||||
color: Appearance.m3colors.m3error
|
||||
font.pixelSize: Metrics.fontSize(15)
|
||||
}
|
||||
StyledText {
|
||||
text: "Failed to authenticate, incorrect password."
|
||||
color: Appearance.m3colors.m3error
|
||||
font.pixelSize: Metrics.fontSize(15)
|
||||
}
|
||||
}
|
||||
LoadingIcon {
|
||||
visible: promptContainer.authenticating
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
StyledButton {
|
||||
radius: Metrics.radius(10)
|
||||
topRightRadius: Metrics.radius(5)
|
||||
bottomRightRadius: Metrics.radius(5)
|
||||
secondary: true
|
||||
text: "Cancel"
|
||||
// enabled: !promptContainer.authenticating (Allows to cancel if stuck in loop)
|
||||
onClicked: Polkit.flow.cancelAuthenticationRequest()
|
||||
}
|
||||
StyledButton {
|
||||
id: okButton
|
||||
radius: Metrics.radius(10)
|
||||
topLeftRadius: Metrics.radius(5)
|
||||
bottomLeftRadius: Metrics.radius(5)
|
||||
text: promptContainer.authenticating ? "Authenticating..." : "OK"
|
||||
enabled: !promptContainer.authenticating
|
||||
onClicked: {
|
||||
promptContainer.authenticating = true;
|
||||
Polkit.flow.submit(textfield.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Polkit.flow
|
||||
function onIsCompletedChanged() {
|
||||
if (Polkit.flow.isCompleted) {
|
||||
promptContainer.authenticating = false;
|
||||
}
|
||||
}
|
||||
function onFailedChanged() {
|
||||
if (Polkit.flow.failed) {
|
||||
promptContainer.authenticating = false;
|
||||
}
|
||||
}
|
||||
function onIsCancelledChanged() {
|
||||
if (Polkit.flow.isCancelled) {
|
||||
promptContainer.authenticating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
PanelWindow {
|
||||
id: window
|
||||
property bool isClosing: false
|
||||
default property alias content: contentContainer.data
|
||||
signal fadeOutFinished()
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "nucleus:prompt"
|
||||
|
||||
function closeWithAnimation() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
fadeOutAnim.start()
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
window.closeWithAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: screencopy
|
||||
visible: hasContent
|
||||
captureSource: window.screen
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
scale: 1
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 1
|
||||
blurMax: 32
|
||||
brightness: -0.05
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: true
|
||||
blur: 1
|
||||
blurMax: 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: fadeInAnim
|
||||
target: screencopy
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
running: screencopy.visible && !window.isClosing
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: scaleInAnim
|
||||
running: screencopy.visible && !window.isClosing
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "scale"
|
||||
from: 0.9
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
ColorAnimation {
|
||||
target: window
|
||||
property: "color"
|
||||
from: "transparent"
|
||||
to: Appearance.m3colors.m3surface
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: fadeOutAnim
|
||||
NumberAnimation {
|
||||
target: screencopy
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
ColorAnimation {
|
||||
target: window
|
||||
property: "color"
|
||||
to: "transparent"
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "scale"
|
||||
to: 0.9
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
onFinished: {
|
||||
window.visible = false
|
||||
window.fadeOutFinished()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
scale: 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import qs.modules.interface.lockscreen
|
||||
import qs.modules.components
|
||||
|
||||
PanelWindow {
|
||||
id: powermenu
|
||||
|
||||
WlrLayershell.keyboardFocus: Compositor.require("hyprland") && Globals.visiblility.powermenu
|
||||
|
||||
function togglepowermenu() {
|
||||
Globals.visiblility.powermenu = !Globals.visiblility.powermenu; // Simple toggle logic kept in a function as it might have more things to it later on.
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "nucleus:powermenu"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
visible: Config.initialized && Globals.visiblility.powermenu
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
implicitWidth: DisplayMetrics.scaledWidth(0.25)
|
||||
implicitHeight: DisplayMetrics.scaledWidth(0.168)
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
|
||||
active: Compositor.require("hyprland")
|
||||
windows: [powermenu]
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: container
|
||||
|
||||
color: Appearance.m3colors.m3background
|
||||
radius: Metrics.radius("verylarge")
|
||||
implicitWidth: powermenu.implicitWidth
|
||||
anchors.fill: parent
|
||||
|
||||
FocusScope {
|
||||
focus: true
|
||||
anchors.fill: parent
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
Globals.visiblility.powermenu = false;
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
anchors.margins: Metrics.radius(12)
|
||||
anchors.topMargin: Metrics.radius(16)
|
||||
anchors.leftMargin: Metrics.radius(18)
|
||||
anchors.fill: parent
|
||||
|
||||
Grid {
|
||||
columns: 3
|
||||
rows: 3
|
||||
rowSpacing: Metrics.spacing(10)
|
||||
columnSpacing: Metrics.spacing(10)
|
||||
anchors.fill: parent
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "power_settings_new"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["poweroff"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "logout"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["hyprctl", "dispatch", "exit"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "sleep"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["systemctl", "suspend"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "lock"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "lockscreen", "lock"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "restart_alt"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["reboot"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "light_off"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["systemctl", "hibernate"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
running: Config.runtime.appearance.animations.enabled
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle() {
|
||||
togglepowermenu();
|
||||
}
|
||||
|
||||
target: "powermenu"
|
||||
}
|
||||
|
||||
component PowerMenuButton: StyledButton {
|
||||
property string buttonIcon
|
||||
|
||||
icon: buttonIcon
|
||||
iconSize: Metrics.iconSize(50)
|
||||
width: powermenu.implicitWidth / 3.4
|
||||
height: powermenu.implicitHeight / 2.3
|
||||
radius: beingHovered ? Metrics.radius("verylarge") * 2 : Metrics.radius("large")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
property bool active: false
|
||||
property rect selectedRegion: Qt.rect(0, 0, 0, 0)
|
||||
property string tempScreenshot: ""
|
||||
|
||||
IpcHandler {
|
||||
target: "screen"
|
||||
function capture() {
|
||||
if (root.active) {
|
||||
console.info("screencap", "already active");
|
||||
return;
|
||||
}
|
||||
console.info("screencap", "starting capture");
|
||||
root.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.active
|
||||
component: PanelWindow {
|
||||
id: win
|
||||
property bool closing: false
|
||||
property bool ready: false
|
||||
property bool processing: false
|
||||
property bool windowMode: false
|
||||
property string savedPath: ""
|
||||
property bool savedSuccess: false
|
||||
|
||||
color: Appearance.m3colors.m3surface
|
||||
anchors { top: true; left: true; right: true; bottom: true }
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "nucleus:screencapture"
|
||||
|
||||
Component.onCompleted: {
|
||||
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
||||
root.tempScreenshot = "/tmp/screenshot_" + ts + ".png";
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (closing) return;
|
||||
closing = true;
|
||||
closeAnim.start();
|
||||
}
|
||||
|
||||
function saveFullscreen() {
|
||||
console.info("screencap", "saveFullscreen started");
|
||||
win.processing = true;
|
||||
screencopy.grabToImage(function(result) {
|
||||
console.info("screencap", "fullscreen grabbed");
|
||||
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
||||
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + ".png";
|
||||
|
||||
console.info("screencap", "saving to: " + win.savedPath);
|
||||
if (result.saveToFile(win.savedPath)) {
|
||||
console.info("screencap", "saved, copying");
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", "cat '" + win.savedPath + "' | wl-copy --type image/png"]
|
||||
});
|
||||
win.savedSuccess = true;
|
||||
} else {
|
||||
console.info("screencap", "save failed");
|
||||
win.savedSuccess = false;
|
||||
}
|
||||
win.processing = false;
|
||||
console.info("screencap", "closing window");
|
||||
win.close();
|
||||
});
|
||||
}
|
||||
|
||||
Component {
|
||||
id: ffmpegProc
|
||||
Process {
|
||||
property string outputPath
|
||||
property bool success: false
|
||||
|
||||
onExited: (code) => {
|
||||
console.info("screencap", "ffmpeg exited: " + code);
|
||||
success = code === 0;
|
||||
|
||||
if (success) {
|
||||
console.info("screencap", "copying to clipboard");
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", "cat '" + outputPath + "' | wl-copy --type image/png"]
|
||||
});
|
||||
}
|
||||
|
||||
Quickshell.execDetached({ command: ["rm", root.tempScreenshot] });
|
||||
|
||||
win.savedSuccess = success;
|
||||
win.processing = false;
|
||||
console.info("screencap", "done, closing");
|
||||
win.close();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveRegion(rect, suffix) {
|
||||
console.info("screencap", "saveRegion started: " + rect.x + "," + rect.y + " " + rect.width + "x" + rect.height);
|
||||
screencopy.grabToImage(function(result) {
|
||||
console.info("screencap", "full screenshot grabbed for cropping");
|
||||
if (!result.saveToFile(root.tempScreenshot)) {
|
||||
console.info("screencap", "ERROR: failed to save temp screenshot");
|
||||
win.savedSuccess = false;
|
||||
win.processing = false;
|
||||
win.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("screencap", "temp saved, cropping with ffmpeg");
|
||||
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
||||
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + suffix + ".png";
|
||||
|
||||
ffmpegProc.createObject(win, {
|
||||
command: ["ffmpeg", "-i", root.tempScreenshot, "-vf", "crop=" + Math.floor(rect.width) + ":" + Math.floor(rect.height) + ":" + Math.floor(rect.x) + ":" + Math.floor(rect.y), "-y", win.savedPath],
|
||||
outputPath: win.savedPath,
|
||||
running: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function captureFullscreen() {
|
||||
win.processing = true;
|
||||
saveFullscreen();
|
||||
}
|
||||
|
||||
function captureWindow(rect) {
|
||||
win.processing = true;
|
||||
saveRegion(rect, "_window");
|
||||
}
|
||||
|
||||
function captureRegion() {
|
||||
if (!ready || !selection.hasSelection) return;
|
||||
win.processing = true;
|
||||
saveRegion(root.selectedRegion, "_region");
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: screencopy
|
||||
anchors.fill: parent
|
||||
captureSource: win.screen
|
||||
z: -999
|
||||
live: false
|
||||
|
||||
onHasContentChanged: {
|
||||
console.info("screencap", "hasContent: " + hasContent);
|
||||
if (hasContent) {
|
||||
console.info("screencap", "grabbing for preview");
|
||||
grabToImage(function(result) {
|
||||
console.info("screencap", "preview grabbed: " + result.url);
|
||||
frozen.source = result.url;
|
||||
readyTimer.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: readyTimer
|
||||
interval: Metrics.chronoDuration("normal") + 50
|
||||
onTriggered: {
|
||||
console.info("screencap", "UI ready");
|
||||
win.ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: win.close()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_F) {
|
||||
win.captureFullscreen();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_W) {
|
||||
win.windowMode = !win.windowMode;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
source: Config.runtime.appearance.background.path
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: 0
|
||||
scale: 1
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 1.0
|
||||
blurMax: 64
|
||||
brightness: -0.1
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready) fadeIn.start();
|
||||
}
|
||||
|
||||
NumberAnimation on opacity {
|
||||
id: fadeIn
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: container
|
||||
anchors.centerIn: parent
|
||||
width: win.width
|
||||
height: win.height
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowOpacity: 1
|
||||
shadowColor: Appearance.m3colors.m3shadow
|
||||
}
|
||||
|
||||
Image {
|
||||
id: frozen
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
cache: false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: darkOverlay
|
||||
anchors.fill: parent
|
||||
visible: (selection.hasSelection || selection.selecting) && !win.windowMode
|
||||
|
||||
Rectangle {
|
||||
y: 0
|
||||
width: parent.width
|
||||
height: selection.sy
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
Rectangle {
|
||||
y: selection.sy + selection.h
|
||||
width: parent.width
|
||||
height: parent.height - (selection.sy + selection.h)
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
Rectangle {
|
||||
x: 0
|
||||
y: selection.sy
|
||||
width: selection.sx
|
||||
height: selection.h
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
Rectangle {
|
||||
x: selection.sx + selection.w
|
||||
y: selection.sy
|
||||
width: parent.width - (selection.sx + selection.w)
|
||||
height: selection.h
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: selection.sx
|
||||
y: selection.sy
|
||||
width: selection.w
|
||||
height: selection.h
|
||||
color: "black"
|
||||
opacity: win.processing ? 0.6 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
LoadingIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: win.processing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: outline
|
||||
x: selection.sx
|
||||
y: selection.sy
|
||||
width: selection.w
|
||||
height: selection.h
|
||||
color: "transparent"
|
||||
border.color: Appearance.m3colors.m3primary
|
||||
border.width: 2
|
||||
visible: (selection.selecting || selection.hasSelection) && !win.windowMode
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: selection.selecting
|
||||
anchors.top: outline.bottom
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
anchors.horizontalCenter: outline.horizontalCenter
|
||||
width: coords.width + 10
|
||||
height: coords.height + 10
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius(20)
|
||||
|
||||
StyledText {
|
||||
id: coords
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
animate: false
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
property real scaleX: container.width / win.width
|
||||
property real scaleY: container.height / win.height
|
||||
text: Math.floor(selection.sx/scaleX) + "," + Math.floor(selection.sy/scaleY) + " " + Math.floor(selection.w/scaleX) + "x" + Math.floor(selection.h/scaleY)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: selection
|
||||
anchors.fill: parent
|
||||
enabled: win.ready && !win.windowMode
|
||||
|
||||
property real x1: 0
|
||||
property real y1: 0
|
||||
property real x2: 0
|
||||
property real y2: 0
|
||||
property bool selecting: false
|
||||
property bool hasSelection: false
|
||||
|
||||
property real xp: 0
|
||||
property real yp: 0
|
||||
property real wp: 0
|
||||
property real hp: 0
|
||||
|
||||
property real sx: xp * parent.width
|
||||
property real sy: yp * parent.height
|
||||
property real w: wp * parent.width
|
||||
property real h: hp * parent.height
|
||||
|
||||
onPressed: mouse => {
|
||||
if (!win.ready) return;
|
||||
x1 = Math.max(0, Math.min(mouse.x, width));
|
||||
y1 = Math.max(0, Math.min(mouse.y, height));
|
||||
x2 = x1;
|
||||
y2 = y1;
|
||||
selecting = true;
|
||||
hasSelection = false;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (selecting) {
|
||||
x2 = Math.max(0, Math.min(mouse.x, width));
|
||||
y2 = Math.max(0, Math.min(mouse.y, height));
|
||||
xp = Math.min(x1, x2) / width;
|
||||
yp = Math.min(y1, y2) / height;
|
||||
wp = Math.abs(x2 - x1) / width;
|
||||
hp = Math.abs(y2 - y1) / height;
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
if (!selecting) return;
|
||||
|
||||
x2 = Math.max(0, Math.min(mouse.x, width));
|
||||
y2 = Math.max(0, Math.min(mouse.y, height));
|
||||
selecting = false;
|
||||
|
||||
hasSelection = Math.abs(x2 - x1) > 5 && Math.abs(y2 - y1) > 5;
|
||||
|
||||
if (hasSelection) {
|
||||
xp = Math.min(x1, x2) / width;
|
||||
yp = Math.min(y1, y2) / height;
|
||||
wp = Math.abs(x2 - x1) / width;
|
||||
hp = Math.abs(y2 - y1) / height;
|
||||
|
||||
root.selectedRegion = Qt.rect(
|
||||
Math.min(x1, x2) * win.screen.width / width,
|
||||
Math.min(y1, y2) * win.screen.height / height,
|
||||
Math.abs(x2 - x1) * win.screen.width / width,
|
||||
Math.abs(y2 - y1) * win.screen.height / height
|
||||
);
|
||||
|
||||
win.captureRegion();
|
||||
} else {
|
||||
win.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!win.windowMode || !win.ready) return [];
|
||||
var ws = Hyprland.focusedMonitor?.activeWorkspace;
|
||||
return ws?.toplevels ? ws.toplevels.values : [];
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
property var w: modelData?.lastIpcObject
|
||||
visible: w?.at && w?.size
|
||||
|
||||
property real barX: 0
|
||||
property real barY: 0
|
||||
property real sx: container.width / (win.screen.width - barX)
|
||||
property real sy: container.height / (win.screen.height - barY)
|
||||
|
||||
x: visible ? (w.at[0] - barX) * sx : 0
|
||||
y: visible ? (w.at[1] - barY) * sy : 0
|
||||
width: visible ? w.size[0] * sx : 0
|
||||
height: visible ? w.size[1] * sy : 0
|
||||
z: w?.floating ? (hover.containsMouse ? 1000 : 100) : (hover.containsMouse ? 50 : 0)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Appearance.m3colors.m3primary
|
||||
border.width: hover.containsMouse ? 3 : 0
|
||||
radius: Metrics.radius(8)
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: Metrics.chronoDuration(150) }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3primary
|
||||
opacity: hover.containsMouse ? 0.15 : 0
|
||||
radius: Metrics.radius(8)
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Metrics.chronoDuration(150) }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
win.captureWindow(Qt.rect(w.at[0], w.at[1], w.size[0], w.size[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: win.visible && !win.closing && frozen.source != ""
|
||||
|
||||
NumberAnimation {
|
||||
target: bg
|
||||
property: "scale"
|
||||
to: bg.scale + 0.05
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "width"
|
||||
to: win.width * 0.8
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "height"
|
||||
to: win.height * 0.8
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: closeAnim
|
||||
|
||||
NumberAnimation {
|
||||
target: bg
|
||||
property: "scale"
|
||||
to: bg.scale - 0.05
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "width"
|
||||
to: win.width
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "height"
|
||||
to: win.height
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: darkOverlay
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: outline
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
onFinished: {
|
||||
root.active = false;
|
||||
if (win.savedSuccess) {
|
||||
Quickshell.execDetached({
|
||||
command: ["notify-send", "Screenshot saved", win.savedPath.split("/").pop() + " (copied)"]
|
||||
});
|
||||
} else if (win.savedPath !== "") {
|
||||
Quickshell.execDetached({
|
||||
command: ["notify-send", "Screenshot failed", "Could not save"]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Metrics.margin(30)
|
||||
width: row.width + 20
|
||||
height: row.height + 20
|
||||
visible: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius("large")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledButton {
|
||||
icon: "fullscreen"
|
||||
text: "Full screen"
|
||||
tooltipText: "Capture the whole screen [F]"
|
||||
onClicked: win.captureFullscreen()
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: 2
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
opacity: 0.2
|
||||
}
|
||||
StyledButton {
|
||||
icon: "window"
|
||||
checkable: true
|
||||
checked: win.windowMode
|
||||
text: "Window"
|
||||
tooltipText: "Hover and click a window [W]"
|
||||
onClicked: win.windowMode = !win.windowMode
|
||||
}
|
||||
StyledButton {
|
||||
secondary: true
|
||||
icon: "close"
|
||||
tooltipText: "Exit [Escape]"
|
||||
onClicked: win.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [win]
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) grab.active = true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: grab
|
||||
function onActiveChanged() {
|
||||
if (!grab.active && !win.closing) win.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
property int logoOffset: -30
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: 460
|
||||
spacing: Metrics.spacing(12)
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Metrics.fontSize(200)
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.osIcon
|
||||
anchors.centerIn: parent
|
||||
x: root.logoOffset
|
||||
font.pixelSize: Metrics.fontSize(200)
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Nucleus Shell"
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "Outfit ExtraBold"
|
||||
font.pixelSize: Metrics.fontSize(26)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "A shell built to get things done."
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledButton {
|
||||
text: "View on GitHub"
|
||||
icon: "code"
|
||||
secondary: true
|
||||
onClicked: Qt.openUrlExternally("https://github.com/xZepyx/nucleus-shell")
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Report Issue"
|
||||
icon: "bug_report"
|
||||
secondary: true
|
||||
onClicked: Qt.openUrlExternally("https://github.com/xZepyx/nucleus-shell/issues")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Nucleus-Shell v" + Config.runtime.shell.version
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Metrics.margin(24)
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 52
|
||||
height: 52
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Metrics.margin(24)
|
||||
|
||||
StyledText {
|
||||
text: "↻"
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Metrics.fontSize(22)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
Globals.states.settingsOpen = false
|
||||
|
||||
Quickshell.execDetached(["notify-send", "Updating Nucleus Shell"])
|
||||
|
||||
Quickshell.execDetached([
|
||||
"kitty",
|
||||
"--hold",
|
||||
"bash",
|
||||
"-c",
|
||||
Directories.scriptsPath + "/system/update.sh"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import qs.plugins
|
||||
|
||||
ContentMenu {
|
||||
title: "Appearance"
|
||||
description: "Adjust how the desktop looks like."
|
||||
|
||||
ContentCard {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
StyledText {
|
||||
text: "Select Theme"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose between dark or light mode."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: "#888888"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.leftMargin: Metrics.margin(15)
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
StyledButton {
|
||||
Layout.preferredHeight: 300
|
||||
Layout.preferredWidth: 460
|
||||
Layout.maximumHeight: 400
|
||||
Layout.maximumWidth: 500
|
||||
icon: "dark_mode"
|
||||
iconSize: Metrics.iconSize(64)
|
||||
checked: Config.runtime.appearance.theme === "dark"
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (!Config.runtime.appearance.colors.autogenerated) {
|
||||
const scheme = Config.runtime.appearance.colors.scheme
|
||||
const file = Theme.map[scheme]?.dark
|
||||
if (!file) {
|
||||
Theme.notifyMissingVariant(scheme, "dark")
|
||||
return
|
||||
}
|
||||
|
||||
Config.updateKey("appearance.theme", "dark")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "theme", "switch", file
|
||||
])
|
||||
} else {
|
||||
Config.updateKey("appearance.theme", "dark")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
Layout.preferredHeight: 300
|
||||
Layout.preferredWidth: 460
|
||||
Layout.maximumHeight: 400
|
||||
Layout.maximumWidth: 500
|
||||
icon: "light_mode"
|
||||
iconSize: Metrics.iconSize(64)
|
||||
checked: Config.runtime.appearance.theme === "light"
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (!Config.runtime.appearance.colors.autogenerated) {
|
||||
const scheme = Config.runtime.appearance.colors.scheme
|
||||
const file = Theme.map[scheme]?.light
|
||||
if (!file) {
|
||||
Theme.notifyMissingVariant(scheme, "light")
|
||||
return
|
||||
}
|
||||
|
||||
Config.updateKey("appearance.theme", "light")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "theme", "switch", file
|
||||
])
|
||||
} else {
|
||||
Config.updateKey("appearance.theme", "light")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Metrics.spacing(30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
RowLayout {
|
||||
opacity: autogeneratedColorsSelector.enabled ? 1 : 0.8
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Color Generation Schemes:"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose the scheme for autogenerated color generation."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
id: autogeneratedColorsSelector
|
||||
label: "Color Scheme"
|
||||
model: [
|
||||
"scheme-content",
|
||||
"scheme-expressive",
|
||||
"scheme-fidelity",
|
||||
"scheme-fruit-salad",
|
||||
"scheme-monochrome",
|
||||
"scheme-neutral",
|
||||
"scheme-rainbow",
|
||||
"scheme-tonal-spot"
|
||||
]
|
||||
|
||||
currentIndex: model.indexOf(Config.runtime.appearance.colors.matugenScheme)
|
||||
|
||||
onSelectedIndexChanged: (index) => {
|
||||
if (!Config.runtime.appearance.colors.autogenerated)
|
||||
return
|
||||
const selectedScheme = model[index]
|
||||
Config.updateKey("appearance.colors.matugenScheme", selectedScheme)
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
])
|
||||
}
|
||||
|
||||
enabled: Config.runtime.appearance.colors.autogenerated
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
opacity: predefinedThemeSelector.enabled ? 1 : 0.8
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
text: "Predefined/Custom Themes:"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
text: "Choose a pre-defined theme for your interface."
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
id: predefinedThemeSelector
|
||||
label: "Theme"
|
||||
model: Object.keys(Theme.map)
|
||||
currentIndex: model.indexOf(Config.runtime.appearance.colors.scheme)
|
||||
|
||||
onSelectedIndexChanged: (index) => {
|
||||
if (Config.runtime.appearance.colors.autogenerated)
|
||||
return
|
||||
const selectedTheme = model[index]
|
||||
const variant = Config.runtime.appearance.theme
|
||||
const file = Theme.map[selectedTheme][variant]
|
||||
if (!file) return
|
||||
|
||||
Config.updateKey("appearance.colors.scheme", selectedTheme)
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "theme", "switch", file
|
||||
])
|
||||
}
|
||||
|
||||
enabled: !Config.runtime.appearance.colors.autogenerated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledSwitchOption {
|
||||
title: "Tint Icons"
|
||||
description: "Either tint icons across the shell or keep them colorized."
|
||||
prefField: "appearance.tintIcons"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Use Autogenerated Themes"
|
||||
description: "Use autogenerated themes."
|
||||
prefField: "appearance.colors.autogenerated"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Use User Defined Themes"
|
||||
description: "Enabling this will also run the default `config.toml` in `~/.config/matugen` dir."
|
||||
prefField: "appearance.colors.runMatugenUserWide"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Clock"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Show Clock"
|
||||
description: "Whether to show or disable the clock on the background."
|
||||
prefField: "appearance.background.clock.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Analog Variant"
|
||||
description: "Whether to use analog clock or not."
|
||||
prefField: "appearance.background.clock.isAnalog"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Rotate Clock Polygon"
|
||||
description: "Rotate the shape polygon of the analog clock."
|
||||
prefField: "appearance.background.clock.rotatePolygonBg"
|
||||
enabled: Config.runtime.appearance.background.clock.isAnalog
|
||||
opacity: enabled ? 1 : 0.8
|
||||
}
|
||||
|
||||
NumberStepper {
|
||||
label: "Rotation Duration"
|
||||
description: "Adjust the duration in which the clock rotates 360* (Seconds)."
|
||||
prefField: "appearance.background.clock.rotationDuration"
|
||||
minimum: 1
|
||||
maximum: 40
|
||||
step: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: shapeSelector
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Analog Clock Shape"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose the analog clock's shape."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
label: "Shape Type"
|
||||
model: ["Cookie 7 Sided", "Cookie 9 Sided", "Cookie 12 Sided", "Pixelated Circle", "Circle"]
|
||||
|
||||
currentIndex: Config.runtime.appearance.background.clock.shape
|
||||
|
||||
onSelectedIndexChanged: (index) => {
|
||||
Config.updateKey(
|
||||
"appearance.background.clock.shape",
|
||||
index
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Rounding"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Factor"
|
||||
description: "Adjust the rounding factor."
|
||||
prefField: "appearance.rounding.factor"
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Font"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Scale"
|
||||
description: "Adjust the font scale."
|
||||
prefField: "appearance.font.scale"
|
||||
minimum: 0.1
|
||||
maximum: 2
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Transparency"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Whether to enable or disable transparency."
|
||||
prefField: "appearance.transparency.enabled"
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Factor"
|
||||
description: "Adjust the alpha value for transparency."
|
||||
prefField: "appearance.transparency.alpha"
|
||||
minimum: 0.1
|
||||
maximum: 1
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Animations"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Whether to enable or disable animations (applies everywhere in the shell)."
|
||||
prefField: "appearance.animations.enabled"
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Duration Scale"
|
||||
description: "Adjust the duration scale of the animations."
|
||||
prefField: "appearance.animations.durationScale"
|
||||
minimum: 0.1
|
||||
maximum: 1
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import qs.modules.functions
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Sound"
|
||||
description: "Volume and audio devices"
|
||||
|
||||
ContentCard {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(20)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: Metrics.radius("large")
|
||||
color: Appearance.m3colors.m3primaryContainer
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "volume_up"
|
||||
color: Appearance.m3colors.m3onPrimaryContainer
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Output"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Volume.defaultSink.description
|
||||
font.pixelSize: Metrics.fontSize(13)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Volume"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: Math.round(Volume.defaultSink.audio.volume * 100) + "%"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit SemiBold")
|
||||
color: Appearance.m3colors.m3primary
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSink.audio.muted ? "volume_off"
|
||||
: Volume.defaultSink.audio.volume < 0.33 ? "volume_mute"
|
||||
: Volume.defaultSink.audio.volume < 0.66 ? "volume_down"
|
||||
: "volume_up"
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: outputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
value: Volume.defaultSink.audio.volume * 100
|
||||
onValueChanged: Volume.setVolume(value / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
StyledText {
|
||||
text: "Device"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
Layout.fillWidth: true
|
||||
label: "Output device"
|
||||
model: Volume.sinks.map(d => d.description)
|
||||
currentIndex: {
|
||||
for (let i = 0; i < Volume.sinks.length; i++)
|
||||
if (Volume.sinks[i].name === Volume.defaultSink.name) return i
|
||||
return -1
|
||||
}
|
||||
onSelectedIndexChanged: index => {
|
||||
if (index >= 0 && index < Volume.sinks.length)
|
||||
Volume.setDefaultSink(Volume.sinks[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 56
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Metrics.margin(16)
|
||||
anchors.rightMargin: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSink.audio.muted ? "volume_off" : "volume_up"
|
||||
color: Volume.defaultSink.audio.muted ? Appearance.m3colors.m3error : Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: "Mute output"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: Volume.defaultSink.audio.muted
|
||||
onToggled: Volume.toggleMuted(Volume.defaultSink)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(20)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: Metrics.radius("large")
|
||||
color: Appearance.m3colors.m3secondaryContainer
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "mic"
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Input"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: Volume.sources.length > 0
|
||||
text: Volume.defaultSource.description
|
||||
font.pixelSize: Metrics.fontSize(13)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: Volume.sources.length === 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 120
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: "mic_off"
|
||||
iconSize: Metrics.iconSize(48)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.3)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No input devices"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Volume"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: Math.round(Volume.defaultSource.audio.volume * 100) + "%"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit SemiBold")
|
||||
color: Appearance.m3colors.m3primary
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSource.audio.muted ? "mic_off" : "mic"
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: inputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
value: Volume.defaultSource.audio.volume * 100
|
||||
onValueChanged: Volume.setSourceVolume(value / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
StyledText {
|
||||
text: "Device"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
Layout.fillWidth: true
|
||||
label: "Input device"
|
||||
model: Volume.sources.map(d => d.description)
|
||||
currentIndex: {
|
||||
for (let i = 0; i < Volume.sources.length; i++)
|
||||
if (Volume.sources[i].name === Volume.defaultSource.name) return i
|
||||
return -1
|
||||
}
|
||||
onSelectedIndexChanged: index => {
|
||||
if (index >= 0 && index < Volume.sources.length)
|
||||
Volume.setDefaultSource(Volume.sources[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 56
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Metrics.margin(16)
|
||||
anchors.rightMargin: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSource.audio.muted ? "mic_off" : "mic"
|
||||
color: Volume.defaultSource.audio.muted ? Appearance.m3colors.m3error : Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: "Mute input"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: Volume.defaultSource.audio.muted
|
||||
onToggled: Volume.toggleMuted(Volume.defaultSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
property string barKey: "bar"
|
||||
title: "Bar"
|
||||
description: "Adjust the bar's look."
|
||||
|
||||
ContentCard {
|
||||
id: monitorSelectorCard
|
||||
|
||||
StyledText {
|
||||
text: "Monitor Bar Configuration"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (Config.runtime.monitors?.[monitorSelector.model[monitorSelector.currentIndex]]?.bar)
|
||||
? "This monitor has its own bar configuration."
|
||||
: "This monitor currently uses the global bar."
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing("normal")
|
||||
|
||||
StyledDropDown {
|
||||
id: monitorSelector
|
||||
Layout.preferredWidth: 220
|
||||
model: Xrandr.monitors.map(m => m.name)
|
||||
currentIndex: 0
|
||||
onCurrentIndexChanged: monitorSelectorCard.updateMonitorProperties()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
id: createButton
|
||||
icon: "add"
|
||||
text: "Override Bar: (" + monitorSelector.model[monitorSelector.currentIndex] + ")"
|
||||
Layout.preferredWidth: 280
|
||||
onClicked: {
|
||||
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
|
||||
if (!monitorName) return
|
||||
if (!Config.runtime.monitors) Config.runtime.monitors = {}
|
||||
if (!Config.runtime.monitors[monitorName])
|
||||
Config.runtime.monitors[monitorName] = {}
|
||||
|
||||
const defaultBar = {
|
||||
density: 50,
|
||||
enabled: true,
|
||||
floating: false,
|
||||
gothCorners: true,
|
||||
margins: 16,
|
||||
merged: false,
|
||||
modules: {
|
||||
height: 34,
|
||||
paddingColor: "#1f1f1f",
|
||||
radius: 17,
|
||||
statusIcons: {
|
||||
bluetoothStatusEnabled: true,
|
||||
enabled: true,
|
||||
networkStatusEnabled: true
|
||||
},
|
||||
systemUsage: {
|
||||
cpuStatsEnabled: true,
|
||||
enabled: true,
|
||||
memoryStatsEnabled: true,
|
||||
tempStatsEnabled: true
|
||||
},
|
||||
workspaces: {
|
||||
enabled: true,
|
||||
showAppIcons: true,
|
||||
showJapaneseNumbers: false,
|
||||
workspaceIndicators: 8
|
||||
}
|
||||
},
|
||||
position: "top",
|
||||
radius: 23
|
||||
}
|
||||
|
||||
Config.updateKey("monitors." + monitorName + ".bar", defaultBar)
|
||||
monitorSelectorCard.updateMonitorProperties()
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
id: deleteButton
|
||||
icon: "delete"
|
||||
text: "Use Global Bar: (" + monitorSelector.model[monitorSelector.currentIndex] + ")"
|
||||
secondary: true
|
||||
Layout.preferredWidth: 280
|
||||
onClicked: {
|
||||
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
|
||||
if (!monitorName) return
|
||||
Config.updateKey("monitors." + monitorName + ".bar", undefined)
|
||||
monitorSelectorCard.updateMonitorProperties()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateMonitorProperties() {
|
||||
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
|
||||
const monitorBar = Config.runtime.monitors?.[monitorName]?.bar
|
||||
barKey = monitorBar ? "monitors." + monitorName + ".bar" : "bar"
|
||||
|
||||
createButton.enabled = !monitorBar
|
||||
deleteButton.enabled = !!monitorBar
|
||||
|
||||
monitorSelector.model = Xrandr.monitors.map(m => m.name)
|
||||
monitorSelector.currentIndex = Xrandr.monitors.findIndex(m => m.name === monitorName)
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Bar"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
Repeater {
|
||||
model: ["Top", "Bottom", "Left", "Right"]
|
||||
delegate: StyledButton {
|
||||
property string pos: modelData.toLowerCase()
|
||||
text: modelData
|
||||
Layout.fillWidth: true
|
||||
checked: ConfigResolver.bar(monitorSelector.model[monitorSelector.currentIndex]).position === pos
|
||||
topLeftRadius: Metrics.radius("normal")
|
||||
topRightRadius: Metrics.radius("normal")
|
||||
bottomLeftRadius: Metrics.radius("normal")
|
||||
bottomRightRadius: Metrics.radius("normal")
|
||||
onClicked: Config.updateKey(barKey + ".position", pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Toggle the bar visibility on/off"
|
||||
prefField: barKey + ".enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Floating Bar"
|
||||
description: "Make the bar float above other windows instead of being part of the desktop"
|
||||
prefField: barKey + ".floating"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Goth Corners"
|
||||
description: "Apply gothic-style corner cutouts to the bar"
|
||||
prefField: barKey + ".gothCorners"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Merged Layout"
|
||||
description: "Merge all modules into a single continuous layout"
|
||||
prefField: barKey + ".merged"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Bar Rounding & Size"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
NumberStepper {
|
||||
label: "Bar Density"
|
||||
prefField: barKey + ".density"
|
||||
description: "Modify the bar's density"
|
||||
minimum: 40
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Bar Radius"
|
||||
prefField: barKey + ".radius"
|
||||
description: "Modify the bar's radius"
|
||||
minimum: 10
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Module Container Radius"
|
||||
prefField: barKey + ".modules.radius"
|
||||
description: "Modify the bar's module.radius"
|
||||
minimum: 10
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Module Height"
|
||||
prefField: barKey + ".modules.height"
|
||||
description: "Modify the bar's module.height"
|
||||
minimum: 10
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Workspace Indicators"
|
||||
prefField: barKey + ".modules.workspaces.workspaceIndicators"
|
||||
description: "Adjust how many workspace indicators to show."
|
||||
minimum: 1
|
||||
maximum: 10
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Bar Modules"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Workspaces"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Show workspace indicator module"
|
||||
prefField: barKey + ".modules.workspaces.enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show App Icons"
|
||||
description: "Display application icons in workspace indicators"
|
||||
prefField: barKey + ".modules.workspaces.showAppIcons"
|
||||
enabled: !barKey.modules.workspaces.showJapaneseNumbers && Compositor.require("hyprland")
|
||||
opacity: !barKey.modules.workspaces.showJapaneseNumbers && Compositor.require("hyprland") ? 1 : 0.8
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Japanese Numbers"
|
||||
description: "Use Japanese-style numbers instead of standard numerals"
|
||||
prefField: barKey + ".modules.workspaces.showJapaneseNumbers"
|
||||
enabled: !barKey.modules.workspaces.showAppIcons
|
||||
opacity: !barKey.modules.workspaces.showAppIcons ? 1 : 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Status Icons"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Show status icons module (wifi, bluetooth)"
|
||||
prefField: barKey + ".modules.statusIcons.enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Wifi Status"
|
||||
description: "Display wifi connection status and signal strength"
|
||||
prefField: barKey + ".modules.statusIcons.networkStatusEnabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Bluetooth Status"
|
||||
description: "Display bluetooth connection status"
|
||||
prefField: barKey + ".modules.statusIcons.bluetoothStatusEnabled"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "System Stats"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Show system resource monitoring module"
|
||||
prefField: barKey + ".modules.systemUsage.enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Cpu Usage Stats"
|
||||
description: "Display CPU usage percentage and load"
|
||||
prefField: barKey + ".modules.systemUsage.cpuStatsEnabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Memory Usage Stats"
|
||||
description: "Display RAM usage and available memory"
|
||||
prefField: barKey + ".modules.systemUsage.memoryStatsEnabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Cpu Temperature Stats"
|
||||
description: "Display CPU temperature readings"
|
||||
prefField: barKey + ".modules.systemUsage.tempStatsEnabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import Quickshell.Bluetooth as QsBluetooth
|
||||
|
||||
ContentMenu {
|
||||
title: "Bluetooth"
|
||||
description: "Manage Bluetooth devices and connections."
|
||||
|
||||
ContentCard {
|
||||
ContentRowCard {
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Bluetooth.defaultAdapter.enabled ? Metrics.padding(10) : Metrics.padding(0)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
StyledText {
|
||||
text: powerSwitch.checked ? "Power: On" : "Power: Off"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
id: powerSwitch
|
||||
checked: Bluetooth.defaultAdapter?.enabled
|
||||
onToggled: Bluetooth.defaultAdapter.enabled = checked
|
||||
}
|
||||
}
|
||||
|
||||
ContentRowCard {
|
||||
visible: Bluetooth.defaultAdapter.enabled
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Metrics.padding(10)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Discoverable"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Allow other devices to find this computer."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
checked: Bluetooth.defaultAdapter?.discoverable
|
||||
onToggled: Bluetooth.defaultAdapter.discoverable = checked
|
||||
}
|
||||
}
|
||||
|
||||
ContentRowCard {
|
||||
visible: Bluetooth.defaultAdapter.enabled
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Metrics.padding(0)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Scanning"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Search for nearby Bluetooth devices."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
checked: Bluetooth.defaultAdapter?.discovering
|
||||
onToggled: Bluetooth.defaultAdapter.discovering = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: connectedDevices.count > 0
|
||||
|
||||
StyledText {
|
||||
text: "Connected Devices"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: connectedDevices
|
||||
model: Bluetooth.devices.filter(d => d.connected)
|
||||
|
||||
delegate: BluetoothDeviceCard {
|
||||
device: modelData
|
||||
statusText: modelData.batteryAvailable
|
||||
? "Connected, " + Math.floor(modelData.battery * 100) + "% left"
|
||||
: "Connected"
|
||||
showDisconnect: true
|
||||
showRemove: true
|
||||
usePrimary: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Bluetooth.defaultAdapter?.enabled
|
||||
|
||||
StyledText {
|
||||
text: "Paired Devices"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: pairedDevices.count === 0
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
text: "No paired devices"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: pairedDevices
|
||||
model: Bluetooth.devices.filter(d => !d.connected && d.paired)
|
||||
|
||||
delegate: BluetoothDeviceCard {
|
||||
device: modelData
|
||||
statusText: "Not connected"
|
||||
showConnect: true
|
||||
showRemove: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Bluetooth.defaultAdapter?.enabled
|
||||
|
||||
StyledText {
|
||||
text: "Available Devices"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: discoveredDevices.count === 0 && !Bluetooth.defaultAdapter.discovering
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "No new devices found"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: discoveredDevices
|
||||
model: Bluetooth.devices.filter(d => !d.paired && !d.connected)
|
||||
|
||||
delegate: BluetoothDeviceCard {
|
||||
device: modelData
|
||||
statusText: "Discovered"
|
||||
showConnect: true
|
||||
showPair: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.components
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import Quickshell.Bluetooth as QsBluetooth
|
||||
|
||||
ContentRowCard {
|
||||
id: deviceRow
|
||||
property var device
|
||||
property string statusText: ""
|
||||
property bool usePrimary: false
|
||||
property bool showConnect: false
|
||||
property bool showDisconnect: false
|
||||
property bool showPair: false
|
||||
property bool showRemove: false
|
||||
|
||||
cardMargin: Metrics.margin(0)
|
||||
cardSpacing: Metrics.spacing(10)
|
||||
verticalPadding: Metrics.padding(0)
|
||||
opacity: device.state === QsBluetooth.BluetoothDeviceState.Connecting ||
|
||||
device.state === QsBluetooth.BluetoothDeviceState.Disconnecting ? 0.6 : 1
|
||||
|
||||
function mapBluetoothIcon(dbusIcon, name) {
|
||||
console.log(dbusIcon, " / ", name)
|
||||
const iconMap = {
|
||||
"audio-headset": "headset",
|
||||
"audio-headphones": "headphones",
|
||||
"input-keyboard": "keyboard",
|
||||
"input-mouse": "mouse",
|
||||
"input-gaming": "sports_esports",
|
||||
"phone": "phone_android",
|
||||
"computer": "computer",
|
||||
"printer": "print",
|
||||
"camera": "photo_camera",
|
||||
"unknown": "bluetooth"
|
||||
}
|
||||
return iconMap[dbusIcon] || "bluetooth"
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
icon: mapBluetoothIcon(device.icon, device.name)
|
||||
font.pixelSize: Metrics.fontSize(32)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Metrics.spacing(0)
|
||||
|
||||
StyledText {
|
||||
text: device.name || device.address
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: statusText
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: usePrimary
|
||||
? Appearance.m3colors.m3primary
|
||||
: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
visible: showConnect
|
||||
icon: "link"
|
||||
onClicked: device.connect()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showDisconnect
|
||||
icon: "link_off"
|
||||
onClicked: device.disconnect()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showPair
|
||||
icon: "add"
|
||||
onClicked: device.pair()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showRemove
|
||||
icon: "delete"
|
||||
onClicked: Bluetooth.removeDevice(device)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Launcher"
|
||||
description: "Adjust launcher's settings."
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Filters & Search"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Fuzzy Search"
|
||||
description: "Enable or disable fuzzy search."
|
||||
prefField: "launcher.fuzzySearchEnabled"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: webEngineSelector
|
||||
|
||||
property string title: "Web Search Engine"
|
||||
property string description: "Choose the web search engine for web searches."
|
||||
property string prefField: ''
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: webEngineSelector.title
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: webEngineSelector.description
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
label: "Engine"
|
||||
model: ["Google", "Brave", "DuckDuckGo", "Bing"]
|
||||
// Set the initial index based on the lowercase value in Config
|
||||
currentIndex: {
|
||||
switch (Config.runtime.launcher.webSearchEngine.toLowerCase()) {
|
||||
case "google":
|
||||
return 0;
|
||||
case "brave":
|
||||
return 1;
|
||||
case "duckduckgo":
|
||||
return 2;
|
||||
case "bing":
|
||||
return 3;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
onSelectedIndexChanged: (index) => {
|
||||
// Update Config with lowercase version of selected model
|
||||
Config.updateKey("launcher.webSearchEngine", model[index].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Miscellaneous"
|
||||
description: "Configure misc settings."
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Versions"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: releaseChannelSelector
|
||||
|
||||
property string title: "Release Channel"
|
||||
property string description: "Choose the release channel for updates."
|
||||
property string prefField: ''
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: releaseChannelSelector.title
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: releaseChannelSelector.description
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
label: "Type"
|
||||
model: ["Stable", "Edge (indev)"]
|
||||
currentIndex: Config.runtime.shell.releaseChannel === "edge" ? 1 : 0
|
||||
onSelectedIndexChanged: (index) => {
|
||||
Config.updateKey("shell.releaseChannel", index === 1 ? "edge" : "stable");
|
||||
UpdateNotifier.notified = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Intelligence"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enable or disable intelligence."
|
||||
prefField: "misc.intelligence.enabled"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Intelligence Bearer/API"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: apiKeyTextField
|
||||
|
||||
clip: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
placeholderText: Config.runtime.misc.intelligence.apiKey !== "" ? Config.runtime.misc.intelligence.apiKey : "Bearer Key"
|
||||
Layout.fillWidth: true
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_S && (event.modifiers & Qt.ControlModifier)) {
|
||||
event.accepted = true;
|
||||
Config.updateKey("misc.intelligence.apiKey", apiKeyTextField.text);
|
||||
Quickshell.execDetached(["notify-send", "Saved Bearer/API Key"])
|
||||
}
|
||||
}
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 20
|
||||
}
|
||||
|
||||
InfoCard {
|
||||
title: "How to save the api key"
|
||||
description: "In order to save the api key press Ctrl+S and it will save the api key to the config."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import qs.modules.functions
|
||||
|
||||
Item {
|
||||
id: networkRow
|
||||
property var connection
|
||||
property bool isActive: false
|
||||
property bool showConnect: false
|
||||
property bool showDisconnect: false
|
||||
property bool showPasswordField: false
|
||||
property string password: ""
|
||||
|
||||
width: parent.width
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
function signalIcon(strength, secure) {
|
||||
if (!connection) return "network_wifi";
|
||||
if (connection.type === "ethernet") return "settings_ethernet";
|
||||
if (strength >= 75) return "network_wifi";
|
||||
if (strength >= 50) return "network_wifi_3_bar";
|
||||
if (strength >= 25) return "network_wifi_2_bar";
|
||||
if (strength > 0) return "network_wifi_1_bar";
|
||||
return "network_wifi_1_bar";
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
// Signal icon with lock overlay
|
||||
Item {
|
||||
width: Metrics.spacing(32)
|
||||
height: Metrics.spacing(32)
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.fill: parent
|
||||
icon: connection ? signalIcon(connection.strength, connection.isSecure) : "network_wifi"
|
||||
font.pixelSize: Metrics.fontSize(32)
|
||||
}
|
||||
|
||||
// Lock overlay (anchors are safe because Item is not layout-managed)
|
||||
MaterialSymbol {
|
||||
icon: "lock"
|
||||
visible: connection && connection.type === "wifi" && connection.isSecure
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Metrics.spacing(0)
|
||||
|
||||
StyledText {
|
||||
text: connection ? connection.name : ""
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: connection ? (
|
||||
isActive ? "Connected" :
|
||||
connection.type === "ethernet" ? connection.device || "Ethernet" :
|
||||
connection.isSecure ? "Secured" : "Open"
|
||||
) : ""
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: isActive
|
||||
? Appearance.m3colors.m3primary
|
||||
: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.4)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
visible: showConnect && !showPasswordField
|
||||
icon: "link"
|
||||
onClicked: {
|
||||
if (!connection) return;
|
||||
if (connection.type === "ethernet") Network.connect(connection, "")
|
||||
else if (connection.isSecure) showPasswordField = true
|
||||
else Network.connect(connection, "")
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showDisconnect && !showPasswordField
|
||||
icon: "link_off"
|
||||
onClicked: Network.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Password row
|
||||
RowLayout {
|
||||
visible: showPasswordField && connection && connection.type === "wifi"
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledTextField {
|
||||
padding: Metrics.padding(10)
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Enter password"
|
||||
echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password
|
||||
onTextChanged: networkRow.password = text
|
||||
onAccepted: {
|
||||
if (!connection) return;
|
||||
Network.connect(connection, networkRow.password)
|
||||
showPasswordField = false
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
property bool showPassword: false
|
||||
icon: parent.showPassword ? "visibility" : "visibility_off"
|
||||
onClicked: parent.showPassword = !parent.showPassword
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "link"
|
||||
onClicked: {
|
||||
if (!connection) return;
|
||||
Network.connect(connection, networkRow.password)
|
||||
showPasswordField = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.functions
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Network"
|
||||
description: "Manage network connections."
|
||||
|
||||
ContentCard {
|
||||
ContentRowCard {
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Network.wifiEnabled ? Metrics.padding(10) : Metrics.padding(0)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
StyledText {
|
||||
text: powerSwitch.checked ? "Wi-Fi: On" : "Wi-Fi: Off"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
id: powerSwitch
|
||||
checked: Network.wifiEnabled
|
||||
onToggled: Network.enableWifi(checked)
|
||||
}
|
||||
}
|
||||
|
||||
ContentRowCard {
|
||||
visible: Network.wifiEnabled
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Metrics.padding(10)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Scanning"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Search for nearby Wi-Fi networks."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: ColorUtils.transparentize(
|
||||
Appearance.m3colors.m3onSurface, 0.4
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
checked: Network.scanning
|
||||
onToggled: {
|
||||
if (checked)
|
||||
Network.rescan()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InfoCard {
|
||||
visible: Network.message !== "" && Network.message !== "ok"
|
||||
icon: "error"
|
||||
backgroundColor: Appearance.m3colors.m3error
|
||||
contentColor: Appearance.m3colors.m3onError
|
||||
title: "Failed to connect to " + Network.lastNetworkAttempt
|
||||
description: Network.message
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.active !== null
|
||||
|
||||
StyledText {
|
||||
text: "Active Connection"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
NetworkCard {
|
||||
connection: Network.active
|
||||
isActive: true
|
||||
showDisconnect: Network.active?.type === "wifi"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.connections.filter(c => c.type === "ethernet").length > 0
|
||||
|
||||
StyledText {
|
||||
text: "Ethernet"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Network.connections.filter(c => c.type === "ethernet" && !c.active)
|
||||
delegate: NetworkCard {
|
||||
connection: modelData
|
||||
showConnect: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.wifiEnabled
|
||||
|
||||
StyledText {
|
||||
text: "Available Wi-Fi Networks"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: Network.connections.filter(c => c.type === "wifi").length === 0 && !Network.scanning
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
text: "No networks found"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.4)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Network.connections.filter(c => c.type === "wifi" && !c.active)
|
||||
delegate: NetworkCard {
|
||||
connection: modelData
|
||||
showConnect: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.savedNetworks.length > 0
|
||||
StyledText {
|
||||
text: "Remembered Networks"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: Network.savedNetworks.length === 0
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
text: "No remembered networks"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Network.connections.filter(c => c.type === "wifi" && c.saved && !c.active)
|
||||
delegate: NetworkCard {
|
||||
connection: modelData
|
||||
showConnect: false
|
||||
showDisconnect: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import qs.modules.functions
|
||||
|
||||
ContentMenu {
|
||||
title: "Notifications & Overlays"
|
||||
description: "Adjust notification and overlay settings."
|
||||
|
||||
function indexFromPosition(pos, model) {
|
||||
pos = pos.toLowerCase()
|
||||
for (let i = 0; i < model.length; i++) {
|
||||
if (model[i].toLowerCase().replace(" ", "-") === pos)
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
|
||||
StyledText {
|
||||
text: "Notifications"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enable or disable built-in notification daemon."
|
||||
prefField: "notifications.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Do not disturb enabled"
|
||||
description: "Enable or disable dnd."
|
||||
prefField: "notifications.doNotDisturb"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Notification Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Select where notification will be shown."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
id: notificationDropdown
|
||||
label: "Position"
|
||||
|
||||
property var positions: ["Top Left", "Top Right", "Top"]
|
||||
|
||||
model: positions
|
||||
|
||||
currentIndex:
|
||||
indexFromPosition(
|
||||
Config.runtime.notifications.position,
|
||||
positions
|
||||
)
|
||||
|
||||
onSelectedIndexChanged: function(index) {
|
||||
Config.updateKey(
|
||||
"notifications.position",
|
||||
positions[index].toLowerCase().replace(" ", "-")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Test Notifications"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Run a test notification."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
text: "Test"
|
||||
icon: "chat"
|
||||
|
||||
onClicked:
|
||||
Quickshell.execDetached([
|
||||
"notify-send",
|
||||
"Quickshell",
|
||||
"This is a test notification"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
|
||||
StyledText {
|
||||
text: "Overlays / OSDs"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enable or disable built-in osd daemon."
|
||||
prefField: "overlays.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Volume OSD enabled"
|
||||
description: "Enable or disable volume osd."
|
||||
prefField: "overlays.volumeOverlayEnabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Brightness OSD enabled"
|
||||
description: "Enable or disable brightness osd."
|
||||
prefField: "overlays.brightnessOverlayEnabled"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Brightness OSD Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose where brightness OSD is shown."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
|
||||
property var positions:
|
||||
["Top Left","Top Right","Bottom Left","Bottom Right","Top","Bottom"]
|
||||
|
||||
model: positions
|
||||
|
||||
currentIndex:
|
||||
indexFromPosition(
|
||||
Config.runtime.overlays.brightnessOverlayPosition,
|
||||
positions
|
||||
)
|
||||
|
||||
onSelectedIndexChanged: function(index) {
|
||||
Config.updateKey(
|
||||
"overlays.brightnessOverlayPosition",
|
||||
positions[index].toLowerCase().replace(" ", "-")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Volume OSD Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose where volume OSD is shown."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
|
||||
property var positions:
|
||||
["Top Left","Top Right","Bottom Left","Bottom Right","Top","Bottom"]
|
||||
|
||||
model: positions
|
||||
|
||||
currentIndex:
|
||||
indexFromPosition(
|
||||
Config.runtime.overlays.volumeOverlayPosition,
|
||||
positions
|
||||
)
|
||||
|
||||
onSelectedIndexChanged: function(index) {
|
||||
Config.updateKey(
|
||||
"overlays.volumeOverlayPosition",
|
||||
positions[index].toLowerCase().replace(" ", "-")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.plugins
|
||||
|
||||
ContentMenu {
|
||||
title: "Plugins"
|
||||
description: "Modify and Customize Installed Plugins."
|
||||
|
||||
ContentCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
color: "transparent"
|
||||
|
||||
GridLayout {
|
||||
id: grid
|
||||
columns: 1
|
||||
Layout.fillWidth: true
|
||||
columnSpacing: Metrics.spacing(16)
|
||||
rowSpacing: Metrics.spacing(16)
|
||||
|
||||
StyledText {
|
||||
text: "Plugins not found!"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
visible: PluginLoader.plugins.length === 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: PluginLoader.plugins
|
||||
|
||||
delegate: ContentCard {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
asynchronous: true
|
||||
source: Qt.resolvedUrl(
|
||||
Directories.shellConfig + "/plugins/" + modelData + "/Settings.qml"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Scope {
|
||||
property var settingsWindow: null
|
||||
|
||||
IpcHandler {
|
||||
function open(menu: string) {
|
||||
Globals.states.settingsOpen = true;
|
||||
|
||||
if (menu !== "" && settingsWindow !== null) {
|
||||
for (var i = 0; i < settingsWindow.menuModel.length; i++) {
|
||||
var item = settingsWindow.menuModel[i];
|
||||
if (!item.header && item.label.toLowerCase() === menu.toLowerCase()) {
|
||||
settingsWindow.selectedIndex = item.page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
target: "settings"
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: Globals.states.settingsOpen
|
||||
|
||||
Window {
|
||||
id: root
|
||||
width: 1280
|
||||
height: 720
|
||||
visible: true
|
||||
title: "Nucleus - Settings"
|
||||
color: Appearance.m3colors.m3background
|
||||
onClosing: Globals.states.settingsOpen = false
|
||||
Component.onCompleted: settingsWindow = root
|
||||
|
||||
property int selectedIndex: 0
|
||||
property bool sidebarCollapsed: false
|
||||
|
||||
property var menuModel: [
|
||||
{ "header": true, "label": "System" },
|
||||
{ "icon": "bluetooth", "label": "Bluetooth", "page": 0 },
|
||||
{ "icon": "network_wifi", "label": "Network", "page": 1 },
|
||||
{ "icon": "volume_up", "label": "Audio", "page": 2 },
|
||||
{ "icon": "instant_mix", "label": "Appearance", "page": 3 },
|
||||
|
||||
{ "header": true, "label": "Customization" },
|
||||
{ "icon": "toolbar", "label": "Bar", "page": 4 },
|
||||
{ "icon": "wallpaper", "label": "Wallpapers", "page": 5 },
|
||||
{ "icon": "apps", "label": "Launcher", "page": 6 },
|
||||
{ "icon": "chat", "label": "Notifications", "page": 7 },
|
||||
{ "icon": "extension", "label": "Plugins", "page": 8 },
|
||||
{ "icon": "apps", "label": "Store", "page": 9 },
|
||||
{ "icon": "build", "label": "Miscellaneous", "page": 10 },
|
||||
|
||||
{ "header": true, "label": "About" },
|
||||
{ "icon": "info", "label": "About", "page": 11 }
|
||||
]
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Rectangle {
|
||||
id: sidebarBG
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.sidebarCollapsed ? 80 : 350
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(40)
|
||||
spacing: Metrics.spacing(5)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: "Settings"
|
||||
font.family: "Outfit ExtraBold"
|
||||
font.pixelSize: Metrics.fontSize(28)
|
||||
visible: !root.sidebarCollapsed
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
Layout.preferredHeight: 40
|
||||
icon: root.sidebarCollapsed ? "chevron_right" : "chevron_left"
|
||||
secondary: true
|
||||
onClicked: root.sidebarCollapsed = !root.sidebarCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: sidebarList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: root.menuModel
|
||||
spacing: Metrics.spacing(5)
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
width: sidebarList.width
|
||||
height: modelData.header ? (root.sidebarCollapsed ? 0 : 30) : 42
|
||||
visible: !modelData.header || !root.sidebarCollapsed
|
||||
|
||||
// header
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
StyledText {
|
||||
y: (parent.height - height) * 0.5
|
||||
x: 10
|
||||
text: modelData.label
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.bold: true
|
||||
opacity: modelData.header ? 1 : 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !modelData.header
|
||||
radius: Appearance.rounding.large
|
||||
color: root.selectedIndex === modelData.page
|
||||
? Appearance.m3colors.m3primary
|
||||
: "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
spacing: 10
|
||||
|
||||
MaterialSymbol {
|
||||
visible: !modelData.header
|
||||
icon: modelData.icon ? modelData.icon : ""
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.label
|
||||
visible: !root.sidebarCollapsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: modelData.page !== undefined
|
||||
onClicked: {
|
||||
root.selectedIndex = modelData.page
|
||||
settingsStack.currentIndex = modelData.page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 180; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StackLayout {
|
||||
id: settingsStack
|
||||
anchors.left: sidebarBG.right
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
currentIndex: root.selectedIndex
|
||||
|
||||
BluetoothConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
NetworkConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
AudioConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
AppearanceConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
BarConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
WallpaperConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
LauncherConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
NotificationConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
Plugins { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
Store { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
MiscConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
About { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.plugins
|
||||
|
||||
ContentMenu {
|
||||
title: "Store"
|
||||
description: "Manage plugins and other stuff for the shell."
|
||||
|
||||
ContentCard {
|
||||
Layout.fillWidth: true
|
||||
|
||||
GridLayout {
|
||||
columns: 1
|
||||
Layout.fillWidth: true
|
||||
columnSpacing: Metrics.spacing(16)
|
||||
rowSpacing: Metrics.spacing(16)
|
||||
|
||||
Repeater {
|
||||
model: PluginParser.model
|
||||
|
||||
delegate: StyledRect {
|
||||
Layout.preferredHeight: 90
|
||||
Layout.fillWidth: true
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("normal")
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("large")
|
||||
text: name
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(6)
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
text: author
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
text: "| Requires Nucleus " + requires_nucleus
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
text: description
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
StyledButton {
|
||||
icon: "download"
|
||||
text: "Install"
|
||||
visible: !installed
|
||||
secondary: true
|
||||
Layout.preferredWidth: 140
|
||||
onClicked: PluginParser.install(id)
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "update"
|
||||
text: "Update"
|
||||
visible: installed
|
||||
secondary: true
|
||||
Layout.preferredWidth: 140
|
||||
onClicked: PluginParser.update(id)
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "delete"
|
||||
text: "Remove"
|
||||
visible: installed
|
||||
secondary: true
|
||||
Layout.preferredWidth: 140
|
||||
onClicked: PluginParser.uninstall(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
property string displayName: root.screen?.name ?? ""
|
||||
property var intervalOptions: [{
|
||||
"value": 5,
|
||||
"label": "5 minutes"
|
||||
}, {
|
||||
"value": 15,
|
||||
"label": "15 minutes"
|
||||
}, {
|
||||
"value": 30,
|
||||
"label": "30 minutes"
|
||||
}, {
|
||||
"value": 60,
|
||||
"label": "1 hour"
|
||||
}, {
|
||||
"value": 120,
|
||||
"label": "2 hours"
|
||||
}, {
|
||||
"value": 360,
|
||||
"label": "6 hours"
|
||||
}]
|
||||
|
||||
function getIntervalIndex(minutes) {
|
||||
for (let i = 0; i < intervalOptions.length; i++) {
|
||||
if (intervalOptions[i].value === minutes)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
title: "Wallpaper"
|
||||
description: "Manage your wallpapers"
|
||||
|
||||
ContentCard {
|
||||
ClippingRectangle {
|
||||
id: wpContainer
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
width: root.screen.width / 2
|
||||
height: width * root.screen.height / root.screen.width
|
||||
radius: Metrics.radius("unsharpenmore")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
StyledText {
|
||||
text: "Current Wallpaper:"
|
||||
font.pixelSize: Metrics.fontSize("big")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: wpPreview
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignCenter
|
||||
anchors.fill: parent
|
||||
radius: Metrics.radius("unsharpenmore")
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
layer.enabled: true
|
||||
|
||||
StyledText {
|
||||
opacity: !Config.runtime.appearance.background.enabled ? 1 : 0
|
||||
font.pixelSize: Metrics.fontSize("title")
|
||||
text: "Wallpaper Manager Disabled"
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
Anim { }
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
opacity: Config.runtime.appearance.background.enabled ? 1 : 0
|
||||
anchors.fill: parent
|
||||
source: previewImg + "?t=" + Date.now()
|
||||
property string previewImg: {
|
||||
const displays = Config.runtime.monitors
|
||||
const fallback = Config.runtime.appearance.background.defaultPath
|
||||
|
||||
if (!displays)
|
||||
return fallback
|
||||
|
||||
const monitor = displays?.[displayName]
|
||||
return monitor?.wallpaper ?? fallback
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
Anim { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "wallpaper"
|
||||
text: "Change Wallpaper"
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "background", "change"]);
|
||||
}
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enabled or disable built-in wallpaper daemon."
|
||||
prefField: "appearance.background.enabled"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Parallax Effect"
|
||||
font.pixelSize: Metrics.fontSize("big")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enabled or disable wallpaper parallax effect."
|
||||
prefField: "appearance.background.parallax.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled for Sidebar Left"
|
||||
description: "Show parralax effect when sidebarLeft is opened."
|
||||
prefField: "appearance.background.parallax.enableSidebarLeft"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled for Sidebar Right"
|
||||
description: "Show parralax effect when sidebarRight is opened."
|
||||
prefField: "appearance.background.parallax.enableSidebarRight"
|
||||
}
|
||||
|
||||
NumberStepper {
|
||||
label: "Zoom Amount"
|
||||
description: "Adjust the zoom of the parallax effect."
|
||||
prefField: "appearance.background.parallax.zoom"
|
||||
step: 0.1
|
||||
minimum: 1.10
|
||||
maximum: 2
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Wallpaper Slideshow"
|
||||
font.pixelSize: Metrics.fontSize("big")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enable Slideshow"
|
||||
description: "Automatically rotate wallpapers from a folder."
|
||||
prefField: "appearance.background.slideshow.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Include Subfolders"
|
||||
description: "Also search for wallpapers in subfolders."
|
||||
prefField: "appearance.background.slideshow.includeSubfolders"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
StyledText {
|
||||
text: "Wallpaper Folder"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Config.runtime.appearance.background.slideshow.folder || "No folder selected"
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "folder_open"
|
||||
text: "Browse"
|
||||
onClicked: folderPickerProc.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: skipWallpaper
|
||||
|
||||
property string title: "Skip To Next Wallpaper"
|
||||
property string description: "Skip to the next wallpaper in the wallpaper directory."
|
||||
property string prefField: ''
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: skipWallpaper.title
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: skipWallpaper.description
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
icon: "skip_next"
|
||||
text: "Skip Next"
|
||||
enabled: WallpaperSlideshow.wallpapers.length > 0
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "background", "next"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
StyledText {
|
||||
text: "Change Interval"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "How often to change the wallpaper."
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
label: "Interval"
|
||||
model: intervalOptions.map((opt) => {
|
||||
return opt.label;
|
||||
})
|
||||
currentIndex: getIntervalIndex(Config.runtime.appearance.background.slideshow.interval)
|
||||
onSelectedIndexChanged: (index) => {
|
||||
Config.updateKey("appearance.background.slideshow.interval", intervalOptions[index].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: folderPickerProc
|
||||
|
||||
command: ["bash", Directories.scriptsPath + "/interface/selectfolder.sh", Config.runtime.appearance.background.slideshow.folder || Directories.pictures]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const out = text.trim();
|
||||
if (out !== "null" && out.length > 0)
|
||||
Config.updateKey("appearance.background.slideshow.folder", out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool initialChatSelected: false
|
||||
|
||||
function appendMessage(sender, message) {
|
||||
messageModel.append({
|
||||
"sender": sender,
|
||||
"message": message
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function updateChatsList(files) {
|
||||
let existing = {
|
||||
};
|
||||
for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true
|
||||
for (let file of files) {
|
||||
let name = file.trim();
|
||||
if (!name.length)
|
||||
continue;
|
||||
|
||||
if (name.endsWith(".txt"))
|
||||
name = name.slice(0, -4);
|
||||
|
||||
if (!existing[name])
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
|
||||
delete existing[name];
|
||||
}
|
||||
// remove chats that no longer exist
|
||||
for (let name in existing) {
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === name) {
|
||||
chatListModel.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ensure default exists
|
||||
let hasDefault = false;
|
||||
for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") {
|
||||
hasDefault = true;
|
||||
}
|
||||
if (!hasDefault) {
|
||||
chatListModel.insert(0, {
|
||||
"name": "default"
|
||||
});
|
||||
FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt");
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
chatView.positionViewAtEnd();
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (userInput.text === "" || Zenith.loading)
|
||||
return ;
|
||||
|
||||
Zenith.pendingInput = userInput.text;
|
||||
appendMessage("You", userInput.text);
|
||||
userInput.text = "";
|
||||
Zenith.loading = true;
|
||||
Zenith.send();
|
||||
}
|
||||
|
||||
function loadChatHistory(chatName) {
|
||||
messageModel.clear();
|
||||
Zenith.loadChat(chatName);
|
||||
}
|
||||
|
||||
function selectDefaultChat() {
|
||||
let defaultIndex = -1;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === "default") {
|
||||
defaultIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultIndex !== -1) {
|
||||
chatSelector.currentIndex = defaultIndex;
|
||||
Zenith.currentChat = "default";
|
||||
loadChatHistory("default");
|
||||
} else if (chatListModel.count > 0) {
|
||||
chatSelector.currentIndex = 0;
|
||||
Zenith.currentChat = chatListModel.get(0).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
// { sender: "You" | "AI", message: string }
|
||||
|
||||
id: messageModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: chatListModel
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Intelligence is disabled!"
|
||||
Layout.leftMargin: Metrics.margin(24)
|
||||
font.pixelSize: Appearance.font.size.huge
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Go to the settings to enable intelligence"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.topMargin: Metrics.margin(74)
|
||||
radius: Metrics.radius("normal")
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: Config.runtime.misc.intelligence.enabled
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: chatSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: chatListModel
|
||||
textRole: "name"
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentIndexChanged: {
|
||||
if (!initialChatSelected)
|
||||
return ;
|
||||
|
||||
if (currentIndex < 0 || currentIndex >= chatListModel.count)
|
||||
return ;
|
||||
|
||||
let chatName = chatListModel.get(currentIndex).name;
|
||||
Zenith.currentChat = chatName;
|
||||
loadChatHistory(chatName);
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "add"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
let name = "new-chat-" + chatListModel.count;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.createFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
chatSelector.currentIndex = chatListModel.count - 1;
|
||||
Zenith.currentChat = name;
|
||||
messageModel.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "edit"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0
|
||||
onClicked: renameDialog.open()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "delete"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default"
|
||||
onClicked: {
|
||||
let name = chatSelector.currentText;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.removeFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.remove(chatSelector.currentIndex);
|
||||
selectDefaultChat();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: modelSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"]
|
||||
currentIndex: 0
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentTextChanged: Zenith.currentModel = currentText
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "fullscreen"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "openWindow"]);
|
||||
Globals.visiblility.sidebarLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: chatView
|
||||
|
||||
model: messageModel
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(12)
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic
|
||||
|
||||
width: chatView.width
|
||||
height: bubble.implicitHeight + 6
|
||||
Component.onCompleted: {
|
||||
chatView.forceLayout();
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
Item {
|
||||
width: sender === "AI" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: bubble
|
||||
|
||||
radius: Metrics.radius("normal")
|
||||
color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh
|
||||
implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8)
|
||||
implicitHeight: textItem.implicitHeight
|
||||
anchors.right: sender === "You" ? parent.right : undefined
|
||||
anchors.left: sender === "AI" ? parent.left : undefined
|
||||
anchors.topMargin: Metrics.margin(2)
|
||||
|
||||
TextEdit {
|
||||
id: textItem
|
||||
|
||||
text: StringUtils.markdownToHtml(message)
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true // make it selectable but not editable
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
anchors.leftMargin: Metrics.margin(12)
|
||||
color: Appearance.syntaxHighlightingTheme
|
||||
padding: Metrics.padding(8)
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent);
|
||||
p.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
width: sender === "You" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
height: 50
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
spacing: 10
|
||||
|
||||
StyledTextField {
|
||||
// Shift+Enter → insert newline
|
||||
// Enter → send message
|
||||
|
||||
id: userInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Type your message..."
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
padding: Metrics.padding(8)
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (event.modifiers & Qt.ShiftModifier)
|
||||
insert("\n");
|
||||
else
|
||||
sendMessage();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Send"
|
||||
enabled: userInput.text.trim().length > 0 && !Zenith.loading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: sendMessage()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: renameDialog
|
||||
|
||||
title: "Rename Chat"
|
||||
modal: true
|
||||
visible: false
|
||||
standardButtons: Dialog.NoButton
|
||||
x: (root.width - 360) / 2 // center horizontally
|
||||
y: (root.height - 160) / 2 // center vertically
|
||||
width: 360
|
||||
height: 200
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
StyledText {
|
||||
text: "Enter a new name for the chat"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: renameInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "New name"
|
||||
filled: false
|
||||
highlight: false
|
||||
text: chatSelector.currentText
|
||||
font.pixelSize: Metrics.iconSize(16)
|
||||
Layout.preferredHeight: 45
|
||||
padding: Metrics.padding(8)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
StyledButton {
|
||||
text: "Cancel"
|
||||
Layout.preferredWidth: 80
|
||||
onClicked: renameDialog.close()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Rename"
|
||||
Layout.preferredWidth: 100
|
||||
enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText
|
||||
onClicked: {
|
||||
let oldName = chatSelector.currentText;
|
||||
let newName = renameInput.text.trim();
|
||||
let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt";
|
||||
let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt";
|
||||
FileUtils.renameFile(oldPath, newPath, function(success) {
|
||||
if (success) {
|
||||
chatListModel.set(chatSelector.currentIndex, {
|
||||
"name": newName
|
||||
});
|
||||
Zenith.currentChat = newName;
|
||||
renameDialog.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
header: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Thinking…"
|
||||
visible: Zenith.loading
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
leftMargin: Metrics.margin(22)
|
||||
bottomMargin: Metrics.margin(76)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onChatsListed(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
updateChatsList(lines);
|
||||
// only auto-select once
|
||||
if (!initialChatSelected) {
|
||||
selectDefaultChat();
|
||||
initialChatSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onAiReply(text) {
|
||||
appendMessage("AI", text.slice(5));
|
||||
Zenith.loading = false;
|
||||
}
|
||||
|
||||
function onChatLoaded(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
let batch = [];
|
||||
for (let l of lines) {
|
||||
let line = l.trim();
|
||||
if (!line.length)
|
||||
continue;
|
||||
|
||||
let u = line.match(/^\[\d{4}-.*\] User: (.*)$/);
|
||||
let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/);
|
||||
if (u)
|
||||
batch.push({
|
||||
"sender": "You",
|
||||
"message": u[1]
|
||||
});
|
||||
else if (a)
|
||||
batch.push({
|
||||
"sender": "AI",
|
||||
"message": a[1]
|
||||
});
|
||||
else if (batch.length)
|
||||
batch[batch.length - 1].message += "\n" + line;
|
||||
}
|
||||
messageModel.clear();
|
||||
for (let m of batch) messageModel.append(m)
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
target: Zenith
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
PanelWindow {
|
||||
id: sidebarLeft
|
||||
WlrLayershell.namespace: "nucleus:sidebarLeft"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
visible: Config.initialized && Globals.visiblility.sidebarLeft && !Globals.visiblility.sidebarRight
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
WlrLayershell.keyboardFocus: Compositor.require("hyprland") && Globals.visiblility.sidebarLeft
|
||||
|
||||
property real sidebarLeftWidth: 500
|
||||
|
||||
implicitWidth: Compositor.screenW
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
active: Compositor.require("hyprland")
|
||||
windows: [sidebarLeft]
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: (Config.runtime.bar.position === "top" || Config.runtime.bar.position === "bottom" || Config.runtime.bar.position === "left")
|
||||
bottom: true
|
||||
right: (Config.runtime.bar.position === "right")
|
||||
}
|
||||
|
||||
margins {
|
||||
top: Config.runtime.bar.margins
|
||||
bottom: Config.runtime.bar.margins
|
||||
left: Metrics.margin("small")
|
||||
right: Metrics.margin("small")
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: 0
|
||||
onPressed: Globals.visiblility.sidebarLeft = false
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: container
|
||||
z: 1
|
||||
color: Appearance.m3colors.m3background
|
||||
radius: Metrics.radius("large")
|
||||
width: sidebarLeft.sidebarLeftWidth
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
focus: true
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
Globals.visiblility.sidebarLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
SidebarLeftContent {}
|
||||
}
|
||||
}
|
||||
|
||||
function togglesidebarLeft() {
|
||||
Globals.visiblility.sidebarLeft = !Globals.visiblility.sidebarLeft;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "sidebarLeft"
|
||||
function toggle() {
|
||||
togglesidebarLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
SwipeView {
|
||||
id: view
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// IntelligencePanel {}
|
||||
SystemOverview {}
|
||||
// WallpapersPage {}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 2
|
||||
width: parent.width - Metrics.margin("verylarge")
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
opacity: 0.6
|
||||
|
||||
anchors {
|
||||
top: view.top
|
||||
topMargin: segmentedIndicator.height + Metrics.margin("verysmall")
|
||||
horizontalCenter: view.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: activeTabIndicator
|
||||
|
||||
height: 2
|
||||
width: 96
|
||||
radius: Metrics.radius(1)
|
||||
color: Appearance.m3colors.m3primary
|
||||
x: (segmentedIndicator.width / view.count) * view.currentIndex + (segmentedIndicator.width / view.count - width) / 2
|
||||
|
||||
anchors {
|
||||
top: segmentedIndicator.bottom
|
||||
topMargin: Metrics.margin(8)
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(220)
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: segmentedIndicator
|
||||
|
||||
height: 56
|
||||
width: parent.width
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
// {
|
||||
// "icon": "neurology",
|
||||
// "text": "Intelligence"
|
||||
// },
|
||||
{
|
||||
"icon": "overview",
|
||||
"text": "Overview"
|
||||
},
|
||||
// {
|
||||
// "icon": "wallpaper",
|
||||
// "text": "Wallpapers"
|
||||
// }
|
||||
]
|
||||
|
||||
Item {
|
||||
width: segmentedIndicator.width / view.count
|
||||
height: parent.height
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: view.currentIndex = index
|
||||
}
|
||||
|
||||
// Icon (true center)
|
||||
MaterialSymbol {
|
||||
icon: modelData.icon
|
||||
iconSize: Metrics.iconSize("huge")
|
||||
color: view.currentIndex === index ? Appearance.m3colors.m3primary : Appearance.m3colors.m3onSurfaceVariant
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
topMargin: Metrics.margin(12)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Label (independent centering)
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
font.pixelSize: Metrics.fontSize("large")
|
||||
font.weight: Font.Medium
|
||||
color: view.currentIndex === index ? Appearance.m3colors.m3primary : Appearance.m3colors.m3onSurfaceVariant
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: parent ? parent.height : 500
|
||||
|
||||
ColumnLayout {
|
||||
anchors.topMargin: Metrics.margin(90)
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("normal")
|
||||
spacing: Metrics.margin("small")
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.margin("normal")
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.osIcon
|
||||
font.family: Metrics.fontFamily("nerdIcons")
|
||||
font.pixelSize: Metrics.fontSize(48)
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.osName
|
||||
font.pixelSize: Metrics.fontSize("large")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${SystemDetails.username}@${SystemDetails.hostname}`
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
StyledText {
|
||||
text: `qs ${SystemDetails.qsVersion}`
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `nucleus-shell v${Config.runtime.shell.version}`
|
||||
font.pixelSize: Metrics.fontSize("smaller")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 56
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("small")
|
||||
|
||||
StyledText {
|
||||
text: "Uptime"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.uptime
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 56
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("small")
|
||||
|
||||
StyledText {
|
||||
text: "Operating System"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.osName
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 320
|
||||
radius: Metrics.radius("large")
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("large")
|
||||
spacing: Metrics.margin("normal")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(6)
|
||||
|
||||
RowLayout {
|
||||
StyledText {
|
||||
text: "CPU Usage"
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: SystemDetails.cpuLoad
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 10
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * SystemDetails.cpuPercent
|
||||
height: parent.height
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(6)
|
||||
|
||||
RowLayout {
|
||||
StyledText {
|
||||
text: "Ram Usage"
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: SystemDetails.ramUsage
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 10
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * SystemDetails.ramPercent
|
||||
height: parent.height
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(6)
|
||||
|
||||
RowLayout {
|
||||
StyledText {
|
||||
text: "Disk Usage"
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: SystemDetails.diskUsage
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 10
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * SystemDetails.diskPercent
|
||||
height: parent.height
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(6)
|
||||
|
||||
RowLayout {
|
||||
StyledText {
|
||||
text: "Swap Usage"
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.swapUsage
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 10
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * SystemDetails.swapPercent
|
||||
height: parent.height
|
||||
radius: Metrics.radius(5)
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 72
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("small")
|
||||
spacing: Metrics.margin("large")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Kernel"
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.kernelVersion
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
StyledText {
|
||||
text: "Architecture"
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.architecture
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 72
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer2
|
||||
visible: UPower.batteryPresent
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("small")
|
||||
spacing: Metrics.margin("large")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Battery"
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `${Math.round(UPower.percentage)}%`
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
StyledText {
|
||||
text: "AC"
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: UPower.acOnline ? "online" : "battery"
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 56
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("small")
|
||||
|
||||
StyledText {
|
||||
text: "Running Processes"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.runningProcesses
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 56
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("small")
|
||||
|
||||
StyledText {
|
||||
text: "Logged-in Users"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.loggedInUsers
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: wallpapersPage
|
||||
property string displayName: screen?.name ?? ""
|
||||
|
||||
FolderListModel {
|
||||
id: wallpaperModel
|
||||
|
||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) + "/Wallpapers"
|
||||
nameFilters: ["*.png", "*.jpg", "*.jpeg", "*.webp", "mp4", "mkv", "webm", "avi", "mov", "flv", "wmv", "m4v"]
|
||||
showDirs: false
|
||||
showDotAndDotDot: false
|
||||
}
|
||||
|
||||
// EMPTY STATE
|
||||
StyledText {
|
||||
visible: wallpaperModel.count === 0
|
||||
text: "Put some wallpapers in\n~/Pictures/Wallpapers"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
font.pixelSize: Appearance.font.size.large
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
// WALLPAPER LIST
|
||||
ListView {
|
||||
anchors.topMargin: 90
|
||||
visible: wallpaperModel.count > 0
|
||||
anchors.fill: parent
|
||||
model: wallpaperModel
|
||||
spacing: Appearance.margin.normal
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
width: ListView.view.width
|
||||
height: 240
|
||||
|
||||
StyledRect {
|
||||
id: imgContainer
|
||||
property bool activeWallpaper:
|
||||
Config.runtime.monitors?.[wallpapersPage.displayName]?.wallpaper === fileUrl
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 20
|
||||
anchors.rightMargin: 20
|
||||
radius: Appearance.rounding.normal
|
||||
color: activeWallpaper
|
||||
? Appearance.m3colors.m3secondaryContainer
|
||||
: Appearance.m3colors.m3surfaceContainerLow
|
||||
layer.enabled: true
|
||||
|
||||
Image {
|
||||
id: wallImg
|
||||
|
||||
anchors.fill: parent
|
||||
source: fileUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
clip: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Unsupported / Corrupted Image"
|
||||
visible: wallImg.status === Image.Error
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Config.updateKey(
|
||||
"monitors." + wallpapersPage.displayName + ".wallpaper",
|
||||
fileUrl
|
||||
);
|
||||
if (Config.runtime.appearance.colors.autogenerated) {
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
]);
|
||||
}
|
||||
}
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
|
||||
maskSource: Rectangle {
|
||||
width: imgContainer.width
|
||||
height: imgContainer.height
|
||||
radius: imgContainer.radius
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Services.Pipewire
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
PanelWindow {
|
||||
id: sidebarRight
|
||||
WlrLayershell.namespace: "nucleus:sidebarRight"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
visible: Config.initialized && Globals.visiblility.sidebarRight && !Globals.visiblility.sidebarLeft
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
WlrLayershell.keyboardFocus: Compositor.require("hyprland") && Globals.visiblility.sidebarRight
|
||||
|
||||
property real sidebarRightWidth: 500
|
||||
|
||||
implicitWidth: Compositor.screenW
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
active: Compositor.require("hyprland")
|
||||
windows: [sidebarRight]
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: (Config.runtime.bar.position === "top" || Config.runtime.bar.position === "bottom" || Config.runtime.bar.position === "right")
|
||||
bottom: true
|
||||
left: (Config.runtime.bar.position === "left")
|
||||
}
|
||||
|
||||
margins {
|
||||
top: Config.runtime.bar.margins
|
||||
bottom: Config.runtime.bar.margins
|
||||
left: Metrics.margin("small")
|
||||
right: Metrics.margin("small")
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink]
|
||||
}
|
||||
|
||||
property var sink: Pipewire.defaultAudioSink?.audio
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: 0
|
||||
onPressed: Globals.visiblility.sidebarRight = false
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: container
|
||||
z: 1
|
||||
color: Appearance.m3colors.m3background
|
||||
radius: Metrics.radius("large")
|
||||
width: sidebarRight.sidebarRightWidth
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
focus: true
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
Globals.visiblility.sidebarRight = false;
|
||||
}
|
||||
}
|
||||
|
||||
SidebarRightContent {}
|
||||
}
|
||||
}
|
||||
|
||||
function togglesidebarRight() {
|
||||
Globals.visiblility.sidebarRight = !Globals.visiblility.sidebarRight;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "sidebarRight"
|
||||
function toggle() {
|
||||
togglesidebarRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import qs.modules.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Services.Pipewire
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "content/"
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Metrics.margin("normal")
|
||||
anchors.rightMargin: Metrics.margin("normal")
|
||||
anchors.topMargin: Metrics.margin("large")
|
||||
anchors.bottomMargin: Metrics.margin("large")
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Metrics.margin("tiny")
|
||||
anchors.rightMargin: Metrics.margin("tiny")
|
||||
anchors.margins: Metrics.margin("large")
|
||||
spacing: Metrics.margin("large")
|
||||
|
||||
RowLayout {
|
||||
id: topSection
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Metrics.margin(10)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.osIcon
|
||||
font.pixelSize: Metrics.fontSize("hugeass") + 6
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.uptime
|
||||
font.pixelSize: Metrics.fontSize("large")
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.bottomMargin: Metrics.margin(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Row {
|
||||
spacing: Metrics.spacing(6)
|
||||
Layout.leftMargin: Metrics.margin(25)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
StyledRect {
|
||||
id: screenshotbtncontainer
|
||||
color: "transparent"
|
||||
radius: Metrics.radius("large")
|
||||
implicitHeight: screenshotButton.height + Metrics.margin("tiny")
|
||||
implicitWidth: screenshotButton.width + Metrics.margin("small")
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.topMargin: Metrics.margin(10)
|
||||
Layout.leftMargin: Metrics.margin(15)
|
||||
|
||||
MaterialSymbolButton {
|
||||
id: screenshotButton
|
||||
icon: "edit"
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize("hugeass") + 2
|
||||
tooltipText: "Take a screenshot"
|
||||
|
||||
onButtonClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "screen", "capture"])
|
||||
Globals.visiblility.sidebarRight = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: reloadbtncontainer
|
||||
color: "transparent"
|
||||
radius: Metrics.radius("large")
|
||||
implicitHeight: reloadButton.height + Metrics.margin("tiny")
|
||||
implicitWidth: reloadButton.width + Metrics.margin("small")
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.topMargin: Metrics.margin(10)
|
||||
Layout.leftMargin: Metrics.margin(15)
|
||||
|
||||
MaterialSymbolButton {
|
||||
id: reloadButton
|
||||
icon: "refresh"
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize("hugeass") + 4
|
||||
tooltipText: "Reload Nucleus Shell"
|
||||
|
||||
onButtonClicked: {
|
||||
Quickshell.execDetached(["nucleus", "run", "--reload"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: settingsbtncontainer
|
||||
color: "transparent"
|
||||
radius: Metrics.radius("large")
|
||||
implicitHeight: settingsButton.height + Metrics.margin("tiny")
|
||||
implicitWidth: settingsButton.width + Metrics.margin("small")
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.topMargin: Metrics.margin(10)
|
||||
Layout.leftMargin: Metrics.margin(15)
|
||||
|
||||
MaterialSymbolButton {
|
||||
id: settingsButton
|
||||
icon: "settings"
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize("hugeass") + 2
|
||||
tooltipText: "Open Settings"
|
||||
onButtonClicked: {
|
||||
Globals.visiblility.sidebarRight = false
|
||||
Globals.states.settingsOpen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: powerbtncontainer
|
||||
color: "transparent"
|
||||
radius: Metrics.radius("large")
|
||||
implicitHeight: settingsButton.height + Metrics.margin("tiny")
|
||||
implicitWidth: settingsButton.width + Metrics.margin("small")
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.topMargin: Metrics.margin(10)
|
||||
Layout.leftMargin: Metrics.margin(15)
|
||||
|
||||
MaterialSymbolButton {
|
||||
id: powerButton
|
||||
icon: "power_settings_new"
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize("hugeass") + 2
|
||||
tooltipText: "Open PowerMenu"
|
||||
|
||||
onButtonClicked: {
|
||||
Globals.visiblility.sidebarRight = false
|
||||
Globals.visiblility.powermenu = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
radius: Metrics.radius(1)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sliderColumn
|
||||
Layout.fillWidth: true
|
||||
|
||||
VolumeSlider {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 50
|
||||
icon: "volume_up"
|
||||
iconSize: Metrics.iconSize("large") + 3
|
||||
}
|
||||
|
||||
BrightnessSlider {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 50
|
||||
icon: "brightness_high"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
radius: Metrics.radius(1)
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: middleGrid
|
||||
Layout.fillWidth: true
|
||||
columns: 1
|
||||
columnSpacing: Metrics.spacing(8)
|
||||
rowSpacing: Metrics.spacing(8)
|
||||
Layout.preferredWidth: parent.width
|
||||
|
||||
RowLayout {
|
||||
NetworkToggle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 80
|
||||
}
|
||||
FlightModeToggle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 80
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
BluetoothToggle {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.preferredHeight: 80
|
||||
}
|
||||
ThemeToggle {
|
||||
Layout.preferredHeight: 80
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NightModeToggle {
|
||||
Layout.preferredHeight: 80
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.margin("small")
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
radius: Metrics.radius(1)
|
||||
Layout.topMargin: Metrics.margin(5)
|
||||
Layout.bottomMargin: Metrics.margin(5)
|
||||
}
|
||||
|
||||
NotifModal {
|
||||
Layout.preferredHeight: (Config.runtime.bar.position === "left" || Config.runtime.bar.position === "right") ? 480 : 470
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
width: 200
|
||||
height: 80
|
||||
radius: Metrics.radius("verylarge")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
readonly property bool adapterPresent: Bluetooth.defaultAdapter !== null
|
||||
readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
readonly property var activeDevice: Bluetooth.activeDevice
|
||||
|
||||
readonly property string iconName: Bluetooth.icon
|
||||
|
||||
readonly property string statusText: {
|
||||
if (!adapterPresent)
|
||||
return "No adapter";
|
||||
if (!enabled)
|
||||
return "Disabled";
|
||||
if (activeDevice)
|
||||
return activeDevice.name;
|
||||
return Bluetooth.defaultAdapter.discovering
|
||||
? "Scanning…"
|
||||
: "Enabled";
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: iconBg
|
||||
width: 50
|
||||
height: 50
|
||||
radius: Metrics.radius("verylarge")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin("small")
|
||||
|
||||
color: {
|
||||
if (!enabled)
|
||||
return Appearance.m3colors.m3surfaceContainerHigh;
|
||||
if (activeDevice)
|
||||
return Appearance.m3colors.m3primaryContainer;
|
||||
return Appearance.m3colors.m3secondaryContainer;
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize(35)
|
||||
icon: iconName
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: iconBg.right
|
||||
anchors.leftMargin: Metrics.margin("small")
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Bluetooth"
|
||||
font.pixelSize: Metrics.fontSize("large")
|
||||
elide: Text.ElideRight
|
||||
width: root.width - iconBg.width - 30
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: statusText
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
width: root.width - iconBg.width - 30
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (!adapterPresent)
|
||||
return;
|
||||
|
||||
Bluetooth.defaultAdapter.enabled =
|
||||
!Bluetooth.defaultAdapter.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Controls
|
||||
|
||||
|
||||
StyledSlider {
|
||||
id: brightnessSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
property var monitor: Brightness.monitors.length > 0 ? Brightness.monitors[0] : null
|
||||
value: monitor ? monitor.brightness : 0.5
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
property real level: brightnessSlider.value * 100
|
||||
|
||||
onMoved: if (monitor) {
|
||||
monitor.setBrightness(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
width: 150
|
||||
height: 50
|
||||
radius: Metrics.radius("verylarge")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
property bool flightMode
|
||||
property string flightModeText: flightMode ? "Enabled" : "Disabled"
|
||||
|
||||
Process {
|
||||
id: toggleflightModeProc
|
||||
running: false
|
||||
command: []
|
||||
|
||||
function toggle() {
|
||||
flightMode = !flightMode;
|
||||
const cmd = flightMode ? "off" : "on";
|
||||
toggleflightModeProc.command = ["bash", "-c", `nmcli radio all ${cmd}`];
|
||||
toggleflightModeProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: iconBg
|
||||
width: 50
|
||||
height: 50
|
||||
radius: Metrics.radius("large")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
color: !flightMode ? Appearance.m3colors.m3surfaceContainerHigh : Appearance.m3colors.m3primaryContainer
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize(35)
|
||||
icon: "flight"
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: iconBg.right
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
|
||||
StyledText {
|
||||
text: "Flight Mode"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: flightModeText
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
onClicked: toggleflightModeProc.toggle()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Io
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.interface.notifications
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
Layout.fillWidth: true
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
ClippingRectangle {
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: Metrics.radius("normal")
|
||||
implicitHeight: 90
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Metrics.margin("small")
|
||||
|
||||
ClippingRectangle {
|
||||
implicitWidth: 140
|
||||
implicitHeight: 140
|
||||
Layout.leftMargin: Metrics.margin("large")
|
||||
radius: Metrics.radius("normal")
|
||||
clip: true
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: Mpris.artUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Metrics.margin("small")
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
Text {
|
||||
text: Mpris.albumTitle
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 190
|
||||
font.family: Metrics.fontFamily("title")
|
||||
font.pixelSize: Metrics.fontSize("hugeass")
|
||||
font.bold: true
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Mpris.albumArtist
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 160
|
||||
font.family: Metrics.fontFamily("main")
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
Process {
|
||||
id: control
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
onClicked: Quickshell.execDetached(["playerctl", "previous"])
|
||||
|
||||
background: Rectangle {
|
||||
radius: Metrics.radius("large")
|
||||
color: Appearance.colors.colLayer2
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "skip_previous"
|
||||
font.pixelSize: Metrics.fontSize(24)
|
||||
color: Appearance.colors.colOnLayer2
|
||||
fill: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: 42
|
||||
Layout.preferredHeight: 42
|
||||
onClicked: Quickshell.execDetached(["playerctl", "play-pause"])
|
||||
|
||||
background: Rectangle {
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: parent.top
|
||||
icon: "play_arrow"
|
||||
font.pixelSize: Metrics.fontSize(36)
|
||||
color: Appearance.colors.colOnPrimary
|
||||
fill: 1
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
onClicked: Quickshell.execDetached(["playerctl", "next"])
|
||||
|
||||
background: Rectangle {
|
||||
radius: Metrics.radius("large")
|
||||
color: Appearance.colors.colLayer2
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "skip_next"
|
||||
font.pixelSize: Metrics.fontSize(24)
|
||||
color: Appearance.colors.colOnLayer2
|
||||
fill: 1
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Metrics.margin(15)
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
Text {
|
||||
text: Mpris.formatTime(Mpris.positionSec)
|
||||
font.pixelSize: Metrics.fontSize("smallest")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 20
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.colors.colLayer2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * (Mpris.lengthSec > 0 ? Mpris.positionSec / Mpris.lengthSec : 0)
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.colors.colPrimary
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Mpris.formatTime(Mpris.lengthSec)
|
||||
font.pixelSize: Metrics.fontSize("smallest")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
width: 150
|
||||
height: 50
|
||||
radius: Metrics.radius("verylarge")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
// Service bindings
|
||||
readonly property bool wifiEnabled: Network.wifiEnabled
|
||||
readonly property bool hasActive: Network.active !== null
|
||||
readonly property string iconName: Network.icon
|
||||
readonly property string titleText: Network.label
|
||||
readonly property string statusText: Network.status
|
||||
|
||||
// Icon background
|
||||
StyledRect {
|
||||
id: iconBg
|
||||
width: 50
|
||||
height: 50
|
||||
radius: Metrics.radius("large")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
|
||||
color: {
|
||||
if (!wifiEnabled)
|
||||
return Appearance.m3colors.m3surfaceContainerHigh;
|
||||
if (hasActive)
|
||||
return Appearance.m3colors.m3primaryContainer;
|
||||
return Appearance.m3colors.m3secondaryContainer;
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: iconName
|
||||
iconSize: Metrics.iconSize(35)
|
||||
}
|
||||
}
|
||||
|
||||
// Labels
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: iconBg.right
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: titleText
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
elide: Text.ElideRight
|
||||
width: root.width - iconBg.width - 30
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: statusText
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
width: root.width - iconBg.width - 30
|
||||
}
|
||||
}
|
||||
|
||||
// Interaction
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Network.toggleWifi()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool nightTime
|
||||
width: 200
|
||||
height: 80
|
||||
radius: Metrics.radius("childish")
|
||||
color: !nightTime ? Appearance.m3colors.m3surfaceContainer : Appearance.m3colors.m3paddingContainer
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.margins: 0
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize(35)
|
||||
icon: "coffee"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
nightTime = !nightTime;
|
||||
nightTime ? Quickshell.execDetached(["gammastep", "-O", "4000"]) : Quickshell.execDetached(["killall", "gammastep"]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.functions
|
||||
import qs.modules.interface.notifications
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
Layout.fillWidth: true
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer1
|
||||
property bool dndActive: Config.runtime.notifications.doNotDisturb
|
||||
|
||||
function toggleDnd() {
|
||||
Config.updateKey("notifications.doNotDisturb", !dndActive);
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
id: clearButton
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.bottomMargin: Metrics.margin(10)
|
||||
anchors.rightMargin: Metrics.margin(10)
|
||||
icon: "clear_all"
|
||||
text: "Clear"
|
||||
implicitHeight: 40
|
||||
implicitWidth: 100
|
||||
secondary: true
|
||||
|
||||
onClicked: {
|
||||
for (let i = 0; i < NotifServer.history.length; i++) {
|
||||
let n = NotifServer.history[i];
|
||||
if (n?.notification) n.notification.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
id: silentButton
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.bottomMargin: Metrics.margin(10)
|
||||
anchors.rightMargin: clearButton.implicitWidth + Metrics.margin(15)
|
||||
text: "Silent"
|
||||
icon: "do_not_disturb_on"
|
||||
implicitHeight: 40
|
||||
implicitWidth: 100
|
||||
secondary: true
|
||||
checkable: true
|
||||
checked: Config.runtime.notifications.doNotDisturb
|
||||
|
||||
onClicked: {
|
||||
toggleDnd()
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.bottomMargin: Metrics.margin(15)
|
||||
anchors.leftMargin: Metrics.margin(15)
|
||||
text: NotifServer.history.length + " Notifications"
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "No notifications"
|
||||
visible: NotifServer.history.length < 1
|
||||
font.pixelSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: notifList
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: clearButton.top
|
||||
anchors.margins: Metrics.margin(10)
|
||||
|
||||
clip: true
|
||||
spacing: Metrics.spacing(8)
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.vertical: ScrollBar { }
|
||||
|
||||
model: Config.runtime.notifications.enabled
|
||||
? NotifServer.history
|
||||
: []
|
||||
|
||||
delegate: NotificationChild {
|
||||
width: notifList.width
|
||||
title: model.summary
|
||||
body: model.body
|
||||
image: model.image || model.appIcon
|
||||
rawNotif: model
|
||||
buttons: model.actions.map((action) => ({
|
||||
"label": action.text,
|
||||
"onClick": () => action.invoke()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
readonly property bool isDark: Config.runtime.appearance.theme === "dark"
|
||||
property string themestatusicon: isDark ? "dark_mode" : "light_mode"
|
||||
|
||||
width: 200
|
||||
height: 80
|
||||
radius: Metrics.radius("childish")
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.margins: 0
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: Metrics.iconSize(35)
|
||||
icon: themestatusicon
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "global", "toggleTheme"]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Controls
|
||||
|
||||
|
||||
StyledSlider {
|
||||
id: volSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
value: sink ? sink.volume : 0
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
property real level: volSlider.value * 100
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink]
|
||||
}
|
||||
|
||||
property var sink: Pipewire.defaultAudioSink?.audio
|
||||
|
||||
|
||||
onMoved: {
|
||||
if (sink) sink.volume = value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user