feat: dashboard show on hover, media and weather tabs added

This commit is contained in:
2026-09-30 14:22:22 +02:00
parent d259c6ffbe
commit 2c90339c77
97 changed files with 4480 additions and 990 deletions
+5 -5
View File
@@ -46,10 +46,10 @@ Item {
} }
function syncFromPenColor() { function syncFromPenColor() {
if (!drawing.drawingState) if (!drawing.screenState)
return; return;
const c = drawing.drawingState.penColor; const c = drawing.screenState.penColor;
if (c.hsvSaturation > 0) { if (c.hsvSaturation > 0) {
currentHue = c.hsvHue; currentHue = c.hsvHue;
@@ -86,7 +86,7 @@ Item {
currentHue = relative / arcSweep; currentHue = relative / arcSweep;
lastChromaticHue = currentHue; lastChromaticHue = currentHue;
drawing.drawingState.penColor = Qt.hsva(currentHue, drawing.drawingState.penColor.hsvSaturation, drawing.drawingState.penColor.hsvValue, drawing.drawingState.penColor.a); drawing.screenState.penColor = Qt.hsva(currentHue, drawing.screenState.penColor.hsvSaturation, drawing.screenState.penColor.hsvValue, drawing.screenState.penColor.a);
} }
implicitHeight: 180 implicitHeight: 180
@@ -112,7 +112,7 @@ Item {
root.syncFromPenColor(); root.syncFromPenColor();
} }
target: root.drawing.drawingState target: root.drawing.screenState
} }
Canvas { Canvas {
@@ -152,7 +152,7 @@ Item {
CustomRect { CustomRect {
anchors.centerIn: parent anchors.centerIn: parent
color: root.drawing?.drawingState.penColor color: root.drawing?.screenState.penColor
implicitHeight: implicitWidth implicitHeight: implicitWidth
implicitWidth: canvas.height - root.handleSize - Tokens.padding.extraLarge * 2 implicitWidth: canvas.height - root.handleSize - Tokens.padding.extraLarge * 2
radius: Tokens.rounding.full radius: Tokens.rounding.full
+2 -2
View File
@@ -1,12 +1,12 @@
import QtQuick import QtQuick
import qs.Helpers import qs.Services
Flickable { Flickable {
id: root id: root
property bool doneFakeFlick property bool doneFakeFlick
interactive: !Visibilities.getForActive().isDrawing interactive: !ShellState.forActive().isDrawing
maximumFlickVelocity: 3000 maximumFlickVelocity: 3000
rebound: Transition { rebound: Transition {
+2 -2
View File
@@ -1,12 +1,12 @@
import QtQuick import QtQuick
import qs.Helpers import qs.Services
ListView { ListView {
id: root id: root
property bool doneFakeFlick property bool doneFakeFlick
interactive: !Visibilities.getForActive()?.isDrawing interactive: !ShellState.forActive()?.isDrawing
maximumFlickVelocity: 3000 maximumFlickVelocity: 3000
rebound: Transition { rebound: Transition {
+7 -3
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Templates import QtQuick.Templates
import qs.Helpers import qs.Services
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -17,8 +17,12 @@ ScrollBar {
property bool shouldBeActive property bool shouldBeActive
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0 readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
enabled: !Visibilities.getForActive().isDrawing property bool shouldBeActive
implicitWidth: Tokens.padding.extraSmall * 2 property bool inverted
enabled: !ShellState.forActive().isDrawing
implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
contentItem: Item { contentItem: Item {
} }
+23 -21
View File
@@ -15,28 +15,33 @@ TextFieldBase {
readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius) readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real smallFontScale: smallFontSize / font.pointSize
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property int horizontalPadding: Tokens.padding.large
property int verticalPadding: Tokens.padding.large
property int radius: Tokens.rounding.small
property string supportingText
property string trailingIcon
property int smallFontSize: Tokens.font.size.small
property int type: CustomTextField.Outlined
property var validate // Regex or function
property bool emptyIsValid: true property bool emptyIsValid: true
property string errorText property string errorText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int horizontalPadding: Tokens.padding.large
property bool isError property bool isError
property string leadingIcon property string leadingIcon
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
property int radius: Tokens.rounding.small
readonly property real smallFontScale: smallFontSize / font.pointSize
property int smallFontSize: Tokens.font.size.small
property string supportingText
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
property string trailingIcon
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
property int type: CustomTextField.Outlined
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property var validate // Regex or function
bottomPadding: Tokens.padding.large + supportingTextOffset - filledOffset bottomPadding: verticalPadding + supportingTextOffset - filledOffset
leftPadding: horizontalPadding + leadingOffset leftPadding: horizontalPadding + leadingOffset
rightPadding: horizontalPadding + trailingOffset rightPadding: horizontalPadding + trailingOffset
topPadding: Tokens.padding.large + filledOffset topPadding: verticalPadding + filledOffset
background: Loader { background: Loader {
anchors.bottomMargin: root.supportingTextOffset anchors.bottomMargin: root.supportingTextOffset
@@ -212,12 +217,10 @@ TextFieldBase {
} }
} }
Behavior on strokeColor { Behavior on strokeColor {
CAnim { CAnim {}
}
} }
Behavior on strokeWidth { Behavior on strokeWidth {
Anim { Anim {}
}
} }
PathLine { PathLine {
@@ -292,8 +295,7 @@ TextFieldBase {
implicitHeight: root.activeFocus ? 2 : 1 implicitHeight: root.activeFocus ? 2 : 1
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {}
}
} }
} }
} }
+467
View File
@@ -0,0 +1,467 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {
id: root
required property list<var> values
property int xSteps: 24
property real yMin: NaN
property real yMax: NaN
property real yPaddingTop: 0
property real yPaddingBottom: 0
property int verticalLineStep: 0
property int horizontalLineStep: 0
property bool firstXLine: true
property bool lastXLine: false
property int minHorizontalLines: 0
property var xLabel: null
property var yLabel: null
property real smoothing: 0
property bool curveSmoothing: true
property real markerX: -1
property real markerGap: 2 // px gap between line and marker dot edge, each side
property color lineColor: "white"
property color gridColor: "white"
property color markerColor: "white"
property color labelColor: "white"
property real gridAlpha: 0.12
property real labelAlpha: 0.6
property real fillAlpha: 0.15
property real lineWidth: 2
property real markerDotRadius: 4
property real labelFontSize: 10
Canvas {
id: canvas
anchors.fill: parent
renderStrategy: Canvas.Immediate
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
function strokeCurve(ctx, pts) {
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
if (root.curveSmoothing && pts.length > 2) {
const k = 1 / 6;
const n = pts.length;
for (let i = 0; i < n - 1; i++) {
const p0 = pts[Math.max(i - 1, 0)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(i + 2, n - 1)];
let c1x = p1.x + (p2.x - p0.x) * k;
let c1y = p1.y + (p2.y - p0.y) * k;
let c2x = p2.x - (p3.x - p1.x) * k;
let c2y = p2.y - (p3.y - p1.y) * k;
const minY = Math.min(p1.y, p2.y);
const maxY = Math.max(p1.y, p2.y);
c1y = Math.max(minY, Math.min(maxY, c1y));
c2y = Math.max(minY, Math.min(maxY, c2y));
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, p2.x, p2.y);
}
} else {
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i];
const p1 = pts[i + 1];
const cx = (p0.x + p1.x) * 0.5;
ctx.bezierCurveTo(cx, p0.y, cx, p1.y, p1.x, p1.y);
}
}
}
// Control points for segment i (pts[i] -> pts[i+1]), matching strokeCurve
function segmentControls(pts, i) {
const p0 = pts[Math.max(i - 1, 0)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(i + 2, pts.length - 1)];
if (root.curveSmoothing && pts.length > 2) {
const k = 1 / 6;
let c1x = p1.x + (p2.x - p0.x) * k;
let c1y = p1.y + (p2.y - p0.y) * k;
let c2x = p2.x - (p3.x - p1.x) * k;
let c2y = p2.y - (p3.y - p1.y) * k;
const minY = Math.min(p1.y, p2.y);
const maxY = Math.max(p1.y, p2.y);
c1y = Math.max(minY, Math.min(maxY, c1y));
c2y = Math.max(minY, Math.min(maxY, c2y));
return [c1x, c1y, c2x, c2y];
}
const cx = (p1.x + p2.x) * 0.5;
return [cx, p1.y, cx, p2.y];
}
// Point on the curve at a given x (x is monotonic per segment)
function pointAtX(pts, targetX) {
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const p1 = pts[j];
const p2 = pts[j + 1];
const [c1x, c1y, c2x, c2y] = segmentControls(pts, j);
let tlo = 0;
let thi = 1;
for (let it = 0; it < 24; it++) {
const tm = (tlo + thi) / 2;
const u = 1 - tm;
const x = u * u * u * p1.x + 3 * u * u * tm * c1x + 3 * u * tm * tm * c2x + tm * tm * tm * p2.x;
if (x < targetX)
tlo = tm;
else
thi = tm;
}
const t = (tlo + thi) / 2;
const u = 1 - t;
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
return { x: targetX, y };
}
// Part of the curve left/right of a cut x, including the cut point
function cutLeft(pts, targetX) {
if (targetX <= pts[0].x)
return [];
if (targetX >= pts[pts.length - 1].x)
return pts.slice();
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const left = pts.slice(0, j + 1);
if (targetX > pts[j].x)
left.push(pointAtX(pts, targetX));
return left;
}
function cutRight(pts, targetX) {
if (targetX >= pts[pts.length - 1].x)
return [];
if (targetX <= pts[0].x)
return pts.slice();
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const right = pts.slice(j + 1);
if (targetX < pts[j + 1].x)
right.unshift(pointAtX(pts, targetX));
return right;
}
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const W = width;
const H = height;
if (W < 2 || H < 2)
return;
const padLeft = root.labelFontSize * 3.4;
const padRight = 2;
const padTop = 2;
const padBottom = root.labelFontSize + 8;
const plot = {
x: padLeft,
y: padTop,
w: W - padLeft - padRight,
h: H - padTop - padBottom
};
if (plot.w < 2 || plot.h < 2)
return;
// Auto-fit: NaN yMin/yMax derive from the data, snapped to the
// horizontal step grid, honoring minHorizontalLines
let lo = root.yMin;
let hi = root.yMax;
const loAuto = Number.isNaN(lo);
const hiAuto = Number.isNaN(hi);
if (loAuto || hiAuto) {
let dmin = Infinity;
let dmax = -Infinity;
for (const v of root.values) {
if (v == null)
continue;
if (v < dmin)
dmin = v;
if (v > dmax)
dmax = v;
}
if (dmin === Infinity) {
dmin = 0;
dmax = 1;
}
const step = root.horizontalLineStep;
const minSteps = root.minHorizontalLines > 1 ? root.minHorizontalLines - 1 : 0;
if (step > 0) {
if (hiAuto) {
const baseLo = loAuto ? Math.floor(dmin / step) * step : lo;
const span = Math.max(Math.ceil((dmax - baseLo) / step), minSteps, 1);
lo = baseLo;
hi = baseLo + span * step;
} else {
const span = Math.max(Math.ceil((hi - dmin) / step), minSteps, 1);
lo = hi - span * step;
}
} else {
if (loAuto)
lo = dmin;
if (hiAuto)
hi = dmax;
if (hi <= lo)
hi = lo + 1;
}
}
const scaleBottom = lo - root.yPaddingBottom;
const scaleTop = hi + root.yPaddingTop;
const range = scaleTop - scaleBottom;
if (range <= 0 || root.xSteps < 1)
return;
let series = root.values;
if (root.smoothing > 0 && series.length > 0) {
const radius = Math.round(root.smoothing);
const raw = series;
series = raw.map((v, i) => {
if (v == null)
return null;
let sum = 0;
let n = 0;
for (let j = -radius; j <= radius; j++) {
const u = raw[i + j];
if (u != null) {
sum += u;
n++;
}
}
return n > 0 ? sum / n : null;
});
}
const xAt = i => plot.x + (i / root.xSteps) * plot.w;
const yAt = v => plot.y + (1 - (v - scaleBottom) / range) * plot.h;
const crisp = v => Math.floor(v) + 0.5;
// 1. Grid lines
ctx.strokeStyle = Qt.alpha(root.gridColor, root.gridAlpha);
ctx.lineWidth = 1;
if (root.verticalLineStep > 0) {
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
if (i === 0 && !root.firstXLine)
continue;
if (i === root.xSteps && !root.lastXLine)
continue;
const x = crisp(xAt(i));
ctx.beginPath();
ctx.moveTo(x, plot.y);
ctx.lineTo(x, H);
ctx.stroke();
}
}
// Lines extend down into yPaddingBottom, snapped to the grid
const vStart = lo - (root.yPaddingBottom > 0 ? Math.floor(root.yPaddingBottom / root.horizontalLineStep) * root.horizontalLineStep : 0);
if (root.horizontalLineStep > 0) {
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
const y = crisp(yAt(v));
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(plot.x + plot.w, y);
ctx.stroke();
}
}
// 2. Labels
ctx.fillStyle = Qt.alpha(root.labelColor, root.labelAlpha);
ctx.font = root.labelFontSize + "px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "top";
if (root.verticalLineStep > 0 && root.xLabel) {
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
if (i === 0 && !root.firstXLine)
continue;
if (i === root.xSteps && !root.lastXLine)
continue;
const text = root.xLabel(i);
if (text)
ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 4);
}
}
ctx.textBaseline = "bottom";
if (root.horizontalLineStep > 0 && root.yLabel) {
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
const text = root.yLabel(v);
if (text)
ctx.fillText(text, 2, yAt(v) - 2);
}
}
// 3. Curve, split into segments at null gaps
const segments = [];
let current = null;
for (let i = 0; i < series.length; i++) {
if (series[i] == null) {
if (current)
segments.push(current);
current = null;
continue;
}
const pt = {
x: xAt(i),
y: yAt(series[i])
};
if (!current)
current = [];
current.push(pt);
}
if (current)
segments.push(current);
ctx.lineCap = "round";
ctx.lineJoin = "round";
const hasMarker = root.markerX >= 0 && root.markerX <= root.xSteps;
const markerPx = hasMarker ? xAt(root.markerX) : 0;
let gapPx = 0;
if (hasMarker) {
// markerGap is measured from the dot edge, perpendicular to the
// curve; project onto x via the local curve slope
let slope = 0;
const seg = segments.find(s => s[0].x <= markerPx && markerPx <= s[s.length - 1].x);
if (seg && seg.length > 2 && markerPx - 1 >= seg[0].x && markerPx + 1 <= seg[seg.length - 1].x)
slope = (pointAtX(seg, markerPx + 1).y - pointAtX(seg, markerPx - 1).y) / 2;
gapPx = (root.markerDotRadius + root.markerGap) / Math.sqrt(1 + slope * slope);
}
for (const seg of segments) {
if (seg.length < 2)
continue;
// Fill stays continuous under the marker
ctx.fillStyle = Qt.alpha(root.lineColor, root.fillAlpha);
strokeCurve(ctx, seg);
ctx.lineTo(seg[seg.length - 1].x, plot.y + plot.h);
ctx.lineTo(seg[0].x, plot.y + plot.h);
ctx.closePath();
ctx.fill();
// Stroke has a gap around the marker
const pieces = hasMarker ? [cutLeft(seg, markerPx - gapPx), cutRight(seg, markerPx + gapPx)] : [seg];
for (const pts of pieces) {
if (pts.length < 2)
continue;
ctx.strokeStyle = root.lineColor;
ctx.lineWidth = root.lineWidth;
strokeCurve(ctx, pts);
ctx.stroke();
}
}
// 4. Marker: dashed vertical + dot
if (root.markerX >= 0 && root.markerX <= root.xSteps) {
const x = markerPx;
ctx.setLineDash([4, 4]);
ctx.strokeStyle = Qt.alpha(root.markerColor, 0.6);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(crisp(x), plot.y);
ctx.lineTo(crisp(x), H);
ctx.stroke();
ctx.setLineDash([]);
const i0 = Math.floor(root.markerX);
const v0 = series[i0];
const v1 = series[i0 + 1];
let y = null;
if (v0 != null && v1 != null)
y = yAt(v0 + (v1 - v0) * (root.markerX - i0));
else if (v0 != null)
y = yAt(v0);
if (y != null) {
ctx.fillStyle = root.markerColor;
ctx.beginPath();
ctx.arc(x, y, root.markerDotRadius, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
onValuesChanged: canvas.requestPaint()
onXStepsChanged: canvas.requestPaint()
onYMinChanged: canvas.requestPaint()
onYMaxChanged: canvas.requestPaint()
onYPaddingTopChanged: canvas.requestPaint()
onYPaddingBottomChanged: canvas.requestPaint()
onVerticalLineStepChanged: canvas.requestPaint()
onHorizontalLineStepChanged: canvas.requestPaint()
onFirstXLineChanged: canvas.requestPaint()
onLastXLineChanged: canvas.requestPaint()
onMinHorizontalLinesChanged: canvas.requestPaint()
onXLabelChanged: canvas.requestPaint()
onYLabelChanged: canvas.requestPaint()
onSmoothingChanged: canvas.requestPaint()
onCurveSmoothingChanged: canvas.requestPaint()
onMarkerXChanged: canvas.requestPaint()
onMarkerGapChanged: canvas.requestPaint()
onLineColorChanged: canvas.requestPaint()
onGridColorChanged: canvas.requestPaint()
onMarkerColorChanged: canvas.requestPaint()
onLabelColorChanged: canvas.requestPaint()
onGridAlphaChanged: canvas.requestPaint()
onLabelAlphaChanged: canvas.requestPaint()
onFillAlphaChanged: canvas.requestPaint()
onLineWidthChanged: canvas.requestPaint()
onMarkerDotRadiusChanged: canvas.requestPaint()
onLabelFontSizeChanged: canvas.requestPaint()
}
+37
View File
@@ -0,0 +1,37 @@
import QtQuick
import Quickshell
PersistentProperties {
required property ShellScreen modelData
reloadableId: "screenState"
// Drawer visibilities
property bool bar
property bool clipboard
property bool dashboard
property bool dock
property bool isDrawing
property bool launcher
property bool osd
property bool resources
property bool settings
property bool sidebar
// Dashboard state
property int dashboardTab
property date dashboardDate: new Date()
// Sidebar state
property int sidebarTab
property list<string> expandedNotifs: []
// Utilities state
property string recordingConfirmDelete
property bool recordingListExpanded
property string recordingMode
// Drawing state
property color penColor: "white"
property int penWidth: 4
}
+2 -2
View File
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import ZShell import ZShell
import ZShell.Config import ZShell.Config
import qs.Helpers import qs.Services
import qs.Services import qs.Services
MouseArea { MouseArea {
@@ -52,7 +52,7 @@ MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: !enabled ? undefined : Qt.PointingHandCursor cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing enabled: parent.enabled && !ShellState.forActive()?.isDrawing
hoverEnabled: true hoverEnabled: true
Behavior on stateOpacity { Behavior on stateOpacity {
+2 -1
View File
@@ -10,6 +10,7 @@ import ZShell
import qs.Components import qs.Components
import qs.Modules import qs.Modules
import qs.Helpers import qs.Helpers
import qs.Services
import qs.Paths import qs.Paths
import ZShell.Config import ZShell.Config
@@ -25,7 +26,7 @@ Singleton {
property alias server: server property alias server: server
function shouldThrottle(appName: string): bool { function shouldThrottle(appName: string): bool {
if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) if (props.dnd || ShellState.anySidebarOpen())
return false; return false;
const key = (appName || "unknown").trim().toLowerCase(); const key = (appName || "unknown").trim().toLowerCase();
+5 -10
View File
@@ -3,28 +3,23 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Internal import ZShell.Internal
import qs.Components
Item { Item {
id: root id: root
readonly property alias content: contentLoader.item readonly property alias content: contentLoader.item
readonly property PersistentProperties drawingState: PersistentProperties { required property ScreenState screenState
property color penColor: "white"
property int penWidth: 4
reloadableId: "drawingState"
}
required property PersistentProperties visibilities
Loader { Loader {
id: contentLoader id: contentLoader
active: root.visibilities.isDrawing active: root.screenState.isDrawing
anchors.fill: parent anchors.fill: parent
sourceComponent: StrokeCanvas { sourceComponent: StrokeCanvas {
penColor: root.drawingState.penColor penColor: root.screenState.penColor
penWidth: root.drawingState.penWidth penWidth: root.screenState.penWidth
} }
} }
} }
+75 -55
View File
@@ -19,7 +19,7 @@ Item {
required property ShellScreen screen required property ShellScreen screen
property bool singleGestureTriggered: false property bool singleGestureTriggered: false
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
required property PersistentProperties visibilities required property ScreenState screenState
function inBottomPanel(panel: Item, x: real, y: real): bool { function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -63,7 +63,7 @@ Item {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
dragThreshold: 0 dragThreshold: 0
enabled: !root.visibilities.isDrawing enabled: !root.screenState.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1 maximumPointCount: 1
minimumPointCount: 1 minimumPointCount: 1
@@ -76,48 +76,60 @@ Item {
onCentroidChanged: { onCentroidChanged: {
const x = centroid.position.x; const x = centroid.position.x;
const y = centroid.position.y; const y = centroid.position.y;
const dragX = x - centroid.pressPosition.x; const pressX = centroid.pressPosition.x;
const dragY = y - centroid.pressPosition.y; const pressY = centroid.pressPosition.y;
const dragX = x - pressX;
const dragY = y - pressY;
if (centroid.pressPosition.y >= root.screen.height - Config.bar.border && centroid.pressPosition.x > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold) if (pressY >= root.screen.height - Config.bar.border && pressX > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold)
root.visibilities.launcher = true; root.screenState.launcher = true;
if (root.singleGestureTriggered) if (root.singleGestureTriggered)
return; return;
if (root.geometry.insetTop(root.borderThickness) > centroid.pressPosition.y) { // if (root.geometry.insetTop(root.borderThickness) > pressY) {
if (dragY > 20) { // if (dragY > 20) {
root.visibilities.settings = true; // root.screenState.settings = true;
root.singleGestureTriggered = true; // root.singleGestureTriggered = true;
} else if (dragY < -20) { // } else if (dragY < -20) {
root.visibilities.settings = false; // root.screenState.settings = false;
root.singleGestureTriggered = true; // root.singleGestureTriggered = true;
} // }
} // }
if (centroid.pressPosition.y > root.screen.height - root.geometry.insetBottom(root.borderThickness) && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50) if (pressY > root.screen.height - root.geometry.insetBottom(root.borderThickness) && pressX < root.screen.width / 5 && dragY < -50)
root.visibilities.clipboard = true; root.screenState.clipboard = true;
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.geometry.insetTop(root.borderThickness) && centroid.pressPosition.x > root.screen.width / 5 && !root.visibilities.launcher) if (!Config.dock.hoverToReveal && pressY > root.screen.height - root.geometry.insetTop(root.borderThickness) && pressX > root.screen.width / 5 && !root.screenState.launcher)
if (dragY < -10) { if (dragY < -10) {
root.visibilities.dock = true; root.screenState.dock = true;
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
if (centroid.pressPosition.x > root.screen.width - Config.bar.border && centroid.pressPosition.y < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) { if (pressX > root.screen.width - Config.bar.border && pressY < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) {
root.visibilities.sidebar = true; root.screenState.sidebar = true;
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
if (centroid.pressPosition.x >= root.screen.width - Config.bar.border && centroid.pressPosition.y > (root.screen.height / 2) && dragX < -20) { if (pressX >= root.screen.width - Config.bar.border && pressY > (root.screen.height / 2) && dragX < -20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`); Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
if (centroid.pressPosition.x <= Config.bar.border && dragX > 20) { if (pressX <= Config.bar.border && dragX > 20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`); Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
if (root.inTopPanel(root.panels.dashboardWrapper, pressX, pressY) && root.withinPanelWidth(root.panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold) {
root.screenState.dashboard = true;
root.singleGestureTriggered = true;
} else if (dragY < -Config.dashboard.dragThreshold) {
root.screenState.dashboard = false;
root.singleGestureTriggered = true;
}
}
} }
} }
@@ -127,7 +139,7 @@ Item {
property bool setInitialPoint: false property bool setInitialPoint: false
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.visibilities.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded) enabled: root.screenState.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded)
onActiveChanged: { onActiveChanged: {
if (!active) { if (!active) {
@@ -178,12 +190,12 @@ Item {
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
cursorShape: root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined cursorShape: root.screenState.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
onHoveredChanged: { onHoveredChanged: {
if (!hovered) { if (!hovered) {
if (!root.osdShortcutActive) { if (!root.osdShortcutActive) {
root.visibilities.osd = false; root.screenState.osd = false;
root.panels.osd.hovered = false; root.panels.osd.hovered = false;
} }
@@ -193,13 +205,16 @@ Item {
if (Config.bar.autoHide || root.bar.fullscreen) if (Config.bar.autoHide || root.bar.fullscreen)
root.bar.isHovered = false; root.bar.isHovered = false;
if (root.screenState.dashboard && Config.dashboard.showOnHover)
root.screenState.dashboard = false;
} }
} }
onPointChanged: { onPointChanged: {
const x = point.position.x; const x = point.position.x;
const y = point.position.y; const y = point.position.y;
if (root.visibilities.isDrawing) { if (root.screenState.isDrawing) {
if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) { if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) {
root.panels.drawing.expand(); root.panels.drawing.expand();
root.drawing.content.hideHover(); root.drawing.content.hideHover();
@@ -210,7 +225,7 @@ Item {
return; return;
} }
if (!root.visibilities.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true)) if (!root.screenState.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true))
root.bar.isHovered = true; root.bar.isHovered = true;
if (root.bar.fullscreen && !root.bar.isHovered) if (root.bar.fullscreen && !root.bar.isHovered)
@@ -228,7 +243,7 @@ Item {
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y); const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
if (!root.osdShortcutActive) { if (!root.osdShortcutActive) {
root.visibilities.osd = showOsd; root.screenState.osd = showOsd;
root.panels.osd.hovered = showOsd; root.panels.osd.hovered = showOsd;
} else if (showOsd) { } else if (showOsd) {
root.osdShortcutActive = false; root.osdShortcutActive = false;
@@ -236,8 +251,13 @@ Item {
} }
} }
if (Config.dock.enable && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y)) if (Config.dock.enable && Config.dock.hoverToReveal && !root.screenState.dock && !root.screenState.launcher && root.inBottomPanel(root.panels.dock, x, y))
root.visibilities.dock = true; root.screenState.dock = true;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, x, y);
if (Config.dashboard.showOnHover)
root.screenState.dashboard = inDashboard;
if (root.geometry.barContains(x, y)) if (root.geometry.barContains(x, y))
root.bar.checkPopout(root.geometry.axisPos(x, y)); root.bar.checkPopout(root.geometry.axisPos(x, y));
@@ -246,16 +266,16 @@ Item {
Connections { Connections {
function onDashboardChanged() { function onDashboardChanged() {
if (root.visibilities.dashboard) { if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x) if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x)
root.visibilities.launcher = false; root.screenState.launcher = false;
root.visibilities.settings = false; root.screenState.settings = false;
root.visibilities.sidebar = false; root.screenState.sidebar = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} else { } else {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
@@ -263,12 +283,12 @@ Item {
} }
function onIsDrawingChanged() { function onIsDrawingChanged() {
if (!root.visibilities.isDrawing) if (!root.screenState.isDrawing)
root.drawing.clear(); root.drawing.clear();
} }
function onLauncherChanged() { function onLauncherChanged() {
if (!root.visibilities.launcher) { if (!root.screenState.launcher) {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
root.osdShortcutActive = false; root.osdShortcutActive = false;
root.utilitiesShortcutActive = false; root.utilitiesShortcutActive = false;
@@ -276,23 +296,23 @@ Item {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
root.visibilities.osd = false; root.screenState.osd = false;
root.panels.osd.hovered = false; root.panels.osd.hovered = false;
} }
} }
if (root.visibilities.launcher) { if (root.screenState.launcher) {
if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) { if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) {
root.visibilities.dashboard = false; root.screenState.dashboard = false;
} }
root.visibilities.dock = false; root.screenState.dock = false;
root.visibilities.settings = false; root.screenState.settings = false;
} }
} }
function onOsdChanged() { function onOsdChanged() {
if (root.visibilities.osd) { if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
root.osdShortcutActive = true; root.osdShortcutActive = true;
@@ -303,32 +323,32 @@ Item {
} }
function onResourcesChanged() { function onResourcesChanged() {
if (root.visibilities.resources && (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))) { if (root.screenState.resources && (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))) {
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} }
if (root.visibilities.resources) if (root.screenState.resources)
root.visibilities.settings = false; root.screenState.settings = false;
} }
function onSettingsChanged() { function onSettingsChanged() {
if (root.visibilities.settings) { if (root.screenState.settings) {
root.visibilities.resources = false; root.screenState.resources = false;
root.visibilities.dashboard = false; root.screenState.dashboard = false;
root.visibilities.sidebar = false; root.screenState.sidebar = false;
root.panels.popouts.hasCurrent = false; root.panels.popouts.hasCurrent = false;
root.visibilities.launcher = false; root.screenState.launcher = false;
} }
} }
function onSidebarChanged() { function onSidebarChanged() {
if (root.visibilities.sidebar) { if (root.screenState.sidebar) {
root.visibilities.dashboard = false; root.screenState.dashboard = false;
root.visibilities.settings = false; root.screenState.settings = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} }
} }
target: root.visibilities target: root.screenState
} }
} }
+16 -19
View File
@@ -42,7 +42,7 @@ Item {
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias toasts: toasts readonly property alias toasts: toasts
readonly property alias utilities: utilities readonly property alias utilities: utilities
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottomMargin: geometry.insetBottom(borderThickness) anchors.bottomMargin: geometry.insetBottom(borderThickness)
anchors.fill: parent anchors.fill: parent
@@ -64,7 +64,7 @@ Item {
id: resources id: resources
position: root.geometry.position position: root.geometry.position
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -85,7 +85,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
screen: root.screen screen: root.screen
sidebarOrSessionVisible: sidebar.visible sidebarOrSessionVisible: sidebar.visible
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -96,7 +96,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
drawing: root.drawingItem drawing: root.drawingItem
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
Popouts.ClipWrapper { Popouts.ClipWrapper {
@@ -122,7 +122,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
panels: root panels: root
sidebarPanel: sidebar sidebarPanel: sidebar
visibilities: root.visibilities screenState: root.screenState
} }
Launcher.Wrapper { Launcher.Wrapper {
@@ -132,7 +132,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
panels: root panels: root
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
Utils.Wrapper { Utils.Wrapper {
@@ -142,7 +142,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
popouts: popouts popouts: popouts
sidebar: sidebar sidebar: sidebar
visibilities: root.visibilities screenState: root.screenState
} }
Item { Item {
@@ -150,13 +150,11 @@ Item {
property real offsetScale: dashboard.shouldBeActive ? 0 : 1 property real offsetScale: dashboard.shouldBeActive ? 0 : 1
anchors.bottom: root.geometry.barOnLeft || root.geometry.barOnBottom ? parent.bottom : undefined anchors.top: parent.top
anchors.left: root.geometry.barOnLeft ? parent.left : undefined anchors.horizontalCenter: parent.horizontalCenter
anchors.right: root.geometry.barOnLeft ? undefined : parent.right
anchors.top: root.geometry.barOnBottom || root.geometry.barOnLeft ? undefined : parent.top
clip: true clip: true
implicitHeight: root.geometry.horizontal ? dashboard.implicitHeight * (1 - offsetScale) : dashboard.implicitHeight implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: root.geometry.horizontal ? dashboard.implicitWidth : dashboard.implicitWidth * (1 - offsetScale) implicitWidth: dashboard.implicitWidth
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {
@@ -169,8 +167,7 @@ Item {
id: dashboard id: dashboard
offsetScale: dashboardWrapper.offsetScale offsetScale: dashboardWrapper.offsetScale
position: root.geometry.position screenState: root.screenState
visibilities: root.visibilities
} }
} }
@@ -181,7 +178,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: notifications.bottom anchors.top: notifications.bottom
panels: root panels: root
visibilities: root.visibilities screenState: root.screenState
} }
Item { Item {
@@ -197,7 +194,7 @@ Item {
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
panels: root panels: root
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -208,7 +205,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
panels: root panels: root
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
Clipboard.Wrapper { Clipboard.Wrapper {
@@ -217,6 +214,6 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
+36 -53
View File
@@ -50,16 +50,17 @@ CustomWindow {
readonly property alias menuRegion: regions.menuPopoutRegion readonly property alias menuRegion: regions.menuPopoutRegion
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
readonly property ScreenState screenState: ShellState.forScreen(screen)
readonly property real sdfBorderOffset: 2 * fsTransitionProg readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg)) readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg))
property color surfaceColor: Colors.tPalette.m3surface property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: screenState.launcher || screenState.settings || screenState.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent" color: "transparent"
contentItem.focus: true contentItem.focus: true
mask: visibilities.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions) mask: screenState.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
name: "Bar" name: "Bar"
Behavior on fsTransitionProg { Behavior on fsTransitionProg {
@@ -73,26 +74,26 @@ CustomWindow {
contentItem.Keys.onEscapePressed: { contentItem.Keys.onEscapePressed: {
if (Config.bar.autoHide) if (Config.bar.autoHide)
visibilities.bar = false; screenState.bar = false;
visibilities.launcher = false; screenState.launcher = false;
visibilities.sidebar = false; screenState.sidebar = false;
visibilities.dashboard = false; screenState.dashboard = false;
visibilities.osd = false; screenState.osd = false;
visibilities.settings = false; screenState.settings = false;
visibilities.resources = false; screenState.resources = false;
visibilities.dock = false; screenState.dock = false;
visibilities.clipboard = false; screenState.clipboard = false;
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
onHasFullscreenChanged: { onHasFullscreenChanged: {
visibilities.launcher = false; screenState.launcher = false;
visibilities.sidebar = false; screenState.sidebar = false;
visibilities.dashboard = false; screenState.dashboard = false;
visibilities.osd = false; screenState.osd = false;
visibilities.settings = false; screenState.settings = false;
visibilities.resources = false; screenState.resources = false;
visibilities.clipboard = false; screenState.clipboard = false;
visibilities.dock = false; screenState.dock = false;
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
@@ -144,44 +145,26 @@ CustomWindow {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!Config.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1)) active: root.screenState.dock || root.screenState.resources || root.screenState.launcher || root.screenState.sidebar || (!Config.dashboard.showOnHover && root.screenState.dashboard) || root.screenState.settings || root.screenState.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!Config.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1))
windows: [root] windows: [root]
onCleared: { onCleared: {
visibilities.launcher = false; root.screenState.launcher = false;
visibilities.sidebar = false; root.screenState.sidebar = false;
visibilities.dashboard = false; root.screenState.dashboard = false;
visibilities.osd = false; root.screenState.osd = false;
visibilities.settings = false; root.screenState.settings = false;
visibilities.resources = false; root.screenState.resources = false;
visibilities.clipboard = false; root.screenState.clipboard = false;
visibilities.dock = false; root.screenState.dock = false;
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
} }
PersistentProperties {
id: visibilities
property bool bar
property bool clipboard
property bool dashboard
property bool dock
property bool isDrawing
property bool launcher
property bool notif: NotifServer.popups.length > 0
property bool osd
property bool resources
property bool settings
property bool sidebar
Component.onCompleted: Visibilities.load(root.screen, this)
}
Binding { Binding {
property: "bar" property: "bar"
target: visibilities target: root.screenState
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || (!Config.bar.hideWhenNotif && visibilities.notif) || visibilities.resources || visibilities.settings || bar.isHovered value: root.screenState.sidebar || root.screenState.dashboard || root.screenState.osd || (!Config.bar.hideWhenNotif && NotifServer.popups.length > 0) || root.screenState.resources || root.screenState.settings || bar.isHovered
when: Config.bar.autoHide when: Config.bar.autoHide
} }
@@ -348,7 +331,7 @@ CustomWindow {
anchors.fill: parent anchors.fill: parent
layer.enabled: true layer.enabled: true
visibilities: visibilities screenState: root.screenState
z: 2 z: 2
layer.effect: MultiEffect { layer.effect: MultiEffect {
@@ -389,7 +372,7 @@ CustomWindow {
panels: panels panels: panels
popouts: panels.popouts popouts: panels.popouts
screen: root.screen screen: root.screen
visibilities: visibilities screenState: root.screenState
Panels { Panels {
id: panels id: panels
@@ -399,7 +382,7 @@ CustomWindow {
drawingItem: drawing drawingItem: drawing
geometry: geometry geometry: geometry
screen: root.screen screen: root.screen
visibilities: visibilities screenState: root.screenState
clipboard.transform: Matrix4x4 { clipboard.transform: Matrix4x4 {
matrix: clipboardBg.deformMatrix matrix: clipboardBg.deformMatrix
@@ -442,14 +425,14 @@ CustomWindow {
anchors.bottom: geometry.barOnBottom ? parent.bottom : undefined anchors.bottom: geometry.barOnBottom ? parent.bottom : undefined
anchors.left: parent.left anchors.left: parent.left
anchors.top: geometry.barOnBottom ? undefined : parent.top anchors.top: geometry.barOnBottom ? undefined : parent.top
enabled: !visibilities.isDrawing enabled: !screenState.isDrawing
fullscreen: root.hasFullscreen fullscreen: root.hasFullscreen
height: geometry.horizontal ? implicitHeight : parent.height height: geometry.horizontal ? implicitHeight : parent.height
popouts: panels.popouts popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper popoutsWrapper: panels.popoutsWrapper
position: geometry.position position: geometry.position
screen: root.screen screen: root.screen
visibilities: visibilities screenState: root.screenState
width: geometry.horizontal ? parent.width : implicitWidth width: geometry.horizontal ? parent.width : implicitWidth
} }
} }
+2 -1
View File
@@ -119,7 +119,8 @@ ColumnLayout {
length: count length: count
}, () => null); }, () => null);
return forecast.slice(0, count); const start = Math.max(0, Weather.currentHourIndex);
return forecast.slice(start, start + count);
} }
ColumnLayout { ColumnLayout {
+9
View File
@@ -5,6 +5,15 @@ import Quickshell
Singleton { Singleton {
property var _regexCache: ({}) property var _regexCache: ({})
function percent(value: int): string {
// TRANSLATORS: %1 = a number
return qsTr("%1%").arg(value);
}
function percentOne(value: real): string {
return percent(Math.round(value * 100));
}
function testRegexList(filterList: list<string>, target: string): bool { function testRegexList(filterList: list<string>, target: string): bool {
const regexChecker = /^\^.*\$$/; const regexChecker = /^\^.*\$$/;
for (const filter of filterList) { for (const filter of filterList) {
-16
View File
@@ -1,16 +0,0 @@
pragma Singleton
import Quickshell
Singleton {
property var bars: new Map()
property var screens: new Map()
function getForActive(): PersistentProperties {
return screens.get(Hypr.focusedMonitor);
}
function load(screen: ShellScreen, visibilities: var): void {
screens.set(Hypr.monitorFor(screen), visibilities);
}
}
+28 -7
View File
@@ -3,25 +3,36 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
import ZShell import ZShell
import ZShell.Internal
import ZShell.Config import ZShell.Config
Singleton { Singleton {
id: root id: root
readonly property var cachedCities: new Map() readonly property var cachedCities: new Map()
property var cc
property string city
readonly property string description: cc?.weatherDesc ?? qsTr("No weather") readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
readonly property string feelsLike: formatTemp(cc?.feelsLikeC) readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
property list<var> forecast
property list<var> hourlyForecast
readonly property int humidity: cc?.humidity ?? 0 readonly property int humidity: cc?.humidity ?? 0
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
property string loc
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string temp: formatTemp(cc?.tempC) readonly property string temp: formatTemp(cc?.tempC)
readonly property real windSpeed: cc?.windSpeed ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0
readonly property CircularBuffer precipitationForecast: precipitationBuffer
property var cc
property string city
property list<var> forecast
property list<var> hourlyForecast
readonly property int currentHourIndex: {
const now = new Date();
for (let i = 0; i < hourlyForecast.length; i++) {
if (new Date(hourlyForecast[i].timestamp.replace("T", " ")) >= now)
return i;
}
return -1;
}
property string loc
function fetchCityFromCoords(coords: string): void { function fetchCityFromCoords(coords: string): void {
if (cachedCities.has(coords)) { if (cachedCities.has(coords)) {
@@ -112,11 +123,12 @@ Singleton {
forecast = forecastList; forecast = forecastList;
const hourlyList = []; const hourlyList = [];
const now = new Date(); const startOfToday = new Date();
startOfToday.setHours(0, 0, 0, 0);
for (let i = 0; i < json.hourly.time.length; i++) { for (let i = 0; i < json.hourly.time.length; i++) {
const time = new Date(json.hourly.time[i].replace("T", " ")); const time = new Date(json.hourly.time[i].replace("T", " "));
if (time < now) if (time < startOfToday)
continue; continue;
hourlyList.push({ hourlyList.push({
@@ -129,6 +141,10 @@ Singleton {
}); });
} }
hourlyForecast = hourlyList; hourlyForecast = hourlyList;
precipitationBuffer.clear();
for (const hour of hourlyList)
precipitationBuffer.push(hour.precipChance);
}); });
} }
@@ -258,6 +274,11 @@ Singleton {
onLocChanged: fetchWeatherData() onLocChanged: fetchWeatherData()
CircularBuffer {
id: precipitationBuffer
capacity: 24
}
Connections { Connections {
function onWeatherLocationChanged(): void { function onWeatherLocationChanged(): void {
root.reload(); root.reload();
+6 -5
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Modules.Bar.Components import qs.Modules.Bar.Components
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
@@ -17,7 +18,7 @@ GridLayout {
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property ScreenState screenState
function axisCenterOf(item: Item): real { function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2); const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
@@ -34,7 +35,7 @@ GridLayout {
return; return;
} }
if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings) if (screenState.sidebar || screenState.dashboard || screenState.resources || screenState.settings)
return; return;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) { if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
@@ -149,7 +150,7 @@ GridLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
Resources { Resources {
horizontal: root.horizontal horizontal: root.horizontal
visibilities: root.visibilities screenState: root.screenState
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
} }
@@ -173,7 +174,7 @@ GridLayout {
NotifBell { NotifBell {
horizontal: root.horizontal horizontal: root.horizontal
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities screenState: root.screenState
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
} }
@@ -187,7 +188,7 @@ GridLayout {
horizontal: root.horizontal horizontal: root.horizontal
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities screenState: root.screenState
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
} }
+3 -3
View File
@@ -21,9 +21,9 @@ Item {
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property string position required property string position
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeVisible: (!fullscreen && (!Config.bar.autoHide || visibilities.bar)) || isHovered readonly property bool shouldBeVisible: (!fullscreen && (!Config.bar.autoHide || screenState.bar)) || isHovered
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property ScreenState screenState
function checkPopout(pos: real): void { function checkPopout(pos: real): void {
(content.item as Bar)?.checkPopout(pos); (content.item as Bar)?.checkPopout(pos);
@@ -85,7 +85,7 @@ Item {
popouts: root.popouts popouts: root.popouts
popoutsWrapper: root.popoutsWrapper popoutsWrapper: root.popoutsWrapper
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
} }
+1 -1
View File
@@ -11,7 +11,7 @@ Item {
id: root id: root
required property Item bar required property Item bar
required property PersistentProperties visibilities required property ScreenState screenState
anchors.fill: parent anchors.fill: parent
+5 -5
View File
@@ -13,7 +13,7 @@ import qs.Services
WidgetBase { WidgetBase {
id: root id: root
readonly property color contentColor: visibilities.dashboard ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface readonly property color contentColor: screenState.dashboard ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
readonly property string effectiveFormat: { readonly property string effectiveFormat: {
let fmt = Config.general.dateFormat; let fmt = Config.general.dateFormat;
if (!root.horizontal) if (!root.horizontal)
@@ -44,10 +44,10 @@ WidgetBase {
} }
required property GridLayout loader required property GridLayout loader
required property Wrapper popouts required property Wrapper popouts
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.small * 2 : verticalColumn.implicitHeight + Tokens.padding.small * 2 readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
required property PersistentProperties visibilities required property ScreenState screenState
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: screenState.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: horizontal ? baseSize : size implicitHeight: horizontal ? baseSize : size
implicitWidth: horizontal ? size : baseSize implicitWidth: horizontal ? size : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
@@ -151,7 +151,7 @@ WidgetBase {
color: root.contentColor color: root.contentColor
onClicked: { onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard; root.screenState.dashboard = !root.screenState.dashboard;
} }
} }
} }
+6 -6
View File
@@ -11,17 +11,17 @@ WidgetBase {
id: root id: root
required property Wrapper popouts required property Wrapper popouts
required property PersistentProperties visibilities required property ScreenState screenState
color: visibilities.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: screenState.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.full radius: Tokens.rounding.full
MaterialIcon { MaterialIcon {
id: notificationCenterIcon id: notificationCenterIcon
anchors.centerIn: parent anchors.centerIn: parent
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.visibilities.sidebar ? 1 : 0 fill: root.screenState.sidebar ? 1 : 0
font.pointSize: Tokens.font.size.larger font.pointSize: Tokens.font.size.larger
text: NotifServer.list.length ? "\uf4fe" : "\ue7f4" text: NotifServer.list.length ? "\uf4fe" : "\ue7f4"
@@ -36,10 +36,10 @@ WidgetBase {
} }
StateLayer { StateLayer {
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: { onClicked: {
root.visibilities.sidebar = !root.visibilities.sidebar; root.screenState.sidebar = !root.screenState.sidebar;
} }
} }
} }
+12 -12
View File
@@ -12,18 +12,18 @@ import qs.Components
WidgetBase { WidgetBase {
id: root id: root
required property PersistentProperties visibilities required property ScreenState screenState
clip: true clip: true
color: visibilities.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: screenState.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: horizontal ? baseSize : gridLayout.implicitHeight + Tokens.padding.medium * 2 implicitHeight: horizontal ? baseSize : gridLayout.implicitHeight + Tokens.padding.medium * 2
implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.medium * 2 : baseSize implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.medium * 2 : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
StateLayer { StateLayer {
color: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: root.visibilities.resources = !root.visibilities.resources onClicked: root.screenState.resources = !root.screenState.resources
} }
GridLayout { GridLayout {
@@ -53,8 +53,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "memory" icon: "memory"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Cpu.percentage percentage: Cpu.percentage
warningThreshold: 95 warningThreshold: 95
} }
@@ -64,8 +64,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "memory_alt" icon: "memory_alt"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
percentage: Memory.percentage percentage: Memory.percentage
warningThreshold: 80 warningThreshold: 80
} }
@@ -75,8 +75,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "gamepad" icon: "gamepad"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
percentage: Gpu.percentage percentage: Gpu.percentage
} }
@@ -85,8 +85,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "developer_board" icon: "developer_board"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Gpu.memoryUsed / Gpu.memoryTotal percentage: Gpu.memoryUsed / Gpu.memoryTotal
} }
} }
+4 -4
View File
@@ -62,10 +62,10 @@ Item {
root.popouts.currentName = `traymenu${root.ind}`; root.popouts.currentName = `traymenu${root.ind}`;
root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x); root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x);
root.popouts.hasCurrent = true; root.popouts.hasCurrent = true;
if (visibilities.sidebar || visibilities.dashboard || visibilities.settings) { if (screenState.sidebar || screenState.dashboard || screenState.settings) {
visibilities.sidebar = false; screenState.sidebar = false;
visibilities.dashboard = false; screenState.dashboard = false;
visibilities.settings = false; screenState.settings = false;
} }
} }
} }
+2 -2
View File
@@ -14,7 +14,7 @@ Item {
readonly property int itemHeight: Config.clipboard.sizes.itemHeight readonly property int itemHeight: Config.clipboard.sizes.itemHeight
required property ShellScreen screen required property ShellScreen screen
required property PersistentProperties visibilities required property ScreenState screenState
implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing
implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin
@@ -49,7 +49,7 @@ Item {
Keys.onUpPressed: view.decrementCurrentIndex() Keys.onUpPressed: view.decrementCurrentIndex()
onAccepted: { onAccepted: {
ClipHistory.copy(view.currentItem.modelData); ClipHistory.copy(view.currentItem.modelData);
root.visibilities.clipboard = false; root.screenState.clipboard = false;
} }
} }
} }
+3 -3
View File
@@ -11,8 +11,8 @@ Item {
property int contentHeight property int contentHeight
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.clipboard && Config.clipboard.enabled readonly property bool shouldBeActive: screenState.clipboard && Config.clipboard.enabled
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Tokens.padding.small * 2 implicitHeight: content.implicitHeight + Tokens.padding.small * 2
@@ -35,7 +35,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
} }
+127 -28
View File
@@ -5,74 +5,135 @@ import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell
import qs.Components import qs.Components
Item { Item {
id: root id: root
required property PersistentProperties dashState
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
required property PersistentProperties visibilities readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
required property ScreenState screenState
readonly property var dashboardTabs: {
const allTabs = [
{
component: dashComponent,
iconName: "dashboard",
text: qsTr("Dashboard"),
enabled: true
},
{
component: weatherComponent,
iconName: "cloud",
text: qsTr("Weather"),
enabled: true
},
{
component: mediaComponent,
iconName: "queue_music",
text: qsTr("Media"),
enabled: true
},
// {
// component: testingComponent,
// iconName: "speed",
// text: qsTr("Testing"),
// enabled: true
// },
];
return allTabs.filter(tab => tab.enabled);
}
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {}
}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { Anim {}
} }
Tabs {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
anchors.margins: Tokens.padding.large
nonAnimWidth: root.nonAnimWidth - anchors.margins * 2
screenState: root.screenState
tabs: root.dashboardTabs
} }
ClippingRectangle { ClippingRectangle {
id: viewWrapper id: viewWrapper
anchors.bottom: parent.bottom anchors.top: tabs.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.small
color: "transparent" color: "transparent"
radius: Tokens.rounding.extraLarge - anchors.margins radius: Tokens.rounding.extraLarge - anchors.margins
Item { Flickable {
id: view id: view
readonly property int currentIndex: root.dashState?.currentTab readonly property int currentIndex: root.screenState.dashboardTab
readonly property Item currentItem: row.children[currentIndex] readonly property Item currentItem: {
repeater.count;
return repeater.itemAt(currentIndex);
}
anchors.fill: parent anchors.fill: parent
implicitHeight: currentItem.implicitHeight
implicitWidth: currentItem.implicitWidth implicitWidth: currentItem?.implicitWidth ?? 0
implicitHeight: currentItem?.implicitHeight ?? 0
contentX: currentItem?.x ?? 0
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
interactive: false
onContentXChanged: {
if (!moving || !currentItem)
return;
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
}
RowLayout { RowLayout {
id: row id: row
Pane { Repeater {
index: 0 id: repeater
sourceComponent: Dash { model: ScriptModel {
dashState: root.dashState values: root.dashboardTabs
visibilities: root.visibilities
}
}
}
}
} }
component Pane: Loader { delegate: Loader {
id: pane id: paneLoader
required property int index required property int index
required property var modelData
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
sourceComponent: modelData.component
Component.onCompleted: active = Qt.binding(() => { Component.onCompleted: active = Qt.binding(() => {
// Always keep current tab loaded if (index === view.currentIndex)
if (pane.index === view.currentIndex)
return true; return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
@@ -80,3 +141,41 @@ Item {
}) })
} }
} }
}
Component {
id: dashComponent
Dash {
screenState: root.screenState
}
}
Component {
id: weatherComponent
WeatherTab {}
}
Component {
id: mediaComponent
Media {
screenState: root.screenState
}
}
// Component {
// id: testingComponent
//
// WeatherTest {}
// }
Behavior on contentX {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
+5 -21
View File
@@ -11,10 +11,9 @@ import qs.Services
GridLayout { GridLayout {
id: root id: root
required property PersistentProperties dashState readonly property bool dashboardVisible: screenState.dashboard
readonly property bool dashboardVisible: visibilities.dashboard
property int radius: Tokens.rounding.medium property int radius: Tokens.rounding.medium
required property PersistentProperties visibilities required property ScreenState screenState
columnSpacing: Tokens.spacing.small columnSpacing: Tokens.spacing.small
rowSpacing: Tokens.spacing.small rowSpacing: Tokens.spacing.small
@@ -61,7 +60,7 @@ GridLayout {
User { User {
id: user id: user
dashState: root.dashState screenState: root.screenState
} }
} }
@@ -72,8 +71,7 @@ GridLayout {
Layout.row: 0 Layout.row: 0
radius: root.radius radius: root.radius
Weather { Weather {}
}
} }
Rect { Rect {
@@ -87,7 +85,7 @@ GridLayout {
Calendar { Calendar {
id: calendar id: calendar
dashState: root.dashState screenState: root.screenState
} }
} }
@@ -104,20 +102,6 @@ GridLayout {
} }
} }
CustomClippingRect {
Layout.column: 0
Layout.columnSpan: 5
Layout.fillWidth: true
Layout.preferredHeight: media.implicitHeight
Layout.row: 2
color: Colors.tPalette.m3surfaceContainer
radius: root.radius
Media {
id: media
}
}
component Rect: CustomRect { component Rect: CustomRect {
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
} }
+9 -9
View File
@@ -12,12 +12,12 @@ CustomMouseArea {
id: root id: root
property int activeGrid: 1 property int activeGrid: 1
required property PersistentProperties dashState required property ScreenState screenState
property bool initialized: false property bool initialized: false
property int month1 property int month1
property int month2 property int month2
readonly property int realCurrMonth: dashState.currentDate.getMonth() readonly property int realCurrMonth: screenState.dashboardDate.getMonth()
readonly property int realCurrYear: dashState.currentDate.getFullYear() readonly property int realCurrYear: screenState.dashboardDate.getFullYear()
property int year1 property int year1
property int year2 property int year2
@@ -43,9 +43,9 @@ CustomMouseArea {
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
if (event.angleDelta.y > 0) if (event.angleDelta.y > 0)
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1); root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
else if (event.angleDelta.y < 0) else if (event.angleDelta.y < 0)
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1); root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
} }
acceptedButtons: Qt.MiddleButton acceptedButtons: Qt.MiddleButton
@@ -61,7 +61,7 @@ CustomMouseArea {
initialized = true; initialized = true;
} }
onClicked: root.dashState.currentDate = new Date() onClicked: root.screenState.dashboardDate = new Date()
onRealCurrMonthChanged: handleDateChange() onRealCurrMonthChanged: handleDateChange()
onRealCurrYearChanged: handleDateChange() onRealCurrYearChanged: handleDateChange()
@@ -135,7 +135,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall padding: Tokens.padding.extraSmall
type: IconButton.Text type: IconButton.Text
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1) onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
} }
Item { Item {
@@ -158,7 +158,7 @@ CustomMouseArea {
} }
} }
onClicked: root.dashState.currentDate = new Date() onClicked: root.screenState.dashboardDate = new Date()
} }
Item { Item {
@@ -206,7 +206,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall padding: Tokens.padding.extraSmall
type: IconButton.Text type: IconButton.Text
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1) onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
} }
} }
-329
View File
@@ -1,329 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import QtQuick.Effects
import ZShell.Components
import ZShell.Services
import ZShell.Config
import qs.Modules.Dashboard.Dash.Media
import qs.Daemons
import qs.Components
import qs.Helpers
import qs.Services
Item {
id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
property real playerProgress: {
const active = Players.active;
return active?.length ? (active.position % active.length) / active.length : 0;
}
property int rowHeight: Tokens.padding.large + Config.dashboard.sizes.mediaProgressThickness + Tokens.spacing.small
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
Behavior on playerProgress {
Anim {
duration: Tokens.anim.durations.large
}
}
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
ServiceRef {
service: Audio.cava
}
BackgroundShapes {
anchors.fill: parent
}
Shape {
id: visualizer
readonly property int bars: Config.services.visualizerBars
property color color: Qt.alpha(Colors.palette.m3primary, 0.5)
property color fillColor: Qt.alpha(color, 0.1)
anchors.fill: parent
anchors.leftMargin: -(shape.strokeWidth / 2)
layer.enabled: true
asynchronous: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: shape
strokeColor: visualizer.color
fillColor: visualizer.fillColor
strokeWidth: 2
PathSvg {
path: shape.curvePath
}
property string curvePath: {
const values = Audio.cava.values;
const n = values.length;
if (n < 2)
return "";
const h = root.height + shape.strokeWidth / 2;
const w = root.width + shape.strokeWidth;
function x(i) {
return i * w / (n - 1);
}
function y(i) {
return h - (values[i] * Config.dashboard.sizes.mediaVisualizerSize) / 2;
}
let d = `M 0 ${h} `;
d += `L ${x(0)} ${y(0)} `;
for (let i = 0; i < n - 1; ++i) {
const x0 = x(i);
const y0 = y(i);
const x1 = x(i + 1);
const y1 = y(i + 1);
const prev = Math.max(0, i - 1);
const next = Math.min(n - 1, i + 2);
const c1x = x0 + (x1 - x(prev)) / 6;
const c1y = y0 + (y1 - y(prev)) / 6;
const c2x = x1 - (x(next) - x0) / 6;
const c2y = y1 - (y(next) - y0) / 6;
d += `C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x1} ${y1} `;
}
d += `L ${w} ${h} `;
d += `L 0 ${h} Z`;
return d;
}
}
}
CustomRect {
anchors.fill: visualizer
color: Qt.alpha(Colors.palette.m3shadow, 0.2)
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: visualizer
}
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.large
anchors.leftMargin: Tokens.padding.extraLarge
CoverVisualizer {
Layout.fillHeight: true
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.largeIncreased
Layout.rightMargin: Tokens.spacing.largeIncreased
spacing: Tokens.spacing.small
MarqueeText {
id: title
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
implicitWidth: parent.width - Tokens.padding.large * 4
}
MarqueeText {
id: album
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.small
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
implicitWidth: parent.width - Tokens.padding.large * 4
}
MarqueeText {
id: artist
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3secondary
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
implicitWidth: parent.width - Tokens.padding.large * 4
}
RowLayout {
id: positionSliderLayout
Layout.topMargin: Tokens.spacing.large
Layout.fillWidth: true
spacing: Tokens.spacing.small
TextMetrics {
id: timeMetrics
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
}
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
Layout.leftMargin: positionSliderLayout.implicitWidth / 4
Layout.rightMargin: positionSliderLayout.implicitWidth / 4
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
font.pointSize: Tokens.font.size.large
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
font.pointSize: Tokens.font.size.large
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
font.pointSize: Tokens.font.size.large
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
}
}
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import qs.Helpers
Row { Row {
id: root id: root
required property PersistentProperties dashState required property ScreenState screenState
padding: 20 padding: 20
spacing: 12 spacing: 12
+152
View File
@@ -0,0 +1,152 @@
import QtQuick
import QtQuick.Layouts
import M3Shapes
import ZShell.Config
import qs.Modules.Dashboard.Media
import qs.Components
import qs.Services
import qs.Helpers
Item {
id: root
property ScreenState screenState
implicitWidth: 1000
implicitHeight: 320
BackgroundShapes {
anchors.fill: parent
}
RowLayout {
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.extraLarge
CoverVisualizer {
Layout.fillHeight: true
implicitWidth: 250
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
state: Players.active ? "" : "noMedia"
states: State {
name: "noMedia"
PropertyChanges {
noMedia.opacity: 1
content.opacity: 0
}
}
transitions: [
Transition {
from: ""
SequentialAnimation {
Anim {
target: content
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
target: noMedia
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
to: ""
SequentialAnimation {
Anim {
target: noMedia
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
target: content
property: "opacity"
type: Anim.SlowEffects
}
}
}
]
Loader {
id: noMedia
anchors.centerIn: parent
anchors.horizontalCenterOffset: -Tokens.padding.extraLarge * 2
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialShape {
Layout.topMargin: (pathBounds().height - implicitSize) / 2
Layout.bottomMargin: (pathBounds().height - implicitSize) / 2 + Tokens.spacing.small
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3primaryContainer
implicitSize: icon.implicitHeight + Tokens.padding.extraLarge * 2
shape: MaterialShape.ClamShell
Behavior on color {
CAnim {}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "queue_music"
font.pointSize: Tokens.font.size.large
color: Colors.palette.m3onPrimaryContainer
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Nothing playing")
font.pointSize: Tokens.font.size.normal
}
CustomText {
text: qsTr("Play something for it to show up here!")
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
}
}
}
Loader {
id: content
anchors.fill: parent
asynchronous: true
active: opacity > 0
sourceComponent: RowLayout {
spacing: Tokens.spacing.extraLarge
Details {
Layout.fillWidth: true
}
LyricsAndSelector {
Layout.fillHeight: true
implicitWidth: 300
}
}
}
}
}
}
@@ -11,8 +11,8 @@ Item {
id: cover id: cover
anchors.centerIn: parent anchors.centerIn: parent
implicitHeight: Config.dashboard.sizes.mediaCoverArtSize implicitHeight: 200
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize implicitWidth: 200
shape.shape: MaterialShape.Cookie9Sided shape.shape: MaterialShape.Cookie9Sided
} }
} }
+194
View File
@@ -0,0 +1,194 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import ZShell.Components
import ZShell.Config
import qs.Components
import qs.Services
import qs.Helpers
ColumnLayout {
id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
spacing: Tokens.spacing.extraSmall
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackTitle ?? ""
font.pointSize: Tokens.font.size.large
elide: Text.ElideRight
animate: true
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackArtist || qsTr("Unknown artist")
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
elide: Text.ElideRight
animate: true
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackAlbum || qsTr("Unknown album")
color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.normal
elide: Text.ElideRight
animate: true
}
RowLayout {
Layout.topMargin: Tokens.spacing.extraLargeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.small
TextMetrics {
id: timeMetrics
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
font.pointSize: Tokens.font.size.normal
}
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
font.pointSize: Tokens.font.size.large
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
implicitWidth: Math.round(implicitHeight * 0.9)
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
font.pointSize: Tokens.font.size.extraLarge
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
font.pointSize: Tokens.font.size.extraLarge
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
font.pointSize: Tokens.font.size.extraLarge
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
font.pointSize: Tokens.font.size.large
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
implicitWidth: Math.round(implicitHeight * 0.9)
}
}
}
+317
View File
@@ -0,0 +1,317 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Effects
import qs.Services
import qs.Helpers
Item {
id: root
// Funny binding hack to make lyrics update
readonly property var _: {
const p = Players.active;
if (p)
Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length);
else
Lyrics.clearTrack();
}
readonly property real fadeAmount: 0.1
property list<string> lyricList: Lyrics.lyrics
layer.enabled: true
layer.effect: Mask {
maskSource: mask
Rectangle {
id: mask
layer.enabled: true
visible: false
implicitWidth: root.width
implicitHeight: root.height
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
color: Qt.alpha("black", 0)
position: 0
}
GradientStop {
color: Qt.alpha("black", 1)
position: root.fadeAmount
}
GradientStop {
color: Qt.alpha("black", 1)
position: 1 - root.fadeAmount
}
GradientStop {
color: Qt.alpha("black", 0)
position: 1
}
}
}
}
state: {
if (Lyrics.hasLyrics)
return "hasLyrics";
if (Lyrics.loading)
return "loading";
return "noLyrics";
}
states: [
State {
name: "loading"
PropertyChanges {
loadingIndicator.opacity: 1
lyrics.opacity: 0
noLyrics.opacity: 0
}
},
State {
name: "hasLyrics"
PropertyChanges {
loadingIndicator.opacity: 0
lyrics.opacity: 1
noLyrics.opacity: 0
}
},
State {
name: "noLyrics"
PropertyChanges {
loadingIndicator.opacity: 0
lyrics.opacity: 0
noLyrics.opacity: 1
}
}
]
transitions: [
Transition {
from: "loading"
SequentialAnimation {
Anim {
target: loadingIndicator
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [lyrics, noLyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
from: "hasLyrics"
SequentialAnimation {
Anim {
target: lyrics
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [loadingIndicator, noLyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
from: "noLyrics"
SequentialAnimation {
Anim {
target: noLyrics
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [loadingIndicator, lyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
}
]
Loader {
id: loadingIndicator
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.large
CustomRect {
Layout.alignment: Qt.AlignHCenter
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
color: Colors.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: shape
anchors.centerIn: parent
implicitSize: Math.round(300 / 5)
containsIcon: true // This removes the pentagon, which is not centered
}
}
CustomText {
text: qsTr("Loading lyrics...")
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Loader {
id: noLyrics
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "sentiment_sad"
font.pointSize: Tokens.font.size.extraLarge
color: Colors.palette.m3outline
}
CustomText {
text: qsTr("No lyrics found")
color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.normal
}
}
}
CustomListView {
id: lyrics
anchors.fill: parent
anchors.topMargin: parent.height * root.fadeAmount / 2
anchors.bottomMargin: parent.height * root.fadeAmount / 2
displayMarginBeginning: anchors.topMargin
displayMarginEnd: anchors.bottomMargin
model: root.lyricList
Component.onCompleted: {
currentIndex = Qt.binding(() => {
model; // Force update when lyrics change
return Lyrics.indexForTime(Players.active?.position ?? 0);
});
positionViewAtIndex(currentIndex, ListView.Center);
}
onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center))
highlightRangeMode: ListView.ApplyRange
highlightMoveDuration: Tokens.anim.durations.large
highlightMoveVelocity: -1
preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2
preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2
spacing: Tokens.spacing.small
opacity: 0
enabled: opacity > 0
delegate: CustomText {
id: lyric
required property string modelData
required property int index
property real effectScale: ListView.isCurrentItem ? 1 : 0
anchors.left: lyrics.contentItem.left
anchors.right: lyrics.contentItem.right
text: modelData || ". . ."
color: ListView.isCurrentItem ? Colors.palette.m3primary : mouse.containsMouse ? Colors.palette.m3onSurface : Colors.palette.m3outline
font.pointSize: Tokens.font.size.medium
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
layer.enabled: effectScale > 0
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Colors.palette.m3primary
shadowOpacity: 0.5 * lyric.effectScale
shadowBlur: 0.6 * lyric.effectScale
blur: 0.4 * lyric.effectScale
}
Behavior on effectScale {
Anim {
type: Anim.SlowEffects
}
}
MouseArea {
id: mouse
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
const p = Players.active;
if (p)
p.position = Lyrics.timeForIndex(lyric.index);
}
}
}
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
Behavior on lyricList {
SequentialAnimation {
Anim {
target: lyrics
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
PropertyAction {}
Anim {
target: lyrics
property: "opacity"
to: 1
type: Anim.SlowEffects
}
}
}
}
@@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
Item {
ColumnLayout {
id: layout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium
RowLayout {
Layout.bottomMargin: -Tokens.spacing.medium
spacing: Tokens.spacing.medium
z: 1
MaterialIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
text: "lyrics"
font.pointSize: Tokens.font.size.large
}
CustomText {
Layout.fillWidth: true
text: qsTr("Lyrics")
font.pointSize: Tokens.font.size.large
}
LyricsInfo {}
}
LyricList {
Layout.fillWidth: true
Layout.fillHeight: true
}
SplitButton {
Layout.alignment: Qt.AlignHCenter
type: SplitButton.Tonal
enabled: Players.list.length
active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null
menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData
menuItems: playerList.instances
fallbackIcon: "music_off"
fallbackText: qsTr("No players", "no media players active")
minLeftWidth: layout.width - expandBtn.implicitWidth - spacing
label.Layout.maximumWidth: minLeftWidth - iconLabel.implicitWidth - textRow.spacing - textRow.anchors.horizontalCenterOffset / 2 - horizontalPadding * 2
label.elide: Text.ElideRight
stateLayer.enabled: true
menuOnTop: true
Variants {
id: playerList
model: Players.list
PlayerItem {}
}
}
}
component PlayerItem: MenuItem {
required property MprisPlayer modelData
icon: modelData === Players.active ? "check" : ""
text: Players.getIdentity(modelData)
activeIcon: "animated_images"
}
}
+212
View File
@@ -0,0 +1,212 @@
import QtQuick
import QtQuick.Layouts
import ZShell
import ZShell.Blobs
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
property bool open
readonly property real padding: Tokens.padding.large
implicitWidth: btn.implicitWidth * 0.9
implicitHeight: btn.implicitHeight * 0.9
BlobGroup {
id: blobGroup
color: Colors.palette.m3surfaceContainerHighest
smoothing: Tokens.rounding.medium
cornerFill: false
Behavior on color {
CAnim {}
}
}
BlobRect {
id: btnRect
anchors.fill: parent
anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0
group: blobGroup
radius: Tokens.rounding.medium
Behavior on anchors.margins {
Anim {}
}
}
BlobRect {
id: rect
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: parent.width
implicitHeight: parent.height
group: blobGroup
radius: Tokens.rounding.medium
deformScale: 0.00001
states: State {
name: "open"
when: root.open
PropertyChanges {
rect.anchors.rightMargin: root.width - Tokens.spacing.small
rect.anchors.topMargin: -Tokens.padding.medium
rect.implicitWidth: Math.max(layout.implicitWidth, placeholder.implicitWidth) + root.padding * 2
rect.implicitHeight: Math.max(layout.implicitHeight, placeholder.implicitHeight) + root.padding * 2
content.opacity: 1
}
}
transitions: Transition {
Anim {
properties: "rightMargin,implicitWidth"
}
Anim {
properties: "topMargin,implicitHeight"
easing: Tokens.anim.expressiveFastSpatial
}
Anim {
property: "opacity"
type: Anim.DefaultEffects
}
}
Behavior on implicitWidth {
Anim {}
}
Behavior on implicitHeight {
Anim {}
}
Item {
id: content
anchors.fill: parent
clip: true
opacity: 0
state: Lyrics.loading || !Lyrics.hasLyrics ? "" : "hasLyrics"
states: State {
name: "hasLyrics"
PropertyChanges {
layout.opacity: 1
placeholder.opacity: 0
}
}
transitions: [
Transition {
from: "hasLyrics"
SequentialAnimation {
Anim {
target: layout
property: "opacity"
type: Anim.FastEffects
}
Anim {
target: placeholder
property: "opacity"
type: Anim.DefaultEffects
}
}
},
Transition {
to: "hasLyrics"
SequentialAnimation {
Anim {
target: placeholder
property: "opacity"
type: Anim.FastEffects
}
Anim {
target: layout
property: "opacity"
type: Anim.DefaultEffects
}
}
}
]
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall
opacity: 0
CustomText {
// TRANSLATORS: %1 = lyrics backend name, e.g. LRCLIB
text: qsTr("Backend: %1").arg(ZUtils.enumToString(Lyrics, "backend"))
color: Colors.palette.m3onSurfaceVariant
animate: true
}
CustomText {
Layout.maximumWidth: 1000 / 2
// TRANSLATORS: %1/%2/%3 = matched track title, artist and album
text: qsTr("Selected candidate: %1 | %2 | %3").arg(Lyrics.selectedCandidate.title).arg(Lyrics.selectedCandidate.artist).arg(Lyrics.selectedCandidate.album)
color: Colors.palette.m3onSurfaceVariant
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
animate: true
}
CustomText {
// TRANSLATORS: %1 = lyric timing offset; ms is the millisecond unit, leave untranslated
text: qsTr("Offset: %1 ms").arg(Lyrics.offset)
color: Colors.palette.m3onSurfaceVariant
animate: true
}
}
Item {
id: placeholder
anchors.centerIn: parent
implicitWidth: placeholderText.implicitWidth
implicitHeight: placeholderText.implicitHeight
CustomText {
id: placeholderText
text: Lyrics.loading ? qsTr("Loading...") : qsTr("No lyrics found")
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
animate: true
}
}
}
}
MouseArea {
id: btn
anchors.centerIn: parent
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Tokens.padding.extraSmall * 2
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: root.open = !root.open
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "more_vert"
font.pointSize: Tokens.font.size.large
}
}
}
+88 -142
View File
@@ -1,23 +1,24 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import qs.Components
import ZShell.Config
import qs.Helpers
import qs.Modules
import Quickshell
import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Layouts
import QtQuick.Templates
import Quickshell
import ZShell.Config
import qs.Components
import qs.Services import qs.Services
Item { Item {
id: root id: root
readonly property alias count: bar.count
required property real nonAnimWidth required property real nonAnimWidth
required property PersistentProperties state required property ScreenState screenState
required property var tabs
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight readonly property alias count: bar.count
readonly property int tabIndicatorSpacing: Tokens.spacing.small
implicitHeight: bar.implicitHeight + bar.anchors.topMargin + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
TabBar { TabBar {
id: bar id: bar
@@ -25,85 +26,99 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: root.tabIndicatorSpacing
currentIndex: root.screenState.dashboardTab
onCurrentIndexChanged: root.screenState.dashboardTab = currentIndex
implicitHeight: contentHeight
background: null background: null
currentIndex: root.state.currentTab contentItem: RowLayout {
spacing: 0
onCurrentIndexChanged: root.state.currentTab = currentIndex Repeater {
model: bar.contentModel
Tab { }
iconName: "dashboard"
text: qsTr("Dashboard")
} }
Tab { Repeater {
iconName: "queue_music" model: ScriptModel {
text: qsTr("Media") values: root.tabs
} }
Tab { delegate: Tab {
iconName: "speed" required property var modelData
text: qsTr("Performance")
}
Tab { iconName: modelData.iconName
iconName: "cloud" text: modelData.text
text: qsTr("Weather") }
} }
// Tab {
// iconName: "workspaces"
// text: qsTr("Workspaces")
// }
} }
Item { Item {
id: indicator id: indicator
anchors.top: bar.bottom anchors.top: bar.bottom
clip: true anchors.topMargin: root.tabIndicatorSpacing
implicitHeight: 40
implicitWidth: bar.currentItem.implicitWidth implicitWidth: {
const tab = bar.currentItem;
if (tab)
return tab.implicitWidth;
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
return width;
}
implicitHeight: 3
x: { x: {
const tab = bar.currentItem; const tab = bar.currentItem;
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; const tabWidth = tab?.implicitWidth ?? width;
return width * bar.currentIndex + (width - tabWidth) / 2;
}
clip: true
CustomRect {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight * 2
color: Colors.palette.m3primary
radius: Tokens.rounding.full
}
Behavior on x {
Anim {}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { Anim {}
}
}
Behavior on x {
Anim {
}
}
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
color: Colors.palette.m3primary
implicitHeight: parent.implicitHeight * 2
radius: Tokens.rounding.full
} }
} }
CustomRect { CustomRect {
id: separator id: separator
anchors.top: indicator.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: indicator.bottom
color: Colors.palette.m3outlineVariant
implicitHeight: 1 implicitHeight: 1
color: Colors.palette.m3outlineVariant
} }
component Tab: TabButton { component Tab: TabButton {
id: tab id: tab
readonly property bool current: TabBar.tabBar.currentItem === this
required property string iconName required property string iconName
readonly property bool current: TabBar.tabBar.currentItem === this
Layout.fillWidth: true
Layout.preferredWidth: 1 // Uniform width across all tabs
implicitWidth: implicitContentWidth
implicitHeight: implicitContentHeight
background: null background: null
contentItem: CustomMouseArea { contentItem: CustomMouseArea {
@@ -111,117 +126,47 @@ Item {
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
if (event.angleDelta.y < 0) if (event.angleDelta.y < 0)
root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, bar.count - 1);
else if (event.angleDelta.y > 0) else if (event.angleDelta.y > 0)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0); root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
} }
cursorShape: Qt.PointingHandCursor
implicitHeight: icon.height + label.height
implicitWidth: Math.max(icon.width, label.width) implicitWidth: Math.max(icon.width, label.width)
implicitHeight: icon.height + label.height
onPressed: event => { hoverEnabled: true
root.state.currentTab = tab.TabBar.index; cursorShape: Qt.PointingHandCursor
const stateY = stateWrapper.y; onPressed: root.screenState.dashboardTab = tab.TabBar.index
rippleAnim.x = event.x;
rippleAnim.y = event.y - stateY;
const dist = (ox, oy) => ox * ox + oy * oy; StateLayer {
rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y))); id: stateLayer
rippleAnim.restart();
}
SequentialAnimation {
id: rippleAnim
property real radius
property real x
property real y
PropertyAction {
property: "x"
target: ripple
value: rippleAnim.x
}
PropertyAction {
property: "y"
target: ripple
value: rippleAnim.y
}
PropertyAction {
property: "opacity"
target: ripple
value: 0.08
}
Anim {
from: 0
properties: "implicitWidth,implicitHeight"
target: ripple
to: rippleAnim.radius * 2
}
Anim {
easing.type: Easing.BezierSpline
property: "opacity"
target: ripple
to: 0
}
}
ClippingRectangle {
id: stateWrapper
anchors.fill: undefined
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: "transparent" implicitHeight: parent.height + root.tabIndicatorSpacing * 2
implicitHeight: parent.height + 8 * 2
radius: Tokens.rounding.small
CustomRect { radius: Tokens.rounding.medium
id: stateLayer
anchors.fill: parent
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 onClicked: root.screenState.dashboardTab = tab.TabBar.index
Behavior on opacity {
Anim {
}
}
}
CustomRect {
id: ripple
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
opacity: 0
radius: Tokens.rounding.full
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
}
}
} }
MaterialIcon { MaterialIcon {
id: icon id: icon
anchors.bottom: label.top
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0 fill: tab.current ? 1 : 0
font.pointSize: 18 font.pointSize: Tokens.font.size.large
text: tab.iconName
Behavior on fill { Behavior on fill {
Anim { Anim {
type: Anim.DefaultEffects
} }
} }
} }
@@ -229,10 +174,11 @@ Item {
CustomText { CustomText {
id: label id: label
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant anchors.bottom: parent.bottom
text: tab.text text: tab.text
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
} }
} }
} }
+145
View File
@@ -0,0 +1,145 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Services
import qs.Helpers
RowLayout {
id: root
spacing: Tokens.spacing.largeIncreased
readonly property list<var> forecastToday: Weather.hourlyForecast.slice(0, 25)
property var now: new Date()
property bool useTwelveHourClock: Config.services.useTwelveHourClock
property bool curveSmoothing: true
property real smoothing: 2
property real markerGap: Math.min(Math.max(Tokens.spacing.medium / 2, 0), 6)
readonly property real markerX: {
const d = root.now;
return d.getHours() + d.getMinutes() / 60;
}
function formatHour(h) {
if (root.useTwelveHourClock) {
const hr = h % 12 === 0 ? 12 : h % 12;
return hr + (h < 12 ? " AM" : " PM");
}
return h;
}
Timer {
interval: 60000
repeat: true
running: true
onTriggered: root.now = new Date()
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: root.width / 2
RowLayout {
Layout.fillWidth: true
MaterialIcon {
text: "device_thermostat"
font.pointSize: Tokens.font.size.large
color: Colors.palette.m3secondary
}
CustomText {
text: qsTr("Temperature")
}
}
Item {
id: temperature
implicitHeight: 150
Layout.fillWidth: true
LineGraph {
id: tempGraph
anchors.fill: parent
values: root.forecastToday.map(e => e.tempC)
verticalLineStep: 3
xSteps: 24
horizontalLineStep: 10
minHorizontalLines: 4
yPaddingTop: horizontalLineStep
yPaddingBottom: horizontalLineStep
xLabel: i => root.formatHour(i)
yLabel: v => Math.round(v) + "°"
markerX: root.markerX
smoothing: root.smoothing
curveSmoothing: root.curveSmoothing
markerGap: root.markerGap
lineColor: Colors.palette.m3secondary
gridColor: Colors.palette.m3onSurface
markerColor: Colors.palette.m3secondary
labelColor: Colors.palette.m3onSurfaceVariant
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: root.width / 2
RowLayout {
Layout.fillWidth: true
MaterialIcon {
text: "water_drop"
font.pointSize: Tokens.font.size.large
color: Colors.palette.m3tertiary
}
CustomText {
text: qsTr("Chance of precipitation")
}
}
Item {
id: precipitation
Layout.fillWidth: true
implicitHeight: 150
LineGraph {
id: graph
anchors.fill: parent
values: root.forecastToday.map(e => e?.precipChance ?? null)
xSteps: 24
yMin: 0
yMax: 100
yPaddingTop: 20
verticalLineStep: 3
lastXLine: false
horizontalLineStep: 20
xLabel: i => root.formatHour(i)
yLabel: v => v + "%"
markerX: root.markerX
smoothing: root.smoothing
curveSmoothing: root.curveSmoothing
markerGap: root.markerGap
lineColor: Colors.palette.m3tertiary
gridColor: Colors.palette.m3onSurface
markerColor: Colors.palette.m3tertiary
labelColor: Colors.palette.m3onSurfaceVariant
}
}
}
}
+259
View File
@@ -0,0 +1,259 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Modules.Dashboard.Weather
import qs.Services
import qs.Helpers
Item {
id: root
readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null
implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840
implicitHeight: layout.implicitHeight
Component.onCompleted: Weather.reload()
ColumnLayout {
id: layout
anchors.fill: parent
spacing: Tokens.spacing.medium
RowLayout {
Layout.leftMargin: Tokens.padding.large
Layout.rightMargin: Tokens.padding.large
Layout.fillWidth: true
Column {
spacing: Tokens.spacing.extraSmall
CustomText {
text: Weather.city || qsTr("Loading...")
font.pointSize: Tokens.font.size.large
color: Colors.palette.m3onSurface
}
CustomText {
text: new Date().toLocaleDateString(Qt.locale("en_US"), "dddd, MMMM d")
font.pointSize: Tokens.font.size.small
color: Colors.palette.m3onSurfaceVariant
}
}
Item {
Layout.fillWidth: true
}
Row {
spacing: Tokens.spacing.largeIncreased
WeatherStat {
icon: "wb_twilight"
label: qsTr("Sunrise")
value: Weather.sunrise
colour: Colors.palette.m3tertiary
}
WeatherStat {
icon: "bedtime"
label: qsTr("Sunset")
value: Weather.sunset
colour: Colors.palette.m3tertiary
}
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: bigInfoRow.implicitHeight + bigInfoRow.anchors.margins * 2
radius: Tokens.rounding.large
color: Colors.tPalette.m3surfaceContainer
Graphs {
id: bigInfoRow
anchors.fill: parent
anchors.margins: Tokens.padding.medium
}
}
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.medium
DetailCard {
icon: Weather.icon
label: qsTr("Temperature")
value: Weather.temp
colour: Colors.palette.m3primary
}
DetailCard {
icon: "thermostat"
label: qsTr("Feels like", "apparent temperature")
value: Weather.feelsLike
colour: Colors.palette.m3primary
}
DetailCard {
icon: "water_drop"
label: qsTr("Humidity")
value: Strings.percent(Weather.humidity)
colour: Colors.palette.m3secondary
}
DetailCard {
icon: "air"
label: qsTr("Wind")
value: Weather.windSpeed ? qsTr("%1 km/h").arg(Weather.windSpeed) : "--"
colour: Colors.palette.m3tertiary
}
}
CustomText {
Layout.topMargin: Tokens.spacing.medium
Layout.leftMargin: Tokens.padding.medium
visible: forecastRepeater.count > 0
text: qsTr("7-day forecast")
font.pointSize: Tokens.font.size.normal
color: Colors.palette.m3onSurface
}
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.medium
Repeater {
id: forecastRepeater
model: Weather.forecast
CustomRect {
id: forecastItem
required property int index
required property var modelData
Layout.fillWidth: true
implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.large
color: Colors.tPalette.m3surfaceContainer
ColumnLayout {
id: forecastItemColumn
anchors.centerIn: parent
spacing: Tokens.spacing.small
CustomText {
Layout.alignment: Qt.AlignHCenter
text: forecastItem.index === 0 ? qsTr("Today", "forecast column") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale("en_US"), "ddd")
font.pointSize: Tokens.font.size.small
color: Colors.palette.m3primary
}
CustomText {
Layout.topMargin: -Tokens.spacing.extraSmall
Layout.alignment: Qt.AlignHCenter
text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale("en_US"), "MMM d")
font.pointSize: Tokens.font.size.small
opacity: 0.7
color: Colors.palette.m3onSurfaceVariant
}
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: forecastItem.modelData.icon
font.pointSize: Tokens.font.size.extraLarge
color: Colors.palette.m3secondary
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: {
const min = Weather.formatTemp(forecastItem.modelData.minTempC, true);
const max = Weather.formatTemp(forecastItem.modelData.maxTempC, true);
return qsTr("%1 / %2", "min/max temperature").arg(min).arg(max);
}
font.pointSize: Tokens.font.size.small
color: Colors.palette.m3tertiary
}
}
}
}
}
}
component DetailCard: CustomRect {
id: detailRoot
property string icon
property string label
property string value
property color colour
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer
Row {
anchors.centerIn: parent
spacing: Tokens.spacing.medium
MaterialIcon {
text: detailRoot.icon
color: detailRoot.colour
font.pointSize: Tokens.font.size.large
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
text: detailRoot.label
font.pointSize: Tokens.font.size.small
opacity: 0.7
horizontalAlignment: Text.AlignLeft
}
CustomText {
text: detailRoot.value
font.pointSize: Tokens.font.size.small
horizontalAlignment: Text.AlignLeft
}
}
}
}
component WeatherStat: Row {
id: weatherStat
property string icon
property string label
property string value
property color colour
spacing: Tokens.spacing.small
MaterialIcon {
text: weatherStat.icon
font.pointSize: Tokens.font.size.extraLarge
color: weatherStat.colour
}
Column {
CustomText {
text: weatherStat.label
font.pointSize: Tokens.font.size.small
color: Colors.palette.m3onSurfaceVariant
}
CustomText {
text: weatherStat.value
font.pointSize: Tokens.font.size.small
color: Colors.palette.m3onSurface
}
}
}
}
+7 -22
View File
@@ -8,24 +8,13 @@ import ZShell.Config
Item { Item {
id: root id: root
readonly property PersistentProperties dashState: PersistentProperties {
property date currentDate: new Date()
property int currentTab
reloadableId: "dashboardState"
}
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
required property real offsetScale required property real offsetScale
required property string position readonly property bool shouldBeActive: root.screenState.dashboard && Config.dashboard.enabled
readonly property bool shouldBeActive: root.visibilities.dashboard && Config.dashboard.enabled required property ScreenState screenState
required property PersistentProperties visibilities
anchors.bottom: position === "bottom" ? parent.bottom : undefined anchors.top: parent.top
anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0 anchors.topMargin: (-implicitHeight - 5) * offsetScale
anchors.left: position === "left" ? parent.left : undefined
anchors.leftMargin: position === "left" ? (-implicitWidth - 5) * offsetScale : 0
anchors.top: position === "top" ? parent.top : undefined
anchors.topMargin: position === "top" ? (-implicitHeight - 5) * offsetScale : 0
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 854 implicitWidth: content.implicitWidth || 854
opacity: 1 - offsetScale opacity: 1 - offsetScale
@@ -35,15 +24,11 @@ Item {
id: content id: content
active: root.shouldBeActive || root.visible active: root.shouldBeActive || root.visible
anchors.bottom: root.position === "top" ? parent.bottom : undefined anchors.bottom: parent.bottom
anchors.horizontalCenter: root.position !== "left" ? parent.horizontalCenter : undefined anchors.horizontalCenter: parent.horizontalCenter
anchors.right: root.position !== "left" ? undefined : parent.right
anchors.top: root.position === "bottom" ? parent.top : undefined
anchors.verticalCenter: root.position === "left" ? undefined : parent.verticalCenter
sourceComponent: Content { sourceComponent: Content {
dashState: root.dashState screenState: root.screenState
visibilities: root.visibilities
} }
} }
} }
@@ -8,6 +8,7 @@ import ZShell.Config
import qs.Paths import qs.Paths
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
import qs.Services
Item { Item {
id: root id: root
@@ -97,8 +98,8 @@ Item {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
const visibilities = Visibilities.getForActive(); const state = ShellState.forActive();
visibilities.settings = true; state.settings = true;
root.close() root.close()
} }
} }
+3 -3
View File
@@ -27,7 +27,7 @@ Item {
property var pendingCommitIds: [] property var pendingCommitIds: []
readonly property int rounding: Tokens.rounding.largeIncreased readonly property int rounding: Tokens.rounding.largeIncreased
required property ShellScreen screen required property ShellScreen screen
required property PersistentProperties visibilities required property ScreenState screenState
property var visualIds: [] property var visualIds: []
function beginVisualDrag(appId, modelData, item) { function beginVisualDrag(appId, modelData, item) {
@@ -198,7 +198,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
appListRoot: root appListRoot: root
appToplevel: modelData appToplevel: modelData
visibilities: root.visibilities screenState: root.screenState
visible: root.draggedAppId !== slot.appId visible: root.draggedAppId !== slot.appId
} }
@@ -314,7 +314,7 @@ Item {
appListRoot: root appListRoot: root
appToplevel: root.draggedModelData appToplevel: root.draggedModelData
enabled: false enabled: false
visibilities: root.visibilities screenState: root.screenState
} }
} }
} }
+3 -3
View File
@@ -19,7 +19,7 @@ CustomRect {
property real iconSize: implicitHeight - 20 property real iconSize: implicitHeight - 20
readonly property bool isSeparator: appToplevel?.appId === "__dock_separator__" readonly property bool isSeparator: appToplevel?.appId === "__dock_separator__"
property int lastFocused: -1 property int lastFocused: -1
required property PersistentProperties visibilities required property ScreenState screenState
implicitHeight: Config.dock.height implicitHeight: Config.dock.height
implicitWidth: isSeparator ? 1 : implicitHeight implicitWidth: isSeparator ? 1 : implicitHeight
@@ -62,12 +62,12 @@ CustomRect {
onClicked: { onClicked: {
if (appToplevel?.toplevels.length === 0) { if (appToplevel?.toplevels.length === 0) {
root.desktopEntry?.execute(); root.desktopEntry?.execute();
root.visibilities.dock = false; root.screenState.dock = false;
return; return;
} }
lastFocused = (lastFocused + 1) % appToplevel?.toplevels.length; lastFocused = (lastFocused + 1) % appToplevel?.toplevels.length;
appToplevel?.toplevels[lastFocused].activate(); appToplevel?.toplevels[lastFocused].activate();
root.visibilities.dock = false; root.screenState.dock = false;
} }
} }
+3 -3
View File
@@ -12,8 +12,8 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.dock && Config.dock.enable readonly property bool shouldBeActive: screenState.dock && Config.dock.enable
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
@@ -39,7 +39,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
panels: root.panels panels: root.panels
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
} }
+9 -9
View File
@@ -12,7 +12,7 @@ Item {
readonly property var colors1: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4"] readonly property var colors1: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4"]
readonly property var colors2: ["#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"] readonly property var colors2: ["#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"]
required property var drawing required property var drawing
required property var visibilities required property ScreenState screenState
required property Wrapper wrapper required property Wrapper wrapper
function syncFromPenColor() { function syncFromPenColor() {
@@ -20,17 +20,17 @@ Item {
return; return;
if (!saturationSlider.pressed) if (!saturationSlider.pressed)
saturationSlider.value = drawing.drawingState.penColor.hsvSaturation; saturationSlider.value = drawing.screenState.penColor.hsvSaturation;
if (!brightnessSlider.pressed) if (!brightnessSlider.pressed)
brightnessSlider.value = drawing.drawingState.penColor.hsvValue; brightnessSlider.value = drawing.screenState.penColor.hsvValue;
} }
function updatePenColorFromHsv() { function updatePenColorFromHsv() {
if (!drawing) if (!drawing)
return; return;
drawing.drawingState.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.drawingState.penColor.a); drawing.screenState.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.screenState.penColor.a);
} }
implicitHeight: column.height + Tokens.padding.small * 2 implicitHeight: column.height + Tokens.padding.small * 2
@@ -43,7 +43,7 @@ Item {
root.syncFromPenColor(); root.syncFromPenColor();
} }
target: root.drawing.drawingState target: root.drawing.screenState
} }
Column { Column {
@@ -133,9 +133,9 @@ Item {
multiplier: 1 multiplier: 1
orientation: Qt.Horizontal orientation: Qt.Horizontal
to: 45 to: 45
value: root.drawing.drawingState.penWidth value: root.drawing.screenState.penWidth
onMoved: root.drawing.drawingState.penWidth = value onMoved: root.drawing.screenState.penWidth = value
} }
ButtonRow { ButtonRow {
@@ -153,7 +153,7 @@ Item {
shapeMorph: true shapeMorph: true
text: "Exit" text: "Exit"
onClicked: root.visibilities.isDrawing = false onClicked: root.screenState.isDrawing = false
} }
IconTextButton { IconTextButton {
@@ -203,6 +203,6 @@ Item {
shapeMorph: true shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0 shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.drawing.drawingState.penColor = modelData onClicked: root.drawing.screenState.penColor = modelData
} }
} }
+3 -3
View File
@@ -15,8 +15,8 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
property bool pinned: false property bool pinned: false
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.isDrawing readonly property bool shouldBeActive: screenState.isDrawing
required property var visibilities required property ScreenState screenState
function collapse(): void { function collapse(): void {
if (pinned) if (pinned)
@@ -98,7 +98,7 @@ Item {
} }
sourceComponent: Content { sourceComponent: Content {
drawing: root.drawing drawing: root.drawing
visibilities: root.visibilities screenState: root.screenState
wrapper: root wrapper: root
} }
} }
+5 -5
View File
@@ -15,7 +15,7 @@ CustomListView {
property string displayText property string displayText
readonly property string requestedState: stateForText(search.text) readonly property string requestedState: stateForText(search.text)
required property SearchBar search required property SearchBar search
required property PersistentProperties visibilities required property ScreenState screenState
function resultsForText(text: string): var { function resultsForText(text: string): var {
switch (stateForText(text)) { switch (stateForText(text)) {
@@ -44,7 +44,7 @@ CustomListView {
} }
function syncDisplayText(): void { function syncDisplayText(): void {
if (visibilities.launcher && requestedState === displayState) if (screenState.launcher && requestedState === displayState)
displayText = search.text; displayText = search.text;
} }
@@ -55,7 +55,7 @@ CustomListView {
preferredHighlightBegin: 0 preferredHighlightBegin: 0
preferredHighlightEnd: height preferredHighlightEnd: height
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
state: visibilities.launcher ? requestedState : displayState state: screenState.launcher ? requestedState : displayState
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
CustomScrollBar.vertical: CustomScrollBar { CustomScrollBar.vertical: CustomScrollBar {
@@ -236,7 +236,7 @@ CustomListView {
id: appItem id: appItem
AppItem { AppItem {
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -277,6 +277,6 @@ CustomListView {
root.syncDisplayText(); root.syncDisplayText();
} }
target: root.visibilities target: root.screenState
} }
} }
+3 -3
View File
@@ -13,14 +13,14 @@ CarouselView {
readonly property var screen: QsWindow.window?.screen readonly property var screen: QsWindow.window?.screen
required property SearchBar search required property SearchBar search
readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" ")) readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" "))
required property var visibilities required property ScreenState screenState
currentIndex: { currentIndex: {
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent); const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
return idx >= 0 ? idx : 0; return idx >= 0 ? idx : 0;
} }
delegateProperties: ({ delegateProperties: ({
visibilities: root.visibilities screenState: root.screenState
}) })
focalWidth: Config.launcher.sizes.wallpaperWidth focalWidth: Config.launcher.sizes.wallpaperWidth
glideDuration: 250 glideDuration: 250
@@ -32,7 +32,7 @@ CarouselView {
return 0; return 0;
const barMargins = Config.bar.border * 2; const barMargins = Config.bar.border * 2;
let margins = Math.round(screen.width / 6); let margins = Math.round(screen.width / 6);
if (visibilities.utilities || visibilities.sidebar) if (screenState.utilities || screenState.sidebar)
margins = Config.sidebar.sizes.width; margins = Config.sidebar.sizes.width;
return screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4; return screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4;
} }
+7 -7
View File
@@ -15,7 +15,7 @@ Item {
readonly property int padding: Tokens.padding.largeIncreased / 4 readonly property int padding: Tokens.padding.largeIncreased / 4
required property var panels required property var panels
readonly property int rounding: Tokens.rounding.largeIncreased readonly property int rounding: Tokens.rounding.largeIncreased
required property PersistentProperties visibilities required property ScreenState screenState
implicitHeight: search.height + listWrapper.height + padding * 2 implicitHeight: search.height + listWrapper.height + padding * 2
implicitWidth: listWrapper.width + padding * 2 implicitWidth: listWrapper.width + padding * 2
@@ -38,7 +38,7 @@ Item {
panels: root.panels panels: root.panels
rounding: root.rounding rounding: root.rounding
search: search search: search
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -55,7 +55,7 @@ Item {
Component.onCompleted: forceActiveFocus() Component.onCompleted: forceActiveFocus()
Keys.onDownPressed: list.currentList?.decrementCurrentIndex() Keys.onDownPressed: list.currentList?.decrementCurrentIndex()
Keys.onEscapePressed: root.visibilities.launcher = false Keys.onEscapePressed: root.screenState.launcher = false
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
if (event.key === Qt.Key_J) { if (event.key === Qt.Key_J) {
@@ -81,7 +81,7 @@ Item {
if (Colors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent) if (Colors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent)
Wallpapers.previewColorLock = true; Wallpapers.previewColorLock = true;
Wallpapers.setWallpaper(currentItem.modelData.path); Wallpapers.setWallpaper(currentItem.modelData.path);
root.visibilities.launcher = false; root.screenState.launcher = false;
} else if (text.startsWith(Config.launcher.actionPrefix)) { } else if (text.startsWith(Config.launcher.actionPrefix)) {
if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
currentItem.onClicked(); currentItem.onClicked();
@@ -89,18 +89,18 @@ Item {
currentItem.modelData.onClicked(list.currentList); currentItem.modelData.onClicked(list.currentList);
} else { } else {
Apps.launch(currentItem.modelData); Apps.launch(currentItem.modelData);
root.visibilities.launcher = false; root.screenState.launcher = false;
} }
} }
} }
Connections { Connections {
function onLauncherChanged(): void { function onLauncherChanged(): void {
if (!root.visibilities.launcher) if (!root.screenState.launcher)
search.text = ""; search.text = "";
} }
target: root.visibilities target: root.screenState
} }
} }
} }
+5 -5
View File
@@ -20,7 +20,7 @@ Item {
required property int rounding required property int rounding
required property SearchBar search required property SearchBar search
readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `)
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -28,7 +28,7 @@ Item {
state: showWallpapers ? "wallpapers" : "apps" state: showWallpapers ? "wallpapers" : "apps"
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.visibilities.launcher enabled: root.screenState.launcher
Anim { Anim {
duration: Tokens.anim.durations.small duration: Tokens.anim.durations.small
@@ -36,7 +36,7 @@ Item {
} }
} }
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.visibilities.launcher enabled: root.screenState.launcher
Anim { Anim {
duration: Tokens.anim.durations.small duration: Tokens.anim.durations.small
@@ -99,7 +99,7 @@ Item {
sourceComponent: AppList { sourceComponent: AppList {
search: root.search search: root.search
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -115,7 +115,7 @@ Item {
sourceComponent: CarouselView { sourceComponent: CarouselView {
panels: root.panels panels: root.panels
search: root.search search: root.search
visibilities: root.visibilities screenState: root.screenState
} }
} }
+2 -2
View File
@@ -12,7 +12,7 @@ Item {
id: root id: root
required property DesktopEntry modelData required property DesktopEntry modelData
required property PersistentProperties visibilities required property ScreenState screenState
anchors.left: parent?.left anchors.left: parent?.left
anchors.right: parent?.right anchors.right: parent?.right
@@ -23,7 +23,7 @@ Item {
onClicked: { onClicked: {
Apps.launch(root.modelData); Apps.launch(root.modelData);
root.visibilities.launcher = false; root.screenState.launcher = false;
} }
} }
+2 -2
View File
@@ -16,7 +16,7 @@ Item {
function onClicked(): void { function onClicked(): void {
Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]);
root.list.visibilities.launcher = false; root.list.screenState.launcher = false;
} }
anchors.left: parent?.left anchors.left: parent?.left
@@ -81,7 +81,7 @@ Item {
onClicked: { onClicked: {
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]);
root.list.visibilities.launcher = false; root.list.screenState.launcher = false;
} }
} }
+141
View File
@@ -0,0 +1,141 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Modules.Launcher.Services
import qs.Services
Item {
id: root
required property var list
required property var modelData
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: Config.launcher.sizes.itemHeight
function onClicked(): void {
root.list.screenState.launcher = false;
if (root.modelData?.path)
Files.launch(["xdg-open", root.modelData.path]);
}
StateLayer {
id: sLayer
radius: Tokens.rounding.medium
manualHoverOverride: openLocation.hovered
onClicked: {
root.onClicked();
}
}
IconButton {
id: openLocation
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.medium
anchors.verticalCenter: parent.verticalCenter
icon: "open_in_new"
isRound: true
scale: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0.4
opacity: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on scale {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: {
root.list.screenState.launcher = false;
if (root.modelData?.path)
Files.showInFolder(root.modelData.path);
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small
IconImage {
id: icon
anchors.verticalCenter: parent.verticalCenter
implicitSize: Tokens.font.size.extraLarge
source: Quickshell.iconPath(root.modelData?.icon) ?? ""
visible: !root.modelData?.isImage
}
Loader {
active: opacity > 0
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
opacity: root.modelData?.isImage && image.status === Image.Loading ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
sourceComponent: LoadingIndicator {
color: Colors.palette.m3primaryContainer
implicitSize: Tokens.font.size.extraLarge
}
}
FadeImage {
id: image
property int implicitSize: Tokens.font.size.extraLarge
width: implicitSize
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
source: root.modelData?.isImage ? root.modelData?.path ?? "" : ""
visible: root.modelData?.isImage
}
Item {
anchors.left: icon.right
anchors.leftMargin: Tokens.spacing.medium
anchors.verticalCenter: icon.verticalCenter
implicitHeight: name.implicitHeight + desc.implicitHeight
implicitWidth: parent.width - icon.width
CustomText {
id: name
font.pointSize: Tokens.font.size.normal
text: root.modelData?.name ?? ""
elide: Text.ElideRight
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
}
CustomText {
id: desc
anchors.top: name.bottom
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: root.modelData?.path ?? ""
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
}
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ Item {
required property var modelData // FileSystemEntry-like {path, relativePath} required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight required property real tileHeight
required property real tileWidth required property real tileWidth
required property PersistentProperties visibilities required property ScreenState screenState
signal requestActivate signal requestActivate
+2 -2
View File
@@ -42,10 +42,10 @@ Searcher {
if (command[0] === "autocomplete" && command.length > 1) { if (command[0] === "autocomplete" && command.length > 1) {
list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; list.search.text = `${Config.launcher.actionPrefix}${command[1]} `;
} else if (command[0] === "setMode" && command.length > 1) { } else if (command[0] === "setMode" && command.length > 1) {
list.visibilities.launcher = false; list.screenState.launcher = false;
Colors.setMode(command[1]); Colors.setMode(command[1]);
} else { } else {
list.visibilities.launcher = false; list.screenState.launcher = false;
Quickshell.execDetached(command); Quickshell.execDetached(command);
} }
} }
+1 -1
View File
@@ -79,7 +79,7 @@ Searcher {
required property string variant required property string variant
function onClicked(list: AppList): void { function onClicked(list: AppList): void {
list.visibilities.launcher = false; list.screenState.launcher = false;
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]);
Config.colors.schemeType = variant; Config.colors.schemeType = variant;
} }
+4 -4
View File
@@ -12,7 +12,7 @@ Item {
property int contentHeight property int contentHeight
readonly property real maxHeight: { readonly property real maxHeight: {
let max = screen.height - Tokens.spacing.largeIncreased * 2; let max = screen.height - Tokens.spacing.largeIncreased * 2;
if (visibilities.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x) if (screenState.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x)
max -= panels.resources.nonAnimHeight; max -= panels.resources.nonAnimHeight;
if (panels.popouts.hasCurrent) if (panels.popouts.hasCurrent)
if (panels.popouts.current?.x + panels.popouts.current?.width > root.x && panels.popouts.current?.x < root.x + root.width) if (panels.popouts.current?.x + panels.popouts.current?.width > root.x && panels.popouts.current?.x < root.x + root.width)
@@ -22,8 +22,8 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.launcher readonly property bool shouldBeActive: screenState.launcher
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
@@ -57,7 +57,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
maxHeight: root.maxHeight maxHeight: root.maxHeight
panels: root.panels panels: root.panels
visibilities: root.visibilities screenState: root.screenState
} }
} }
} }
+2 -2
View File
@@ -60,7 +60,7 @@ CustomRect {
MaterialShape { MaterialShape {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Qt.alpha(Colors.palette.m3primary, hour.index === 0 ? 1 : 0) color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
implicitSize: temp.implicitHeight + Tokens.padding.small * 2 implicitSize: temp.implicitHeight + Tokens.padding.small * 2
shape: MaterialShape.Cookie4Sided shape: MaterialShape.Cookie4Sided
@@ -73,7 +73,7 @@ CustomRect {
id: temp id: temp
anchors.centerIn: parent anchors.centerIn: parent
color: hour.index === 0 ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font.pointSize: Tokens.font.size.normal font.pointSize: Tokens.font.size.normal
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
} }
+4 -4
View File
@@ -13,7 +13,7 @@ Item {
readonly property int padding: Tokens.padding.smaller readonly property int padding: Tokens.padding.smaller
required property Item panels required property Item panels
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
@@ -27,16 +27,16 @@ Item {
for (let i = 0; i < count; i++) for (let i = 0; i < count; i++)
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || visibilities.dashboard) if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard)
return 0; return 0;
if (visibilities.osd) { if (screenState.osd) {
const h = panels.osd.y - Tokens.spacing.small * 2 - padding * 2; const h = panels.osd.y - Tokens.spacing.small * 2 - padding * 2;
if (height > h) if (height > h)
height = h; height = h;
} }
if (visibilities.session) { if (screenState.session) {
const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2; const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2;
if (height > h) if (height > h)
height = h; height = h;
+2 -4
View File
@@ -7,8 +7,7 @@ import qs.Services
Item { Item {
id: root id: root
required property Props props required property ScreenState screenState
required property var visibilities
ColumnLayout { ColumnLayout {
id: layout id: layout
@@ -23,8 +22,7 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
NotifDock { NotifDock {
props: root.props screenState: root.screenState
visibilities: root.visibilities
} }
} }
+2 -3
View File
@@ -15,8 +15,7 @@ CustomRect {
required property bool expanded required property bool expanded
required property NotifServer.Notif modelData required property NotifServer.Notif modelData
readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height
required property Props props required property ScreenState screenState
required property PersistentProperties visibilities
color: { color: {
const c = root.modelData?.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2); const c = root.modelData?.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2);
@@ -137,7 +136,7 @@ CustomRect {
onLinkActivated: link => { onLinkActivated: link => {
Qt.openUrlExternally(link); Qt.openUrlExternally(link);
root.visibilities.sidebar = false; root.screenState.sidebar = false;
} }
} }
+2 -4
View File
@@ -14,8 +14,7 @@ Item {
id: root id: root
readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
required property Props props required property ScreenState screenState
required property var visibilities
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
@@ -120,8 +119,7 @@ Item {
id: notifList id: notifList
container: view container: view
props: root.props screenState: root.screenState
visibilities: root.visibilities
} }
} }
} }
@@ -12,8 +12,7 @@ LazyListView {
id: root id: root
required property Flickable container required property Flickable container
required property Props props required property ScreenState screenState
required property PersistentProperties visibilities
anchors.left: parent?.left anchors.left: parent?.left
anchors.right: parent?.right anchors.right: parent?.right
@@ -118,8 +117,7 @@ LazyListView {
container: root.container container: root.container
modelData: notif.modelData modelData: notif.modelData
props: root.props screenState: root.screenState
visibilities: root.visibilities
} }
} }
} }
+6 -8
View File
@@ -16,7 +16,7 @@ CustomRect {
readonly property list<var> activeNotifs: notifs.filter(n => !n.closed) readonly property list<var> activeNotifs: notifs.filter(n => !n.closed)
readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
required property Flickable container required property Flickable container
readonly property bool expanded: props.expandedNotifs.includes(modelData) readonly property bool expanded: screenState.expandedNotifs.includes(modelData)
readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? "" readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? ""
required property string modelData required property string modelData
readonly property int nonAnimHeight: { readonly property int nonAnimHeight: {
@@ -26,7 +26,7 @@ CustomRect {
} }
readonly property int notifCount: activeNotifs.length readonly property int notifCount: activeNotifs.length
readonly property list<var> notifs: NotifServer.list.filter(n => n.appName === modelData) readonly property list<var> notifs: NotifServer.list.filter(n => n.appName === modelData)
required property Props props required property ScreenState screenState
readonly property int urgency: { readonly property int urgency: {
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical)) if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical))
return NotificationUrgency.Critical; return NotificationUrgency.Critical;
@@ -34,14 +34,13 @@ CustomRect {
return NotificationUrgency.Normal; return NotificationUrgency.Normal;
return NotificationUrgency.Low; return NotificationUrgency.Low;
} }
required property PersistentProperties visibilities
function toggleExpand(expand: bool): void { function toggleExpand(expand: bool): void {
if (expand) { if (expand) {
if (!expanded) if (!expanded)
props.expandedNotifs.push(modelData); screenState.expandedNotifs.push(modelData);
} else if (expanded) { } else if (expanded) {
props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
} }
} }
@@ -59,7 +58,7 @@ CustomRect {
Component.onDestruction: { Component.onDestruction: {
if (notifCount === 0 && expanded) if (notifCount === 0 && expanded)
props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
} }
RowLayout { RowLayout {
@@ -233,8 +232,7 @@ CustomRect {
container: root.container container: root.container
expanded: root.expanded expanded: root.expanded
notifs: root.notifs notifs: root.notifs
props: root.props screenState: root.screenState
visibilities: root.visibilities
onRequestToggleExpand: expand => root.toggleExpand(expand) onRequestToggleExpand: expand => root.toggleExpand(expand)
} }
@@ -14,8 +14,7 @@ LazyListView {
required property Flickable container required property Flickable container
required property bool expanded required property bool expanded
required property list<var> notifs required property list<var> notifs
required property Props props required property ScreenState screenState
required property PersistentProperties visibilities
signal requestToggleExpand(expand: bool) signal requestToggleExpand(expand: bool)
@@ -122,8 +121,7 @@ LazyListView {
anchors.fill: parent anchors.fill: parent
expanded: root.expanded expanded: root.expanded
modelData: notif.modelData modelData: notif.modelData
props: root.props screenState: root.screenState
visibilities: root.visibilities
} }
} }
} }
-7
View File
@@ -1,7 +0,0 @@
import Quickshell
PersistentProperties {
property list<string> expandedNotifs: []
reloadableId: "sidebar"
}
@@ -11,8 +11,8 @@ import qs.Services
CustomRect { CustomRect {
id: root id: root
required property var props required property ScreenState screenState
required property PersistentProperties visibilities
Layout.fillWidth: true Layout.fillWidth: true
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
@@ -72,7 +72,7 @@ CustomRect {
} }
SplitButton { SplitButton {
active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0] active: menuItems.find(m => root.screenState.recordingMode === m.icon + m.text) ?? menuItems[0]
enabled: !Recorder.running enabled: !Recorder.running
menuOnTop: true menuOnTop: true
@@ -107,7 +107,7 @@ CustomRect {
} }
] ]
menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text menu.onItemSelected: item => root.screenState.recordingMode = item.icon + item.text
} }
} }
@@ -190,8 +190,7 @@ CustomRect {
id: recordingList id: recordingList
RecordingList { RecordingList {
props: root.props screenState: root.screenState
visibilities: root.visibilities
} }
} }
@@ -14,8 +14,7 @@ import qs.Services
ColumnLayout { ColumnLayout {
id: root id: root
required property var props required property ScreenState screenState
required property PersistentProperties visibilities
spacing: 0 spacing: 0
@@ -23,7 +22,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
RowLayout { RowLayout {
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
@@ -42,11 +41,11 @@ ColumnLayout {
} }
IconButton { IconButton {
icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more" icon: root.screenState.recordingListExpanded ? "unfold_less" : "unfold_more"
label.animate: true label.animate: true
type: IconButton.Text type: IconButton.Text
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
} }
} }
} }
@@ -57,7 +56,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.rightMargin: -Tokens.spacing.small Layout.rightMargin: -Tokens.spacing.small
clip: true clip: true
implicitHeight: (Tokens.font.size.larger + Tokens.padding.extraSmall) * (root.props.recordingListExpanded ? 10 : 3) implicitHeight: (Tokens.font.size.larger + Tokens.padding.extraSmall) * (root.screenState.recordingListExpanded ? 10 : 3)
CustomScrollBar.vertical: CustomScrollBar { CustomScrollBar.vertical: CustomScrollBar {
flickable: list flickable: list
@@ -109,8 +108,11 @@ ColumnLayout {
type: IconButton.Text type: IconButton.Text
onClicked: { onClicked: {
root.visibilities.sidebar = false; root.screenState.sidebar = false;
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]); Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
else
Quickshell.execDetached([...Config.general.apps.playback, recording.modelData.path]);
} }
} }
@@ -119,8 +121,11 @@ ColumnLayout {
type: IconButton.Text type: IconButton.Text
onClicked: { onClicked: {
root.visibilities.sidebar = false; root.screenState.sidebar = false;
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]); Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
else
Quickshell.execDetached([...Config.general.apps.explorer, recording.modelData.path]);
} }
} }
} }
@@ -170,11 +175,11 @@ ColumnLayout {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0 Layout.preferredHeight: root.screenState.recordingListExpanded ? implicitHeight : 0
color: Colors.palette.m3outline color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.extraLarge font.pointSize: Tokens.font.size.extraLarge
opacity: root.props.recordingListExpanded ? 1 : 0 opacity: root.screenState.recordingListExpanded ? 1 : 0
scale: root.props.recordingListExpanded ? 1 : 0 scale: root.screenState.recordingListExpanded ? 1 : 0
text: "scan_delete" text: "scan_delete"
Behavior on Layout.preferredHeight { Behavior on Layout.preferredHeight {
@@ -196,10 +201,10 @@ ColumnLayout {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0 Layout.preferredWidth: !root.screenState.recordingListExpanded ? implicitWidth : 0
color: Colors.palette.m3outline color: Colors.palette.m3outline
opacity: !root.props.recordingListExpanded ? 1 : 0 opacity: !root.screenState.recordingListExpanded ? 1 : 0
scale: !root.props.recordingListExpanded ? 1 : 0 scale: !root.screenState.recordingListExpanded ? 1 : 0
text: "scan_delete" text: "scan_delete"
Behavior on Layout.preferredWidth { Behavior on Layout.preferredWidth {
@@ -12,13 +12,11 @@ import qs.Services
import qs.Helpers import qs.Helpers
import qs.Daemons import qs.Daemons
import qs.Modules.Settings import qs.Modules.Settings
import qs.Modules.Bar.Popouts as BarPopouts
CustomRect { CustomRect {
id: root id: root
readonly property bool needExtraRow: quickToggles.length > 6 readonly property bool needExtraRow: quickToggles.length > 6
required property BarPopouts.Wrapper popouts
readonly property var quickToggles: { readonly property var quickToggles: {
const seenIds = new Set(); const seenIds = new Set();
@@ -40,8 +38,8 @@ CustomRect {
return true; return true;
}); });
} }
required property ScreenState screenState
readonly property int splitIndex: Math.ceil(quickToggles.length / 2) readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
required property PersistentProperties visibilities
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
@@ -168,8 +166,8 @@ CustomRect {
isToggle: false isToggle: false
onClicked: { onClicked: {
root.visibilities.sidebar = false; root.screenState.sidebar = false;
root.visibilities.settings = true; root.screenState.settings = true;
} }
} }
} }
@@ -1,16 +1,14 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Modules.Bar.Popouts as BarPopouts import qs.Components
import qs.Modules.Notifications.Sidebar.Utils.Cards import qs.Modules.Notifications.Sidebar.Utils.Cards
import ZShell.Config import ZShell.Config
Item { Item {
id: root id: root
required property BarPopouts.Wrapper popouts required property ScreenState screenState
required property PersistentProperties props
required property var visibilities
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth implicitWidth: layout.implicitWidth
@@ -25,15 +23,13 @@ Item {
} }
Record { Record {
props: root.props screenState: root.screenState
visibilities: root.visibilities
z: 1 z: 1
} }
Toggles { Toggles {
Layout.fillWidth: true Layout.fillWidth: true
popouts: root.popouts screenState: root.screenState
visibilities: root.visibilities
} }
} }
} }
@@ -10,17 +10,9 @@ Item {
id: root id: root
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property BarPopouts.Wrapper popouts required property ScreenState screenState
readonly property PersistentProperties props: PersistentProperties { readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive
property string recordingConfirmDelete
property bool recordingListExpanded: false
property string recordingMode
reloadableId: "utilities"
}
readonly property bool shouldBeActive: visibilities.sidebar
required property Item sidebar required property Item sidebar
required property var visibilities
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + 8 * 2 implicitHeight: content.implicitHeight + 8 * 2
@@ -45,9 +37,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
implicitWidth: root.implicitWidth - 8 * 2 implicitWidth: root.implicitWidth - 8 * 2
popouts: root.popouts screenState: root.screenState
props: root.props
visibilities: root.visibilities
} }
} }
} }
+4 -6
View File
@@ -7,12 +7,11 @@ import QtQuick
Item { Item {
id: root id: root
readonly property bool chatActive: screenState.sidebarTab === 1
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
readonly property Props props: Props { required property ScreenState screenState
} readonly property bool shouldBeActive: root.screenState.sidebar && Config.sidebar.enabled
readonly property bool shouldBeActive: root.visibilities.sidebar && Config.sidebar.enabled
required property var visibilities
anchors.rightMargin: (-implicitWidth - 5) * offsetScale anchors.rightMargin: (-implicitWidth - 5) * offsetScale
implicitWidth: Config.sidebar.sizes.width implicitWidth: Config.sidebar.sizes.width
@@ -38,8 +37,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
implicitWidth: Config.sidebar.sizes.width - Tokens.padding.small * 2 implicitWidth: Config.sidebar.sizes.width - Tokens.padding.small * 2
props: root.props screenState: root.screenState
visibilities: root.visibilities
} }
} }
} }
+2 -2
View File
@@ -7,7 +7,7 @@ Item {
required property Item panels required property Item panels
required property Item sidebarPanel required property Item sidebarPanel
required property var visibilities required property ScreenState screenState
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth implicitWidth: content.implicitWidth
@@ -17,6 +17,6 @@ Item {
id: content id: content
panels: root.panels panels: root.panels
visibilities: root.visibilities screenState: root.screenState
} }
} }
+2 -2
View File
@@ -16,7 +16,7 @@ Item {
required property bool muted required property bool muted
required property bool sourceMuted required property bool sourceMuted
required property real sourceVolume required property real sourceVolume
required property var visibilities required property ScreenState screenState
required property real volume required property real volume
implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2
@@ -53,7 +53,7 @@ Item {
// Microphone volume // Microphone volume
WrappedLoader { WrappedLoader {
shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.screenState.session)
sourceComponent: CustomMouseArea { sourceComponent: CustomMouseArea {
function onWheel(event: WheelEvent) { function onWheel(event: WheelEvent) {
+5 -5
View File
@@ -16,16 +16,16 @@ Item {
property bool muted property bool muted
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) readonly property bool shouldBeActive: screenState.osd && Config.osd.enabled && !(screenState.utilities && Config.utilities.enabled)
property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0 property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0
required property bool sidebarOrSessionVisible required property bool sidebarOrSessionVisible
property bool sourceMuted property bool sourceMuted
property real sourceVolume property real sourceVolume
required property var visibilities required property ScreenState screenState
property real volume property real volume
function show(): void { function show(): void {
visibilities.osd = true; screenState.osd = true;
timer.restart(); timer.restart();
} }
@@ -90,7 +90,7 @@ Item {
onTriggered: { onTriggered: {
if (!root.hovered) if (!root.hovered)
root.visibilities.osd = false; root.screenState.osd = false;
} }
} }
@@ -107,7 +107,7 @@ Item {
muted: root.muted muted: root.muted
sourceMuted: root.sourceMuted sourceMuted: root.sourceMuted
sourceVolume: root.sourceVolume sourceVolume: root.sourceVolume
visibilities: root.visibilities screenState: root.screenState
volume: root.volume volume: root.volume
} }
} }
+2 -2
View File
@@ -11,8 +11,8 @@ Item {
readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0 readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property string position required property string position
readonly property bool shouldBeActive: root.visibilities.resources readonly property bool shouldBeActive: root.screenState.resources
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottom: position === "bottom" ? parent.bottom : undefined anchors.bottom: position === "bottom" ? parent.bottom : undefined
anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0 anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0
+85
View File
@@ -0,0 +1,85 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Services
ConnectedRect {
id: root
property alias label: label.text
property string subtext
property string value
property alias placeholderText: input.placeholderText
property string errorText
property alias maximumLength: input.maximumLength
property alias validate: input.validate
property alias validator: input.validator
property bool smallField
readonly property alias field: input
signal valueEdited(value: string)
signal editingFinished(value: string)
function clear(): void {
input.clear();
}
Component.onDestruction: {
if (value !== input.text)
editingFinished(input.text);
}
Layout.fillWidth: true
implicitHeight: rowLayout.implicitHeight + Tokens.padding.medium + Math.max(0, Tokens.padding.large - input.verticalPadding) * 2
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
id: label
Layout.fillWidth: true
font: Tokens.font.body.small
elide: Text.ElideRight
}
CustomText {
Layout.fillWidth: true
visible: root.subtext
text: input.isError && root.errorText ? root.errorText : root.subtext
color: input.isError && root.errorText ? Colors.palette.m3error : Colors.palette.m3outline
font.pointSize: Tokens.font.size.small
elide: Text.ElideRight
animate: root.errorText
}
}
CustomTextField {
id: input
Layout.preferredWidth: root.smallField ? Tokens.sizes.nexus.smallTextFieldWidth : Tokens.sizes.nexus.textFieldWidth
Layout.maximumWidth: root.width / 2
Layout.alignment: Qt.AlignVCenter
verticalPadding: Tokens.padding.small
text: root.value
onTextEdited: root.valueEdited(text)
onEditingFinished: root.editingFinished(text)
}
}
}
@@ -33,5 +33,33 @@ PageBase {
onToggled: Config.dashboard.enabled = checked onToggled: Config.dashboard.enabled = checked
} }
// Behavior
SectionHeader {
text: qsTr("Behavior")
}
ToggleRow {
checked: Config.dashboard.showOnHover
first: true
settingAnchor: "dashboard-showonhover"
text: qsTr("Show on hover")
subtext: qsTr("Dashboard %1 be shown when hovering the middle-top part of the shell").arg(checked ? "will" : "will not")
onToggled: Config.dashboard.showOnHover = checked
}
SpinRow {
from: 30
settingAnchor: "dashboard-drag-threshold"
stepSize: 20
enabled: !Config.dashboard.showOnHover
subtext: qsTr("How far you need to drag before the panel opens")
text: qsTr("Drag threshold")
to: 100
value: Config.dashboard.dragThreshold
onMoved: v => Config.dashboard.dragThreshold = v
}
} }
} }
+2 -2
View File
@@ -12,8 +12,8 @@ Item {
required property var panels required property var panels
property string queuedMode: "wallpaper" property string queuedMode: "wallpaper"
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.settings readonly property bool shouldBeActive: screenState.settings
required property PersistentProperties visibilities required property ScreenState screenState
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth implicitWidth: content.implicitWidth
+13 -12
View File
@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services
Scope { Scope {
id: root id: root
@@ -17,8 +18,8 @@ Scope {
onPressed: { onPressed: {
root.launcherInterrupted = false; root.launcherInterrupted = false;
if (!root.launcherInterrupted && !root.hasFullscreen) { if (!root.launcherInterrupted && !root.hasFullscreen) {
const visibilities = Visibilities.getForActive(); const state = ShellState.forActive();
visibilities.launcher = !visibilities.launcher; state.launcher = !state.launcher;
} }
root.launcherInterrupted = false; root.launcherInterrupted = false;
} }
@@ -28,8 +29,8 @@ Scope {
name: "toggle-drawing" name: "toggle-drawing"
onPressed: { onPressed: {
const visibilities = Visibilities.getForActive(); const state = ShellState.forActive();
visibilities.isDrawing = !visibilities.isDrawing; state.isDrawing = !state.isDrawing;
} }
} }
@@ -37,8 +38,8 @@ Scope {
name: "toggle-nc" name: "toggle-nc"
onPressed: { onPressed: {
const visibilities = Visibilities.getForActive(); const state = ShellState.forActive();
visibilities.sidebar = !visibilities.sidebar; state.sidebar = !state.sidebar;
} }
} }
@@ -46,8 +47,8 @@ Scope {
name: "show-osd" name: "show-osd"
onPressed: { onPressed: {
const visibilities = Visibilities.getForActive(); const state = ShellState.forActive();
visibilities.osd = !visibilities.osd; state.osd = !state.osd;
} }
} }
@@ -55,8 +56,8 @@ Scope {
name: "toggle-settings" name: "toggle-settings"
onPressed: { onPressed: {
const visibilities = Visibilities.getForActive(); const state = ShellState.forActive();
visibilities.settings = !visibilities.settings; state.settings = !state.settings;
} }
} }
@@ -64,8 +65,8 @@ Scope {
name: "toggle-clipboard" name: "toggle-clipboard"
onPressed: { onPressed: {
const visibilities = Visibilities.getForActive(); const state = ShellState.forActive();
visibilities.clipboard = !visibilities.clipboard; state.clipboard = !state.clipboard;
} }
} }
} }
+2
View File
@@ -1,5 +1,6 @@
qml_module(ZShell-config qml_module(ZShell-config
URI ZShell.Config URI ZShell.Config
INCLUDE_PREFIX config
SOURCES SOURCES
confignode.hpp confignode.cpp confignode.hpp confignode.cpp
configobject.hpp configobject.cpp configobject.hpp configobject.cpp
@@ -14,6 +15,7 @@ qml_module(ZShell-config
colors.hpp colors.hpp
dashboard.hpp dashboard.hpp
dock.hpp dock.hpp
enums.hpp
general.hpp general.hpp
launcher.hpp launcher.hpp
lock.hpp lock.hpp
+2
View File
@@ -48,6 +48,8 @@ class Dashboard : public ConfigObject {
QML_ANONYMOUS QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, true) CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(bool, showOnHover, true)
CFG_PROPERTY(int, dragThreshold, 50)
CFG_PROPERTY(int, mediaUpdateInterval, 2000) CFG_PROPERTY(int, mediaUpdateInterval, 2000)
CONFIG_SUBOBJECT(Performance, performance) CONFIG_SUBOBJECT(Performance, performance)
CFG_PROPERTY(int, resourceUpdateInterval, 1000) CFG_PROPERTY(int, resourceUpdateInterval, 1000)
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::config {
#define ENUM(Name, ...) \
namespace Name { \
\
Q_NAMESPACE \
QML_ELEMENT \
\
enum Enum : quint8 { __VA_ARGS__ }; \
Q_ENUM_NS(Enum) \
};
ENUM(BarWorkspaceDisplay, Shapes, Text, Icons)
ENUM(BarWorkspaceCapitalisation, Preserve, Upper, Lower)
ENUM(LyricsBackend, Auto, Local, LRCLIB, NetEase)
ENUM(GpuType, Auto, Nvidia, Generic, None)
ENUM(NotifsFullscreen, On, Off)
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
ENUM(DataUnit, Binary, Decimal)
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
#undef ENUM
} // namespace ZShell::config
+5
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "configlist.hpp" #include "configlist.hpp"
#include "configobject.hpp" #include "configobject.hpp"
#include <QStandardPaths>
#include <QStringList> #include <QStringList>
namespace ZShell::config { namespace ZShell::config {
@@ -141,6 +142,10 @@ class General : public ConfigObject {
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss")) CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CFG_PROPERTY(bool, desktopIcons, true) CFG_PROPERTY(bool, desktopIcons, true)
CONFIG_SUBOBJECT(Idle, idle) CONFIG_SUBOBJECT(Idle, idle)
CFG_PROPERTY(
QString,
lyricsDir,
QStandardPaths::writableLocation(QStandardPaths::MusicLocation) + QStringLiteral("/Lyrics/"))
CFG_PROPERTY(QString, logo, QString()) CFG_PROPERTY(QString, logo, QString())
CFG_PROPERTY(bool, showOverFullscreen, true) CFG_PROPERTY(bool, showOverFullscreen, true)
CFG_PROPERTY( CFG_PROPERTY(
+2
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "configlist.hpp" #include "configlist.hpp"
#include "configobject.hpp" #include "configobject.hpp"
#include "enums.hpp"
namespace ZShell::config { namespace ZShell::config {
@@ -31,6 +32,7 @@ class Services : public ConfigObject {
CFG_PROPERTY(bool, ddcutilService, true) CFG_PROPERTY(bool, ddcutilService, true)
CFG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify")) CFG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
CFG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA")) CFG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA"))
CFG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto)
CFG_PROPERTY(qreal, maxVolume, 1) CFG_PROPERTY(qreal, maxVolume, 1)
CFG_PROPERTY(qreal, minBrightness, 0.01) CFG_PROPERTY(qreal, minBrightness, 0.01)
CONFIG_LIST( CONFIG_LIST(
+103 -8
View File
@@ -1,9 +1,11 @@
#include "sparklineitem.hpp" #include "sparklineitem.hpp"
#include <qmath.h> #include <qmath.h>
#include <qnumeric.h>
#include <qpainter.h> #include <qpainter.h>
#include <qpainterpath.h> #include <qpainterpath.h>
#include <qpen.h> #include <qpen.h>
#include <qtypes.h>
namespace ZShell::internal { namespace ZShell::internal {
@@ -35,9 +37,19 @@ void SparklineItem::drawLine(
const int len = buffer->count(); const int len = buffer->count();
if (len < 2 || m_maxValue <= 0.0) return; if (len < 2 || m_maxValue <= 0.0) return;
const qreal stepX = w / static_cast<qreal>(m_historyLength - 1); qreal stepX = 0.0;
const qreal startX =
w - (len - 1) * stepX - stepX * m_slideProgress + stepX; if (m_sliding)
stepX = w / static_cast<qreal>(m_historyLength - 1);
else
stepX = w / static_cast<qreal>(len - 1);
qreal startX = 0.0;
if (m_sliding)
startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
else
startX = 0;
const qreal strokePad = qCeil(m_lineWidth / 2); const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0; const qreal curvePad = 3.0;
@@ -52,24 +64,75 @@ void SparklineItem::drawLine(
points.reserve(len); points.reserve(len);
for (int i = 0; i < len; ++i) { for (int i = 0; i < len; ++i) {
qreal value = 0.0;
if (m_smoothing > 0.0) {
const int radius = qRound(m_smoothing);
qreal sum = 0.0;
int count = 0;
for (int j = -radius; j <= radius; ++j) {
const int index = i + j;
if (index < 0 || index >= len) continue;
sum += buffer->at(index);
count++;
}
value = sum / static_cast<qreal>(count);
} else {
value = buffer->at(i);
}
value = qBound<qreal>(0.0, value, m_maxValue);
const qreal x = startX + i * stepX; const qreal x = startX + i * stepX;
const qreal value = qBound<qreal>(0.0, buffer->at(i), m_maxValue);
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH; const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y)); points.append(QPointF(x, y));
} }
QPainterPath linePath; QPainterPath linePath;
linePath.moveTo(points[0]); linePath.moveTo(points[0]);
if (m_curveSmoothing && points.size() > 2) {
// 1.0 = standard uniform Catmull-Rom. Lower (e.g. 0.5) = tighter, less overshoot.
const qreal tension = 1.0;
const qreal k =
tension / 6.0; // Catmull-Rom -> Bezier factor (1/6 for uniform)
const int n = points.size();
for (int i = 0; i < n - 1; ++i) {
// Duplicate endpoints so the first/last segments are well-defined
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, n - 1)];
QPointF c1 = p1 + (p2 - p0) * k;
QPointF c2 = p2 - (p3 - p1) * k;
// Keep control points inside the vertical range of the segment's
// endpoints so we never overshoot (no dips below zero, no
// bulges above a peak). Remember: smaller y = higher value.
const qreal segMinY = qMin(p1.y(), p2.y());
const qreal segMaxY = qMax(p1.y(), p2.y());
c1.setY(qBound(segMinY, c1.y(), segMaxY));
c2.setY(qBound(segMinY, c2.y(), segMaxY));
linePath.cubicTo(c1, c2, p2);
}
} else {
// Existing horizontal-tangent (smoothstep-style) curve
for (int i = 0; i < points.size() - 1; ++i) { for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i]; const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1]; const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5; const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
const QPointF c1(ctrlX, p0.y()); linePath.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
const QPointF c2(ctrlX, p1.y()); }
linePath.cubicTo(c1, c2, p1);
} }
QPen pen(color, m_lineWidth); QPen pen(color, m_lineWidth);
@@ -224,4 +287,36 @@ void SparklineItem::setLineWidth(qreal width) {
update(); update();
} }
void SparklineItem::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
emit slidingChanged();
}
bool SparklineItem::sliding() const {
return m_sliding;
};
void SparklineItem::setSmoothing(qreal smoothing) {
smoothing = qMax<qreal>(0.0, smoothing);
if (qFuzzyCompare(m_smoothing, smoothing)) return;
m_smoothing = smoothing;
emit smoothingChanged();
}
qreal SparklineItem::smoothing() const {
return m_smoothing;
};
void SparklineItem::setCurveSmoothing(bool sliding) {
if (m_curveSmoothing == sliding) return;
m_curveSmoothing = sliding;
emit curveSmoothingChanged();
}
bool SparklineItem::curveSmoothing() const {
return m_curveSmoothing;
};
} // namespace ZShell::internal } // namespace ZShell::internal
+23
View File
@@ -4,6 +4,7 @@
#include <qobject.h> #include <qobject.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qquickpainteditem.h> #include <qquickpainteditem.h>
#include <qtmetamacros.h>
#include "circularbuffer.hpp" #include "circularbuffer.hpp"
@@ -40,6 +41,13 @@ class SparklineItem : public QQuickPaintedItem {
Q_PROPERTY( Q_PROPERTY(
qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY
lineWidthChanged) lineWidthChanged)
Q_PROPERTY(bool sliding READ sliding WRITE setSliding NOTIFY slidingChanged)
Q_PROPERTY(
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
smoothingChanged)
Q_PROPERTY(
bool curveSmoothing READ curveSmoothing WRITE setCurveSmoothing NOTIFY
curveSmoothingChanged)
public: public:
explicit SparklineItem(QQuickItem* parent = nullptr); explicit SparklineItem(QQuickItem* parent = nullptr);
@@ -76,6 +84,15 @@ class SparklineItem : public QQuickPaintedItem {
[[nodiscard]] qreal lineWidth() const; [[nodiscard]] qreal lineWidth() const;
void setLineWidth(qreal width); void setLineWidth(qreal width);
[[nodiscard]] bool sliding() const;
void setSliding(bool sliding);
[[nodiscard]] qreal smoothing() const;
void setSmoothing(qreal smoothing);
[[nodiscard]] bool curveSmoothing() const;
void setCurveSmoothing(bool curveSmoothing);
signals: signals:
void line1Changed(); void line1Changed();
void line2Changed(); void line2Changed();
@@ -87,6 +104,9 @@ class SparklineItem : public QQuickPaintedItem {
void slideProgressChanged(); void slideProgressChanged();
void historyLengthChanged(); void historyLengthChanged();
void lineWidthChanged(); void lineWidthChanged();
void slidingChanged();
void smoothingChanged();
void curveSmoothingChanged();
private: private:
void drawLine( void drawLine(
@@ -106,6 +126,9 @@ class SparklineItem : public QQuickPaintedItem {
qreal m_slideProgress = 0.0; qreal m_slideProgress = 0.0;
int m_historyLength = 30; int m_historyLength = 30;
qreal m_lineWidth = 2.0; qreal m_lineWidth = 2.0;
bool m_sliding = true;
qreal m_smoothing = 0.0;
bool m_curveSmoothing = false;
}; };
} // namespace ZShell::internal } // namespace ZShell::internal
+5
View File
@@ -18,6 +18,8 @@ qml_module(ZShell-services
memory.hpp memory.cpp memory.hpp memory.cpp
diskinfo.hpp diskinfo.cpp diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp storage.hpp storage.cpp
lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp
LIBRARIES LIBRARIES
Qt6::Core Qt6::Core
Qt6::Qml Qt6::Qml
@@ -25,4 +27,7 @@ qml_module(ZShell-services
PkgConfig::Aubio PkgConfig::Aubio
PkgConfig::Cava PkgConfig::Cava
Sensors::Sensors Sensors::Sensors
ZShell-config
DEPENDENCIES
ZShell.Config
) )
@@ -0,0 +1,55 @@
#include "lyriccandidate.hpp"
namespace ZShell::services {
LyricCandidate::LyricCandidate(
LyricsBackend backend,
QString id,
QString title,
QString artist,
QString album,
qreal duration)
: m_backend(backend)
, m_id(std::move(id))
, m_title(std::move(title))
, m_artist(std::move(artist))
, m_album(std::move(album))
, m_duration(duration) {}
LyricsBackend LyricCandidate::backend() const {
return m_backend;
}
QString LyricCandidate::id() const {
return m_id;
}
QString LyricCandidate::title() const {
return m_title;
}
QString LyricCandidate::artist() const {
return m_artist;
}
QString LyricCandidate::album() const {
return m_album;
}
qreal LyricCandidate::duration() const {
return m_duration;
}
bool LyricCandidate::isValid() const {
return !m_id.isEmpty();
}
bool LyricCandidate::operator==(const LyricCandidate& o) const noexcept {
return m_backend == o.m_backend && m_id == o.m_id;
}
bool LyricCandidate::operator!=(const LyricCandidate& o) const noexcept {
return !(*this == o);
}
} // namespace ZShell::services
@@ -0,0 +1,53 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include "config/enums.hpp"
namespace ZShell::services {
using LyricsBackend = config::LyricsBackend::Enum;
class LyricCandidate {
Q_GADGET
QML_VALUE_TYPE(lyricCandidate)
Q_PROPERTY(ZShell::config::LyricsBackend::Enum backend READ backend)
Q_PROPERTY(QString id READ id)
Q_PROPERTY(QString title READ title)
Q_PROPERTY(QString artist READ artist)
Q_PROPERTY(QString album READ album)
Q_PROPERTY(qreal duration READ duration)
public:
LyricCandidate() = default;
LyricCandidate(
LyricsBackend backend,
QString id,
QString title,
QString artist,
QString album = {},
qreal duration = 0.0);
[[nodiscard]] LyricsBackend backend() const;
[[nodiscard]] QString id() const;
[[nodiscard]] QString title() const;
[[nodiscard]] QString artist() const;
[[nodiscard]] QString album() const;
[[nodiscard]] qreal duration() const;
[[nodiscard]] bool isValid() const;
bool operator==(const LyricCandidate& o) const noexcept;
bool operator!=(const LyricCandidate& o) const noexcept;
private:
LyricsBackend m_backend = LyricsBackend::Auto;
QString m_id;
QString m_title;
QString m_artist;
QString m_album;
qreal m_duration = 0.0;
};
} // namespace ZShell::services
File diff suppressed because it is too large Load Diff
+162
View File
@@ -0,0 +1,162 @@
#pragma once
#include <qhash.h>
#include <qjsonobject.h>
#include <qnetworkaccessmanager.h>
#include <qnetworkreply.h>
#include <qtimer.h>
#include "lyriccandidate.hpp"
namespace ZShell::services {
struct LyricLine {
qreal time = 0.0;
QString text;
};
class Lyrics : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QStringList lyrics READ lyrics NOTIFY lyricsChanged)
Q_PROPERTY(
ZShell::config::LyricsBackend::Enum backend READ backend NOTIFY
backendChanged)
Q_PROPERTY(
ZShell::config::LyricsBackend::Enum preferredBackend READ
preferredBackend WRITE setPreferredBackend NOTIFY
preferredBackendChanged)
Q_PROPERTY(
QList<ZShell::services::LyricCandidate> lyricCandidates READ
lyricCandidates NOTIFY lyricCandidatesChanged)
Q_PROPERTY(
ZShell::services::LyricCandidate selectedCandidate READ selectedCandidate
WRITE setSelectedCandidate NOTIFY selectedCandidateChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(bool hasLyrics READ hasLyrics NOTIFY hasLyricsChanged)
Q_PROPERTY(qreal offset READ offset WRITE setOffset NOTIFY offsetChanged)
Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY trackChanged)
Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackChanged)
public:
explicit Lyrics(QObject* parent = nullptr);
[[nodiscard]] QStringList lyrics() const;
[[nodiscard]] LyricsBackend backend() const;
[[nodiscard]] LyricsBackend preferredBackend() const;
void setPreferredBackend(LyricsBackend value);
[[nodiscard]] QList<LyricCandidate> lyricCandidates() const;
[[nodiscard]] LyricCandidate selectedCandidate() const;
void setSelectedCandidate(const LyricCandidate& value);
[[nodiscard]] bool loading() const;
[[nodiscard]] bool hasLyrics() const;
[[nodiscard]] qreal offset() const;
void setOffset(qreal value);
[[nodiscard]] QString trackArtist() const;
[[nodiscard]] QString trackTitle() const;
[[nodiscard]] Q_INVOKABLE int indexForTime(qreal time) const;
[[nodiscard]] Q_INVOKABLE qreal timeForIndex(int index) const;
Q_INVOKABLE void setTrack(
const QString& artist,
const QString& title,
const QString& album = {},
qreal duration = 0.0);
Q_INVOKABLE void clearTrack();
Q_INVOKABLE void refresh();
signals:
void lyricsChanged();
void backendChanged();
void preferredBackendChanged();
void lyricCandidatesChanged();
void selectedCandidateChanged();
void loadingChanged();
void hasLyricsChanged();
void offsetChanged();
void trackChanged();
private:
void setBackend(LyricsBackend value);
void setLoading(bool value);
void setLines(QVector<LyricLine> lines, LyricsBackend source);
void clearLines();
void appendCandidates(const QList<LyricCandidate>& add);
void clearCandidates();
void scheduleLoad();
void doLoad();
void cancelInFlight();
int newRequestId();
void tryLocal(int reqId);
void tryLrclib(int reqId);
void tryNetEase(int reqId);
void chainNext(LyricsBackend justFailed, int reqId);
void searchLrclibCandidates(int reqId);
void searchNetEaseCandidates(int reqId);
void fetchLrclibById(const QString& id, int reqId);
void fetchNetEaseLyricsById(const QString& id, int reqId);
QNetworkReply* getJson(
const QUrl& url, const QHash<QByteArray, QByteArray>& headers = {});
void trackReply(int reqId, QNetworkReply* reply);
void onPreferredBackendConfigChanged();
void onLyricsDirChanged();
void loadLyricsMap();
void persistTrackPrefs();
[[nodiscard]] static QString lyricsDir();
[[nodiscard]] static QString lyricsMapPath();
[[nodiscard]] QString trackKey() const;
[[nodiscard]] static QString backendKey(LyricsBackend value);
[[nodiscard]] static LyricsBackend backendFromKey(const QString& key);
[[nodiscard]] static const QString& stateDir();
[[nodiscard]] static const QString& cacheDir();
[[nodiscard]] static QString cachePathFor(
LyricsBackend backend, const QString& id);
[[nodiscard]] static QString readCachedLrc(
LyricsBackend backend, const QString& id);
static void writeCachedLrc(
LyricsBackend backend, const QString& id, const QString& text);
[[nodiscard]] static QVector<LyricLine> parseLrc(const QString& text);
[[nodiscard]] static QString tryReadLocalLrc(
const QString& dir, const QString& artist, const QString& title);
[[nodiscard]] static QString findLocalLrcRecursive(
const QString& dir, const QString& artist, const QString& title);
QNetworkAccessManager* m_nam;
QTimer* m_loadDebounce;
QVector<LyricLine> m_lines;
QStringList m_lyrics;
LyricsBackend m_backend = LyricsBackend::Auto;
LyricsBackend m_preferredBackend = LyricsBackend::Auto;
QList<LyricCandidate> m_candidates;
LyricCandidate m_selected;
bool m_loading = false;
bool m_hasLyrics = false;
qreal m_offset = 0.0;
QString m_artist;
QString m_title;
QString m_album;
qreal m_duration = 0.0;
int m_currentRequestId = 0;
QHash<int, QList<QPointer<QNetworkReply>>> m_pendingReplies;
QJsonObject m_lyricsMap;
bool m_lyricsMapLoaded = false;
bool m_settingFromPrefs = false;
};
} // namespace ZShell::services
+55
View File
@@ -0,0 +1,55 @@
message(STATUS "QML install dir: ${CMAKE_INSTALL_PREFIX}/${INSTALL_QMLDIR}")
function(qml_module arg_TARGET)
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI;INCLUDE_PREFIX" "SOURCES;QML_FILES;QML_SINGLETONS;DEPENDENCIES;IMPORTS;OPTIONAL_IMPORTS;DEFAULT_IMPORTS;LIBRARIES")
set_source_files_properties(${arg_QML_SINGLETONS} PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
qt_add_qml_module(${arg_TARGET}
URI ${arg_URI}
SOURCES ${arg_SOURCES}
QML_FILES ${arg_QML_FILES} ${arg_QML_SINGLETONS}
DEPENDENCIES ${arg_DEPENDENCIES}
IMPORTS ${arg_IMPORTS}
OPTIONAL_IMPORTS ${arg_OPTIONAL_IMPORTS}
DEFAULT_IMPORTS ${arg_DEFAULT_IMPORTS}
)
qt_query_qml_module(${arg_TARGET}
URI module_uri
PLUGIN_TARGET module_plugin_target
TARGET_PATH module_target_path
QMLDIR module_qmldir
TYPEINFO module_typeinfo
)
message(STATUS "Created QML module: ${module_uri}")
string(REPLACE "/" ";" uri_parts "${module_target_path}")
list(GET uri_parts 0 top_level)
set(backing_lib_dir "${INSTALL_QMLDIR}/${top_level}/lib")
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
install(TARGETS ${arg_TARGET}
LIBRARY DESTINATION "${backing_lib_dir}"
RUNTIME DESTINATION "${backing_lib_dir}"
)
install(TARGETS "${module_plugin_target}"
LIBRARY DESTINATION "${module_dir}"
RUNTIME DESTINATION "${module_dir}"
)
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
target_link_libraries(${arg_TARGET} PRIVATE zshell-pch Qt::Core Qt::Qml ${arg_LIBRARIES})
if(arg_INCLUDE_PREFIX)
set(include_dir "${CMAKE_CURRENT_BINARY_DIR}/include")
file(MAKE_DIRECTORY "${include_dir}")
file(CREATE_LINK "${CMAKE_CURRENT_SOURCE_DIR}" "${include_dir}/${arg_INCLUDE_PREFIX}" SYMBOLIC)
target_include_directories(${arg_TARGET} PUBLIC "${include_dir}")
endif()
file(RELATIVE_PATH plugin_to_lib "/${module_target_path}" "/${top_level}/lib")
set_property(TARGET ${module_plugin_target} APPEND PROPERTY INSTALL_RPATH "$ORIGIN/${plugin_to_lib}")
endfunction()
+35
View File
@@ -0,0 +1,35 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Components
import qs.Helpers
Singleton {
function anySidebarOpen(): bool {
return states.instances.some(s => s.sidebar);
}
function forActive(): ScreenState {
const mon = Hypr.focusedMonitor;
for (const s of states.instances)
if (Hypr.monitorFor(s.modelData) === mon)
return s;
return null;
}
function forScreen(screen: ShellScreen): ScreenState {
for (const s of states.instances)
if (s.modelData === screen)
return s;
return null;
}
Variants {
id: states
model: Quickshell.screens
ScreenState {}
}
}