feat: dashboard show on hover, media and weather tabs added
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 49s
Rust / fmt (pull_request) Successful in 26s
Python / verify (pull_request) Successful in 1m44s
Rust / clippy (pull_request) Successful in 44s

This commit is contained in:
2026-09-20 12:59:53 +02:00
parent 7bc1db7f19
commit 84d5abcaab
100 changed files with 4288 additions and 999 deletions
+5 -5
View File
@@ -46,10 +46,10 @@ Item {
}
function syncFromPenColor() {
if (!drawing.drawingState)
if (!drawing.screenState)
return;
const c = drawing.drawingState.penColor;
const c = drawing.screenState.penColor;
if (c.hsvSaturation > 0) {
currentHue = c.hsvHue;
@@ -86,7 +86,7 @@ Item {
currentHue = relative / arcSweep;
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
@@ -112,7 +112,7 @@ Item {
root.syncFromPenColor();
}
target: root.drawing.drawingState
target: root.drawing.screenState
}
Canvas {
@@ -152,7 +152,7 @@ Item {
CustomRect {
anchors.centerIn: parent
color: root.drawing?.drawingState.penColor
color: root.drawing?.screenState.penColor
implicitHeight: implicitWidth
implicitWidth: canvas.height - root.handleSize - Tokens.padding.extraLarge * 2
radius: Tokens.rounding.full
+2 -2
View File
@@ -1,12 +1,12 @@
import QtQuick
import qs.Helpers
import qs.Services
Flickable {
id: root
property bool doneFakeFlick
interactive: !Visibilities.getForActive().isDrawing
interactive: !ShellState.forActive().isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
+2 -2
View File
@@ -1,12 +1,12 @@
import QtQuick
import qs.Helpers
import qs.Services
ListView {
id: root
property bool doneFakeFlick
interactive: !Visibilities.getForActive()?.isDrawing
interactive: !ShellState.forActive()?.isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
+2 -2
View File
@@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Templates
import qs.Helpers
import qs.Services
import ZShell.Config
ScrollBar {
@@ -26,7 +26,7 @@ ScrollBar {
property bool shouldBeActive
property bool inverted
enabled: !Visibilities.getForActive().isDrawing
enabled: !ShellState.forActive().isDrawing
implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
+23 -21
View File
@@ -15,28 +15,33 @@ TextFieldBase {
readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
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 string errorText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int horizontalPadding: Tokens.padding.large
property bool isError
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
rightPadding: horizontalPadding + trailingOffset
topPadding: Tokens.padding.large + filledOffset
topPadding: verticalPadding + filledOffset
background: Loader {
anchors.bottomMargin: root.supportingTextOffset
@@ -212,12 +217,10 @@ TextFieldBase {
}
}
Behavior on strokeColor {
CAnim {
}
CAnim {}
}
Behavior on strokeWidth {
Anim {
}
Anim {}
}
PathLine {
@@ -292,8 +295,7 @@ TextFieldBase {
implicitHeight: root.activeFocus ? 2 : 1
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 ZShell
import ZShell.Config
import qs.Helpers
import qs.Services
import qs.Services
MouseArea {
@@ -52,7 +52,7 @@ MouseArea {
anchors.fill: parent
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing
enabled: parent.enabled && !ShellState.forActive()?.isDrawing
hoverEnabled: true
Behavior on stateOpacity {
+2 -1
View File
@@ -10,6 +10,7 @@ import ZShell
import qs.Components
import qs.Modules
import qs.Helpers
import qs.Services
import qs.Paths
import ZShell.Config
@@ -25,7 +26,7 @@ Singleton {
property alias server: server
function shouldThrottle(appName: string): bool {
if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar))
if (props.dnd || ShellState.anySidebarOpen())
return false;
const key = (appName || "unknown").trim().toLowerCase();
+5 -10
View File
@@ -3,28 +3,23 @@ pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import ZShell.Internal
import qs.Components
Item {
id: root
readonly property alias content: contentLoader.item
readonly property PersistentProperties drawingState: PersistentProperties {
property color penColor: "white"
property int penWidth: 4
reloadableId: "drawingState"
}
required property PersistentProperties visibilities
required property ScreenState screenState
Loader {
id: contentLoader
active: root.visibilities.isDrawing
active: root.screenState.isDrawing
anchors.fill: parent
sourceComponent: StrokeCanvas {
penColor: root.drawingState.penColor
penWidth: root.drawingState.penWidth
penColor: root.screenState.penColor
penWidth: root.screenState.penWidth
}
}
}
+75 -55
View File
@@ -19,7 +19,7 @@ Item {
required property ShellScreen screen
property bool singleGestureTriggered: false
property bool utilitiesShortcutActive
required property PersistentProperties visibilities
required property ScreenState screenState
function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -63,7 +63,7 @@ Item {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
dragThreshold: 0
enabled: !root.visibilities.isDrawing
enabled: !root.screenState.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1
minimumPointCount: 1
@@ -76,48 +76,60 @@ Item {
onCentroidChanged: {
const x = centroid.position.x;
const y = centroid.position.y;
const dragX = x - centroid.pressPosition.x;
const dragY = y - centroid.pressPosition.y;
const pressX = centroid.pressPosition.x;
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)
root.visibilities.launcher = true;
if (pressY >= root.screen.height - Config.bar.border && pressX > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold)
root.screenState.launcher = true;
if (root.singleGestureTriggered)
return;
if (root.geometry.insetTop(root.borderThickness) > centroid.pressPosition.y) {
if (dragY > 20) {
root.visibilities.settings = true;
root.singleGestureTriggered = true;
} else if (dragY < -20) {
root.visibilities.settings = false;
root.singleGestureTriggered = true;
}
}
// if (root.geometry.insetTop(root.borderThickness) > pressY) {
// if (dragY > 20) {
// root.screenState.settings = true;
// root.singleGestureTriggered = true;
// } else if (dragY < -20) {
// root.screenState.settings = false;
// root.singleGestureTriggered = true;
// }
// }
if (centroid.pressPosition.y > root.screen.height - root.geometry.insetBottom(root.borderThickness) && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
root.visibilities.clipboard = true;
if (pressY > root.screen.height - root.geometry.insetBottom(root.borderThickness) && pressX < root.screen.width / 5 && dragY < -50)
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) {
root.visibilities.dock = true;
root.screenState.dock = 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) {
root.visibilities.sidebar = true;
if (pressX > root.screen.width - Config.bar.border && pressY < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) {
root.screenState.sidebar = 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 })`);
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 })`);
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
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: {
if (!active) {
@@ -178,12 +190,12 @@ Item {
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: {
if (!hovered) {
if (!root.osdShortcutActive) {
root.visibilities.osd = false;
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
@@ -193,13 +205,16 @@ Item {
if (Config.bar.autoHide || root.bar.fullscreen)
root.bar.isHovered = false;
if (root.screenState.dashboard && Config.dashboard.showOnHover)
root.screenState.dashboard = false;
}
}
onPointChanged: {
const x = point.position.x;
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)) {
root.panels.drawing.expand();
root.drawing.content.hideHover();
@@ -210,7 +225,7 @@ Item {
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;
if (root.bar.fullscreen && !root.bar.isHovered)
@@ -228,7 +243,7 @@ Item {
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
if (!root.osdShortcutActive) {
root.visibilities.osd = showOsd;
root.screenState.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
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))
root.visibilities.dock = true;
if (Config.dock.enable && Config.dock.hoverToReveal && !root.screenState.dock && !root.screenState.launcher && root.inBottomPanel(root.panels.dock, x, y))
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))
root.bar.checkPopout(root.geometry.axisPos(x, y));
@@ -246,16 +266,16 @@ Item {
Connections {
function onDashboardChanged() {
if (root.visibilities.dashboard) {
if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
if (!inDashboardArea) {
root.dashboardShortcutActive = true;
}
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.visibilities.sidebar = false;
root.screenState.settings = false;
root.screenState.sidebar = false;
root.popouts.hasCurrent = false;
} else {
root.dashboardShortcutActive = false;
@@ -263,12 +283,12 @@ Item {
}
function onIsDrawingChanged() {
if (!root.visibilities.isDrawing)
if (!root.screenState.isDrawing)
root.drawing.clear();
}
function onLauncherChanged() {
if (!root.visibilities.launcher) {
if (!root.screenState.launcher) {
root.dashboardShortcutActive = false;
root.osdShortcutActive = false;
root.utilitiesShortcutActive = false;
@@ -276,23 +296,23 @@ Item {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.visibilities.osd = false;
root.screenState.osd = 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) {
root.visibilities.dashboard = false;
root.screenState.dashboard = false;
}
root.visibilities.dock = false;
root.visibilities.settings = false;
root.screenState.dock = false;
root.screenState.settings = false;
}
}
function onOsdChanged() {
if (root.visibilities.osd) {
if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.osdShortcutActive = true;
@@ -303,32 +323,32 @@ Item {
}
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;
}
if (root.visibilities.resources)
root.visibilities.settings = false;
if (root.screenState.resources)
root.screenState.settings = false;
}
function onSettingsChanged() {
if (root.visibilities.settings) {
root.visibilities.resources = false;
root.visibilities.dashboard = false;
root.visibilities.sidebar = false;
if (root.screenState.settings) {
root.screenState.resources = false;
root.screenState.dashboard = false;
root.screenState.sidebar = false;
root.panels.popouts.hasCurrent = false;
root.visibilities.launcher = false;
root.screenState.launcher = false;
}
}
function onSidebarChanged() {
if (root.visibilities.sidebar) {
root.visibilities.dashboard = false;
root.visibilities.settings = false;
if (root.screenState.sidebar) {
root.screenState.dashboard = false;
root.screenState.settings = 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 toasts: toasts
readonly property alias utilities: utilities
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.bottomMargin: geometry.insetBottom(borderThickness)
anchors.fill: parent
@@ -64,7 +64,7 @@ Item {
id: resources
position: root.geometry.position
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -85,7 +85,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
screen: root.screen
sidebarOrSessionVisible: sidebar.visible
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -96,7 +96,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
drawing: root.drawingItem
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
Popouts.ClipWrapper {
@@ -122,7 +122,7 @@ Item {
anchors.top: parent.top
panels: root
sidebarPanel: sidebar
visibilities: root.visibilities
screenState: root.screenState
}
Launcher.Wrapper {
@@ -132,7 +132,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
panels: root
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
Utils.Wrapper {
@@ -141,7 +141,7 @@ Item {
anchors.bottom: parent.bottom
anchors.right: parent.right
sidebar: sidebar
visibilities: root.visibilities
screenState: root.screenState
}
Item {
@@ -149,13 +149,11 @@ Item {
property real offsetScale: dashboard.shouldBeActive ? 0 : 1
anchors.bottom: root.geometry.barOnLeft || root.geometry.barOnBottom ? parent.bottom : undefined
anchors.left: root.geometry.barOnLeft ? parent.left : undefined
anchors.right: root.geometry.barOnLeft ? undefined : parent.right
anchors.top: root.geometry.barOnBottom || root.geometry.barOnLeft ? undefined : parent.top
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
clip: true
implicitHeight: root.geometry.horizontal ? dashboard.implicitHeight * (1 - offsetScale) : dashboard.implicitHeight
implicitWidth: root.geometry.horizontal ? dashboard.implicitWidth : dashboard.implicitWidth * (1 - offsetScale)
implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: dashboard.implicitWidth
Behavior on offsetScale {
Anim {
@@ -168,8 +166,7 @@ Item {
id: dashboard
offsetScale: dashboardWrapper.offsetScale
position: root.geometry.position
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -180,7 +177,7 @@ Item {
anchors.right: parent.right
anchors.top: notifications.bottom
panels: root
visibilities: root.visibilities
screenState: root.screenState
}
Item {
@@ -196,7 +193,7 @@ Item {
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
panels: root
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -207,7 +204,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
panels: root
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
Clipboard.Wrapper {
@@ -216,6 +213,6 @@ Item {
anchors.bottom: parent.bottom
anchors.left: parent.left
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 HyprlandMonitor monitor: Hypr.monitorFor(screen)
property var root: Quickshell.shellDir
readonly property ScreenState screenState: ShellState.forScreen(screen)
readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg))
property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings || visibilities.sidebar ? 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
color: "transparent"
contentItem.focus: true
mask: visibilities.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
mask: screenState.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
name: "Bar"
Behavior on fsTransitionProg {
@@ -71,26 +72,26 @@ CustomWindow {
contentItem.Keys.onEscapePressed: {
if (Config.bar.autoHide)
visibilities.bar = false;
visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.dock = false;
visibilities.clipboard = false;
screenState.bar = false;
screenState.launcher = false;
screenState.sidebar = false;
screenState.dashboard = false;
screenState.osd = false;
screenState.settings = false;
screenState.resources = false;
screenState.dock = false;
screenState.clipboard = false;
panels.popouts.hasCurrent = false;
}
onHasFullscreenChanged: {
visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.clipboard = false;
visibilities.dock = false;
screenState.launcher = false;
screenState.sidebar = false;
screenState.dashboard = false;
screenState.osd = false;
screenState.settings = false;
screenState.resources = false;
screenState.clipboard = false;
screenState.dock = false;
panels.popouts.hasCurrent = false;
}
@@ -142,44 +143,26 @@ CustomWindow {
HyprlandFocusGrab {
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]
onCleared: {
visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.clipboard = false;
visibilities.dock = false;
root.screenState.launcher = false;
root.screenState.sidebar = false;
root.screenState.dashboard = false;
root.screenState.osd = false;
root.screenState.settings = false;
root.screenState.resources = false;
root.screenState.clipboard = false;
root.screenState.dock = 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 {
property: "bar"
target: visibilities
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || (!Config.bar.hideWhenNotif && visibilities.notif) || visibilities.resources || visibilities.settings || bar.isHovered
target: root.screenState
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
}
@@ -345,7 +328,7 @@ CustomWindow {
anchors.fill: parent
layer.enabled: true
visibilities: visibilities
screenState: root.screenState
z: 2
layer.effect: MultiEffect {
@@ -386,7 +369,7 @@ CustomWindow {
panels: panels
popouts: panels.popouts
screen: root.screen
visibilities: visibilities
screenState: root.screenState
Panels {
id: panels
@@ -396,7 +379,7 @@ CustomWindow {
drawingItem: drawing
geometry: geometry
screen: root.screen
visibilities: visibilities
screenState: root.screenState
clipboard.transform: Matrix4x4 {
matrix: clipboardBg.deformMatrix
@@ -439,14 +422,14 @@ CustomWindow {
anchors.bottom: geometry.barOnBottom ? parent.bottom : undefined
anchors.left: parent.left
anchors.top: geometry.barOnBottom ? undefined : parent.top
enabled: !visibilities.isDrawing
enabled: !screenState.isDrawing
fullscreen: root.hasFullscreen
height: geometry.horizontal ? implicitHeight : parent.height
popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper
position: geometry.position
screen: root.screen
visibilities: visibilities
screenState: root.screenState
width: geometry.horizontal ? parent.width : implicitWidth
}
}
+2 -1
View File
@@ -119,7 +119,8 @@ ColumnLayout {
length: count
}, () => null);
return forecast.slice(0, count);
const start = Math.max(0, Weather.currentHourIndex);
return forecast.slice(start, start + count);
}
ColumnLayout {
+9
View File
@@ -5,6 +5,15 @@ import Quickshell
Singleton {
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 {
const regexChecker = /^\^.*\$$/;
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 Quickshell
import ZShell
import ZShell.Internal
import ZShell.Config
Singleton {
id: root
readonly property var cachedCities: new Map()
property var cc
property string city
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
property list<var> forecast
property list<var> hourlyForecast
readonly property int humidity: cc?.humidity ?? 0
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 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 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 {
if (cachedCities.has(coords)) {
@@ -112,11 +123,12 @@ Singleton {
forecast = forecastList;
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++) {
const time = new Date(json.hourly.time[i].replace("T", " "));
if (time < now)
if (time < startOfToday)
continue;
hourlyList.push({
@@ -129,6 +141,10 @@ Singleton {
});
}
hourlyForecast = hourlyList;
precipitationBuffer.clear();
for (const hour of hourlyList)
precipitationBuffer.push(hour.precipChance);
});
}
@@ -258,6 +274,11 @@ Singleton {
onLocChanged: fetchWeatherData()
CircularBuffer {
id: precipitationBuffer
capacity: 24
}
Connections {
function onWeatherLocationChanged(): void {
root.reload();
+6 -5
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Modules.Bar.Components
import qs.Modules.Bar.Popouts
@@ -17,7 +18,7 @@ GridLayout {
required property ClipWrapper popoutsWrapper
required property ShellScreen screen
readonly property int vPadding: 6
required property PersistentProperties visibilities
required property ScreenState screenState
function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
@@ -34,7 +35,7 @@ GridLayout {
return;
}
if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings)
if (screenState.sidebar || screenState.dashboard || screenState.resources || screenState.settings)
return;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
@@ -149,7 +150,7 @@ GridLayout {
delegate: EntryWrapper {
Resources {
horizontal: root.horizontal
visibilities: root.visibilities
screenState: root.screenState
visible: !root.fullscreen || root.isHovered
}
}
@@ -173,7 +174,7 @@ GridLayout {
NotifBell {
horizontal: root.horizontal
popouts: root.popouts
visibilities: root.visibilities
screenState: root.screenState
visible: !root.fullscreen || root.isHovered
}
}
@@ -187,7 +188,7 @@ GridLayout {
horizontal: root.horizontal
loader: root
popouts: root.popouts
visibilities: root.visibilities
screenState: root.screenState
visible: !root.fullscreen || root.isHovered
}
}
+3 -3
View File
@@ -21,9 +21,9 @@ Item {
required property ClipWrapper popoutsWrapper
required property string position
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
required property PersistentProperties visibilities
required property ScreenState screenState
function checkPopout(pos: real): void {
(content.item as Bar)?.checkPopout(pos);
@@ -85,7 +85,7 @@ Item {
popouts: root.popouts
popoutsWrapper: root.popoutsWrapper
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ Item {
id: root
required property Item bar
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.fill: parent
+4 -4
View File
@@ -13,7 +13,7 @@ import qs.Services
WidgetBase {
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: {
let fmt = Config.general.dateFormat;
if (!root.horizontal)
@@ -45,9 +45,9 @@ WidgetBase {
required property GridLayout loader
required property Wrapper popouts
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
implicitWidth: horizontal ? size : baseSize
radius: Tokens.rounding.full
@@ -147,7 +147,7 @@ WidgetBase {
color: root.contentColor
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
root.screenState.dashboard = !root.screenState.dashboard;
}
}
}
+6 -6
View File
@@ -11,17 +11,17 @@ WidgetBase {
id: root
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
MaterialIcon {
id: notificationCenterIcon
anchors.centerIn: parent
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.visibilities.sidebar ? 1 : 0
color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.screenState.sidebar ? 1 : 0
font.pointSize: Tokens.font.size.larger
text: NotifServer.list.length ? "\uf4fe" : "\ue7f4"
@@ -36,10 +36,10 @@ WidgetBase {
}
StateLayer {
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
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 {
id: root
required property PersistentProperties visibilities
required property ScreenState screenState
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
implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.medium * 2 : baseSize
radius: Tokens.rounding.full
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 {
@@ -53,8 +53,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "memory"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Cpu.percentage
warningThreshold: 95
}
@@ -64,8 +64,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "memory_alt"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
percentage: Memory.percentage
warningThreshold: 80
}
@@ -75,8 +75,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "gamepad"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
percentage: Gpu.percentage
}
@@ -85,8 +85,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "developer_board"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Gpu.memoryUsed / Gpu.memoryTotal
}
}
+4 -4
View File
@@ -62,10 +62,10 @@ Item {
root.popouts.currentName = `traymenu${root.ind}`;
root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x);
root.popouts.hasCurrent = true;
if (visibilities.sidebar || visibilities.dashboard || visibilities.settings) {
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.settings = false;
if (screenState.sidebar || screenState.dashboard || screenState.settings) {
screenState.sidebar = false;
screenState.dashboard = false;
screenState.settings = false;
}
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ Item {
readonly property int itemHeight: Config.clipboard.sizes.itemHeight
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
implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin
@@ -49,7 +49,7 @@ Item {
Keys.onUpPressed: view.decrementCurrentIndex()
onAccepted: {
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 real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.clipboard && Config.clipboard.enabled
required property PersistentProperties visibilities
readonly property bool shouldBeActive: screenState.clipboard && Config.clipboard.enabled
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
@@ -35,7 +35,7 @@ Item {
sourceComponent: Content {
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+127 -28
View File
@@ -5,78 +5,177 @@ import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell
import qs.Components
Item {
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
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
implicitHeight: nonAnimHeight
Behavior on implicitHeight {
Anim {
}
Anim {}
}
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 {
id: viewWrapper
anchors.bottom: parent.bottom
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.small
color: "transparent"
radius: Tokens.rounding.extraLarge - anchors.margins
Item {
Flickable {
id: view
readonly property int currentIndex: root.dashState?.currentTab
readonly property Item currentItem: row.children[currentIndex]
readonly property int currentIndex: root.screenState.dashboardTab
readonly property Item currentItem: {
repeater.count;
return repeater.itemAt(currentIndex);
}
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 {
id: row
Pane {
index: 0
Repeater {
id: repeater
sourceComponent: Dash {
dashState: root.dashState
visibilities: root.visibilities
}
}
}
}
model: ScriptModel {
values: root.dashboardTabs
}
component Pane: Loader {
id: pane
delegate: Loader {
id: paneLoader
required property int index
required property var modelData
Layout.alignment: Qt.AlignTop
sourceComponent: modelData.component
Component.onCompleted: active = Qt.binding(() => {
// Always keep current tab loaded
if (pane.index === view.currentIndex)
if (index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
}
}
}
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 {
id: root
required property PersistentProperties dashState
readonly property bool dashboardVisible: visibilities.dashboard
readonly property bool dashboardVisible: screenState.dashboard
property int radius: Tokens.rounding.medium
required property PersistentProperties visibilities
required property ScreenState screenState
columnSpacing: Tokens.spacing.small
rowSpacing: Tokens.spacing.small
@@ -61,7 +60,7 @@ GridLayout {
User {
id: user
dashState: root.dashState
screenState: root.screenState
}
}
@@ -72,8 +71,7 @@ GridLayout {
Layout.row: 0
radius: root.radius
Weather {
}
Weather {}
}
Rect {
@@ -87,7 +85,7 @@ GridLayout {
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 {
color: Colors.tPalette.m3surfaceContainer
}
+9 -9
View File
@@ -12,12 +12,12 @@ CustomMouseArea {
id: root
property int activeGrid: 1
required property PersistentProperties dashState
required property ScreenState screenState
property bool initialized: false
property int month1
property int month2
readonly property int realCurrMonth: dashState.currentDate.getMonth()
readonly property int realCurrYear: dashState.currentDate.getFullYear()
readonly property int realCurrMonth: screenState.dashboardDate.getMonth()
readonly property int realCurrYear: screenState.dashboardDate.getFullYear()
property int year1
property int year2
@@ -43,9 +43,9 @@ CustomMouseArea {
function onWheel(event: WheelEvent): void {
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)
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
@@ -61,7 +61,7 @@ CustomMouseArea {
initialized = true;
}
onClicked: root.dashState.currentDate = new Date()
onClicked: root.screenState.dashboardDate = new Date()
onRealCurrMonthChanged: handleDateChange()
onRealCurrYearChanged: handleDateChange()
@@ -135,7 +135,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall
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 {
@@ -158,7 +158,7 @@ CustomMouseArea {
}
}
onClicked: root.dashState.currentDate = new Date()
onClicked: root.screenState.dashboardDate = new Date()
}
Item {
@@ -206,7 +206,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall
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 {
id: root
required property PersistentProperties dashState
required property ScreenState screenState
padding: 20
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
anchors.centerIn: parent
implicitHeight: Config.dashboard.sizes.mediaCoverArtSize
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
implicitHeight: 200
implicitWidth: 200
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
import qs.Components
import ZShell.Config
import qs.Helpers
import qs.Modules
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Templates
import Quickshell
import ZShell.Config
import qs.Components
import qs.Services
Item {
id: root
readonly property alias count: bar.count
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 {
id: bar
@@ -25,85 +26,99 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: root.tabIndicatorSpacing
currentIndex: root.screenState.dashboardTab
onCurrentIndexChanged: root.screenState.dashboardTab = currentIndex
implicitHeight: contentHeight
background: null
currentIndex: root.state.currentTab
contentItem: RowLayout {
spacing: 0
onCurrentIndexChanged: root.state.currentTab = currentIndex
Tab {
iconName: "dashboard"
text: qsTr("Dashboard")
Repeater {
model: bar.contentModel
}
}
Tab {
iconName: "queue_music"
text: qsTr("Media")
Repeater {
model: ScriptModel {
values: root.tabs
}
Tab {
iconName: "speed"
text: qsTr("Performance")
}
delegate: Tab {
required property var modelData
Tab {
iconName: "cloud"
text: qsTr("Weather")
iconName: modelData.iconName
text: modelData.text
}
}
// Tab {
// iconName: "workspaces"
// text: qsTr("Workspaces")
// }
}
Item {
id: indicator
anchors.top: bar.bottom
clip: true
implicitHeight: 40
implicitWidth: bar.currentItem.implicitWidth
anchors.topMargin: root.tabIndicatorSpacing
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: {
const tab = bar.currentItem;
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 {
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
Anim {}
}
}
CustomRect {
id: separator
anchors.top: indicator.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.top: indicator.bottom
color: Colors.palette.m3outlineVariant
implicitHeight: 1
color: Colors.palette.m3outlineVariant
}
component Tab: TabButton {
id: tab
readonly property bool current: TabBar.tabBar.currentItem === this
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
contentItem: CustomMouseArea {
@@ -111,117 +126,47 @@ Item {
function onWheel(event: WheelEvent): void {
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)
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)
implicitHeight: icon.height + label.height
onPressed: event => {
root.state.currentTab = tab.TabBar.index;
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
const stateY = stateWrapper.y;
rippleAnim.x = event.x;
rippleAnim.y = event.y - stateY;
onPressed: root.screenState.dashboardTab = tab.TabBar.index
const dist = (ox, oy) => ox * ox + oy * oy;
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)));
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
StateLayer {
id: stateLayer
anchors.fill: undefined
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: "transparent"
implicitHeight: parent.height + 8 * 2
radius: Tokens.rounding.small
implicitHeight: parent.height + root.tabIndicatorSpacing * 2
CustomRect {
id: stateLayer
anchors.fill: parent
radius: Tokens.rounding.medium
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0
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
}
}
onClicked: root.screenState.dashboardTab = tab.TabBar.index
}
MaterialIcon {
id: icon
anchors.bottom: label.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
font.pointSize: 18
text: tab.iconName
font.pointSize: Tokens.font.size.large
Behavior on fill {
Anim {
type: Anim.DefaultEffects
}
}
}
@@ -229,10 +174,11 @@ Item {
CustomText {
id: label
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
anchors.bottom: parent.bottom
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 {
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
required property real offsetScale
required property string position
readonly property bool shouldBeActive: root.visibilities.dashboard && Config.dashboard.enabled
required property PersistentProperties visibilities
readonly property bool shouldBeActive: root.screenState.dashboard && Config.dashboard.enabled
required property ScreenState screenState
anchors.bottom: position === "bottom" ? parent.bottom : undefined
anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0
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
anchors.top: parent.top
anchors.topMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 854
opacity: 1 - offsetScale
@@ -35,15 +24,11 @@ Item {
id: content
active: root.shouldBeActive || root.visible
anchors.bottom: root.position === "top" ? parent.bottom : undefined
anchors.horizontalCenter: root.position !== "left" ? parent.horizontalCenter : undefined
anchors.right: root.position !== "left" ? undefined : parent.right
anchors.top: root.position === "bottom" ? parent.top : undefined
anchors.verticalCenter: root.position === "left" ? undefined : parent.verticalCenter
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: Content {
dashState: root.dashState
visibilities: root.visibilities
screenState: root.screenState
}
}
}
@@ -8,6 +8,7 @@ import ZShell.Config
import qs.Paths
import qs.Helpers
import qs.Services
import qs.Services
Item {
id: root
@@ -97,8 +98,8 @@ Item {
anchors.fill: parent
onClicked: {
const visibilities = Visibilities.getForActive();
visibilities.settings = true;
const state = ShellState.forActive();
state.settings = true;
root.close()
}
}
+3 -3
View File
@@ -27,7 +27,7 @@ Item {
property var pendingCommitIds: []
readonly property int rounding: Tokens.rounding.largeIncreased
required property ShellScreen screen
required property PersistentProperties visibilities
required property ScreenState screenState
property var visualIds: []
function beginVisualDrag(appId, modelData, item) {
@@ -198,7 +198,7 @@ Item {
anchors.centerIn: parent
appListRoot: root
appToplevel: modelData
visibilities: root.visibilities
screenState: root.screenState
visible: root.draggedAppId !== slot.appId
}
@@ -314,7 +314,7 @@ Item {
appListRoot: root
appToplevel: root.draggedModelData
enabled: false
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ CustomRect {
property real iconSize: implicitHeight - 20
readonly property bool isSeparator: appToplevel?.appId === "__dock_separator__"
property int lastFocused: -1
required property PersistentProperties visibilities
required property ScreenState screenState
implicitHeight: Config.dock.height
implicitWidth: isSeparator ? 1 : implicitHeight
@@ -62,12 +62,12 @@ CustomRect {
onClicked: {
if (appToplevel?.toplevels.length === 0) {
root.desktopEntry?.execute();
root.visibilities.dock = false;
root.screenState.dock = false;
return;
}
lastFocused = (lastFocused + 1) % appToplevel?.toplevels.length;
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
required property var panels
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.dock && Config.dock.enable
required property PersistentProperties visibilities
readonly property bool shouldBeActive: screenState.dock && Config.dock.enable
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
@@ -39,7 +39,7 @@ Item {
sourceComponent: Content {
panels: root.panels
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 colors2: ["#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"]
required property var drawing
required property var visibilities
required property ScreenState screenState
required property Wrapper wrapper
function syncFromPenColor() {
@@ -20,17 +20,17 @@ Item {
return;
if (!saturationSlider.pressed)
saturationSlider.value = drawing.drawingState.penColor.hsvSaturation;
saturationSlider.value = drawing.screenState.penColor.hsvSaturation;
if (!brightnessSlider.pressed)
brightnessSlider.value = drawing.drawingState.penColor.hsvValue;
brightnessSlider.value = drawing.screenState.penColor.hsvValue;
}
function updatePenColorFromHsv() {
if (!drawing)
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
@@ -43,7 +43,7 @@ Item {
root.syncFromPenColor();
}
target: root.drawing.drawingState
target: root.drawing.screenState
}
Column {
@@ -133,9 +133,9 @@ Item {
multiplier: 1
orientation: Qt.Horizontal
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 {
@@ -153,7 +153,7 @@ Item {
shapeMorph: true
text: "Exit"
onClicked: root.visibilities.isDrawing = false
onClicked: root.screenState.isDrawing = false
}
IconTextButton {
@@ -203,6 +203,6 @@ Item {
shapeMorph: true
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 bool pinned: false
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.isDrawing
required property var visibilities
readonly property bool shouldBeActive: screenState.isDrawing
required property ScreenState screenState
function collapse(): void {
if (pinned)
@@ -98,7 +98,7 @@ Item {
}
sourceComponent: Content {
drawing: root.drawing
visibilities: root.visibilities
screenState: root.screenState
wrapper: root
}
}
+5 -5
View File
@@ -15,7 +15,7 @@ CustomListView {
property string displayText
readonly property string requestedState: stateForText(search.text)
required property SearchBar search
required property PersistentProperties visibilities
required property ScreenState screenState
function resultsForText(text: string): var {
switch (stateForText(text)) {
@@ -46,7 +46,7 @@ CustomListView {
}
function syncDisplayText(): void {
if (visibilities.launcher && requestedState === displayState)
if (screenState.launcher && requestedState === displayState)
displayText = search.text;
}
@@ -57,7 +57,7 @@ CustomListView {
preferredHighlightBegin: 0
preferredHighlightEnd: height
spacing: Tokens.spacing.small
state: visibilities.launcher ? requestedState : displayState
state: screenState.launcher ? requestedState : displayState
verticalLayoutDirection: ListView.BottomToTop
CustomScrollBar.vertical: CustomScrollBar {
@@ -245,7 +245,7 @@ CustomListView {
id: appItem
AppItem {
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -294,6 +294,6 @@ CustomListView {
root.syncDisplayText();
}
target: root.visibilities
target: root.screenState
}
}
+3 -3
View File
@@ -13,14 +13,14 @@ CarouselView {
readonly property var screen: QsWindow.window?.screen
required property SearchBar search
readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" "))
required property var visibilities
required property ScreenState screenState
currentIndex: {
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
return idx >= 0 ? idx : 0;
}
delegateProperties: ({
visibilities: root.visibilities
screenState: root.screenState
})
focalWidth: Config.launcher.sizes.wallpaperWidth
glideDuration: 250
@@ -32,7 +32,7 @@ CarouselView {
return 0;
const barMargins = Config.bar.border * 2;
let margins = Math.round(screen.width / 6);
if (visibilities.utilities || visibilities.sidebar)
if (screenState.utilities || screenState.sidebar)
margins = Config.sidebar.sizes.width;
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
required property var panels
readonly property int rounding: Tokens.rounding.largeIncreased
required property PersistentProperties visibilities
required property ScreenState screenState
implicitHeight: search.height + listWrapper.height + padding * 2
implicitWidth: listWrapper.width + padding * 2
@@ -38,7 +38,7 @@ Item {
panels: root.panels
rounding: root.rounding
search: search
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -55,7 +55,7 @@ Item {
Component.onCompleted: forceActiveFocus()
Keys.onDownPressed: list.currentList?.decrementCurrentIndex()
Keys.onEscapePressed: root.visibilities.launcher = false
Keys.onEscapePressed: root.screenState.launcher = false
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
if (event.key === Qt.Key_J) {
@@ -81,7 +81,7 @@ Item {
if (Colors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent)
Wallpapers.previewColorLock = true;
Wallpapers.setWallpaper(currentItem.modelData.path);
root.visibilities.launcher = false;
root.screenState.launcher = false;
} else if (text.startsWith(Config.launcher.actionPrefix)) {
if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
currentItem.onClicked();
@@ -91,18 +91,18 @@ Item {
currentItem.modelData.onClicked(list.currentList);
} else {
Apps.launch(currentItem.modelData);
root.visibilities.launcher = false;
root.screenState.launcher = false;
}
}
}
Connections {
function onLauncherChanged(): void {
if (!root.visibilities.launcher)
if (!root.screenState.launcher)
search.text = "";
}
target: root.visibilities
target: root.screenState
}
}
}
+5 -5
View File
@@ -20,7 +20,7 @@ Item {
required property int rounding
required property SearchBar search
readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `)
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
@@ -28,7 +28,7 @@ Item {
state: showWallpapers ? "wallpapers" : "apps"
Behavior on implicitHeight {
enabled: root.visibilities.launcher
enabled: root.screenState.launcher
Anim {
duration: Tokens.anim.durations.small
@@ -36,7 +36,7 @@ Item {
}
}
Behavior on implicitWidth {
enabled: root.visibilities.launcher
enabled: root.screenState.launcher
Anim {
duration: Tokens.anim.durations.small
@@ -99,7 +99,7 @@ Item {
sourceComponent: AppList {
search: root.search
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -115,7 +115,7 @@ Item {
sourceComponent: CarouselView {
panels: root.panels
search: root.search
visibilities: root.visibilities
screenState: root.screenState
}
}
+2 -2
View File
@@ -12,7 +12,7 @@ Item {
id: root
required property DesktopEntry modelData
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.left: parent?.left
anchors.right: parent?.right
@@ -23,7 +23,7 @@ Item {
onClicked: {
Apps.launch(root.modelData);
root.visibilities.launcher = false;
root.screenState.launcher = false;
}
}
+2 -2
View File
@@ -16,7 +16,7 @@ Item {
function onClicked(): void {
Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]);
root.list.visibilities.launcher = false;
root.list.screenState.launcher = false;
}
anchors.left: parent?.left
@@ -81,7 +81,7 @@ Item {
onClicked: {
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]);
root.list.visibilities.launcher = false;
root.list.screenState.launcher = false;
}
}
+2 -2
View File
@@ -18,7 +18,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
function onClicked(): void {
root.list.visibilities.launcher = false;
root.list.screenState.launcher = false;
if (root.modelData?.path)
Files.launch(["xdg-open", root.modelData.path]);
}
@@ -60,7 +60,7 @@ Item {
}
onClicked: {
root.list.visibilities.launcher = false;
root.list.screenState.launcher = false;
if (root.modelData?.path)
Files.showInFolder(root.modelData.path);
}
+1 -1
View File
@@ -12,7 +12,7 @@ Item {
required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight
required property real tileWidth
required property PersistentProperties visibilities
required property ScreenState screenState
signal requestActivate
+2 -2
View File
@@ -42,10 +42,10 @@ Searcher {
if (command[0] === "autocomplete" && command.length > 1) {
list.search.text = `${Config.launcher.actionPrefix}${command[1]} `;
} else if (command[0] === "setMode" && command.length > 1) {
list.visibilities.launcher = false;
list.screenState.launcher = false;
Colors.setMode(command[1]);
} else {
list.visibilities.launcher = false;
list.screenState.launcher = false;
Quickshell.execDetached(command);
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ Searcher {
required property string variant
function onClicked(list: AppList): void {
list.visibilities.launcher = false;
list.screenState.launcher = false;
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]);
Config.colors.schemeType = variant;
}
+4 -4
View File
@@ -12,7 +12,7 @@ Item {
property int contentHeight
readonly property real maxHeight: {
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;
if (panels.popouts.hasCurrent)
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
required property var panels
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.launcher
required property PersistentProperties visibilities
readonly property bool shouldBeActive: screenState.launcher
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
@@ -57,7 +57,7 @@ Item {
sourceComponent: Content {
maxHeight: root.maxHeight
panels: root.panels
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+2 -2
View File
@@ -60,7 +60,7 @@ CustomRect {
MaterialShape {
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
shape: MaterialShape.Cookie4Sided
@@ -73,7 +73,7 @@ CustomRect {
id: temp
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
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
required property Item panels
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.bottom: parent.bottom
anchors.right: parent.right
@@ -27,16 +27,16 @@ Item {
for (let i = 0; i < count; i++)
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;
if (visibilities.osd) {
if (screenState.osd) {
const h = panels.osd.y - Tokens.spacing.small * 2 - padding * 2;
if (height > h)
height = h;
}
if (visibilities.session) {
if (screenState.session) {
const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2;
if (height > h)
height = h;
@@ -3,7 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell
import ZShell.Llm
import qs.Helpers
import qs.Services
Singleton {
id: root
@@ -20,7 +20,7 @@ Singleton {
property bool narrowSidebarExpanded: false
property bool windowIsWide: false
property ShellScreen screen
property bool sidebarVisible: Visibilities.getForActive().sidebar
property bool sidebarVisible: ShellState.forActive().sidebar
onSidebarVisibleChanged: if (!sidebarVisible)
fabExpanded = false
@@ -3,7 +3,7 @@ pragma Singleton
import Quickshell
import QtQuick
import ZShell.Config
import qs.Helpers
import qs.Services
import qs.Services
Singleton {
@@ -11,7 +11,7 @@ Singleton {
function create(parent: Item, props: var): void {
chatComp.createObject(parent ?? dummy, props);
Visibilities.getForActive().sidebar = false;
ShellState.forActive().sidebar = false;
ChatState.isWindow = true;
}
+8 -10
View File
@@ -10,13 +10,12 @@ import qs.Services
Item {
id: root
required property Props props
required property var visibilities
required property ScreenState screenState
Connections {
function onIsWindowChanged(): void {
if (ChatState.isWindow)
root.props.currentTab = 0;
root.screenState.sidebarTab = 0;
}
target: ChatState
@@ -31,7 +30,7 @@ Item {
Tabs {
Layout.fillWidth: true
Layout.preferredHeight: ChatState.isWindow || !Config.llm.enabled ? 0 : implicitHeight
dashState: root.props
screenState: root.screenState
nonAnimWidth: layout.width
visible: height > 0
}
@@ -47,9 +46,9 @@ Item {
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: parent.width
opacity: root.props.currentTab === 0 ? 1 : 0
opacity: root.screenState.sidebarTab === 0 ? 1 : 0
visible: opacity > 0
x: root.props.currentTab === 0 ? 0 : -root.width
x: root.screenState.sidebarTab === 0 ? 0 : -root.width
Behavior on opacity {
Anim {}
@@ -64,8 +63,7 @@ Item {
radius: Tokens.rounding.small
NotifDock {
props: root.props
visibilities: root.visibilities
screenState: root.screenState
}
}
}
@@ -82,9 +80,9 @@ Item {
sourceComponent: Item {
id: chatPage
opacity: root.props.currentTab === 1 ? 1 : 0
opacity: root.screenState.sidebarTab === 1 ? 1 : 0
visible: opacity > 0
x: root.props.currentTab === 0 ? root.width : 0
x: root.screenState.sidebarTab === 0 ? root.width : 0
z: 1
Behavior on opacity {
+2 -3
View File
@@ -15,8 +15,7 @@ CustomRect {
required property bool expanded
required property NotifServer.Notif modelData
readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height
required property Props props
required property PersistentProperties visibilities
required property ScreenState screenState
color: {
const c = root.modelData?.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2);
@@ -137,7 +136,7 @@ CustomRect {
onLinkActivated: link => {
Qt.openUrlExternally(link);
root.visibilities.sidebar = false;
root.screenState.sidebar = false;
}
}
+2 -4
View File
@@ -14,8 +14,7 @@ Item {
id: root
readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
required property Props props
required property var visibilities
required property ScreenState screenState
anchors.fill: parent
anchors.margins: Tokens.padding.small
@@ -120,8 +119,7 @@ Item {
id: notifList
container: view
props: root.props
visibilities: root.visibilities
screenState: root.screenState
}
}
}
@@ -12,8 +12,7 @@ LazyListView {
id: root
required property Flickable container
required property Props props
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.left: parent?.left
anchors.right: parent?.right
@@ -118,8 +117,7 @@ LazyListView {
container: root.container
modelData: notif.modelData
props: root.props
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+6 -8
View File
@@ -16,7 +16,7 @@ CustomRect {
readonly property list<var> activeNotifs: notifs.filter(n => !n.closed)
readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
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 ?? ""
required property string modelData
readonly property int nonAnimHeight: {
@@ -26,7 +26,7 @@ CustomRect {
}
readonly property int notifCount: activeNotifs.length
readonly property list<var> notifs: NotifServer.list.filter(n => n.appName === modelData)
required property Props props
required property ScreenState screenState
readonly property int urgency: {
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical))
return NotificationUrgency.Critical;
@@ -34,14 +34,13 @@ CustomRect {
return NotificationUrgency.Normal;
return NotificationUrgency.Low;
}
required property PersistentProperties visibilities
function toggleExpand(expand: bool): void {
if (expand) {
if (!expanded)
props.expandedNotifs.push(modelData);
screenState.expandedNotifs.push(modelData);
} 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: {
if (notifCount === 0 && expanded)
props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1);
screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
}
RowLayout {
@@ -233,8 +232,7 @@ CustomRect {
container: root.container
expanded: root.expanded
notifs: root.notifs
props: root.props
visibilities: root.visibilities
screenState: root.screenState
onRequestToggleExpand: expand => root.toggleExpand(expand)
}
@@ -14,8 +14,7 @@ LazyListView {
required property Flickable container
required property bool expanded
required property list<var> notifs
required property Props props
required property PersistentProperties visibilities
required property ScreenState screenState
signal requestToggleExpand(expand: bool)
@@ -122,8 +121,7 @@ LazyListView {
anchors.fill: parent
expanded: root.expanded
modelData: notif.modelData
props: root.props
visibilities: root.visibilities
screenState: root.screenState
}
}
}
-9
View File
@@ -1,9 +0,0 @@
import Quickshell
import ZShell.Llm
PersistentProperties {
property int currentTab: 0
property list<string> expandedNotifs: []
reloadableId: "sidebar"
}
+4 -4
View File
@@ -12,7 +12,7 @@ Item {
id: root
readonly property alias count: bar.count
required property PersistentProperties dashState
required property ScreenState screenState
required property real nonAnimWidth
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
@@ -24,7 +24,7 @@ Item {
anchors.right: parent.right
anchors.top: parent.top
background: null
currentIndex: root.dashState.currentTab
currentIndex: root.screenState.sidebarTab
implicitHeight: contentHeight
contentItem: RowLayout {
@@ -35,7 +35,7 @@ Item {
}
}
onCurrentIndexChanged: root.state.currentTab = currentIndex
onCurrentIndexChanged: root.screenState.sidebarTab = currentIndex
Tab {
iconName: "notifications"
@@ -110,7 +110,7 @@ Item {
color: tab.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
radius: Tokens.rounding.small
onClicked: root.dashState.currentTab = tab.TabBar.index
onClicked: root.screenState.sidebarTab = tab.TabBar.index
}
MaterialIcon {
@@ -11,8 +11,8 @@ import qs.Services
CustomRect {
id: root
required property var props
required property PersistentProperties visibilities
required property ScreenState screenState
Layout.fillWidth: true
color: Colors.tPalette.m3surfaceContainer
@@ -72,7 +72,7 @@ CustomRect {
}
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
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
RecordingList {
props: root.props
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -14,8 +14,8 @@ import qs.Services
ColumnLayout {
id: root
required property var props
required property PersistentProperties visibilities
required property ScreenState screenState
spacing: 0
@@ -23,7 +23,7 @@ ColumnLayout {
Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
RowLayout {
spacing: Tokens.spacing.small
@@ -42,11 +42,11 @@ ColumnLayout {
}
IconButton {
icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more"
icon: root.screenState.recordingListExpanded ? "unfold_less" : "unfold_more"
label.animate: true
type: IconButton.Text
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
}
}
}
@@ -57,7 +57,7 @@ ColumnLayout {
Layout.fillWidth: true
Layout.rightMargin: -Tokens.spacing.small
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 {
flickable: list
@@ -109,7 +109,7 @@ ColumnLayout {
type: IconButton.Text
onClicked: {
root.visibilities.sidebar = false;
root.screenState.sidebar = false;
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
else
@@ -122,7 +122,7 @@ ColumnLayout {
type: IconButton.Text
onClicked: {
root.visibilities.sidebar = false;
root.screenState.sidebar = false;
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
else
@@ -174,11 +174,11 @@ ColumnLayout {
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0
Layout.preferredHeight: root.screenState.recordingListExpanded ? implicitHeight : 0
color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.extraLarge
opacity: root.props.recordingListExpanded ? 1 : 0
scale: root.props.recordingListExpanded ? 1 : 0
opacity: root.screenState.recordingListExpanded ? 1 : 0
scale: root.screenState.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredHeight {
@@ -197,10 +197,10 @@ ColumnLayout {
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0
Layout.preferredWidth: !root.screenState.recordingListExpanded ? implicitWidth : 0
color: Colors.palette.m3outline
opacity: !root.props.recordingListExpanded ? 1 : 0
scale: !root.props.recordingListExpanded ? 1 : 0
opacity: !root.screenState.recordingListExpanded ? 1 : 0
scale: !root.screenState.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredWidth {
@@ -39,7 +39,7 @@ CustomRect {
});
}
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
required property PersistentProperties visibilities
required property ScreenState screenState
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
@@ -166,8 +166,8 @@ CustomRect {
isToggle: false
onClicked: {
root.visibilities.sidebar = false;
root.visibilities.settings = true;
root.screenState.sidebar = false;
root.screenState.settings = true;
}
}
}
@@ -1,14 +1,14 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules.Notifications.Sidebar.Utils.Cards
import ZShell.Config
Item {
id: root
required property PersistentProperties props
required property var visibilities
required property ScreenState screenState
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
@@ -22,14 +22,13 @@ Item {
IdleInhibit {}
Record {
props: root.props
visibilities: root.visibilities
screenState: root.screenState
z: 1
}
Toggles {
Layout.fillWidth: true
visibilities: root.visibilities
screenState: root.screenState
}
}
}
@@ -9,16 +9,9 @@ Item {
id: root
property real offsetScale: shouldBeActive ? 0 : 1
readonly property PersistentProperties props: PersistentProperties {
property string recordingConfirmDelete
property bool recordingListExpanded: false
property string recordingMode
reloadableId: "utilities"
}
readonly property bool shouldBeActive: visibilities.sidebar && !sidebar.chatActive
readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive
required property Item sidebar
required property var visibilities
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
@@ -43,8 +36,7 @@ Item {
sourceComponent: Content {
implicitWidth: root.implicitWidth - 8 * 2
props: root.props
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+4 -7
View File
@@ -13,11 +13,9 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels
readonly property Props props: Props {
}
readonly property bool shouldBeActive: root.visibilities.sidebar && Config.sidebar.enabled
readonly property bool chatActive: props.currentTab === 1
required property var visibilities
readonly property bool shouldBeActive: root.screenState.sidebar && Config.sidebar.enabled
readonly property bool chatActive: screenState.sidebarTab === 1
required property ScreenState screenState
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
implicitWidth: Config.sidebar.sizes.width
@@ -54,8 +52,7 @@ Item {
sourceComponent: Content {
implicitWidth: Config.sidebar.sizes.width - Tokens.padding.small * 2
props: root.props
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ Item {
required property Item panels
required property Item sidebarPanel
required property var visibilities
required property ScreenState screenState
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth
@@ -17,6 +17,6 @@ Item {
id: content
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 sourceMuted
required property real sourceVolume
required property var visibilities
required property ScreenState screenState
required property real volume
implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2
@@ -53,7 +53,7 @@ Item {
// Microphone volume
WrappedLoader {
shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session)
shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.screenState.session)
sourceComponent: CustomMouseArea {
function onWheel(event: WheelEvent) {
+5 -5
View File
@@ -16,16 +16,16 @@ Item {
property bool muted
property real offsetScale: shouldBeActive ? 0 : 1
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
required property bool sidebarOrSessionVisible
property bool sourceMuted
property real sourceVolume
required property var visibilities
required property ScreenState screenState
property real volume
function show(): void {
visibilities.osd = true;
screenState.osd = true;
timer.restart();
}
@@ -90,7 +90,7 @@ Item {
onTriggered: {
if (!root.hovered)
root.visibilities.osd = false;
root.screenState.osd = false;
}
}
@@ -107,7 +107,7 @@ Item {
muted: root.muted
sourceMuted: root.sourceMuted
sourceVolume: root.sourceVolume
visibilities: root.visibilities
screenState: root.screenState
volume: root.volume
}
}
+2 -2
View File
@@ -11,8 +11,8 @@ Item {
readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0
property real offsetScale: shouldBeActive ? 0 : 1
required property string position
readonly property bool shouldBeActive: root.visibilities.resources
required property PersistentProperties visibilities
readonly property bool shouldBeActive: root.screenState.resources
required property ScreenState screenState
anchors.bottom: position === "bottom" ? parent.bottom : undefined
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
}
// 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
property string queuedMode: "wallpaper"
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.settings
required property PersistentProperties visibilities
readonly property bool shouldBeActive: screenState.settings
required property ScreenState screenState
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth
+13 -12
View File
@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Io
import qs.Components
import qs.Helpers
import qs.Services
Scope {
id: root
@@ -17,8 +18,8 @@ Scope {
onPressed: {
root.launcherInterrupted = false;
if (!root.launcherInterrupted && !root.hasFullscreen) {
const visibilities = Visibilities.getForActive();
visibilities.launcher = !visibilities.launcher;
const state = ShellState.forActive();
state.launcher = !state.launcher;
}
root.launcherInterrupted = false;
}
@@ -28,8 +29,8 @@ Scope {
name: "toggle-drawing"
onPressed: {
const visibilities = Visibilities.getForActive();
visibilities.isDrawing = !visibilities.isDrawing;
const state = ShellState.forActive();
state.isDrawing = !state.isDrawing;
}
}
@@ -37,8 +38,8 @@ Scope {
name: "toggle-nc"
onPressed: {
const visibilities = Visibilities.getForActive();
visibilities.sidebar = !visibilities.sidebar;
const state = ShellState.forActive();
state.sidebar = !state.sidebar;
}
}
@@ -46,8 +47,8 @@ Scope {
name: "show-osd"
onPressed: {
const visibilities = Visibilities.getForActive();
visibilities.osd = !visibilities.osd;
const state = ShellState.forActive();
state.osd = !state.osd;
}
}
@@ -55,8 +56,8 @@ Scope {
name: "toggle-settings"
onPressed: {
const visibilities = Visibilities.getForActive();
visibilities.settings = !visibilities.settings;
const state = ShellState.forActive();
state.settings = !state.settings;
}
}
@@ -64,8 +65,8 @@ Scope {
name: "toggle-clipboard"
onPressed: {
const visibilities = Visibilities.getForActive();
visibilities.clipboard = !visibilities.clipboard;
const state = ShellState.forActive();
state.clipboard = !state.clipboard;
}
}
}
+2
View File
@@ -1,5 +1,6 @@
qml_module(ZShell-config
URI ZShell.Config
INCLUDE_PREFIX config
SOURCES
confignode.hpp confignode.cpp
configobject.hpp configobject.cpp
@@ -16,6 +17,7 @@ qml_module(ZShell-config
dashboard.hpp
display.hpp
dock.hpp
enums.hpp
general.hpp
launcher.hpp
llm.hpp
+2
View File
@@ -48,6 +48,8 @@ class Dashboard : public ConfigObject {
QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(bool, showOnHover, true)
CFG_PROPERTY(int, dragThreshold, 50)
CFG_PROPERTY(int, mediaUpdateInterval, 2000)
CONFIG_SUBOBJECT(Performance, performance)
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
#include "configlist.hpp"
#include "configobject.hpp"
#include <QStandardPaths>
#include <QStringList>
namespace ZShell::config {
@@ -118,6 +119,10 @@ class General : public ConfigObject {
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CFG_PROPERTY(bool, desktopIcons, true)
CONFIG_SUBOBJECT(Idle, idle)
CFG_PROPERTY(
QString,
lyricsDir,
QStandardPaths::writableLocation(QStandardPaths::MusicLocation) + QStringLiteral("/Lyrics/"))
CFG_PROPERTY(QString, logo, QString())
CFG_PROPERTY(bool, showOverFullscreen, true)
CFG_PROPERTY(
+2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "enums.hpp"
namespace ZShell::config {
@@ -31,6 +32,7 @@ class Services : public ConfigObject {
CFG_PROPERTY(bool, ddcutilService, true)
CFG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
CFG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA"))
CFG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto)
CFG_PROPERTY(qreal, maxVolume, 1)
CFG_PROPERTY(qreal, minBrightness, 0.01)
CONFIG_LIST(
+103 -8
View File
@@ -1,9 +1,11 @@
#include "sparklineitem.hpp"
#include <qmath.h>
#include <qnumeric.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
#include <qtypes.h>
namespace ZShell::internal {
@@ -35,9 +37,19 @@ void SparklineItem::drawLine(
const int len = buffer->count();
if (len < 2 || m_maxValue <= 0.0) return;
const qreal stepX = w / static_cast<qreal>(m_historyLength - 1);
const qreal startX =
w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
qreal stepX = 0.0;
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 curvePad = 3.0;
@@ -52,24 +64,75 @@ void SparklineItem::drawLine(
points.reserve(len);
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 value = qBound<qreal>(0.0, buffer->at(i), m_maxValue);
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath linePath;
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) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
const QPointF c1(ctrlX, p0.y());
const QPointF c2(ctrlX, p1.y());
linePath.cubicTo(c1, c2, p1);
linePath.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
}
}
QPen pen(color, m_lineWidth);
@@ -224,4 +287,36 @@ void SparklineItem::setLineWidth(qreal width) {
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
+23
View File
@@ -4,6 +4,7 @@
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <qtmetamacros.h>
#include "circularbuffer.hpp"
@@ -40,6 +41,13 @@ class SparklineItem : public QQuickPaintedItem {
Q_PROPERTY(
qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY
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:
explicit SparklineItem(QQuickItem* parent = nullptr);
@@ -76,6 +84,15 @@ class SparklineItem : public QQuickPaintedItem {
[[nodiscard]] qreal lineWidth() const;
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:
void line1Changed();
void line2Changed();
@@ -87,6 +104,9 @@ class SparklineItem : public QQuickPaintedItem {
void slideProgressChanged();
void historyLengthChanged();
void lineWidthChanged();
void slidingChanged();
void smoothingChanged();
void curveSmoothingChanged();
private:
void drawLine(
@@ -106,6 +126,9 @@ class SparklineItem : public QQuickPaintedItem {
qreal m_slideProgress = 0.0;
int m_historyLength = 30;
qreal m_lineWidth = 2.0;
bool m_sliding = true;
qreal m_smoothing = 0.0;
bool m_curveSmoothing = false;
};
} // namespace ZShell::internal
+6 -1
View File
@@ -18,6 +18,8 @@ qml_module(ZShell-services
memory.hpp memory.cpp
diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp
lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp
LIBRARIES
Qt6::Core
Qt6::Qml
@@ -25,4 +27,7 @@ qml_module(ZShell-services
PkgConfig::Aubio
PkgConfig::Cava
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
+1 -1
View File
@@ -1,7 +1,7 @@
message(STATUS "QML install dir: ${CMAKE_INSTALL_PREFIX}/${INSTALL_QMLDIR}")
function(qml_module arg_TARGET)
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;QML_SINGLETONS;DEPENDENCIES;IMPORTS;OPTIONAL_IMPORTS;DEFAULT_IMPORTS;LIBRARIES")
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)
+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 {}
}
}