Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c02ef0a934 | ||
|
|
58c187dadf | ||
|
|
6391bc85c4 | ||
|
|
d1a045787e | ||
|
|
7c55d9627a | ||
|
|
5fd2720f00 | ||
|
|
74261f0a49 | ||
|
|
e76350fa7f | ||
|
|
015c1fb27d | ||
|
|
f6cb7e44c2 | ||
|
|
5a8939bc1e | ||
|
|
6071223009 | ||
|
|
2d40ce33d1 | ||
|
|
954dc10fbb | ||
|
|
15ccfd24c6 | ||
|
|
33b08523e9 | ||
|
|
742bece30b | ||
|
|
a64eadd14a | ||
|
|
f9eeb793db | ||
|
|
6bb193c78c | ||
|
|
aadecbfe44 | ||
|
|
53f0aa80ae | ||
|
|
d9856c9409 | ||
|
|
8dc9753e2b | ||
|
|
2a4bd3dfdd | ||
|
|
f75f51ef50 | ||
|
|
e0873304d1 | ||
|
|
99a0155c17 | ||
|
|
f309debea5 | ||
|
|
0979fd014d | ||
|
|
656bd0a54d | ||
|
|
f01001dcfa | ||
|
|
bc6b0d50ab | ||
|
|
f27c9afc25 | ||
|
|
5962fc8354 | ||
|
|
1adcb9841c | ||
|
|
0e0f8555ed | ||
|
|
1d63e67554 |
@@ -1,3 +0,0 @@
|
||||
|
||||
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
|
||||
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
|
||||
@@ -50,7 +50,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(QT_NO_PRIVATE_MODULE_WARNING ON)
|
||||
set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
|
||||
|
||||
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
|
||||
|
||||
@@ -16,7 +16,7 @@ CustomRect {
|
||||
property color activeOnColor
|
||||
property bool checked
|
||||
property real checkedRadius: Tokens.rounding.medium
|
||||
property real defaultRadius: Tokens.rounding.large
|
||||
property real defaultRadius: Math.min(width, height) / 2
|
||||
property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1)
|
||||
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
|
||||
property bool fillWidth
|
||||
|
||||
@@ -34,6 +34,7 @@ BusyIndicator {
|
||||
anchors.fill: parent
|
||||
bgColor: root.bgColor
|
||||
fgColor: root.fgColor
|
||||
hasEndIndicator: false
|
||||
padding: root.padding
|
||||
rotation: manager.rotation
|
||||
startAngle: manager.startFraction * 360
|
||||
@@ -53,6 +54,7 @@ BusyIndicator {
|
||||
Anim {
|
||||
duration: manager.completeEndDuration * Tokens.anim.durations.scale
|
||||
properties: "opacity,internalStrokeWidth"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
|
||||
Item {
|
||||
@@ -20,7 +19,7 @@ Item {
|
||||
}
|
||||
|
||||
ShaderEffectSource {
|
||||
id: src
|
||||
id: source
|
||||
|
||||
hideSource: true
|
||||
sourceItem: contentItem
|
||||
@@ -28,7 +27,7 @@ Item {
|
||||
|
||||
RoundedMask {
|
||||
anchors.fill: parent
|
||||
source: src
|
||||
source: source
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
|
||||
@@ -150,6 +150,7 @@ TextFieldBase {
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: root.leadingIcon
|
||||
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Flickable {
|
||||
id: root
|
||||
|
||||
interactive: !ShellState.forActive().isDrawing
|
||||
|
||||
rebound: Transition {
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
}
|
||||
|
||||
readonly property KineticWheelScroller _scroller: KineticWheelScroller {
|
||||
id: wheelScroller
|
||||
anchors.fill: parent
|
||||
parent: root
|
||||
z: 12
|
||||
flickable: root
|
||||
}
|
||||
}
|
||||
@@ -1,654 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
// Taken from hyprfm: https://github.com/soyeb-jim285/hyprfm/blob/main/src/qml/views/KineticWheelScroller.qml
|
||||
// Adapted to fit this project where needed.
|
||||
|
||||
// Deliberately not a MouseArea: a MouseArea claims the arrow cursor for its
|
||||
// whole area, which silently killed every cursorShape in the views this
|
||||
// overlays (column resize grips, pointer hands). A WheelHandler on a plain
|
||||
// Item takes wheel events without touching the cursor.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var flickable
|
||||
|
||||
property real wheelStep: 56
|
||||
property real mouseWheelMultiplier: 1.0
|
||||
property real touchpadMultiplier: 1.35
|
||||
property real minVelocity: 480
|
||||
property real maxVelocity: 5200
|
||||
property real kineticGain: 1.35
|
||||
property real smoothing: 0.72
|
||||
property real edgeEpsilon: 1.0
|
||||
// Browser-style overscroll: larger visible rubber-band, softer
|
||||
// resistance, and a springy settle on release.
|
||||
property real overshootLimit: 140
|
||||
property real overshootResistance: 0.38
|
||||
property real maxOvershootInput: 520
|
||||
// Bounce magnitude per unit of impact velocity. Keep this deliberately
|
||||
// small: slow edge hits should settle, not fire a visibly faster bounce.
|
||||
property real kineticBounceGain: 0.018
|
||||
property real maxKineticBounceInput: 90
|
||||
// Minimum impact velocity that actually produces a visible bounce.
|
||||
// Very slow landings skip bounce entirely — matches what browsers do
|
||||
// when a fling decays to near-zero before reaching the edge.
|
||||
property real minBounceVelocity: 260
|
||||
|
||||
property real velocity: 0
|
||||
property double lastWheelTimestamp: 0
|
||||
property bool kineticCandidate: false
|
||||
property real overshootInput: 0
|
||||
property real overshootReleaseVelocity: 0
|
||||
property real pendingKineticBounceVelocity: 0
|
||||
// Set by runKineticGlide when the glide will clamp against an edge.
|
||||
// It is the glide velocity at the instant of impact, which drives the
|
||||
// elastic bounce amount and duration — browser-style: slow impact
|
||||
// makes a small gentle bounce; fast impact makes a big springy one.
|
||||
property real pendingImpactVelocity: 0
|
||||
|
||||
// Browser-style exponential-feeling glide replaces the linear
|
||||
// flickDeceleration decay. Tail uses OutQuart — soft landing.
|
||||
property real kineticTau: 0.42
|
||||
|
||||
property real mouseGlideTime: 0.09 // decay time constant (s); higher = longer coast
|
||||
property real mouseAccelWindow: 140 // ms between notches that counts as "continuous"
|
||||
property real mouseAccelStep: 0.15 // extra impulse multiplier per consecutive notch
|
||||
property real mouseMaxAccel: 2.5
|
||||
property real mouseSnap: 0.2 // fraction of each notch applied instantly (0 = fully glided, 1 = no smoothing)
|
||||
|
||||
property real mouseVelocity: 0
|
||||
property int mouseDir: 0
|
||||
property int mouseStreak: 0
|
||||
property double mouseLastTime: 0
|
||||
|
||||
// Escape hatch if auto-detection misjudges a device
|
||||
property bool forceMouseWheel: false
|
||||
|
||||
readonly property real visualOvershoot: overshootInput < 0 ? rubberDistance(-overshootInput) : -rubberDistance(overshootInput)
|
||||
|
||||
signal scrollStarted
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
// `blocking` decides whether the event continues to the Flickable
|
||||
// underneath; handleWheel() flips it per event (the old MouseArea
|
||||
// code toggled wheel.accepted for the same purpose).
|
||||
onWheel: wheel => root.handleWheel(wheel)
|
||||
}
|
||||
|
||||
function isMouseWheel(wheel) {
|
||||
if (forceMouseWheel)
|
||||
return true;
|
||||
var ay = wheel.angleDelta.y;
|
||||
if (ay === 0)
|
||||
return false;
|
||||
return Math.abs(ay) % 120 === 0;
|
||||
}
|
||||
|
||||
function minimumContentY() {
|
||||
if (!flickable)
|
||||
return 0;
|
||||
|
||||
return flickable.originY;
|
||||
}
|
||||
|
||||
function maximumContentY() {
|
||||
if (!flickable)
|
||||
return 0;
|
||||
|
||||
return Math.max(minimumContentY(), minimumContentY() + flickable.contentHeight - flickable.height);
|
||||
}
|
||||
|
||||
function boundedContentY(value) {
|
||||
return Math.max(minimumContentY(), Math.min(maximumContentY(), value));
|
||||
}
|
||||
|
||||
function rubberDistance(distance) {
|
||||
if (distance <= 0)
|
||||
return 0;
|
||||
|
||||
var scaled = (distance * overshootResistance) / overshootLimit;
|
||||
return overshootLimit * (1 - 1 / (1 + scaled));
|
||||
}
|
||||
|
||||
function setOvershootInput(value) {
|
||||
overshootInput = Math.max(-maxOvershootInput, Math.min(maxOvershootInput, value));
|
||||
}
|
||||
|
||||
function hasOvershoot() {
|
||||
return Math.abs(overshootInput) > edgeEpsilon;
|
||||
}
|
||||
|
||||
function absorbOvershoot(delta) {
|
||||
if (!hasOvershoot())
|
||||
return delta;
|
||||
|
||||
var sameDirection = (overshootInput < 0 && delta < 0) || (overshootInput > 0 && delta > 0);
|
||||
|
||||
if (sameDirection) {
|
||||
setOvershootInput(overshootInput + delta);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var combined = overshootInput + delta;
|
||||
if ((overshootInput < 0 && combined < 0) || (overshootInput > 0 && combined > 0)) {
|
||||
setOvershootInput(combined);
|
||||
return 0;
|
||||
}
|
||||
|
||||
setOvershootInput(0);
|
||||
return combined;
|
||||
}
|
||||
|
||||
function deltaFor(wheel) {
|
||||
var rawDelta;
|
||||
if (isMouseWheel(wheel))
|
||||
rawDelta = (wheel.angleDelta.y / 120.0) * wheelStep * mouseWheelMultiplier;
|
||||
else
|
||||
rawDelta = wheel.pixelDelta.y !== 0 ? wheel.pixelDelta.y * touchpadMultiplier : (wheel.angleDelta.y / 120.0) * wheelStep * mouseWheelMultiplier;
|
||||
|
||||
return wheel.inverted ? rawDelta : -rawDelta;
|
||||
}
|
||||
|
||||
function shouldUseKinetic(wheel, gap) {
|
||||
return wheel.pixelDelta.y !== 0 || (wheel.phase !== Qt.NoScrollPhase && wheel.angleDelta.y === 0);
|
||||
}
|
||||
|
||||
function scrollBy(delta) {
|
||||
if (!flickable || Math.abs(delta) < 0.01)
|
||||
return;
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
|
||||
var remainingDelta = absorbOvershoot(delta);
|
||||
if (Math.abs(remainingDelta) < 0.01)
|
||||
return;
|
||||
var minY = minimumContentY();
|
||||
var maxY = maximumContentY();
|
||||
var next = flickable.contentY + remainingDelta;
|
||||
|
||||
if (next < minY) {
|
||||
flickable.contentY = minY;
|
||||
setOvershootInput(overshootInput + next - minY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (next > maxY) {
|
||||
flickable.contentY = maxY;
|
||||
setOvershootInput(overshootInput + next - maxY);
|
||||
return;
|
||||
}
|
||||
|
||||
flickable.contentY = next;
|
||||
}
|
||||
|
||||
function remainingDistanceFor(velocityValue) {
|
||||
if (!flickable)
|
||||
return 0;
|
||||
|
||||
if (velocityValue > 0)
|
||||
return Math.max(0, maximumContentY() - flickable.contentY);
|
||||
|
||||
if (velocityValue < 0)
|
||||
return Math.max(0, flickable.contentY - minimumContentY());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function clampedContentVelocity(velocityValue) {
|
||||
if (!flickable || velocityValue === 0)
|
||||
return 0;
|
||||
|
||||
var remaining = remainingDistanceFor(velocityValue);
|
||||
if (remaining <= edgeEpsilon)
|
||||
return 0;
|
||||
|
||||
var maxAllowed = Math.sqrt(2 * flickable.flickDeceleration * remaining);
|
||||
var magnitude = Math.min(Math.abs(velocityValue), maxAllowed);
|
||||
return velocityValue < 0 ? -magnitude : magnitude;
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
overshootReleaseVelocity = 0;
|
||||
finishTimer.stop();
|
||||
kineticAnim.stop();
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
stopMouseGlide();
|
||||
}
|
||||
|
||||
function bounceBack() {
|
||||
if (!hasOvershoot()) {
|
||||
setOvershootInput(0);
|
||||
overshootReleaseVelocity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var mag = Math.min(1.0, Math.abs(overshootInput) / maxOvershootInput);
|
||||
var speed = Math.abs(overshootReleaseVelocity);
|
||||
var duration = speed >= minBounceVelocity ? Math.round(Math.max(280, Math.min(620, Math.abs(overshootInput) / speed * 1600))) : Math.round(380 + mag * 260);
|
||||
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
bounceBackAnimation.duration = duration;
|
||||
bounceBackAnimation.from = overshootInput;
|
||||
bounceBackAnimation.to = 0;
|
||||
overshootReleaseVelocity = 0;
|
||||
bounceBackAnimation.start();
|
||||
}
|
||||
|
||||
function triggerKineticBounce(contentVelocity) {
|
||||
if (!flickable || contentVelocity === 0)
|
||||
return;
|
||||
var atTop = flickable.contentY <= minimumContentY() + edgeEpsilon;
|
||||
var atBottom = flickable.contentY >= maximumContentY() - edgeEpsilon;
|
||||
if ((contentVelocity < 0 && !atTop) || (contentVelocity > 0 && !atBottom))
|
||||
return;
|
||||
var speed = Math.abs(contentVelocity);
|
||||
var effectiveVelocity = contentVelocity;
|
||||
if (speed < minBounceVelocity)
|
||||
effectiveVelocity = (contentVelocity < 0 ? -1 : 1) * Math.max(90, speed * 0.6);
|
||||
|
||||
var target = Math.max(-maxKineticBounceInput, Math.min(maxKineticBounceInput, effectiveVelocity * kineticBounceGain));
|
||||
if (Math.abs(target) < 2.5)
|
||||
target = contentVelocity < 0 ? -2.5 : 2.5;
|
||||
|
||||
var mag = Math.min(1.0, Math.abs(target) / maxKineticBounceInput);
|
||||
var inDur = Math.round(150 + mag * 120);
|
||||
var outDur = Math.round(320 + mag * 300);
|
||||
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
overshootReleaseVelocity = 0;
|
||||
overshootInput = 0;
|
||||
kineticBounceIn.from = 0;
|
||||
kineticBounceIn.to = target;
|
||||
kineticBounceIn.duration = inDur;
|
||||
kineticBounceOut.from = target;
|
||||
kineticBounceOut.to = 0;
|
||||
kineticBounceOut.duration = outDur;
|
||||
kineticBounceSequence.start();
|
||||
}
|
||||
|
||||
function stopAndSettle() {
|
||||
kineticAnim.stop();
|
||||
if (flickable && flickable.flicking)
|
||||
flickable.cancelFlick();
|
||||
|
||||
resetState();
|
||||
bounceBack();
|
||||
}
|
||||
|
||||
function runKineticGlide(contentVelocity) {
|
||||
// contentVelocity > 0 means the content should scroll downward
|
||||
// (contentY increases), matching deltaFor()'s sign convention.
|
||||
if (!flickable || contentVelocity === 0) {
|
||||
pendingImpactVelocity = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var current = flickable.contentY;
|
||||
var unclampedTarget = current + contentVelocity * kineticTau;
|
||||
var target = boundedContentY(unclampedTarget);
|
||||
var distUnclamped = Math.abs(unclampedTarget - current);
|
||||
var distActual = Math.abs(target - current);
|
||||
if (distActual < 4) {
|
||||
pendingImpactVelocity = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the glide is clipped by an edge, compute the velocity at the
|
||||
// moment of impact using the OutQuart velocity profile. That is
|
||||
// the velocity the bounce should feed on — gentle impacts get
|
||||
// gentle bounces, hard impacts get big springy ones.
|
||||
var hitsEdge = distActual + 0.5 < distUnclamped;
|
||||
if (hitsEdge && distUnclamped > 0) {
|
||||
var f = distActual / distUnclamped; // fraction covered
|
||||
// OutQuart: y = 1 - (1-x)^4, so velocity fraction at distance
|
||||
// fraction f equals (1-f)^(3/4) (times the constant peak).
|
||||
var velFraction = Math.pow(Math.max(0, 1 - f), 0.75);
|
||||
pendingImpactVelocity = contentVelocity * velFraction;
|
||||
} else {
|
||||
pendingImpactVelocity = 0;
|
||||
}
|
||||
|
||||
kineticAnim.stop();
|
||||
kineticAnim.from = current;
|
||||
kineticAnim.to = target;
|
||||
// Duration from distance and velocity; clamped so short flicks
|
||||
// stay snappy and long flicks don't run forever.
|
||||
kineticAnim.duration = Math.round(Math.min(1400, Math.max(180, distActual / Math.abs(contentVelocity) * 1800)));
|
||||
kineticAnim.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
function atEdgeFor(contentVelocity) {
|
||||
if (!flickable)
|
||||
return false;
|
||||
if (contentVelocity > 0)
|
||||
return flickable.contentY >= maximumContentY() - edgeEpsilon;
|
||||
if (contentVelocity < 0)
|
||||
return flickable.contentY <= minimumContentY() + edgeEpsilon;
|
||||
return false;
|
||||
}
|
||||
|
||||
function finishScroll() {
|
||||
if (!flickable)
|
||||
return;
|
||||
finishTimer.stop();
|
||||
|
||||
if (hasOvershoot()) {
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
kineticAnim.stop();
|
||||
bounceBack();
|
||||
return;
|
||||
}
|
||||
|
||||
var rawContentVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, velocity * kineticGain));
|
||||
var contentVelocity = clampedContentVelocity(rawContentVelocity);
|
||||
var useKinetic = kineticCandidate && Math.abs(rawContentVelocity) >= minVelocity;
|
||||
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
|
||||
if (useKinetic) {
|
||||
if (atEdgeFor(rawContentVelocity)) {
|
||||
// Already at the edge — fling bounces immediately.
|
||||
pendingKineticBounceVelocity = 0;
|
||||
triggerKineticBounce(rawContentVelocity);
|
||||
} else if (Math.abs(contentVelocity) >= minVelocity && runKineticGlide(contentVelocity)) {
|
||||
// Glide started — bounce fires on kineticAnim.onFinished
|
||||
// if it lands at an edge.
|
||||
pendingKineticBounceVelocity = contentVelocity;
|
||||
} else {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
flickable.returnToBounds();
|
||||
}
|
||||
} else {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
flickable.returnToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(wheel) {
|
||||
wheelHandler.blocking = true;
|
||||
if (!enabled || !visible || !flickable || !flickable.visible || !flickable.interactive) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wheel.modifiers !== Qt.NoModifier) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (maximumContentY() <= minimumContentY()) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = deltaFor(wheel);
|
||||
if (delta === 0) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any in-flight glide — user input always wins, position
|
||||
// stays where it is, then new delta is applied from there.
|
||||
if (kineticAnim.running) {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
kineticAnim.stop();
|
||||
}
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
if (flickable.flicking) {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
flickable.cancelFlick();
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var isTouchpad = !isMouseWheel(wheel) && (wheel.pixelDelta.y !== 0 || (wheel.phase !== Qt.NoScrollPhase && wheel.angleDelta.y === 0));
|
||||
console.log("wheel", wheel.angleDelta.y, wheel.pixelDelta.y, wheel.phase);
|
||||
if (!isTouchpad) {
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
scrollStarted();
|
||||
handleMouseDelta(delta, now);
|
||||
return;
|
||||
}
|
||||
|
||||
// Touchpad path (unchanged below), but cancel any mouse glide first
|
||||
stopMouseGlide();
|
||||
|
||||
var gap = lastWheelTimestamp > 0 ? now - lastWheelTimestamp : 16;
|
||||
var dt = Math.max(8, Math.min(24, gap));
|
||||
var instantVelocity = (delta / dt) * 1000;
|
||||
|
||||
kineticCandidate = kineticCandidate || shouldUseKinetic(wheel, gap);
|
||||
scrollStarted();
|
||||
scrollBy(delta);
|
||||
|
||||
if (hasOvershoot()) {
|
||||
overshootReleaseVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, instantVelocity));
|
||||
velocity = 0;
|
||||
kineticCandidate = false;
|
||||
lastWheelTimestamp = now;
|
||||
finishTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
velocity = lastWheelTimestamp <= 0 ? instantVelocity : velocity * (1.0 - smoothing) + instantVelocity * smoothing;
|
||||
lastWheelTimestamp = now;
|
||||
finishTimer.restart();
|
||||
|
||||
if (wheel.phase === Qt.ScrollEnd)
|
||||
finishScroll();
|
||||
}
|
||||
|
||||
function stopMouseGlide() {
|
||||
mouseGlide.stop();
|
||||
mouseVelocity = 0;
|
||||
mouseDir = 0;
|
||||
mouseStreak = 0;
|
||||
mouseLastTime = 0;
|
||||
}
|
||||
|
||||
function handleMouseDelta(delta, now) {
|
||||
var dir = delta > 0 ? 1 : -1;
|
||||
|
||||
// Edge or active overshoot: existing rubber-band path
|
||||
if (hasOvershoot() || atEdgeFor(dir)) {
|
||||
stopMouseGlide();
|
||||
scrollBy(delta);
|
||||
overshootReleaseVelocity = 0;
|
||||
finishTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
var continuous = dir === mouseDir && (now - mouseLastTime) <= mouseAccelWindow;
|
||||
if (!continuous)
|
||||
mouseVelocity = 0; // new gesture or reversal: start clean
|
||||
mouseStreak = continuous ? mouseStreak + 1 : 0;
|
||||
|
||||
// Compute the gap BEFORE overwriting mouseLastTime
|
||||
var gap = mouseLastTime > 0 ? Math.max(12, Math.min(mouseAccelWindow, now - mouseLastTime)) : 0;
|
||||
|
||||
mouseDir = dir;
|
||||
mouseLastTime = now;
|
||||
|
||||
var accel = Math.min(mouseMaxAccel, 1 + mouseStreak * mouseAccelStep);
|
||||
|
||||
// --- velocity update (this is the part that changed) ---
|
||||
|
||||
// Part of the notch lands immediately
|
||||
var instant = delta * mouseSnap;
|
||||
flickable.contentY = boundedContentY(flickable.contentY + instant);
|
||||
|
||||
// The rest becomes glide velocity, stacked onto the current velocity
|
||||
var glide = delta * (1 - mouseSnap) * accel;
|
||||
var v = mouseVelocity + glide / mouseGlideTime;
|
||||
|
||||
// While notches keep arriving, don't fall below the rate they imply
|
||||
if (continuous && gap > 0) {
|
||||
var rate = glide / (gap / 1000);
|
||||
if (Math.abs(rate) > Math.abs(v))
|
||||
v = rate;
|
||||
}
|
||||
mouseVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, v));
|
||||
|
||||
// --- end velocity update ---
|
||||
|
||||
if (!mouseGlide.running)
|
||||
mouseGlide.start();
|
||||
}
|
||||
|
||||
function stepMouseGlide(dt) {
|
||||
if (!flickable || flickable.dragging) {
|
||||
stopMouseGlide();
|
||||
return;
|
||||
}
|
||||
|
||||
dt = Math.min(dt, 0.05);
|
||||
var decay = Math.exp(-dt / mouseGlideTime);
|
||||
// Exact integral of v0 * e^(-t/tau) over dt, so it's frame-rate independent
|
||||
var step = mouseVelocity * mouseGlideTime * (1 - decay);
|
||||
var next = flickable.contentY + step;
|
||||
var minY = minimumContentY();
|
||||
var maxY = maximumContentY();
|
||||
|
||||
if (next <= minY || next >= maxY) {
|
||||
var impact = mouseVelocity;
|
||||
flickable.contentY = next <= minY ? minY : maxY;
|
||||
mouseGlide.stop();
|
||||
mouseVelocity = 0;
|
||||
mouseDir = 0;
|
||||
mouseStreak = 0;
|
||||
triggerKineticBounce(impact); // bounce scales with the real impact speed
|
||||
return;
|
||||
}
|
||||
|
||||
flickable.contentY = next;
|
||||
mouseVelocity *= decay;
|
||||
|
||||
if (Math.abs(mouseVelocity) < 40) // was 8
|
||||
stopMouseGlide();
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
id: mouseGlide
|
||||
onTriggered: root.stepMouseGlide(frameTime)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: finishTimer
|
||||
interval: 45
|
||||
repeat: false
|
||||
onTriggered: root.finishScroll()
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: kineticAnim
|
||||
target: root.flickable
|
||||
property: "contentY"
|
||||
easing.type: Easing.OutQuart
|
||||
onFinished: {
|
||||
// Prefer the impact-time velocity (computed in runKineticGlide)
|
||||
// so the bounce matches the residual energy at the moment the
|
||||
// glide hit the edge — slow landings → soft bounce, fast
|
||||
// landings → big springy bounce.
|
||||
var bv = Math.abs(root.pendingImpactVelocity) > root.edgeEpsilon ? root.pendingImpactVelocity : root.pendingKineticBounceVelocity;
|
||||
root.pendingImpactVelocity = 0;
|
||||
root.pendingKineticBounceVelocity = 0;
|
||||
if (Math.abs(bv) > root.edgeEpsilon)
|
||||
root.triggerKineticBounce(bv);
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: bounceBackAnimation
|
||||
target: root
|
||||
property: "overshootInput"
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: kineticBounceSequence
|
||||
|
||||
NumberAnimation {
|
||||
id: kineticBounceIn
|
||||
target: root
|
||||
property: "overshootInput"
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: kineticBounceOut
|
||||
target: root
|
||||
property: "overshootInput"
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Translate {
|
||||
id: overshootTranslate
|
||||
y: root.visualOvershoot
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.flickable
|
||||
|
||||
function onInteractiveChanged() {
|
||||
if (!root.flickable.interactive)
|
||||
root.stopAndSettle();
|
||||
}
|
||||
|
||||
function onContentHeightChanged() {
|
||||
if (!root.flickable.moving) {
|
||||
root.flickable.contentY = root.boundedContentY(root.flickable.contentY);
|
||||
root.bounceBack();
|
||||
}
|
||||
}
|
||||
|
||||
function onHeightChanged() {
|
||||
if (!root.flickable.moving) {
|
||||
root.flickable.contentY = root.boundedContentY(root.flickable.contentY);
|
||||
root.bounceBack();
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibleChanged() {
|
||||
if (!root.flickable.visible)
|
||||
root.stopAndSettle();
|
||||
}
|
||||
|
||||
function onMovementEnded() {
|
||||
if (Math.abs(root.pendingKineticBounceVelocity) > root.edgeEpsilon) {
|
||||
var bounceVelocity = root.pendingKineticBounceVelocity;
|
||||
root.pendingKineticBounceVelocity = 0;
|
||||
root.triggerKineticBounce(bounceVelocity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!flickable)
|
||||
return;
|
||||
flickable.flickDeceleration = 2800;
|
||||
flickable.maximumFlickVelocity = Math.max(flickable.maximumFlickVelocity, maxVelocity);
|
||||
flickable.contentItem.transform = [overshootTranslate];
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -34,7 +34,7 @@ MouseArea {
|
||||
cursorShape: expanded ? Qt.ArrowCursor : undefined
|
||||
enabled: expanded
|
||||
hoverEnabled: expanded
|
||||
layer.enabled: false
|
||||
layer.enabled: opacity < 1
|
||||
opacity: expanded ? 1 : 0
|
||||
parent: {
|
||||
const win = QsWindow.window;
|
||||
@@ -101,7 +101,8 @@ MouseArea {
|
||||
yScale: root.expanded ? 1 : 0.1
|
||||
|
||||
Behavior on yScale {
|
||||
Anim {}
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +137,8 @@ MouseArea {
|
||||
radius: menu.radius - column.anchors.margins
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
|
||||
+261
-15
@@ -4,11 +4,14 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Services
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import ZShell
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Paths
|
||||
import qs.Services
|
||||
import qs.Paths
|
||||
import ZShell.Config
|
||||
|
||||
Singleton {
|
||||
@@ -16,9 +19,11 @@ Singleton {
|
||||
|
||||
readonly property var appCooldownMap: new Map()
|
||||
property alias dnd: props.dnd
|
||||
property alias list: store.rows
|
||||
readonly property list<NotificationRow> notClosed: store.rows.filter(n => !n.closed)
|
||||
readonly property list<NotificationRow> popups: store.rows.filter(n => n.popup)
|
||||
property list<Notif> list: []
|
||||
property bool loaded
|
||||
readonly property list<Notif> notClosed: list.filter(n => !n.closed)
|
||||
readonly property list<Notif> popups: list.filter(n => n.popup)
|
||||
property alias server: server
|
||||
|
||||
function shouldThrottle(appName: string): bool {
|
||||
if (props.dnd || ShellState.anySidebarOpen())
|
||||
@@ -41,6 +46,33 @@ Singleton {
|
||||
return true;
|
||||
}
|
||||
|
||||
onListChanged: {
|
||||
if (loaded) {
|
||||
saveTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
interval: 1000
|
||||
|
||||
onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({
|
||||
time: n.time,
|
||||
id: n.id,
|
||||
summary: n.summary,
|
||||
body: n.body,
|
||||
appIcon: n.appIcon,
|
||||
appName: n.appName,
|
||||
image: n.image,
|
||||
expireTimeout: n.expireTimeout,
|
||||
urgency: n.urgency,
|
||||
resident: n.resident,
|
||||
hasActionIcons: n.hasActionIcons,
|
||||
actions: n.actions
|
||||
}))))
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: props
|
||||
|
||||
@@ -49,12 +81,6 @@ Singleton {
|
||||
reloadableId: "notifs"
|
||||
}
|
||||
|
||||
NotificationStore {
|
||||
id: store
|
||||
|
||||
path: `${Paths.state}/notifs.sqlite`
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
@@ -63,12 +89,40 @@ Singleton {
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
keepOnReload: true
|
||||
keepOnReload: false
|
||||
persistenceSupported: true
|
||||
|
||||
onNotification: notif => {
|
||||
notif.tracked = true;
|
||||
store.insert(notif, root.shouldThrottle(notif.appName));
|
||||
|
||||
const is_popup = root.shouldThrottle(notif.appName);
|
||||
|
||||
const comp = notifComp.createObject(root, {
|
||||
popup: is_popup,
|
||||
notification: notif
|
||||
});
|
||||
root.list = [comp, ...root.list];
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: storage
|
||||
|
||||
path: `${Paths.state}/notifs.json`
|
||||
printErrors: false
|
||||
|
||||
onLoadFailed: err => {
|
||||
if (err === FileViewError.FileNotFound) {
|
||||
root.loaded = true;
|
||||
Qt.callLater(() => setText("[]"));
|
||||
}
|
||||
}
|
||||
onLoaded: {
|
||||
const data = JSON.parse(text());
|
||||
for (const notif of data)
|
||||
root.list.push(notifComp.createObject(root, notif));
|
||||
root.list.sort((a, b) => b.time - a.time);
|
||||
root.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +130,16 @@ Singleton {
|
||||
description: "Clear all notifications"
|
||||
name: "clearnotifs"
|
||||
|
||||
onPressed: store.clear()
|
||||
onPressed: {
|
||||
for (const notif of root.list.slice())
|
||||
notif.close();
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function clear(): void {
|
||||
store.clear();
|
||||
for (const notif of root.list.slice())
|
||||
notif.close();
|
||||
}
|
||||
|
||||
function disableDnd(): void {
|
||||
@@ -102,4 +160,192 @@ Singleton {
|
||||
|
||||
target: "notifs"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notifComp
|
||||
|
||||
Notif {
|
||||
}
|
||||
}
|
||||
|
||||
component Notif: QtObject {
|
||||
id: notif
|
||||
|
||||
property list<var> actions
|
||||
property string appIcon
|
||||
property string appName
|
||||
property string body
|
||||
property string cachedImageSource: ""
|
||||
property bool cachingImage: false
|
||||
property bool closed
|
||||
readonly property Connections conn: Connections {
|
||||
function onActionsChanged(): void {
|
||||
notif.actions = notif.notification.actions.map(a => ({
|
||||
identifier: a.identifier,
|
||||
text: a.text,
|
||||
invoke: () => a.invoke()
|
||||
}));
|
||||
}
|
||||
|
||||
function onAppIconChanged(): void {
|
||||
notif.appIcon = notif.notification.appIcon;
|
||||
}
|
||||
|
||||
function onAppNameChanged(): void {
|
||||
notif.appName = notif.notification.appName;
|
||||
}
|
||||
|
||||
function onBodyChanged(): void {
|
||||
notif.body = notif.notification.body;
|
||||
}
|
||||
|
||||
function onClosed(): void {
|
||||
notif.close();
|
||||
}
|
||||
|
||||
function onExpireTimeoutChanged(): void {
|
||||
notif.expireTimeout = notif.notification.expireTimeout;
|
||||
}
|
||||
|
||||
function onHasActionIconsChanged(): void {
|
||||
notif.hasActionIcons = notif.notification.hasActionIcons;
|
||||
}
|
||||
|
||||
function onHintsChanged(): void {
|
||||
notif.hints = notif.notification.hints;
|
||||
}
|
||||
|
||||
function onImageChanged(): void {
|
||||
notif.imageSource = notif.notification.image || "";
|
||||
notif.image = notif.imageSource;
|
||||
notif.cacheImageIfNeeded();
|
||||
}
|
||||
|
||||
function onResidentChanged(): void {
|
||||
notif.resident = notif.notification.resident;
|
||||
}
|
||||
|
||||
function onSummaryChanged(): void {
|
||||
notif.summary = notif.notification.summary;
|
||||
}
|
||||
|
||||
function onUrgencyChanged(): void {
|
||||
notif.urgency = notif.notification.urgency;
|
||||
}
|
||||
|
||||
target: notif.notification
|
||||
}
|
||||
property real expireTimeout: 5
|
||||
property bool hasActionIcons
|
||||
property var hints
|
||||
property string image
|
||||
property string imageSource
|
||||
property var locks: new Set()
|
||||
property string notifId
|
||||
property Notification notification
|
||||
property bool popup
|
||||
property bool resident
|
||||
property string summary
|
||||
property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
const diff = Time.date.getTime() - time.getTime();
|
||||
const m = Math.floor(diff / 60000);
|
||||
|
||||
if (m < 1)
|
||||
return qsTr("now");
|
||||
|
||||
const h = Math.floor(m / 60);
|
||||
const d = Math.floor(h / 24);
|
||||
|
||||
if (d > 0)
|
||||
return `${d}d`;
|
||||
if (h > 0)
|
||||
return `${h}h`;
|
||||
return `${m}m`;
|
||||
}
|
||||
readonly property Timer timer: Timer {
|
||||
property bool paused: false
|
||||
property int remainingTime: totalTime
|
||||
property int totalTime: Config.notifs.defaultExpireTimeout
|
||||
|
||||
interval: 50
|
||||
repeat: true
|
||||
running: !paused
|
||||
|
||||
onTriggered: {
|
||||
remainingTime -= interval;
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
remainingTime = 0;
|
||||
notif.popup = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
property int urgency: NotificationUrgency.Normal
|
||||
|
||||
function cacheImageIfNeeded(): void {
|
||||
const source = imageSource;
|
||||
|
||||
if (!source || cachingImage)
|
||||
return;
|
||||
|
||||
if (cachedImageSource === source)
|
||||
return;
|
||||
|
||||
cachingImage = true;
|
||||
|
||||
ZShellIo.cacheImage(Qt.resolvedUrl(source), Paths.notifimagecache, (path, url) => {
|
||||
cachedImageSource = source;
|
||||
image = path;
|
||||
cachingImage = false;
|
||||
}, () => {
|
||||
cachingImage = false;
|
||||
});
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
closed = true;
|
||||
if (locks.size === 0 && root.list.includes(this)) {
|
||||
root.list = root.list.filter(n => n !== this);
|
||||
notification?.dismiss();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function lock(item: Item): void {
|
||||
locks.add(item);
|
||||
}
|
||||
|
||||
function unlock(item: Item): void {
|
||||
locks.delete(item);
|
||||
if (closed)
|
||||
close();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!notification)
|
||||
return;
|
||||
|
||||
notifId = notification.id;
|
||||
summary = notification.summary;
|
||||
body = notification.body;
|
||||
appIcon = notification.appIcon;
|
||||
appName = notification.appName;
|
||||
imageSource = notification.image || "";
|
||||
image = imageSource;
|
||||
hints = notification.hints;
|
||||
expireTimeout = notification.expireTimeout;
|
||||
urgency = notification.urgency;
|
||||
resident = notification.resident;
|
||||
hasActionIcons = notification.hasActionIcons;
|
||||
actions = notification.actions.map(a => ({
|
||||
identifier: a.identifier,
|
||||
text: a.text,
|
||||
invoke: () => a.invoke()
|
||||
}));
|
||||
|
||||
cacheImageIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,13 @@ Scope {
|
||||
hasBar: root.geometry.barOnBottom
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
|
||||
onTriggered: console.log("top height:", top.exclusiveZone, "left width:", left.exclusiveZone, "right width:", right.exclusiveZone, "bottom height:", bottom.exclusiveZone)
|
||||
}
|
||||
|
||||
component ExclusionZone: CustomWindow {
|
||||
property bool hasBar
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import ZShell.Blobs
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Modules.Bar
|
||||
import qs.Modules.Bar.Popouts.Network as N
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Drawers
|
||||
|
||||
@@ -77,16 +77,6 @@ Singleton {
|
||||
"99": "thunderstorm"
|
||||
})
|
||||
|
||||
function getAppCategoryIcon(name: string, fallback: string): string {
|
||||
const categories = DesktopEntries.heuristicLookup(name)?.categories;
|
||||
|
||||
if (categories)
|
||||
for (const [key, value] of Object.entries(categoryIcons))
|
||||
if (categories.includes(key))
|
||||
return value;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getAppIcon(name: string, fallback: string): string {
|
||||
const icon = DesktopEntries.heuristicLookup(name)?.icon;
|
||||
if (fallback !== "undefined")
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Networking
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
readonly property list<Network> connectedNetworks: networks.filter(n => n.connected)
|
||||
property Network connectingNetwork
|
||||
readonly property list<NetworkDevice> devices: Networking.devices.values
|
||||
property Network failedNetwork
|
||||
readonly property list<Network> knownNetworks: networks.filter(n => n.known && !n.connected)
|
||||
readonly property list<Network> networks: {
|
||||
const list = [];
|
||||
for (const d of wifiDevices) {
|
||||
for (const n of d.networks.values) {
|
||||
if (!list.includes(n))
|
||||
list.push(n);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
property bool scanning: false
|
||||
readonly property list<Network> unknownNetworks: networks.filter(n => !n.known)
|
||||
readonly property list<WifiDevice> wifiDevices: devices.filter(d => wifiDevice(d))
|
||||
readonly property bool wifiEnabled: Networking.wifiEnabled
|
||||
|
||||
signal networkConnected
|
||||
|
||||
function forgetNetwork(target): void {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.forget();
|
||||
}
|
||||
|
||||
function isSecure(security): bool {
|
||||
return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae);
|
||||
}
|
||||
|
||||
function requestConnect(network: Network, secret = undefined): void {
|
||||
connectingNetwork = network;
|
||||
if (secret === undefined || secret === "") {
|
||||
network.connect();
|
||||
} else {
|
||||
(network as WifiNetwork).connectWithPsk(secret);
|
||||
}
|
||||
}
|
||||
|
||||
function setScan(value: bool): void {
|
||||
for (const d of wifiDevices) {
|
||||
d.scannerEnabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setWifi(value: bool): void {
|
||||
Networking.wifiEnabled = value;
|
||||
}
|
||||
|
||||
function wifiDevice(dev): bool {
|
||||
return dev.type === DeviceType.Wifi;
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
for (const d of wifiDevices)
|
||||
d.scannerEnabled = active;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: flushFailed
|
||||
|
||||
interval: 5000
|
||||
running: root.failedNetwork
|
||||
|
||||
onTriggered: {
|
||||
root.failedNetwork = null;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: checkConnection
|
||||
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: root.connectingNetwork
|
||||
|
||||
onTriggered: {
|
||||
var completedNetwork;
|
||||
|
||||
switch (root.connectingNetwork.state) {
|
||||
case ConnectionState.Connecting:
|
||||
break;
|
||||
case ConnectionState.Connected:
|
||||
completedNetwork = root.connectingNetwork;
|
||||
root.networkConnected();
|
||||
break;
|
||||
case ConnectionState.Disconnected:
|
||||
root.failedNetwork = root.connectingNetwork;
|
||||
completedNetwork = root.connectingNetwork;
|
||||
break;
|
||||
}
|
||||
|
||||
if (completedNetwork) {
|
||||
root.connectingNetwork = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: checkConnection.running
|
||||
|
||||
onTriggered: {
|
||||
root.failedNetwork = root.connectingNetwork;
|
||||
root.connectingNetwork = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
CustomText {
|
||||
required property var modelData
|
||||
|
||||
bottomPadding: Tokens.spacing.extraSmall
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
text: {
|
||||
if (modelData.type === "known") {
|
||||
const count = Network.knownNetworks.filter(n => !n.connected).length;
|
||||
|
||||
return qsTr("%1 known network%2").arg(count).arg(count === 1 ? "" : "s");
|
||||
}
|
||||
|
||||
if (modelData.type === "unknown") {
|
||||
const count = Network.unknownNetworks.filter(n => !n.connected).length;
|
||||
|
||||
return qsTr("%1 network%2 available").arg(count).arg(count === 1 ? "" : "s");
|
||||
}
|
||||
|
||||
return qsTr("object unknown");
|
||||
}
|
||||
topPadding: Tokens.spacing.small
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Helpers.Network
|
||||
import qs.Services
|
||||
|
||||
ConnectedRect {
|
||||
id: networkItem
|
||||
|
||||
readonly property int connectedCount: Network.connectedNetworks.length
|
||||
readonly property bool connecting: Network.connectingNetwork === modelData
|
||||
property int horizontalPadding: Tokens.padding.largeIncreased
|
||||
readonly property bool inConnectedGroup: index < connectedCount
|
||||
// sectioning first / last item in list reminder
|
||||
// if index === network.connectedNetworks.length - 1
|
||||
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - 2
|
||||
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length
|
||||
// list.model.values.length - 1
|
||||
required property int index
|
||||
readonly property bool isSecure: Network.isSecure(modelData.security)
|
||||
required property ListView list
|
||||
required property var modelData
|
||||
property int verticalPadding: Tokens.padding.smaller
|
||||
|
||||
Layout.fillWidth: true
|
||||
first: !inConnectedGroup && index === connectedCount + 1
|
||||
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
|
||||
last: inConnectedGroup ? index === connectedCount - 1 : index === (list?.count ?? -1)
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: networkItem.horizontalPadding
|
||||
anchors.rightMargin: networkItem.horizontalPadding
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Tokens.spacing.extraSmall
|
||||
Layout.rightMargin: Tokens.spacing.extraSmall
|
||||
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
text: networkItem.modelData.name ? networkItem.modelData.name : qsTr("Unknown Network")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: networkItem.modelData.connected ? qsTr("Connected: %1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") : qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
implicitHeight: icon.height
|
||||
implicitWidth: icon.width
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.large
|
||||
opacity: networkItem.connecting ? 0 : 1
|
||||
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
opacity: networkItem.connecting ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CircularIndicator {
|
||||
bgColor: "transparent"
|
||||
running: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
if (networkItem.isSecure && !networkItem.known) {
|
||||
PopupManager.requestOpen(Qt.resolvedUrl("../../Modules/Bar/Popouts/Network/PSKOverlay.qml"), {
|
||||
network: networkItem.modelData
|
||||
});
|
||||
} else {
|
||||
Network.requestConnect(networkItem.modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
import qs.Modules.Settings.Common
|
||||
|
||||
CustomSwitch {
|
||||
id: toggleItem
|
||||
|
||||
readonly property alias bg: bg
|
||||
property alias first: bg.first
|
||||
property alias last: bg.last
|
||||
property string subtext
|
||||
|
||||
Layout.fillWidth: true
|
||||
cLayer: 2
|
||||
horizontalPadding: Tokens.padding.largeIncreased
|
||||
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
|
||||
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
|
||||
indicator.anchors.right: right
|
||||
indicator.anchors.rightMargin: toggleItem.horizontalPadding
|
||||
indicator.anchors.verticalCenter: verticalCenter
|
||||
verticalPadding: Tokens.padding.normal
|
||||
|
||||
background: ConnectedRect {
|
||||
id: bg
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
manualPressOverride: toggleItem.pressed
|
||||
}
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: toggleItem.horizontalPadding
|
||||
anchors.right: toggleItem.indicator.left
|
||||
anchors.rightMargin: Tokens.spacing.normal
|
||||
implicitHeight: column.implicitHeight
|
||||
implicitWidth: column.implicitWidth
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
text: toggleItem.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: toggleItem.subtext
|
||||
visible: toggleItem.subtext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
|
||||
}
|
||||
+43
-105
@@ -5,7 +5,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import ZShell.Models
|
||||
import ZShell.Wallpaper
|
||||
import ZShell.Internal
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
@@ -16,48 +16,24 @@ Searcher {
|
||||
id: root
|
||||
|
||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||
property alias crops: adapter.monitorCrops
|
||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||
readonly property string currentThumbnail: thumbnailFor(current)
|
||||
readonly property bool currentIsVideo: isVideoPath(current)
|
||||
property alias monitorCrops: monitorCrops
|
||||
property bool pendingPreviewClear
|
||||
property bool previewColorLock
|
||||
property string previewPath
|
||||
property bool recentlyChanged
|
||||
property bool showPreview: false
|
||||
property int cropVersion: 0
|
||||
|
||||
function getCategoryFor(w: WallpaperEntry): string {
|
||||
function getCategoryFor(w: FileSystemEntry): string {
|
||||
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
||||
if (category.includes("/"))
|
||||
category = category.slice(0, category.indexOf("/"));
|
||||
return category;
|
||||
}
|
||||
|
||||
readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"]
|
||||
|
||||
function isVideoPath(path: string): bool {
|
||||
if (!path)
|
||||
return false;
|
||||
const dot = path.lastIndexOf(".");
|
||||
if (dot < 0)
|
||||
return false;
|
||||
return videoExts.includes(path.slice(dot + 1).toLowerCase());
|
||||
}
|
||||
|
||||
function thumbnailFor(path: string): string {
|
||||
if (!path)
|
||||
return "";
|
||||
|
||||
for (const entry of root.list) {
|
||||
if (entry.path === path)
|
||||
return entry.thumbnailPath || path;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function getCrop(screen: string): var {
|
||||
return wallpapers.crop(root.current, screen);
|
||||
return root.crops[screen];
|
||||
}
|
||||
|
||||
function preview(path: string): void {
|
||||
@@ -74,18 +50,19 @@ Searcher {
|
||||
else if (zoom > 5.0)
|
||||
zoom = 5.0;
|
||||
|
||||
wallpapers.setCrop(root.current, screen, {
|
||||
root.crops[screen] = {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
zoom: zoom
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
WallpaperPath.currentWallpaperPath = path;
|
||||
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
|
||||
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
||||
if (Config.colors.schemeGen)
|
||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
||||
@@ -119,13 +96,38 @@ Searcher {
|
||||
target: "wallpaper"
|
||||
}
|
||||
|
||||
WallpaperDb {
|
||||
FileView {
|
||||
id: monitorCrops
|
||||
|
||||
path: `${Paths.state}/wallpaper-crops.json`
|
||||
watchChanges: true
|
||||
|
||||
onAdapterUpdated: cropWriteDelay.restart()
|
||||
onFileChanged: reload()
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property var monitorCrops: ({})
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: cropWriteDelay
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: monitorCrops.writeAdapter()
|
||||
}
|
||||
|
||||
FileSystemModel {
|
||||
id: wallpapers
|
||||
|
||||
dbPath: `${Paths.state}/wallpapers.sqlite`
|
||||
cacheSize: wallpaper.largestBufferSize
|
||||
requireHardwareCodec: true
|
||||
filter: FileSystemModel.Images
|
||||
path: Config.general.wallpaperPath
|
||||
recursive: true
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -144,100 +146,36 @@ Searcher {
|
||||
WallpaperService {
|
||||
id: wallpaper
|
||||
|
||||
dbPath: wallpapers.dbPath
|
||||
path: root.current
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: wallpapers
|
||||
|
||||
function onCacheStored(path: string): void {
|
||||
if (path === root.current)
|
||||
wallpaper.invalidateCache();
|
||||
}
|
||||
|
||||
function onCropsChanged(path: string): void {
|
||||
if (path === root.current)
|
||||
root.cropVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: screenVariants
|
||||
model: Quickshell.screens
|
||||
|
||||
WallpaperObject {
|
||||
id: obj
|
||||
readonly property var crops: root.getCrop(modelData.name)
|
||||
required property var modelData
|
||||
|
||||
// Crop animation only for edits, never for wallpaper swaps.
|
||||
property bool animateCrop: false
|
||||
|
||||
cropHeight: crops?.height ?? 1.0
|
||||
cropWidth: crops?.width ?? 1.0
|
||||
cropX: crops?.x ?? 0.0
|
||||
cropY: crops?.y ?? 0.0
|
||||
screen: modelData.name
|
||||
service: wallpaper
|
||||
|
||||
// Apply the wallpaper's own crop. Driven off the service's pathChanged
|
||||
// (which fires after applyPath has snapshotted the outgoing frame with
|
||||
// the outgoing crop) and off crop edits, never as a reactive binding on
|
||||
// the wallpaper path — so a swap applies the incoming crop atomically
|
||||
// after the base is captured.
|
||||
function seedCrop(): void {
|
||||
// Load the current wallpaper's crop without pushing: the service
|
||||
// already read it from the database. This only seeds the values so
|
||||
// a later edit animates from the right starting point.
|
||||
obj.syncCrop(root.getCrop(modelData.name));
|
||||
}
|
||||
|
||||
// Apply an edited crop: assign via the setters so the Behaviors animate
|
||||
// the numbers and push each intermediate value to the service.
|
||||
function editCrop(): void {
|
||||
const c = root.getCrop(modelData.name);
|
||||
obj.cropX = c?.x ?? 0.0;
|
||||
obj.cropY = c?.y ?? 0.0;
|
||||
obj.cropWidth = c?.width ?? 1.0;
|
||||
obj.cropHeight = c?.height ?? 1.0;
|
||||
obj.zoom = c?.zoom ?? 1.0;
|
||||
}
|
||||
|
||||
Component.onCompleted: seedCrop()
|
||||
onScreenChanged: seedCrop()
|
||||
zoom: crops?.zoom ?? 1.0
|
||||
|
||||
Behavior on cropHeight {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
Behavior on cropWidth {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
Behavior on cropX {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
Behavior on cropY {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: wallpaper
|
||||
function onPathChanged(): void {
|
||||
for (const obj of screenVariants.instances) {
|
||||
obj.animateCrop = false;
|
||||
obj.seedCrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCropVersionChanged(): void {
|
||||
for (const obj of screenVariants.instances) {
|
||||
obj.animateCrop = true;
|
||||
obj.editCrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,14 +206,6 @@ GridLayout {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "network"
|
||||
|
||||
delegate: EntryWrapper {
|
||||
NetworkWidget {}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "media"
|
||||
|
||||
|
||||
@@ -45,9 +45,7 @@ WidgetBase {
|
||||
}
|
||||
required property GridLayout loader
|
||||
required property Wrapper popouts
|
||||
readonly property int roundedWidth: Math.round(timeText.implicitWidth)
|
||||
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
|
||||
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||
required property ScreenState screenState
|
||||
|
||||
implicitHeight: horizontal ? baseSize : size
|
||||
|
||||
@@ -24,7 +24,7 @@ Row {
|
||||
color: Battery.getColors(root.percentage, root.devState).bg
|
||||
height: root.batHeight
|
||||
radius: root.radius
|
||||
width: root.batWidth - 1
|
||||
width: root.batWidth
|
||||
|
||||
CustomText {
|
||||
color: Battery.getColors(root.percentage, root.devState).fg
|
||||
|
||||
@@ -13,7 +13,7 @@ WidgetBase {
|
||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||
|
||||
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
||||
implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
|
||||
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Behavior on implicitHeight {
|
||||
|
||||
@@ -12,6 +12,7 @@ GridLayout {
|
||||
property real animatedPercentage: 0
|
||||
readonly property real arcStartAngle: 0.75 * Math.PI
|
||||
readonly property real arcSweep: 1.5 * Math.PI
|
||||
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
|
||||
required property bool horizontal
|
||||
property string icon
|
||||
required property color iconColor
|
||||
@@ -22,12 +23,17 @@ GridLayout {
|
||||
property bool warning: percentage * 100 >= warningThreshold
|
||||
property int warningThreshold: 80
|
||||
|
||||
function snap(v) {
|
||||
return Math.round(v * dpr) / dpr;
|
||||
}
|
||||
|
||||
columnSpacing: Tokens.spacing.medium
|
||||
columns: horizontal ? -1 : 1
|
||||
percentage: 0
|
||||
|
||||
Behavior on animatedPercentage {
|
||||
Anim {}
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: animatedPercentage = percentage
|
||||
@@ -53,7 +59,7 @@ GridLayout {
|
||||
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
||||
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||
radius: Tokens.rounding.full
|
||||
radius: Math.min(width, height) / 2
|
||||
|
||||
Item {
|
||||
id: clipper
|
||||
@@ -63,20 +69,18 @@ GridLayout {
|
||||
anchors.left: root.horizontal ? undefined : parent.left
|
||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||
clip: true
|
||||
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
|
||||
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width
|
||||
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
|
||||
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
||||
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
||||
anchors.left: root.horizontal ? undefined : parent.left
|
||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||
|
||||
color: root.mainColor
|
||||
height: track.height
|
||||
radius: track.radius
|
||||
|
||||
implicitHeight: track.height
|
||||
implicitWidth: track.width
|
||||
width: track.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ WidgetBase {
|
||||
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
||||
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
||||
columnSpacing: Tokens.spacing.small
|
||||
rowSpacing: 0
|
||||
columns: root.horizontal ? -1 : 1
|
||||
|
||||
ServiceRef {
|
||||
@@ -52,7 +51,6 @@ WidgetBase {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillHeight: root.horizontal
|
||||
Layout.fillWidth: !root.horizontal
|
||||
|
||||
horizontal: root.horizontal
|
||||
icon: "memory"
|
||||
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
|
||||
@@ -31,7 +31,7 @@ WidgetBase {
|
||||
return -1;
|
||||
}
|
||||
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
||||
readonly property int spacing: Tokens.spacing.extraSmall
|
||||
readonly property int spacing: Tokens.spacing.medium / 2
|
||||
|
||||
// Entries that can shrink to nothing, spacing included
|
||||
function collapsed(entry: var): bool {
|
||||
@@ -51,12 +51,14 @@ WidgetBase {
|
||||
Behavior on implicitHeight {
|
||||
enabled: !root.horizontal
|
||||
|
||||
Anim {}
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.horizontal
|
||||
|
||||
Anim {}
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
@@ -68,8 +70,6 @@ WidgetBase {
|
||||
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||
columns: root.horizontal ? -1 : 1
|
||||
columnSpacing: root.spacing
|
||||
rowSpacing: root.spacing
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
@@ -105,13 +105,36 @@ WidgetBase {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "bluetooth"
|
||||
|
||||
delegate: EntryWrapper {
|
||||
name: "bluetooth"
|
||||
|
||||
BluetoothWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "network"
|
||||
|
||||
delegate: EntryWrapper {
|
||||
name: "network"
|
||||
|
||||
NetworkWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "power"
|
||||
|
||||
delegate: EntryWrapper {
|
||||
name: "upower"
|
||||
|
||||
UPowerWidget {}
|
||||
UPowerWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,8 +157,8 @@ WidgetBase {
|
||||
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
||||
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
||||
children: item
|
||||
implicitHeight: item?.height ?? item?.implicitHeight ?? 0
|
||||
implicitWidth: item?.width ?? item?.implicitWidth ?? 0
|
||||
implicitHeight: item?.implicitHeight ?? 0
|
||||
implicitWidth: item?.implicitWidth ?? 0
|
||||
|
||||
Behavior on leftGap {
|
||||
Anim {
|
||||
|
||||
@@ -6,7 +6,7 @@ import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MaterialIcon {
|
||||
id: root
|
||||
id: speaker
|
||||
|
||||
required property bool horizontal
|
||||
|
||||
@@ -15,8 +15,6 @@ MaterialIcon {
|
||||
fill: 1
|
||||
fontStyle: Tokens.font.icon.small
|
||||
text: Audio.muted ? "volume_off" : "volume_up"
|
||||
width: Math.round(implicitWidth)
|
||||
height: Math.round(implicitHeight)
|
||||
|
||||
Behavior on Layout.maximumWidth {
|
||||
Anim {}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
|
||||
fill: 1
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: "bluetooth"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MaterialIcon {
|
||||
id: root
|
||||
id: mic
|
||||
|
||||
required property bool horizontal
|
||||
|
||||
@@ -15,8 +15,6 @@ MaterialIcon {
|
||||
fill: 1
|
||||
fontStyle: Tokens.font.icon.small
|
||||
text: Audio.sourceMuted ? "mic_off" : "mic"
|
||||
width: Math.round(implicitWidth)
|
||||
height: Math.round(implicitHeight)
|
||||
|
||||
Behavior on Layout.maximumWidth {
|
||||
Anim {}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
|
||||
fill: 1
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: "android_wifi_4_bar"
|
||||
}
|
||||
@@ -79,7 +79,6 @@ Item {
|
||||
|
||||
anchors.centerIn: parent
|
||||
antialiasing: true
|
||||
backer.retainWhileLoading: true
|
||||
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
implicitSize: Config.bar.tray.trayIconSize * root.dpr
|
||||
layer.enabled: Config.bar.tray.recolorIcons
|
||||
|
||||
@@ -38,7 +38,6 @@ WidgetBase {
|
||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||
columns: root.horizontal ? -1 : 1
|
||||
rowSpacing: -2
|
||||
columnSpacing: Tokens.spacing.extraSmall
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
@@ -12,14 +12,13 @@ import qs.Services
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
|
||||
readonly property int spacing: Tokens.spacing.extraSmall / 2
|
||||
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
|
||||
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
||||
required property bool horizontal
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||
required property ShellScreen screen
|
||||
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2
|
||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
|
||||
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
||||
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
||||
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
||||
@@ -60,17 +59,15 @@ Item {
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.activeWorkspaceMargin
|
||||
columnSpacing: root.spacing
|
||||
columnSpacing: 0
|
||||
columns: root.horizontal ? root.workspacesShown : 1
|
||||
rowSpacing: root.spacing
|
||||
rowSpacing: 0
|
||||
rows: root.horizontal ? 1 : root.workspacesShown
|
||||
layer.enabled: true
|
||||
layer.mipmap: true
|
||||
|
||||
Repeater {
|
||||
model: root.workspaces
|
||||
|
||||
CustomRect {
|
||||
Button {
|
||||
id: button
|
||||
|
||||
required property int index
|
||||
@@ -78,15 +75,24 @@ Item {
|
||||
|
||||
implicitHeight: indicator.indicatorThickness
|
||||
implicitWidth: indicator.indicatorThickness
|
||||
radius: Tokens.rounding.full
|
||||
width: root.workspaceButtonWidth
|
||||
|
||||
background: Item {
|
||||
id: workspaceButtonBackground
|
||||
|
||||
implicitHeight: root.workspaceButtonWidth
|
||||
implicitWidth: root.workspaceButtonWidth
|
||||
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: button.modelData?.name ?? ""
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
onPressed: {
|
||||
const ws = button.modelData.name;
|
||||
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
||||
@@ -94,15 +100,12 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: indicator
|
||||
|
||||
readonly property real step: root.workspaceButtonWidth + root.spacing
|
||||
|
||||
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth + Math.abs(idxPair.idx1 - idxPair.idx2) * root.spacing
|
||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * step + root.activeWorkspaceMargin
|
||||
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth
|
||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
|
||||
property real indicatorThickness: root.workspaceButtonWidth
|
||||
|
||||
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
||||
@@ -126,10 +129,9 @@ Item {
|
||||
implicitHeight: grid.height
|
||||
implicitWidth: grid.width
|
||||
source: grid
|
||||
autoPaddingEnabled: false
|
||||
sourceColor: Colors.palette.m3onSurface
|
||||
x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0
|
||||
y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin
|
||||
x: root.horizontal ? -indicator.x + 3 : 0
|
||||
y: root.horizontal ? 0 : -indicator.y + 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQml.Models
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
readonly property list<var> btConnected: Bluetooth.devices.values.filter(d => d.connected)
|
||||
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
implicitWidth: 500
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.larger
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
CustomText {
|
||||
Layout.rightMargin: Tokens.padding.extraSmall
|
||||
Layout.topMargin: Tokens.padding.normal
|
||||
font.pointSize: Tokens.font.size.large
|
||||
text: qsTr("Bluetooth")
|
||||
}
|
||||
|
||||
Toggle {
|
||||
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
|
||||
first: true
|
||||
subtext: qsTr("Toggle bluetooth device state")
|
||||
text: checked ? qsTr("Enabled") : qsTr("Disabled")
|
||||
|
||||
onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
|
||||
if (adapter)
|
||||
adapter.enabled = checked;
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type
|
||||
last: root.btConnected.length === 0
|
||||
subtext: qsTr("Enable scanning for new devices")
|
||||
text: qsTr("Discover")
|
||||
|
||||
onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
|
||||
if (adapter)
|
||||
adapter.discovering = checked;
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: rows
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
interactive: false
|
||||
section.property: "connected"
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "isDivider"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: true
|
||||
|
||||
delegate: StatusDivider {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
delegate: BluetoothItem {
|
||||
list: rows
|
||||
width: rows.width
|
||||
}
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
objectProp: "address"
|
||||
values: {
|
||||
const byName = (a, b) => a.name.localeCompare(b.name);
|
||||
return [...[...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5),
|
||||
{
|
||||
isDivider: true
|
||||
},
|
||||
...[...Bluetooth.devices.values].filter(d => !d.connected).sort(byName).slice(0, 5)];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
section.delegate: Rectangle {
|
||||
required property string section
|
||||
|
||||
color: "transparent"
|
||||
height: childrenRect.height
|
||||
width: ListView.view.width
|
||||
|
||||
CustomText {
|
||||
text: parent.section
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BluetoothItem: ConnectedRect {
|
||||
id: btItem
|
||||
|
||||
readonly property int connectedCount: root.btConnected.length
|
||||
readonly property bool connecting: Network.connectingNetwork === modelData
|
||||
property int horizontalPadding: Tokens.padding.largeIncreased
|
||||
readonly property bool inConnectedGroup: index < connectedCount
|
||||
required property int index
|
||||
required property ListView list
|
||||
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
property int verticalPadding: Tokens.padding.smaller
|
||||
|
||||
first: !inConnectedGroup && index === connectedCount + 1
|
||||
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
|
||||
last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Timer {
|
||||
interval: 50
|
||||
running: btItem.ListView.delayRemove
|
||||
|
||||
onTriggered: btItem.ListView.delayRemove = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onConnectedChanged(): void {
|
||||
if (!btItem.modelData.connected)
|
||||
btItem.ListView.delayRemove = true;
|
||||
}
|
||||
|
||||
function onPairedChanged(): void {
|
||||
if (btItem.modelData.paired)
|
||||
btItem.modelData.connect();
|
||||
}
|
||||
|
||||
target: btItem.modelData
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: btItem.horizontalPadding
|
||||
anchors.rightMargin: btItem.horizontalPadding
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Tokens.spacing.extraSmall
|
||||
Layout.rightMargin: Tokens.spacing.extraSmall
|
||||
elide: Text.ElideRight
|
||||
text: btItem.modelData.name
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: btItem.modelData.connected ? (btItem.modelData.batteryAvailable ? qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") : qsTr("Battery status unknown")) : btItem.modelData.address
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
implicitHeight: icon.height
|
||||
implicitWidth: icon.width
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
font.pointSize: Tokens.font.size.large
|
||||
opacity: btItem.connecting ? 0 : 1
|
||||
text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_unknown"
|
||||
visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
opacity: btItem.connecting ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CircularIndicator {
|
||||
bgColor: "transparent"
|
||||
running: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
IconTextButton {
|
||||
font.pointSize: Tokens.font.size.small
|
||||
icon: btItem.modelData.connected ? "link_off" : btItem.modelData.paired ? "link" : "add_link"
|
||||
text: btItem.modelData.connected ? qsTr("Disconnect") : btItem.modelData.paired ? qsTr("Connect") : qsTr("Pair")
|
||||
|
||||
onClicked: {
|
||||
if (btItem.modelData.connected)
|
||||
btItem.modelData.disconnect();
|
||||
else if (btItem.modelData.paired)
|
||||
btItem.modelData.connect();
|
||||
else if (!btItem.modelData.paired)
|
||||
btItem.modelData.pair();
|
||||
else if (btItem.modelData.pairing)
|
||||
btItem.modelData.cancelPair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
component ConnectedRect: CustomRect {
|
||||
id: bg
|
||||
|
||||
property bool first
|
||||
property bool last
|
||||
|
||||
bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on bottomRightRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on topLeftRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on topRightRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
component StatusDivider: CustomText {
|
||||
bottomPadding: Tokens.spacing.extraSmall
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
text: {
|
||||
const devices = Bluetooth.devices.values; // qmllint disable unresolved-type
|
||||
const connectedCount = devices.filter(d => d.connected).length;
|
||||
let available = qsTr("%1 device%2 available").arg(devices.length - connectedCount).arg(devices.length === 1 ? "" : "s");
|
||||
return available;
|
||||
}
|
||||
topPadding: Tokens.spacing.small
|
||||
}
|
||||
component Toggle: CustomSwitch {
|
||||
id: toggleItem
|
||||
|
||||
readonly property alias bg: bg
|
||||
property alias first: bg.first
|
||||
property alias last: bg.last
|
||||
property string subtext
|
||||
|
||||
Layout.fillWidth: true
|
||||
cLayer: 2
|
||||
horizontalPadding: Tokens.padding.largeIncreased
|
||||
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
|
||||
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
|
||||
indicator.anchors.right: right
|
||||
indicator.anchors.rightMargin: toggleItem.horizontalPadding
|
||||
indicator.anchors.verticalCenter: verticalCenter
|
||||
verticalPadding: Tokens.padding.normal
|
||||
|
||||
background: ConnectedRect {
|
||||
id: bg
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
manualPressOverride: toggleItem.pressed
|
||||
}
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: toggleItem.horizontalPadding
|
||||
anchors.right: toggleItem.indicator.left
|
||||
anchors.rightMargin: Tokens.spacing.normal
|
||||
implicitHeight: column.implicitHeight
|
||||
implicitWidth: column.implicitWidth
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(label.font);
|
||||
f.pointSize = Tokens.font.size.smaller;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(subtext.font);
|
||||
f.pointSize = Tokens.font.size.small;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.subtext
|
||||
visible: toggleItem.subtext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "bluetooth"
|
||||
|
||||
sourceComponent: BluetoothPopout {
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "updates"
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Helpers.Network
|
||||
import qs.Components
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property bool closing: PopupManager.closed
|
||||
readonly property url currentPopup: PopupManager.currentPopup
|
||||
property bool shouldBeVisible: loader.status === Loader.Ready
|
||||
|
||||
color: Qt.alpha(Colors.palette.m3shadow, 0.3)
|
||||
opacity: shouldBeVisible && !closing ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
PopupManager.requestClose();
|
||||
PopupManager.currentPopup = "";
|
||||
}
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
propagateComposedEvents: false
|
||||
|
||||
onClicked: {
|
||||
const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
|
||||
const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
|
||||
if (insideItemHeight && insideItemWidth)
|
||||
return;
|
||||
|
||||
PopupManager.requestClose();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Connections {
|
||||
function onCurrentPopupChanged(): void {
|
||||
if (PopupManager.currentPopup) {
|
||||
loader.setSource(PopupManager.currentPopup, PopupManager.currentPopupProps);
|
||||
} else {
|
||||
loader.source = null;
|
||||
}
|
||||
}
|
||||
|
||||
target: PopupManager
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNetworkConnected(): void {
|
||||
PopupManager.requestClose();
|
||||
}
|
||||
|
||||
target: Network
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
import qs.Helpers.Network
|
||||
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var network
|
||||
|
||||
color: Colors.palette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.extraLarge
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pointSize: Tokens.font.size.extraLarge * 2
|
||||
text: "lock"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: qsTr("Connect to %1").arg(root.network.name)
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: passwordField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 400
|
||||
echoMode: CustomTextField.Password
|
||||
errorText: qsTr("Failed to connect with given password")
|
||||
isError: Network.failedNetwork
|
||||
leadingIcon: "lock"
|
||||
passwordMaskDelay: 5000
|
||||
placeholderText: qsTr("Input password")
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: buttonRow
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
IconTextButton {
|
||||
fillWidth: true
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
icon: "close"
|
||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
isToggle: false
|
||||
shapeMorph: true
|
||||
text: "Cancel"
|
||||
|
||||
onClicked: PopupManager.requestClose()
|
||||
}
|
||||
|
||||
IconTextButton {
|
||||
fillWidth: true
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
icon: Network.connectingNetwork ? "" : "check"
|
||||
inactiveColor: Network.connectingNetwork ? Colors.palette.m3primaryContainer : Network.failedNetwork ? Colors.palette.m3error : Colors.palette.m3primary
|
||||
inactiveOnColor: Network.connectingNetwork ? Colors.palette.m3onPrimaryContainer : Network.failedNetwork ? Colors.palette.m3onError : Colors.palette.m3onPrimary
|
||||
isRound: true
|
||||
isToggle: false
|
||||
shapeMorph: true
|
||||
text: Network.connectingNetwork ? "" : "Apply"
|
||||
|
||||
onClicked: Network.requestConnect(root.network, passwordField.text)
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: Network.connectingNetwork ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CircularIndicator {
|
||||
bgColor: "transparent"
|
||||
implicitSize: buttonRow.height - Tokens.padding.normal
|
||||
running: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool closed: true
|
||||
property url currentPopup: ""
|
||||
property var currentPopupProps: ({})
|
||||
|
||||
function requestClose(): void {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
function requestOpen(source: url, properties = {}): void {
|
||||
currentPopupProps = properties;
|
||||
currentPopup = source;
|
||||
closed = false;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,142 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Networking
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Modules
|
||||
import qs.Helpers as Helpers
|
||||
import qs.Services
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Helpers.Network
|
||||
|
||||
Item {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var wrapper
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitHeight: networkPopContent.height + networkPopContent.anchors.margins * 2
|
||||
implicitWidth: 500 + 8 * 2
|
||||
radius: (20 - Tokens.padding.small) * Tokens.rounding.scale
|
||||
|
||||
Component.onCompleted: Network.active = true
|
||||
Component.onDestruction: Network.active = false
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
id: networkPopContent
|
||||
|
||||
spacing: 8
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
Repeater {
|
||||
model: Helpers.Network.devices
|
||||
CustomText {
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.rightMargin: Tokens.padding.extraSmall
|
||||
font.pointSize: Tokens.font.size.large
|
||||
text: qsTr("Wifi")
|
||||
}
|
||||
|
||||
CustomRadioButton {
|
||||
id: network
|
||||
Toggle {
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
checked: Network.wifiEnabled
|
||||
first: true
|
||||
last: Network.connectedNetworks.length === 0
|
||||
subtext: qsTr("Toggle WiFi device state")
|
||||
text: checked ? qsTr("Enabled") : qsTr("Disabled")
|
||||
|
||||
required property NetworkDevice modelData
|
||||
onToggled: Network.setWifi(checked)
|
||||
}
|
||||
|
||||
checked: Helpers.Network.activeDevice?.name === modelData.name
|
||||
text: modelData.description
|
||||
visible: modelData.name !== "lo"
|
||||
ListView {
|
||||
id: rows
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
interactive: false
|
||||
section.property: "connected"
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "isDivider"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: true
|
||||
|
||||
delegate: NetworkDivider {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
delegate: NetworkItem {
|
||||
list: rows
|
||||
width: rows.width
|
||||
}
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name);
|
||||
const cNetworks = [...Network.connectedNetworks];
|
||||
const kNetworks = [...Network.knownNetworks];
|
||||
const uNetworks = [...Network.unknownNetworks];
|
||||
|
||||
return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [
|
||||
{
|
||||
isDivider: true,
|
||||
type: "known"
|
||||
},
|
||||
...kNetworks.sort(byStrengthName)] : []), ...(uNetworks.length > 0 ? [
|
||||
{
|
||||
isDivider: true,
|
||||
type: "unknown"
|
||||
},
|
||||
...uNetworks.sort(byStrengthName)] : [])];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
section.delegate: Rectangle {
|
||||
required property string section
|
||||
|
||||
color: "transparent"
|
||||
height: childrenRect.height
|
||||
width: ListView.view.width
|
||||
|
||||
CustomText {
|
||||
text: parent.section
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NetworkOverlay {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
ClipRect {
|
||||
id: preview
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -64,7 +64,6 @@ Item {
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
|
||||
radius: 25
|
||||
clip: true
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
@@ -229,12 +228,11 @@ Item {
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Item {
|
||||
ClipRect {
|
||||
id: textRect
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
|
||||
Item {
|
||||
id: textWrapper
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
@@ -11,7 +9,7 @@ Item {
|
||||
id: root
|
||||
|
||||
required property bool isCurrent
|
||||
required property var modelData
|
||||
required property var modelData // FileSystemEntry-like {path, relativePath}
|
||||
required property real tileHeight
|
||||
required property real tileWidth
|
||||
required property ScreenState screenState
|
||||
@@ -31,75 +29,25 @@ Item {
|
||||
radius: Tokens.rounding.small
|
||||
y: Tokens.padding.large
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
opacity: img.status === Image.Ready ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: CustomRect {
|
||||
color: Colors.palette.m3primaryContainer
|
||||
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
LoadingIndicator {
|
||||
id: loadingIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
containsIcon: true
|
||||
implicitSize: Math.min(image.width, image.height) * 0.3
|
||||
}
|
||||
}
|
||||
color: Colors.tPalette.m3outline
|
||||
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
|
||||
text: "image"
|
||||
}
|
||||
|
||||
Image {
|
||||
id: img
|
||||
CachingImage {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
asynchronous: true
|
||||
cache: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
height: root.tileHeight
|
||||
source: root.modelData ? root.modelData.thumbnailPath : ""
|
||||
path: root.modelData ? root.modelData.path : ""
|
||||
smooth: root.isCurrent
|
||||
sourceSize.height: root.tileHeight
|
||||
sourceSize.width: root.tileHeight * (16 / 9)
|
||||
width: root.tileHeight * (16 / 9)
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||
implicitHeight: implicitWidth
|
||||
radius: Tokens.rounding.full
|
||||
color: Colors.tPalette.m3surface
|
||||
|
||||
MaterialIcon {
|
||||
id: videoIcon
|
||||
anchors.centerIn: parent
|
||||
text: "video_library"
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
|
||||
@@ -5,13 +5,14 @@ import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
ClipRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitWidth: layout.implicitWidth
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
@@ -49,10 +50,22 @@ Item {
|
||||
lock: root.lock
|
||||
}
|
||||
|
||||
NotifDock {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
topRightRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,9 +221,9 @@ WlSessionLockSurface {
|
||||
|
||||
anchors.fill: parent
|
||||
color: Colors.palette.m3surface
|
||||
layer.enabled: true
|
||||
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
|
||||
radius: lockContent.radius
|
||||
layer.enabled: true
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
blurMax: 15
|
||||
|
||||
+6
-11
@@ -7,19 +7,14 @@ import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
ClipRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Tokens.rounding.small
|
||||
bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
@@ -27,6 +22,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
layer.enabled: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
@@ -46,7 +42,6 @@ Item {
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
@@ -82,7 +77,7 @@ Item {
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoPrevious ?? false
|
||||
enabled: Players.active?.canGoPrevious
|
||||
icon: "skip_previous"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
@@ -93,7 +88,7 @@ Item {
|
||||
|
||||
IconButton {
|
||||
checked: Players.active?.isPlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
|
||||
isRound: true
|
||||
@@ -103,7 +98,7 @@ Item {
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoNext ?? false
|
||||
enabled: Players.active?.canGoNext
|
||||
icon: "skip_next"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
|
||||
@@ -11,17 +11,11 @@ import ZShell.Config
|
||||
import qs.Daemons
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
topRightRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.small
|
||||
@@ -34,9 +28,13 @@ CustomRect {
|
||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||
}
|
||||
|
||||
Item {
|
||||
ClipRect {
|
||||
id: clipRect
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
@@ -51,6 +49,19 @@ CustomRect {
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
layer.enabled: true
|
||||
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
|
||||
sourceSize.width: clipRect.width * 0.8
|
||||
|
||||
layer.effect: Coloriser {
|
||||
brightness: 1
|
||||
colorizationColor: Colors.palette.m3outlineVariant
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
@@ -124,5 +135,4 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Services
|
||||
import qs.Modules
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
@@ -278,7 +277,7 @@ CustomRect {
|
||||
component NotifLine: CustomText {
|
||||
id: notifLine
|
||||
|
||||
required property NotificationRow modelData
|
||||
required property NotifServer.Notif modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
||||
|
||||
@@ -16,7 +16,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.large
|
||||
font: Tokens.font.title.large
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ ColumnLayout {
|
||||
|
||||
animate: true
|
||||
color: Colors.palette.m3primary
|
||||
font: Tokens.font.title.large
|
||||
font: Tokens.font.body.large
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ ColumnLayout {
|
||||
animate: true
|
||||
color: Colors.palette.m3secondary
|
||||
text: Weather.icon
|
||||
fontStyle: Tokens.font.icon.large
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +53,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.medium
|
||||
font: Tokens.font.body.normal
|
||||
text: {
|
||||
const today = Weather.forecast[0];
|
||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||
|
||||
@@ -36,7 +36,7 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
VerticalFadeListView {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -45,20 +45,13 @@ CustomRect {
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentItem.childrenRect.height
|
||||
currentIndex: Weather.currentHourIndex
|
||||
// highlightFollowsCurrentItem: true
|
||||
// preferredHighlightEnd: currentItem.implicitWidth
|
||||
// preferredHighlightBegin: 0
|
||||
// highlightRangeMode: ListView.ApplyRange
|
||||
clip: true
|
||||
model: Weather.hourlyForecast.slice(Weather.currentHourIndex - 1)
|
||||
model: Weather.hourlyForecast
|
||||
orientation: VerticalFadeListView.Horizontal
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: hour
|
||||
|
||||
readonly property bool current: ListView.view.currentItem === this
|
||||
readonly property var cond: modelData
|
||||
required property int index
|
||||
required property var modelData
|
||||
@@ -67,7 +60,7 @@ CustomRect {
|
||||
|
||||
MaterialShape {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0)
|
||||
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
|
||||
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
|
||||
shape: MaterialShape.Cookie4Sided
|
||||
|
||||
@@ -79,7 +72,7 @@ CustomRect {
|
||||
id: temp
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
font: Tokens.font.body.medium
|
||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||
}
|
||||
@@ -103,7 +96,7 @@ CustomRect {
|
||||
Layout.topMargin: Tokens.spacing.extraSmall
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.small
|
||||
text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ CustomRect {
|
||||
return base + brief.anchors.topMargin;
|
||||
}
|
||||
radius: Tokens.rounding.small
|
||||
topLeftRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
Timer {
|
||||
interval: 900000 // 15 minutes
|
||||
@@ -49,6 +48,7 @@ CustomRect {
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Forecast {}
|
||||
sourceComponent: Forecast {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
@@ -29,10 +28,7 @@ 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)
|
||||
return 0;
|
||||
|
||||
if (screenState.dashboard && (panels.dashboard.x + panels.dashboard.width >= panels.notifications.x))
|
||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard)
|
||||
return 0;
|
||||
|
||||
if (screenState.osd) {
|
||||
@@ -98,7 +94,7 @@ Item {
|
||||
|
||||
property int idx
|
||||
required property int index
|
||||
required property NotificationRow modelData
|
||||
required property NotifServer.Notif modelData
|
||||
readonly property alias nonAnimHeight: notif.nonAnimHeight
|
||||
|
||||
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.small)
|
||||
|
||||
@@ -5,7 +5,7 @@ import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
@@ -17,7 +17,7 @@ CustomRect {
|
||||
property bool expanded: Config.notifs.openExpanded
|
||||
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
||||
readonly property bool hasImage: modelData.image.length > 0
|
||||
required property NotificationRow modelData
|
||||
required property NotifServer.Notif modelData
|
||||
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
|
||||
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondaryContainer : Colors.tPalette.m3surfaceContainer
|
||||
@@ -56,10 +56,10 @@ CustomRect {
|
||||
if (actions.length === 1)
|
||||
actions[0].invoke();
|
||||
}
|
||||
onEntered: root.modelData.pausePopup()
|
||||
onEntered: root.modelData.timer.stop()
|
||||
onExited: {
|
||||
if (!pressed)
|
||||
root.modelData.resumePopup();
|
||||
root.modelData.timer.start();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
@@ -69,14 +69,14 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
root.modelData.pausePopup();
|
||||
root.modelData.timer.stop();
|
||||
startY = event.y;
|
||||
if (event.button === Qt.MiddleButton)
|
||||
root.modelData.close();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (!containsMouse)
|
||||
root.modelData.resumePopup();
|
||||
root.modelData.timer.start();
|
||||
|
||||
if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold)
|
||||
root.x = 0;
|
||||
@@ -200,7 +200,7 @@ CustomRect {
|
||||
radiusX: progressIndicator.width / 2 - Tokens.padding.extraSmall / 2
|
||||
radiusY: progressIndicator.height / 2 - Tokens.padding.extraSmall / 2
|
||||
startAngle: -90
|
||||
sweepAngle: (root.modelData.progress / 100) * 360
|
||||
sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360
|
||||
|
||||
Behavior on sweepAngle {
|
||||
Anim {
|
||||
@@ -477,12 +477,7 @@ CustomRect {
|
||||
shapeMorph: true
|
||||
text: modelData.text
|
||||
|
||||
onClicked: {
|
||||
if (modelData.invokable)
|
||||
modelData.invoke();
|
||||
else if (!root.modelData.resident)
|
||||
root.modelData.close();
|
||||
}
|
||||
onClicked: modelData.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ Item {
|
||||
opacity: img.status === Image.Ready ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CustomRect {
|
||||
color: Colors.palette.m3primaryContainer
|
||||
@@ -63,43 +64,18 @@ Item {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
retainWhileLoading: true
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||
implicitHeight: implicitWidth
|
||||
radius: Tokens.rounding.full
|
||||
color: Colors.tPalette.m3surface
|
||||
|
||||
MaterialIcon {
|
||||
id: videoIcon
|
||||
anchors.centerIn: parent
|
||||
text: "video_library"
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,9 @@ import qs.Services
|
||||
Item {
|
||||
id: wrapper
|
||||
|
||||
readonly property bool isVideo: Wallpapers.currentIsVideo
|
||||
readonly property string currentWallpaper: Wallpapers.current
|
||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||
readonly property real maxZoom: 5.0
|
||||
property int buttonRowHeight: Tokens.padding.large * 3
|
||||
readonly property bool changesMade: {
|
||||
cropRevision;
|
||||
Wallpapers.cropVersion;
|
||||
wrapper.currentWallpaper;
|
||||
|
||||
const r = cropRectLoader.item;
|
||||
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
||||
@@ -30,9 +25,9 @@ Item {
|
||||
|
||||
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
||||
}
|
||||
|
||||
property int buttonRowHeight: Tokens.padding.large * 3
|
||||
property int cropRevision: 0
|
||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||
readonly property real maxZoom: 5.0
|
||||
property var screens: []
|
||||
property int selectedScreenIndex: 0
|
||||
property bool shouldBeActive: true
|
||||
@@ -118,9 +113,6 @@ Item {
|
||||
refreshScreens();
|
||||
Qt.callLater(syncCropToScreen);
|
||||
}
|
||||
onCurrentWallpaperChanged: {
|
||||
Qt.callLater(syncCropToScreen);
|
||||
}
|
||||
onSelectedScreenIndexChanged: {
|
||||
Qt.callLater(syncCropToScreen);
|
||||
}
|
||||
@@ -135,7 +127,7 @@ Item {
|
||||
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
||||
anchors.top: parent.top
|
||||
|
||||
active: wrapper.screens.length > 1 && !wrapper.isVideo
|
||||
active: wrapper.screens.length > 1
|
||||
|
||||
sourceComponent: ButtonRow {
|
||||
id: screenSelector
|
||||
@@ -171,7 +163,6 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: 30
|
||||
enabled: !wrapper.isVideo
|
||||
|
||||
CustomSlider {
|
||||
id: zoomSlider
|
||||
@@ -283,7 +274,7 @@ Item {
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
source: Wallpapers.currentThumbnail
|
||||
source: Wallpapers.current
|
||||
sourceSize.height: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return imageSlot.height * dpr;
|
||||
@@ -294,37 +285,12 @@ Item {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
opacity: wrapper.isVideo && scaledImg.status === Image.Ready ? 1 : 0
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||
implicitHeight: implicitWidth
|
||||
radius: Tokens.rounding.full
|
||||
color: Colors.tPalette.m3surface
|
||||
|
||||
MaterialIcon {
|
||||
id: videoIcon
|
||||
anchors.centerIn: parent
|
||||
text: "video_library"
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: cropRectLoader
|
||||
|
||||
active: scaledImg.status === Image.Ready && !wrapper.isVideo
|
||||
active: scaledImg.status === Image.Ready
|
||||
|
||||
sourceComponent: Component {
|
||||
CustomRect {
|
||||
@@ -367,6 +333,7 @@ Item {
|
||||
y = t.y;
|
||||
}
|
||||
|
||||
// Where the rect should be according to saved data (same rules as a restore).
|
||||
function savedTarget() {
|
||||
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
||||
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
||||
@@ -469,10 +436,9 @@ Item {
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
enabled: !wrapper.isVideo
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed) {
|
||||
|
||||
@@ -2,8 +2,8 @@ pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.Settings.Pages
|
||||
import qs.Modules.Settings.Pages.Wallpaper
|
||||
@@ -13,6 +13,7 @@ import qs.Modules.Settings.Pages.Panels
|
||||
import qs.Modules.Settings.Pages.Panels.Bar
|
||||
import qs.Modules.Settings.Pages.Panels.Sidebar
|
||||
import qs.Modules.Settings.Pages.Services
|
||||
import qs.Modules.Settings.Pages.Network
|
||||
import qs.Services
|
||||
|
||||
QtObject {
|
||||
@@ -47,7 +48,27 @@ QtObject {
|
||||
|
||||
// Connectivity
|
||||
Component {
|
||||
PlaceholderComp {}
|
||||
StackPage {
|
||||
Component {
|
||||
NetworkPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
AllNetworks {}
|
||||
}
|
||||
|
||||
Component {
|
||||
SavedNetworks {}
|
||||
}
|
||||
|
||||
Component {
|
||||
AddNetwork {}
|
||||
}
|
||||
|
||||
Component {
|
||||
OldNetwork {}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
PlaceholderComp {}
|
||||
|
||||
@@ -38,6 +38,7 @@ Item {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: opacity < 1
|
||||
objectName: "PageContainer"
|
||||
|
||||
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Internal
|
||||
import ZShell.Blobs
|
||||
import ZShell.Services
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Modules.Settings.Common
|
||||
|
||||
PageBase {
|
||||
id: root
|
||||
|
||||
property bool connecting: false
|
||||
property bool failed: false
|
||||
|
||||
// Security model: index 0 = none, 1 = WPA/WPA2/WPA3 personal.
|
||||
readonly property bool secured: securitySelect.active !== noneItem
|
||||
property bool success: false
|
||||
|
||||
function submit(): void {
|
||||
const ssid = ssidField.text.trim();
|
||||
if (ssid.length === 0) {
|
||||
ssidField.isError = true;
|
||||
ssidField.forceActiveFocus();
|
||||
return;
|
||||
}
|
||||
if (root.secured && passwordField.text.length < 8) {
|
||||
passwordField.isError = true;
|
||||
passwordField.forceActiveFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
root.failed = false;
|
||||
root.connecting = true;
|
||||
|
||||
Nmcli.addHiddenNetwork(ssid, root.secured ? passwordField.text : "", root.secured ? "wpa" : "none", hiddenToggle.checked, result => {
|
||||
root.connecting = false;
|
||||
if (result && result.success) {
|
||||
root.success = true;
|
||||
root.nState.closeSubPage();
|
||||
} else {
|
||||
root.failed = true;
|
||||
if (root.secured)
|
||||
passwordField.isError = true;
|
||||
// Clean up the half-created profile so a retry starts fresh.
|
||||
Nmcli.forgetNetwork(ssid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isSubPage: true
|
||||
title: Tr.tr("Add network")
|
||||
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.large
|
||||
width: root.cappedWidth
|
||||
|
||||
Connections {
|
||||
function onSubPageClosed(): void {
|
||||
if (root.success)
|
||||
return;
|
||||
|
||||
const ssid = ssidField.text.trim();
|
||||
if (ssid)
|
||||
Nmcli.forgetNetwork(ssid);
|
||||
}
|
||||
|
||||
target: root.nState
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Tokens.padding.extraSmall
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.small
|
||||
text: Tr.tr("Enter the details below to manually connect to a network.")
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: ssidField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Tokens.spacing.extraSmall
|
||||
errorText: Tr.tr("Network name is required")
|
||||
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
|
||||
leadingIcon: "wifi"
|
||||
// TRANSLATORS: SSID is a protocol term, leave it untranslated
|
||||
placeholderText: Tr.tr("Network name (SSID)")
|
||||
// TRANSLATORS: sample network name, translate the e.g. but the name is a placeholder
|
||||
supportingText: Tr.tr("e.g. MyHiddenNetwork")
|
||||
|
||||
onAccepted: root.secured ? passwordField.forceActiveFocus() : root.submit()
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
id: hiddenToggle
|
||||
|
||||
checked: true
|
||||
first: true
|
||||
subtext: Tr.tr("Actively probe for a network that doesn't broadcast its name")
|
||||
text: Tr.tr("Hidden network")
|
||||
}
|
||||
|
||||
SelectRow {
|
||||
id: securitySelect
|
||||
|
||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||
fallbackIcon: "lock"
|
||||
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
|
||||
fallbackText: Tr.tr("WPA/WPA2/WPA3 Personal")
|
||||
// label: Tr.tr("Security")
|
||||
last: !root.secured
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on bottomRightRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
icon: "lock"
|
||||
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
|
||||
text: Tr.tr("WPA/WPA2/WPA3 Personal")
|
||||
},
|
||||
MenuItem {
|
||||
id: noneItem
|
||||
|
||||
icon: "lock_open"
|
||||
text: Tr.trCtx("None (open)", "wifi security type")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.bottomMargin: root.secured ? 0 : -parent.spacing
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: root.secured ? passwordField.implicitHeight : 0
|
||||
opacity: root.secured ? 1 : 0
|
||||
|
||||
Behavior on Layout.bottomMargin {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: passwordField
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
echoMode: TextInput.Password
|
||||
enabled: root.secured
|
||||
errorText: root.failed ? Tr.tr("Connection failed — check the password") : Tr.tr("Password must be at least 8 characters")
|
||||
leadingIcon: "key"
|
||||
placeholderText: Tr.tr("Password")
|
||||
// TRANSLATORS: WPA is a protocol name, leave it untranslated
|
||||
supportingText: Tr.tr("WPA passwords are at least 8 characters")
|
||||
|
||||
onAccepted: root.submit()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.topMargin: Tokens.spacing.extraSmall - parent.spacing
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
TextButton {
|
||||
Layout.fillHeight: true
|
||||
horizontalPadding: Tokens.padding.extraLarge
|
||||
isRound: true
|
||||
text: Tr.trCtx("Cancel", "button")
|
||||
type: TextButton.Tonal
|
||||
|
||||
onClicked: root.nState.closeSubPage()
|
||||
}
|
||||
|
||||
// Connect button — swaps to a loading spinner while connecting,
|
||||
// matching the Wi-Fi list connect animation.
|
||||
ButtonBase {
|
||||
id: connectBtn
|
||||
|
||||
implicitHeight: connectMetrics.height + Tokens.padding.medium * 2
|
||||
implicitWidth: connectMetrics.width + Tokens.padding.extraLarge * 2
|
||||
// inactiveColour: Colours.palette.m3primary
|
||||
// inactiveOnColour: Colours.palette.m3onPrimary
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
|
||||
// stateLayer.disabled: root.connecting || ssidField.text.trim().length === 0
|
||||
|
||||
onClicked: {
|
||||
if (!root.connecting && ssidField.text.trim().length > 0)
|
||||
root.submit();
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: connectMetrics
|
||||
|
||||
font: connectBtn.font
|
||||
text: Tr.tr("Connect")
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: connectContent
|
||||
|
||||
// anchors.centerIn: parent
|
||||
// inAnimType: Anim.SlowEffects
|
||||
// outAnimType: Anim.SlowEffects
|
||||
// sourceComp: root.connecting ? connectLoadingComp : connectTextComp
|
||||
}
|
||||
|
||||
Component {
|
||||
id: connectLoadingComp
|
||||
|
||||
LoadingIndicator {
|
||||
color: connectBtn.onColour
|
||||
implicitSize: Math.round(Tokens.font.body.medium.pointSize * 1.4)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: connectTextComp
|
||||
|
||||
CustomText {
|
||||
color: connectBtn.onColour
|
||||
font: connectBtn.font
|
||||
text: connectMetrics.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Helpers.Network
|
||||
import qs.Services
|
||||
import qs.Modules.Settings.Common
|
||||
|
||||
PageBase {
|
||||
id: root
|
||||
|
||||
flickable.bottomMargin: Tokens.padding.extraExtraLarge * 2 // Extra scrolling space at the bottom
|
||||
|
||||
isSubPage: true
|
||||
title: Tr.tr("All networks")
|
||||
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
// spacing: Tokens.spacing.extraSmall / 2
|
||||
width: root.cappedWidth
|
||||
|
||||
Timer {
|
||||
interval: 300 // GlobalConfig.nexus.networkRescanInterval
|
||||
repeat: true
|
||||
running: root.visible && Nmcli.wifiEnabled
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: Nmcli.rescanWifi()
|
||||
}
|
||||
|
||||
ConnectedRect {
|
||||
Layout.fillWidth: true
|
||||
first: true
|
||||
implicitHeight: headerLayout.implicitHeight + Tokens.padding.medium * 2
|
||||
|
||||
RowLayout {
|
||||
id: headerLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.largeIncreased
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: 1
|
||||
|
||||
// spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
CustomText {
|
||||
font: Tokens.font.title.small
|
||||
text: Tr.trCtx("Filters", "network filter heading")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
FilterButton {
|
||||
id: savedFilter
|
||||
|
||||
function internalFilter(ap: Nmcli.AccessPoint): bool {
|
||||
return Nmcli.hasSavedProfile(ap.ssid);
|
||||
}
|
||||
|
||||
bottomLeftRadius: pressed ? pressedRadius : implicitHeight / 2
|
||||
text: Tr.trCtx("Saved", "network filter")
|
||||
topLeftRadius: pressed ? pressedRadius : implicitHeight / 2
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on topLeftRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FilterButton {
|
||||
id: secureFilter
|
||||
|
||||
function internalFilter(ap: Nmcli.AccessPoint): bool {
|
||||
return ap.security !== "none";
|
||||
}
|
||||
|
||||
text: Tr.trCtx("Secured", "network filter")
|
||||
}
|
||||
|
||||
FilterButton {
|
||||
id: highFreqFilter
|
||||
|
||||
function internalFilter(ap: Nmcli.AccessPoint): bool {
|
||||
return ap.frequency >= 4900 && ap.frequency <= 5900;
|
||||
}
|
||||
|
||||
text: Tr.trCtx("5 GHz", "network filter")
|
||||
}
|
||||
|
||||
FilterButton {
|
||||
id: lowFreqFilter
|
||||
|
||||
function internalFilter(ap: Nmcli.AccessPoint): bool {
|
||||
return ap.frequency >= 2400 && ap.frequency <= 2500;
|
||||
}
|
||||
|
||||
bottomRightRadius: pressed ? pressedRadius : implicitHeight / 2
|
||||
text: Tr.trCtx("2.4 GHz", "network filter")
|
||||
topRightRadius: pressed ? pressedRadius : implicitHeight / 2
|
||||
|
||||
Behavior on bottomRightRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on topRightRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NetworkList {
|
||||
id: networkList
|
||||
|
||||
function networkFilter(ap: Nmcli.AccessPoint): bool {
|
||||
return savedFilter.filter(ap) && secureFilter.filter(ap) && highFreqFilter.filter(ap) && lowFreqFilter.filter(ap);
|
||||
}
|
||||
|
||||
enableFilter: true
|
||||
last: true
|
||||
// nState: root.nState
|
||||
}
|
||||
}
|
||||
|
||||
component FilterButton: IconTextButton {
|
||||
id: filter
|
||||
|
||||
property int filterState // 0 = default, 1 = on, 2 = negate
|
||||
|
||||
function filter(ap: Nmcli.AccessPoint): bool {
|
||||
if (filterState === 0)
|
||||
return true;
|
||||
if (filterState === 1)
|
||||
return internalFilter(ap);
|
||||
return !internalFilter(ap);
|
||||
}
|
||||
|
||||
function internalFilter(ap: Nmcli.AccessPoint): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultRadius: filterState > 0 ? implicitHeight / 2 : Tokens.rounding.small
|
||||
icon: ["check_indeterminate_small", "check", "close"][filterState]
|
||||
// inactiveColour: [Colours.palette.m3secondaryContainer, Colours.palette.m3secondary, Colours.palette.m3tertiary][filterState]
|
||||
// inactiveOnColour: [Colours.palette.m3onSecondaryContainer, Colours.palette.m3onSecondary, Colours.palette.m3onTertiary][filterState]
|
||||
pressedRadius: Tokens.rounding.extraSmall
|
||||
|
||||
// spacing: Tokens.spacing.extraSmall
|
||||
|
||||
onClicked: filterState = (filterState + 1) % 3
|
||||
}
|
||||
component NetworkList: ItemList {
|
||||
id: netList
|
||||
|
||||
property bool enableFilter
|
||||
property int limit: 0 // 0 = show all
|
||||
|
||||
// required property NexusState nState
|
||||
|
||||
signal networkSelected(ap: Nmcli.AccessPoint)
|
||||
|
||||
function networkFilter(ap: Nmcli.AccessPoint): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
extraHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 // Inline so it isn't affected by anim
|
||||
list.anchors.top: scanningIndicator.bottom
|
||||
placeholderIcon: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off"
|
||||
placeholderText: Nmcli.wifiEnabled ? Tr.tr("No networks found") : Tr.tr("Wi-Fi disabled")
|
||||
showList: Nmcli.wifiEnabled
|
||||
|
||||
delegate: StateLayer {
|
||||
id: network
|
||||
|
||||
property bool currentSelected
|
||||
required property int index
|
||||
required property var modelData
|
||||
property real textOpacity: disabled ? 0.5 : 1
|
||||
|
||||
anchors.fill: undefined
|
||||
anchors.left: netList.list.contentItem.left
|
||||
anchors.right: netList.list.contentItem.right
|
||||
bottomLeftRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
|
||||
bottomRightRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
|
||||
// disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid
|
||||
implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2
|
||||
radius: Tokens.rounding.extraSmall
|
||||
|
||||
Behavior on textOpacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (!modelData.active) {
|
||||
NetworkConnection.handleConnect(modelData);
|
||||
currentSelected = true;
|
||||
netList.networkSelected(modelData);
|
||||
} else {
|
||||
// Active network: open its detail/settings sub-page.
|
||||
netList.nState.selectedNetworkSsid = modelData.ssid;
|
||||
netList.nState.networkDetailsFromSaved = false;
|
||||
netList.nState.openSubPage(3);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onActiveChanged(): void {
|
||||
if (network.modelData.active)
|
||||
network.currentSelected = false;
|
||||
}
|
||||
|
||||
target: network.modelData
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNetworkSelected(ap: Nmcli.AccessPoint): void {
|
||||
if (ap !== network.modelData)
|
||||
network.currentSelected = false;
|
||||
}
|
||||
|
||||
target: netList
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: networkLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.extraLarge
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.rightMargin: Tokens.padding.extraLarge
|
||||
|
||||
// spacing: Tokens.spacing.extraSmall
|
||||
|
||||
MaterialIcon {
|
||||
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
opacity: network.textOpacity
|
||||
text: Icons.getNetworkIcon(network.modelData.strength)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
opacity: network.textOpacity
|
||||
|
||||
// spacing: 0
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.body.small
|
||||
text: network.modelData.ssid
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colours.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.label.small
|
||||
text: {
|
||||
const net = network.modelData;
|
||||
let status = "";
|
||||
if (net.active)
|
||||
status = Tr.trCtx("Connected", "network connected");
|
||||
else if (Nmcli.hasSavedProfile(net.ssid))
|
||||
status = Tr.trCtx("Saved", "network saved");
|
||||
if (status)
|
||||
// TRANSLATORS: %1 = security type, %2 = connection status
|
||||
return Tr.trCtx("Security: %1 • %2", "network security and status").arg(net.security).arg(status);
|
||||
return Tr.trCtx("Security: %1", "network security").arg(net.security);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anim {
|
||||
// // sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp
|
||||
//
|
||||
// Component {
|
||||
// id: iconComp
|
||||
//
|
||||
// MaterialIcon {
|
||||
// color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
// fontStyle: Tokens.font.icon.medium
|
||||
// opacity: network.textOpacity
|
||||
// text: network.modelData.active ? "settings" : "lock"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Component {
|
||||
// id: loadingComp
|
||||
//
|
||||
// LoadingIndicator {
|
||||
// implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// model: ScriptModel {
|
||||
// values: {
|
||||
// const connecting = Nmcli.connectingSsid();
|
||||
// // Lower rank sorts higher in the list
|
||||
// const rank = n => n.active ? 0 : n.ssid === connecting ? 1 : Nmcli.hasSavedProfile(n.ssid) ? 2 : 3;
|
||||
// const sorted = [...Nmcli.networks].sort((a, b) => rank(a) - rank(b) || b.strength - a.strength);
|
||||
// if (netList.limit > 0 && sorted.length > netList.limit)
|
||||
// sorted.length = netList.limit;
|
||||
// return netList.enableFilter ? sorted.filter(netList.networkFilter) : sorted;
|
||||
// }
|
||||
// }
|
||||
|
||||
// StyledProgressBar {
|
||||
// id: scanningIndicator
|
||||
//
|
||||
// anchors.left: parent.left
|
||||
// anchors.margins: 1
|
||||
// anchors.right: parent.right
|
||||
// implicitHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0
|
||||
// indeterminate: true
|
||||
//
|
||||
// Behavior on implicitHeight {
|
||||
// Anim {
|
||||
// type: Anim.DefaultEffects
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Helpers.Network
|
||||
import qs.Services
|
||||
|
||||
PageBase {
|
||||
id: root
|
||||
|
||||
title: qsTr("Begin page")
|
||||
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
width: root.cappedWidth
|
||||
|
||||
NavRow {
|
||||
first: true
|
||||
icon: "dock_to_bottom"
|
||||
// settingAnchor: "panels-bar"
|
||||
text: qsTr("Show all networks")
|
||||
|
||||
onClicked: root.sState.openSubPage(1)
|
||||
}
|
||||
|
||||
NavRow {
|
||||
icon: "dock_to_bottom"
|
||||
text: qsTr("Saved networks")
|
||||
|
||||
onClicked: root.sState.openSubPage(2)
|
||||
}
|
||||
|
||||
NavRow {
|
||||
icon: "dock_to_bottom"
|
||||
text: qsTr("Add network")
|
||||
|
||||
onClicked: root.sState.openSubPage(3)
|
||||
}
|
||||
|
||||
NavRow {
|
||||
icon: "dock_to_bottom"
|
||||
last: true
|
||||
text: qsTr("Old Network Page")
|
||||
|
||||
onClicked: root.sState.openSubPage(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Modules.Settings.Common
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Helpers.Network
|
||||
import qs.Services
|
||||
|
||||
PageBase {
|
||||
id: root
|
||||
|
||||
isSubPage: true
|
||||
title: qsTr("Old network page")
|
||||
|
||||
Component.onCompleted: Network.active = true
|
||||
Component.onDestruction: Network.active = false
|
||||
|
||||
ColumnLayout {
|
||||
id: networkContent
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
width: root.cappedWidth
|
||||
|
||||
Toggle {
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
checked: Network.wifiEnabled
|
||||
first: true
|
||||
last: Network.connectedNetworks.length === 0
|
||||
subtext: qsTr("Toggle WiFi device state")
|
||||
text: checked ? qsTr("Enabled") : qsTr("Disabled")
|
||||
|
||||
onToggled: Network.setWifi(checked)
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: rows
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
interactive: false
|
||||
section.property: "connected"
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "isDivider"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: true
|
||||
|
||||
delegate: NetworkDivider {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: false
|
||||
|
||||
delegate: KnownNetworkManager {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
delegate: NetworkItem {
|
||||
list: rows
|
||||
width: rows.width
|
||||
}
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name);
|
||||
const cNetworks = [...Network.connectedNetworks];
|
||||
const kNetworks = [...Network.knownNetworks];
|
||||
const uNetworks = [...Network.unknownNetworks];
|
||||
|
||||
return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [
|
||||
{
|
||||
isDivider: true,
|
||||
type: "known"
|
||||
},
|
||||
...kNetworks.sort(byStrengthName),
|
||||
{
|
||||
isDivider: false
|
||||
}
|
||||
] : []), ...(uNetworks.length > 0 ? [
|
||||
{
|
||||
isDivider: true,
|
||||
type: "unknown"
|
||||
},
|
||||
...uNetworks.sort(byStrengthName)] : [])];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
section.delegate: Rectangle {
|
||||
required property string section
|
||||
|
||||
color: "transparent"
|
||||
height: childrenRect.height
|
||||
width: ListView.view.width
|
||||
|
||||
CustomText {
|
||||
text: parent.section
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NetworkOverlay {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
component KnownNetworkManager: NetworkSelect {
|
||||
acceptLabel: qsTr("Remove")
|
||||
enabled: Object.keys(model).length > 0
|
||||
header: qsTr("Manage known networks")
|
||||
icon: "Network_Manage"
|
||||
label: qsTr("Manage known networks")
|
||||
model: Network.networks.filter(n => n.known && !n.connected).map(n => ({
|
||||
id: n.name,
|
||||
label: n.name
|
||||
}))
|
||||
rootParent: root.flickable
|
||||
width: rows.width
|
||||
|
||||
onAccepted: {
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
const networkToForget = Network.networks.find(n => n.name === selectedItem);
|
||||
Network.forgetNetwork(networkToForget);
|
||||
}
|
||||
}
|
||||
component NetworkSelect: DialogRowButton {
|
||||
id: klist
|
||||
|
||||
required property var model
|
||||
property var selectedItem
|
||||
|
||||
function keyFor(item: var): string {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function labelFor(item: var): string {
|
||||
return item.label;
|
||||
}
|
||||
|
||||
acceptAllowed: !!selectedItem
|
||||
horizontalContentMargin: -Tokens.padding.extraSmall
|
||||
separateContent: true
|
||||
|
||||
content: Component {
|
||||
VerticalFadeListView {
|
||||
bottomMargin: Tokens.padding.large
|
||||
model: klist.model
|
||||
spacing: 0
|
||||
topMargin: Tokens.padding.large
|
||||
|
||||
delegate: CustomRect {
|
||||
id: item
|
||||
|
||||
required property var modelData
|
||||
readonly property bool selected: klist.selectedItem === klist.keyFor(modelData)
|
||||
|
||||
anchors.left: ListView.view.contentItem.left
|
||||
anchors.margins: 1 // Gets cut off for some reason without this
|
||||
anchors.right: ListView.view.contentItem.right
|
||||
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
|
||||
implicitHeight: label.implicitHeight + Tokens.padding.small * 2
|
||||
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
onClicked: klist.selectedItem = klist.keyFor(item.modelData)
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.right: item.selected ? checkIcon.left : parent.right
|
||||
anchors.rightMargin: item.selected ? Tokens.spacing.medium : anchors.margins
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: item.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
text: klist.labelFor(item.modelData)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: checkIcon
|
||||
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3onTertiaryContainer
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
opacity: item.selected ? 1 : 0
|
||||
text: "check"
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpenChanged: {
|
||||
if (open)
|
||||
selectedItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Internal
|
||||
import ZShell.Blobs
|
||||
import ZShell.Services
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Modules.Settings.Common
|
||||
|
||||
PageBase {
|
||||
id: root
|
||||
|
||||
isSubPage: true
|
||||
title: Tr.tr("Saved networks")
|
||||
|
||||
Component.onCompleted: Nmcli.loadSavedConnections(() => {})
|
||||
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
width: root.cappedWidth
|
||||
|
||||
ItemList {
|
||||
id: savedList
|
||||
|
||||
first: true
|
||||
last: true
|
||||
placeholderIcon: "wifi_find"
|
||||
placeholderText: Tr.tr("No saved networks")
|
||||
showList: true
|
||||
|
||||
delegate: StateLayer {
|
||||
id: saved
|
||||
|
||||
readonly property var ap: Nmcli.findNetwork(modelData)
|
||||
required property int index
|
||||
readonly property bool isActive: !!Nmcli.active && Nmcli.active.ssid === modelData
|
||||
required property var modelData
|
||||
|
||||
anchors.fill: undefined
|
||||
anchors.left: savedList.list.contentItem.left
|
||||
anchors.right: savedList.list.contentItem.right
|
||||
bottomLeftRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
|
||||
bottomRightRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
|
||||
implicitHeight: savedLayout.implicitHeight + savedLayout.anchors.margins * 2
|
||||
radius: Tokens.rounding.extraSmall
|
||||
topLeftRadius: index === 0 ? Tokens.rounding.extraLarge : radius
|
||||
topRightRadius: index === 0 ? Tokens.rounding.extraLarge : radius
|
||||
|
||||
onClicked: {
|
||||
root.nState.selectedNetworkSsid = saved.modelData;
|
||||
root.nState.networkDetailsFromSaved = true;
|
||||
root.nState.openSubPage(3); // Shared network detail/edit sub-page
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: savedLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.extraLarge
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.rightMargin: Tokens.padding.extraLarge
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
MaterialIcon {
|
||||
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: saved.ap ? Icons.getNetworkIcon(saved.ap.strength, !["", "none"].includes(Nmcli.savedSecurityFor(saved.modelData))) : "signal_wifi_off"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.body.small
|
||||
text: saved.modelData
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.label.small
|
||||
text: {
|
||||
let security;
|
||||
if (saved.ap)
|
||||
security = saved.ap.security || Tr.trCtx("Open", "wifi security type");
|
||||
else
|
||||
security = Nmcli.securityLabel(Nmcli.savedSecurityFor(saved.modelData)) || Tr.trCtx("Unknown", "unknown wifi security");
|
||||
if (saved.isActive)
|
||||
// TRANSLATORS: %1 = security type
|
||||
return Tr.trCtx("Connected • %1", "network connected with security").arg(security);
|
||||
return security;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: "chevron_right"
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: [...Nmcli.savedConnectionSsids].sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ PageBase {
|
||||
readonly property var builtinIcons: ({
|
||||
audio: qsTr("Speakers"),
|
||||
microphone: qsTr("Microphone"),
|
||||
power: qsTr("Battery")
|
||||
power: qsTr("Battery"),
|
||||
network: qsTr("Network"),
|
||||
bluetooth: qsTr("Bluetooth")
|
||||
})
|
||||
|
||||
isSubPage: true
|
||||
@@ -93,5 +95,15 @@ PageBase {
|
||||
|
||||
onToggled: Config.bar.popouts.statusIcons = checked
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
checked: Config.bar.popouts.network
|
||||
last: true
|
||||
settingAnchor: "bar-status-network-popout"
|
||||
subtext: qsTr("Show a details popout when hovering the network icon")
|
||||
text: qsTr("Network popout on hover")
|
||||
|
||||
onToggled: Config.bar.popouts.network = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,11 @@ PageBase {
|
||||
}
|
||||
|
||||
WallItem {
|
||||
id: wallItem
|
||||
required property WallpaperEntry modelData
|
||||
required property FileSystemEntry modelData
|
||||
|
||||
enabled: modelData
|
||||
opacity: modelData ? 1 : 0
|
||||
source: String(modelData?.thumbnailPath ?? "")
|
||||
source: String(modelData?.path ?? "")
|
||||
|
||||
onClicked: {
|
||||
Wallpapers.setWallpaper(modelData.path);
|
||||
|
||||
@@ -13,42 +13,6 @@ import qs.Services
|
||||
PageBase {
|
||||
id: root
|
||||
|
||||
readonly property list<MenuItem> wallTransitions: [
|
||||
MenuItem {
|
||||
text: "Bowtie"
|
||||
},
|
||||
MenuItem {
|
||||
text: "Fade"
|
||||
},
|
||||
MenuItem {
|
||||
text: "LinearBlur"
|
||||
},
|
||||
MenuItem {
|
||||
text: "WindowSlices"
|
||||
},
|
||||
MenuItem {
|
||||
text: "WipeRight"
|
||||
},
|
||||
MenuItem {
|
||||
text: "WipeDown"
|
||||
},
|
||||
MenuItem {
|
||||
text: "TilesWave"
|
||||
},
|
||||
MenuItem {
|
||||
text: "SplitSlideInHorizontal"
|
||||
},
|
||||
MenuItem {
|
||||
text: "Hexagonalize"
|
||||
},
|
||||
MenuItem {
|
||||
text: "DoomMelt"
|
||||
},
|
||||
MenuItem {
|
||||
text: "Random"
|
||||
}
|
||||
]
|
||||
|
||||
title: qsTr("Wallpaper & style")
|
||||
|
||||
ColumnLayout {
|
||||
@@ -109,18 +73,6 @@ PageBase {
|
||||
onToggled: Config.background.enabled = checked
|
||||
}
|
||||
|
||||
SelectRow {
|
||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||
active: root.wallTransitions[Config.background.wallTransition]
|
||||
menuItems: root.wallTransitions
|
||||
menuOnTop: true
|
||||
settingAnchor: "style-wallpaper-transition"
|
||||
subtext: qsTr("Currently selected: %1").arg(active.text)
|
||||
text: qsTr("Wallpaper transition")
|
||||
|
||||
onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item)
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||
checked: Colors.transparency.enabled
|
||||
|
||||
@@ -337,7 +337,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
ClipRect {
|
||||
id: expandedTool
|
||||
|
||||
anchors.top: collapsedRow.bottom
|
||||
@@ -348,16 +348,8 @@ Item {
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
|
||||
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
||||
clip: true
|
||||
|
||||
CustomRect {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: fullToolCall
|
||||
|
||||
@@ -59,6 +59,7 @@ Item {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: opacity < 1
|
||||
objectName: "ConversationContainer"
|
||||
|
||||
Component.onCompleted: {
|
||||
|
||||
@@ -125,7 +125,7 @@ Item {
|
||||
current: root.current
|
||||
}
|
||||
|
||||
Item {
|
||||
ClipRect {
|
||||
id: expandedRect
|
||||
|
||||
anchors.left: parent.left
|
||||
@@ -133,19 +133,11 @@ Item {
|
||||
anchors.top: layout.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: Tokens.spacing.small
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
||||
opacity: root.expanded ? 1 : 0
|
||||
visible: opacity > 0
|
||||
clip: true
|
||||
|
||||
CustomRect {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
}
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
|
||||
@@ -4,17 +4,16 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Daemons
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null
|
||||
required property bool expanded
|
||||
required property NotificationRow modelData
|
||||
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 ScreenState screenState
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Effects
|
||||
import qs.Services
|
||||
import qs.Daemons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property NotificationRow notif
|
||||
required property NotifServer.Notif notif
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: flickable.contentHeight
|
||||
@@ -36,7 +36,7 @@ Item {
|
||||
{
|
||||
isClose: true
|
||||
},
|
||||
...Array.from(root.notif?.actions ?? []),
|
||||
...(root.notif?.actions ?? []),
|
||||
{
|
||||
isCopy: true
|
||||
}
|
||||
@@ -84,7 +84,7 @@ Item {
|
||||
Quickshell.clipboardText = root.notif.body;
|
||||
actionInner.item.text = "inventory";
|
||||
copyTimer.start();
|
||||
} else if (action.modelData.invokable) {
|
||||
} else if (action.modelData.invoke) {
|
||||
action.modelData.invoke();
|
||||
} else if (!root.notif.resident) {
|
||||
root.notif.close();
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Modules
|
||||
import qs.Daemons
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Daemons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
@@ -93,14 +92,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
KineticFlickable {
|
||||
CustomFlickable {
|
||||
id: view
|
||||
|
||||
anchors.fill: parent
|
||||
contentHeight: notifList.implicitHeight
|
||||
contentWidth: width
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
clip: true
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: view
|
||||
|
||||
@@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import ZShell.Config
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
@@ -37,7 +37,7 @@ LazyListView {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property NotificationRow modelData
|
||||
required property NotifServer.Notif modelData
|
||||
property int startY
|
||||
|
||||
LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight
|
||||
|
||||
@@ -20,14 +20,6 @@ pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
|
||||
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
||||
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
||||
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
||||
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
|
||||
|
||||
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
|
||||
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
|
||||
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
|
||||
pkg_check_modules(LIBVA QUIET IMPORTED_TARGET libva)
|
||||
|
||||
find_package(CUDAToolkit QUIET)
|
||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||
|
||||
@@ -37,7 +37,6 @@ list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
|
||||
add_subdirectory(Settings)
|
||||
add_subdirectory(Models)
|
||||
add_subdirectory(Internal)
|
||||
add_subdirectory(Wallpaper)
|
||||
add_subdirectory(Services)
|
||||
add_subdirectory(Components)
|
||||
add_subdirectory(Blobs)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
#include "common.hpp"
|
||||
#include "enums.hpp"
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
@@ -12,8 +9,6 @@ class Background : public settings::ObjectNode {
|
||||
|
||||
CONFIG_PROPERTY(bool, enabled, true)
|
||||
CONFIG_PROPERTY(int, wallFadeDuration, 300)
|
||||
CONFIG_ENUM_PROPERTY(
|
||||
WallpaperTransition, wallTransition, WallpaperTransition::Fade)
|
||||
};
|
||||
|
||||
} // namespace ZShell::config
|
||||
|
||||
@@ -23,19 +23,6 @@ ENUM(NotifsFullscreen, On, Off)
|
||||
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
|
||||
ENUM(DataUnit, Binary, Decimal)
|
||||
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
|
||||
ENUM(
|
||||
WallpaperTransition,
|
||||
Bowtie,
|
||||
Fade,
|
||||
LinearBlur,
|
||||
WindowSlices,
|
||||
WipeRight,
|
||||
WipeDown,
|
||||
TilesWave,
|
||||
SplitSlideInHorizontal,
|
||||
Hexagonalize,
|
||||
DoomMelt,
|
||||
Random)
|
||||
|
||||
#undef ENUM
|
||||
|
||||
|
||||
@@ -14,10 +14,9 @@
|
||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||
#include <hyprland/src/render/OpenGL.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#include <hyprland/src/render/Context.hpp>
|
||||
#undef private
|
||||
|
||||
#include <hyprland/src/ipc/s2/S2.hpp>
|
||||
#include <hyprland/src/managers/EventManager.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
@@ -28,7 +27,6 @@ extern "C" {
|
||||
|
||||
using ZShell::services::kelvinToGain;
|
||||
|
||||
using Render::CRenderContext;
|
||||
using Render::IFramebuffer;
|
||||
using Render::GL::CHyprOpenGLImpl;
|
||||
using Render::GL::g_pHyprOpenGL;
|
||||
@@ -151,14 +149,25 @@ static bool gainIsNeutral(const std::array<float, 3>& gain) {
|
||||
}
|
||||
|
||||
static CFunctionHook* g_pBeginHook = nullptr;
|
||||
using origBegin = bool (*)(
|
||||
using origBegin = void (*)(
|
||||
CHyprOpenGLImpl*,
|
||||
CRenderContext&,
|
||||
PHLMONITOR,
|
||||
const CRegion&,
|
||||
SP<IFramebuffer>,
|
||||
std::optional<CRegion>);
|
||||
|
||||
static CFunctionHook* g_pSaveMirrorHook = nullptr;
|
||||
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
|
||||
|
||||
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
|
||||
const bool saved = thisptr->m_applyFinalShader;
|
||||
thisptr->m_applyFinalShader = false;
|
||||
const bool result = (*reinterpret_cast<origSaveMirror>(
|
||||
g_pSaveMirrorHook->m_original))(thisptr, box);
|
||||
thisptr->m_applyFinalShader = saved;
|
||||
return result;
|
||||
}
|
||||
|
||||
static void installShader() {
|
||||
auto* gl = g_pHyprOpenGL.get();
|
||||
if (!gl) return;
|
||||
@@ -189,33 +198,34 @@ static void setUniformGain(const std::array<float, 3>& gain) {
|
||||
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
|
||||
}
|
||||
|
||||
static bool hkBegin(
|
||||
static void hkBegin(
|
||||
CHyprOpenGLImpl* thisptr,
|
||||
CRenderContext& ctx,
|
||||
PHLMONITOR mon,
|
||||
const CRegion& damage,
|
||||
SP<IFramebuffer> fb,
|
||||
std::optional<CRegion> finalDamage) {
|
||||
const bool ok = (*reinterpret_cast<origBegin>(
|
||||
g_pBeginHook->m_original))(thisptr, ctx, mon, damage, fb, finalDamage);
|
||||
if (!ok) return false;
|
||||
(*reinterpret_cast<origBegin>(
|
||||
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
|
||||
|
||||
if (fb) ctx.m_data.blockScreenShader = true;
|
||||
if (fb) {
|
||||
auto* renderer = g_pHyprRenderer.get();
|
||||
if (renderer) renderer->m_renderData.blockScreenShader = true;
|
||||
}
|
||||
|
||||
if (!g_active) return true;
|
||||
if (!g_active) return;
|
||||
|
||||
auto* gl = g_pHyprOpenGL.get();
|
||||
if (!gl) return true;
|
||||
if (!gl) return;
|
||||
|
||||
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
|
||||
installShader();
|
||||
if (!gl->m_finalScreenShader.get() ||
|
||||
gl->m_finalScreenShader->program() < 1)
|
||||
return true;
|
||||
return;
|
||||
|
||||
if (g_transition.active) {
|
||||
auto* monPtr = mon.get();
|
||||
if (!monPtr) return true;
|
||||
if (!monPtr) return;
|
||||
const float elapsed =
|
||||
std::chrono::duration<float>(
|
||||
std::chrono::steady_clock::now() - g_transition.start)
|
||||
@@ -255,7 +265,6 @@ static bool hkBegin(
|
||||
} else if (g_uniformMode) {
|
||||
setUniformGain(g_currentGain);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static constexpr int KELVIN_MIN = 2000;
|
||||
@@ -324,10 +333,9 @@ static int luaNightlightState(lua_State* L) {
|
||||
}
|
||||
|
||||
static void postStateEvent(bool enabled, bool animating) {
|
||||
auto& sock = IPC::Socket2::sock();
|
||||
if (!sock) return;
|
||||
sock->postEvent(
|
||||
IPC::Socket2::SEvent{
|
||||
if (!g_pEventManager) return;
|
||||
g_pEventManager->postEvent(
|
||||
SHyprIPCEvent{
|
||||
.event = "zshell-nightlight",
|
||||
.data = std::format(
|
||||
"{},{},{},{}",
|
||||
@@ -369,6 +377,31 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
|
||||
throw std::runtime_error("[zshell-nightlight] version mismatch");
|
||||
}
|
||||
|
||||
static const auto SAVEMIRROR_METHODS =
|
||||
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
|
||||
const SFunctionMatch* saveMirrorTarget = findExactMethod(
|
||||
SAVEMIRROR_METHODS,
|
||||
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
|
||||
|
||||
if (saveMirrorTarget) {
|
||||
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
|
||||
PHANDLE,
|
||||
saveMirrorTarget->address,
|
||||
reinterpret_cast<void*>(&hkSaveBufferForMirror));
|
||||
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
|
||||
dlog(
|
||||
"saveBufferForMirror hook installed: " +
|
||||
saveMirrorTarget->signature);
|
||||
}
|
||||
|
||||
if (!g_pSaveMirrorHook)
|
||||
HyprlandAPI::addNotification(
|
||||
PHANDLE,
|
||||
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
|
||||
"captures may still show the nightlight",
|
||||
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
|
||||
8000);
|
||||
|
||||
static const auto METHODS =
|
||||
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
|
||||
const SFunctionMatch* target =
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
|
||||
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
|
||||
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
|
||||
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
|
||||
|
||||
qml_module(ZShell-internal
|
||||
URI ZShell.Internal
|
||||
INCLUDE_PREFIX internal
|
||||
@@ -8,6 +13,10 @@ qml_module(ZShell-internal
|
||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||
circularbuffer.hpp circularbuffer.cpp
|
||||
arcgauge.hpp arcgauge.cpp
|
||||
wallpaperimage.hpp wallpaperimage.cpp
|
||||
wallpaperservice.hpp wallpaperservice.cpp
|
||||
wallpapersurface.hpp wallpapersurface.cpp
|
||||
wallpaperobject.hpp wallpaperobject.cpp
|
||||
lidwatcher.hpp lidwatcher.cpp
|
||||
visualizerbars.hpp visualizerbars.cpp
|
||||
linearindicatormanager.hpp linearindicatormanager.cpp
|
||||
@@ -18,14 +27,16 @@ qml_module(ZShell-internal
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Sql
|
||||
Qt::Core
|
||||
Qt::Network
|
||||
Qt::DBus
|
||||
Qt::CanvasPainter
|
||||
Qt::GuiPrivate
|
||||
wlp-layer-shell
|
||||
wlp-xdg-shell
|
||||
wlp-viewporter
|
||||
wlp-fractional-scale
|
||||
PkgConfig::EGL
|
||||
PkgConfig::WAYLAND_EGL
|
||||
PkgConfig::GL
|
||||
ZShell-config
|
||||
)
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
#include <QtConcurrent>
|
||||
#include <QQuickWindow>
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
namespace ZShell::internal {
|
||||
|
||||
QThreadPool* WallpaperImage::loadPool() {
|
||||
static QThreadPool* pool = [] {
|
||||
@@ -352,4 +352,4 @@ QSGNode* WallpaperImage::updatePaintNode(
|
||||
return m_rootNode;
|
||||
}
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
} // namespace ZShell::internal
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
#include <qtmetamacros.h>
|
||||
#include <atomic>
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
namespace ZShell::internal {
|
||||
|
||||
class WallpaperImage : public QQuickItem {
|
||||
Q_OBJECT
|
||||
@@ -144,4 +144,4 @@ class WallpaperImage : public QQuickItem {
|
||||
QSGImageNode* m_oldImageNode = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
} // namespace ZShell::internal
|
||||
+3
-41
@@ -1,6 +1,6 @@
|
||||
#include "wallpaperobject.hpp"
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
namespace ZShell::internal {
|
||||
|
||||
WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {}
|
||||
|
||||
@@ -67,46 +67,8 @@ void WallpaperObject::setZoom(qreal zoom) {
|
||||
push();
|
||||
}
|
||||
|
||||
void WallpaperObject::syncCrop(const QVariantMap& crop) {
|
||||
const qreal x = crop.value("x").toDouble();
|
||||
const qreal y = crop.value("y").toDouble();
|
||||
const qreal width = crop.contains("width") ? crop.value("width").toDouble()
|
||||
: 1.0;
|
||||
const qreal height = crop.contains("height")
|
||||
? crop.value("height").toDouble()
|
||||
: 1.0;
|
||||
const qreal z =
|
||||
crop.contains("zoom") ? crop.value("zoom").toDouble() : 1.0;
|
||||
|
||||
const bool prev = m_pushEnabled;
|
||||
m_pushEnabled = false;
|
||||
|
||||
if (!qFuzzyCompare(m_cropX, x)) {
|
||||
m_cropX = x;
|
||||
emit cropXChanged();
|
||||
}
|
||||
if (!qFuzzyCompare(m_cropY, y)) {
|
||||
m_cropY = y;
|
||||
emit cropYChanged();
|
||||
}
|
||||
if (!qFuzzyCompare(m_cropWidth, width)) {
|
||||
m_cropWidth = width;
|
||||
emit cropWidthChanged();
|
||||
}
|
||||
if (!qFuzzyCompare(m_cropHeight, height)) {
|
||||
m_cropHeight = height;
|
||||
emit cropHeightChanged();
|
||||
}
|
||||
if (!qFuzzyCompare(m_zoom, z)) {
|
||||
m_zoom = z;
|
||||
emit zoomChanged();
|
||||
}
|
||||
|
||||
m_pushEnabled = prev;
|
||||
}
|
||||
|
||||
void WallpaperObject::push() {
|
||||
if (!m_pushEnabled || !m_service || m_screen.isEmpty()) return;
|
||||
if (!m_service || m_screen.isEmpty()) return;
|
||||
|
||||
QVariantMap crop;
|
||||
crop.insert("x", m_cropX);
|
||||
@@ -118,4 +80,4 @@ void WallpaperObject::push() {
|
||||
m_service->setCrop(m_screen, crop);
|
||||
}
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
} // namespace ZShell::internal
|
||||
+2
-6
@@ -9,7 +9,7 @@
|
||||
|
||||
#include "wallpaperservice.hpp"
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
namespace ZShell::internal {
|
||||
|
||||
class WallpaperObject : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -55,8 +55,6 @@ class WallpaperObject : public QObject {
|
||||
qreal zoom() const { return m_zoom; }
|
||||
void setZoom(qreal zoom);
|
||||
|
||||
Q_INVOKABLE void syncCrop(const QVariantMap& crop);
|
||||
|
||||
signals:
|
||||
void serviceChanged();
|
||||
void screenChanged();
|
||||
@@ -79,8 +77,6 @@ class WallpaperObject : public QObject {
|
||||
qreal m_cropWidth = 1.0;
|
||||
qreal m_cropHeight = 1.0;
|
||||
qreal m_zoom = 1.0;
|
||||
|
||||
bool m_pushEnabled = true;
|
||||
};
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
} // namespace ZShell::internal
|
||||
@@ -0,0 +1,756 @@
|
||||
#define EGL_EGLEXT_PROTOTYPES
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
|
||||
#include "wallpaperservice.hpp"
|
||||
|
||||
#include <EGL/eglext.h>
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QImageReader>
|
||||
#include <QScreen>
|
||||
|
||||
#include <qguiapplication_platform.h>
|
||||
|
||||
#include <private/qwaylandscreen_p.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace ZShell::internal {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kVertexShader = R"(
|
||||
#version 300 es
|
||||
void main() {
|
||||
// The corners are (-1,-1), (3,-1) and (-1,3), which cover the whole buffer.
|
||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kFragmentShader = R"(
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
uniform sampler2D source;
|
||||
uniform vec2 size;
|
||||
uniform vec4 crop;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
// The pixels are stored top-down, so the fragment coordinate is flipped.
|
||||
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
||||
outColor = texture(source, crop.xy + p * crop.zw);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kWaitTimeoutMs = 20;
|
||||
|
||||
void logShader(uint32_t object, bool program) {
|
||||
int ok = 0;
|
||||
if (program)
|
||||
glGetProgramiv(object, GL_LINK_STATUS, &ok);
|
||||
else
|
||||
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
|
||||
|
||||
if (ok) return;
|
||||
|
||||
char info[512] = {};
|
||||
if (program)
|
||||
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
|
||||
else
|
||||
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
|
||||
|
||||
qWarning() << "WallpaperService:" << (program ? "linking" : "compiling")
|
||||
<< "failed:" << info;
|
||||
}
|
||||
|
||||
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
|
||||
QVariantMap crop = crops.value(screen).toMap();
|
||||
|
||||
if (crop.isEmpty()) {
|
||||
crop.insert("x", 0.0);
|
||||
crop.insert("y", 0.0);
|
||||
crop.insert("width", 1.0);
|
||||
crop.insert("height", 1.0);
|
||||
crop.insert("zoom", 1.0);
|
||||
}
|
||||
|
||||
return crop;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {}
|
||||
|
||||
WallpaperService::~WallpaperService() {
|
||||
m_stop = true;
|
||||
wake();
|
||||
|
||||
if (m_thread.joinable()) m_thread.join();
|
||||
|
||||
for (auto* surface : m_surfaces)
|
||||
delete surface;
|
||||
m_surfaces.clear();
|
||||
|
||||
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
|
||||
if (eglMakeCurrent(
|
||||
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
|
||||
if (m_texture) glDeleteTextures(1, &m_texture);
|
||||
if (m_program) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
eglDestroyContext(m_eglDisplay, m_context);
|
||||
}
|
||||
|
||||
if (m_queue) wl_event_queue_destroy(m_queue);
|
||||
|
||||
if (m_wake[0] >= 0) {
|
||||
close(m_wake[0]);
|
||||
close(m_wake[1]);
|
||||
}
|
||||
|
||||
if (m_layerShell) m_layerShell->destroy();
|
||||
if (m_viewporter) m_viewporter->destroy();
|
||||
if (m_scaleManager) m_scaleManager->destroy();
|
||||
if (m_registry) wl_registry_destroy(m_registry);
|
||||
}
|
||||
|
||||
QString WallpaperService::path() const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void WallpaperService::setPath(const QString& path) {
|
||||
if (m_path == path) return;
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_path = path;
|
||||
|
||||
m_forceDecode = true;
|
||||
m_imageChanged = true;
|
||||
}
|
||||
|
||||
emit pathChanged();
|
||||
|
||||
start();
|
||||
queueAll();
|
||||
}
|
||||
|
||||
void WallpaperService::addScreen(const QString& screen) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
if (m_screens.contains(screen)) return;
|
||||
|
||||
m_screens.append(screen);
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
queueAll();
|
||||
}
|
||||
|
||||
void WallpaperService::removeScreen(const QString& screen) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
if (!m_screens.removeOne(screen)) return;
|
||||
}
|
||||
|
||||
queueAll();
|
||||
}
|
||||
|
||||
const QVariantMap& WallpaperService::crops() const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return m_crops;
|
||||
}
|
||||
|
||||
void WallpaperService::setCrops(const QVariantMap& crops) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
if (m_crops == crops) return;
|
||||
|
||||
m_crops = crops;
|
||||
}
|
||||
|
||||
emit cropsChanged();
|
||||
queueAll();
|
||||
}
|
||||
|
||||
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
|
||||
WallpaperSurface* surface = nullptr;
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
if (m_crops.value(screen) == crop) return;
|
||||
|
||||
m_crops.insert(screen, crop);
|
||||
|
||||
for (auto* candidate : m_surfaces) {
|
||||
if (candidate->screen() == screen) {
|
||||
surface = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit cropsChanged();
|
||||
|
||||
if (surface) queue(surface);
|
||||
}
|
||||
|
||||
void WallpaperService::redraw() {
|
||||
queueAll();
|
||||
}
|
||||
|
||||
void WallpaperService::start() {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
|
||||
|
||||
started = true;
|
||||
}
|
||||
|
||||
auto* interface =
|
||||
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
||||
if (!interface) {
|
||||
qWarning() << "WallpaperService: this is not a Wayland session";
|
||||
return;
|
||||
}
|
||||
|
||||
m_display = interface->display();
|
||||
m_compositor = interface->compositor();
|
||||
if (!m_display || !m_compositor) return;
|
||||
|
||||
m_queue = wl_display_create_queue(m_display);
|
||||
if (!m_queue) return;
|
||||
|
||||
if (pipe2(m_wake, O_CLOEXEC) < 0) {
|
||||
qWarning() << "WallpaperService: could not make the pipe to wake on";
|
||||
return;
|
||||
}
|
||||
|
||||
bindGlobals();
|
||||
|
||||
if (!setupGl()) return;
|
||||
|
||||
m_drawing = true;
|
||||
m_thread = std::thread([this] { loop(); });
|
||||
|
||||
wake();
|
||||
}
|
||||
|
||||
void WallpaperService::wake() {
|
||||
if (m_wake[1] < 0) return;
|
||||
|
||||
char byte = 0;
|
||||
|
||||
if (::write(m_wake[1], &byte, 1) < 0)
|
||||
qWarning() << "WallpaperService: could not wake the drawing thread";
|
||||
}
|
||||
|
||||
void WallpaperService::queue(WallpaperSurface* surface) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_pending.push_back(surface);
|
||||
}
|
||||
|
||||
wake();
|
||||
}
|
||||
|
||||
void WallpaperService::queueAll() {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
for (auto* surface : m_surfaces)
|
||||
m_pending.push_back(surface);
|
||||
}
|
||||
|
||||
wake();
|
||||
}
|
||||
|
||||
void WallpaperService::loop() {
|
||||
while (!m_stop) {
|
||||
present();
|
||||
|
||||
if (wl_display_prepare_read(m_display) == 0) {
|
||||
wl_display_flush(m_display);
|
||||
|
||||
struct pollfd fds[2] = {
|
||||
{wl_display_get_fd(m_display), POLLIN, 0},
|
||||
{m_wake[0], POLLIN, 0},
|
||||
};
|
||||
|
||||
int ready = poll(fds, 2, kWaitTimeoutMs);
|
||||
|
||||
if (ready > 0 && fds[0].revents) {
|
||||
wl_display_read_events(m_display);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ready > 0 && fds[1].revents) {
|
||||
char byte = 0;
|
||||
::read(m_wake[0], &byte, 1);
|
||||
}
|
||||
|
||||
wl_display_cancel_read(m_display);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
|
||||
|
||||
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
|
||||
|
||||
char byte = 0;
|
||||
::read(m_wake[0], &byte, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void WallpaperService::present() {
|
||||
wl_display_dispatch_queue_pending(m_display, m_queue);
|
||||
|
||||
addSurfaces();
|
||||
|
||||
ensureDecodeSize();
|
||||
|
||||
std::vector<WallpaperSurface*> pending;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
pending.swap(m_pending);
|
||||
}
|
||||
|
||||
for (auto* surface : pending)
|
||||
drawSurface(surface);
|
||||
}
|
||||
|
||||
void WallpaperService::addSurfaces() {
|
||||
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
|
||||
|
||||
QList<WallpaperSurface*> gone;
|
||||
QList<QString> needed;
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
QList<WallpaperSurface*> kept;
|
||||
for (auto* surface : m_surfaces) {
|
||||
if (m_screens.contains(surface->screen()))
|
||||
kept.append(surface);
|
||||
else
|
||||
gone.append(surface);
|
||||
}
|
||||
|
||||
m_surfaces = kept;
|
||||
|
||||
for (const auto& screen : m_screens) {
|
||||
bool exists = false;
|
||||
for (auto* surface : m_surfaces) {
|
||||
if (surface->screen() == screen) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) needed.append(screen);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto* surface : gone)
|
||||
delete surface;
|
||||
|
||||
for (const auto& screen : needed)
|
||||
addSurface(screen);
|
||||
}
|
||||
|
||||
void WallpaperService::registryGlobal(
|
||||
void* data,
|
||||
struct wl_registry* registry,
|
||||
uint32_t name,
|
||||
const char* interface,
|
||||
uint32_t version) {
|
||||
auto* service = static_cast<WallpaperService*>(data);
|
||||
|
||||
void* proxy = nullptr;
|
||||
|
||||
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
|
||||
proxy = new QtWayland::zwlr_layer_shell_v1(
|
||||
registry, int(name), int(version));
|
||||
} else if (strcmp(interface, "wp_viewporter") == 0) {
|
||||
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
|
||||
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
|
||||
proxy = new QtWayland::wp_fractional_scale_manager_v1(
|
||||
registry, int(name), int(version));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
service->storeGlobal(interface, proxy);
|
||||
|
||||
service->queueAll();
|
||||
}
|
||||
|
||||
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
|
||||
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
|
||||
else if (strcmp(interface, "wp_viewporter") == 0)
|
||||
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
|
||||
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
|
||||
m_scaleManager =
|
||||
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
|
||||
}
|
||||
|
||||
void WallpaperService::bindGlobals() {
|
||||
m_registry = wl_display_get_registry(m_display);
|
||||
|
||||
static const struct wl_registry_listener listener = {
|
||||
registryGlobal,
|
||||
nullptr,
|
||||
};
|
||||
|
||||
wl_registry_add_listener(m_registry, &listener, this);
|
||||
}
|
||||
|
||||
bool WallpaperService::setupGl() {
|
||||
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
|
||||
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
|
||||
m_eglDisplay =
|
||||
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
|
||||
} else {
|
||||
m_eglDisplay =
|
||||
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
|
||||
}
|
||||
|
||||
if (m_eglDisplay == EGL_NO_DISPLAY) {
|
||||
qWarning() << "WallpaperService: no EGL display";
|
||||
return false;
|
||||
}
|
||||
|
||||
EGLint major = 0, minor = 0;
|
||||
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
|
||||
qWarning() << "WallpaperService: eglInitialize failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
const EGLint configAttributes[] = {
|
||||
EGL_SURFACE_TYPE,
|
||||
EGL_WINDOW_BIT,
|
||||
EGL_RENDERABLE_TYPE,
|
||||
EGL_OPENGL_ES2_BIT,
|
||||
EGL_RED_SIZE,
|
||||
8,
|
||||
EGL_GREEN_SIZE,
|
||||
8,
|
||||
EGL_BLUE_SIZE,
|
||||
8,
|
||||
EGL_ALPHA_SIZE,
|
||||
8,
|
||||
EGL_NONE};
|
||||
|
||||
EGLint count = 0;
|
||||
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
|
||||
count == 0) {
|
||||
qWarning() << "WallpaperService: no EGL config for window surfaces";
|
||||
return false;
|
||||
}
|
||||
|
||||
const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
|
||||
m_context = eglCreateContext(
|
||||
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
|
||||
if (m_context == EGL_NO_CONTEXT) {
|
||||
qWarning() << "WallpaperService: eglCreateContext failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t WallpaperService::createProgram() {
|
||||
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vertex, 1, &kVertexShader, nullptr);
|
||||
glCompileShader(vertex);
|
||||
logShader(vertex, false);
|
||||
|
||||
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fragment, 1, &kFragmentShader, nullptr);
|
||||
glCompileShader(fragment);
|
||||
logShader(fragment, false);
|
||||
|
||||
uint32_t program = glCreateProgram();
|
||||
glAttachShader(program, vertex);
|
||||
glAttachShader(program, fragment);
|
||||
glLinkProgram(program);
|
||||
logShader(program, true);
|
||||
|
||||
if (glGetUniformLocation(program, "size") < 0 ||
|
||||
glGetUniformLocation(program, "crop") < 0) {
|
||||
qWarning() << "WallpaperService: the program does not have the "
|
||||
"uniforms it needs";
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_sizeUniform = glGetUniformLocation(program, "size");
|
||||
m_cropUniform = glGetUniformLocation(program, "crop");
|
||||
|
||||
glDeleteShader(vertex);
|
||||
glDeleteShader(fragment);
|
||||
return program;
|
||||
}
|
||||
|
||||
void WallpaperService::uploadImage() {
|
||||
if (m_image.isNull()) return;
|
||||
|
||||
if (m_texture == 0) {
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
} else {
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
}
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA8,
|
||||
m_image.width(),
|
||||
m_image.height(),
|
||||
0,
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
m_image.constBits());
|
||||
}
|
||||
|
||||
bool WallpaperService::decode(const QString& path) {
|
||||
if (path.isEmpty()) return false;
|
||||
|
||||
QImageReader reader(path);
|
||||
reader.setAutoTransform(true);
|
||||
|
||||
QSize size = reader.size();
|
||||
if (size.isValid()) m_sourceSize = size;
|
||||
|
||||
if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty())
|
||||
reader.setScaledSize(
|
||||
m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding));
|
||||
|
||||
QImage image = reader.read();
|
||||
if (image.isNull()) {
|
||||
qWarning() << "WallpaperService: could not read the image" << path;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_image = image.convertToFormat(QImage::Format_RGBA8888);
|
||||
return !m_image.isNull();
|
||||
}
|
||||
|
||||
bool WallpaperService::ensureDecodeSize() {
|
||||
QSize largest;
|
||||
QString path;
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
largest = largestBufferSize();
|
||||
if (largest.isEmpty()) return false;
|
||||
|
||||
if (!m_forceDecode && !m_decodeSize.isEmpty() &&
|
||||
qint64(largest.width()) * largest.height() <=
|
||||
qint64(m_decodeSize.width()) * m_decodeSize.height())
|
||||
return false;
|
||||
|
||||
QSize target =
|
||||
m_sourceSize.isEmpty()
|
||||
? largest
|
||||
: m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding);
|
||||
|
||||
if (!m_forceDecode && !m_image.isNull() && target == m_image.size())
|
||||
return false;
|
||||
|
||||
m_forceDecode = false;
|
||||
m_decodeSize = largest;
|
||||
path = m_path;
|
||||
}
|
||||
|
||||
if (!decode(path)) return false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_imageChanged = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QSize WallpaperService::largestBufferSize() {
|
||||
QSize largest;
|
||||
|
||||
for (auto* surface : m_surfaces) {
|
||||
QSize buffer = surface->bufferSize();
|
||||
if (buffer.isEmpty()) continue;
|
||||
|
||||
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
|
||||
qint64(largest.width()) * largest.height())
|
||||
largest = buffer;
|
||||
}
|
||||
|
||||
return largest;
|
||||
}
|
||||
|
||||
QRectF WallpaperService::sourceRect(
|
||||
const QVariantMap& crops, const QString& screen, const QSize& size) const {
|
||||
QVariantMap crop = cropFor(crops, screen);
|
||||
|
||||
QRectF rect(
|
||||
crop.value("x", 0.0).toDouble(),
|
||||
crop.value("y", 0.0).toDouble(),
|
||||
crop.value("width", 1.0).toDouble(),
|
||||
crop.value("height", 1.0).toDouble());
|
||||
|
||||
if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect;
|
||||
|
||||
qreal imageWidth = qreal(m_image.width());
|
||||
qreal imageHeight = qreal(m_image.height());
|
||||
|
||||
QRectF pixels(
|
||||
rect.x() * imageWidth,
|
||||
rect.y() * imageHeight,
|
||||
rect.width() * imageWidth,
|
||||
rect.height() * imageHeight);
|
||||
|
||||
qreal targetRatio = qreal(size.width()) / qreal(size.height());
|
||||
qreal rectRatio = pixels.width() / pixels.height();
|
||||
|
||||
if (rectRatio > targetRatio) {
|
||||
qreal newWidth = pixels.height() * targetRatio;
|
||||
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
|
||||
pixels.setWidth(newWidth);
|
||||
} else if (rectRatio < targetRatio) {
|
||||
qreal newHeight = pixels.width() / targetRatio;
|
||||
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
|
||||
pixels.setHeight(newHeight);
|
||||
}
|
||||
|
||||
return QRectF(
|
||||
pixels.x() / imageWidth,
|
||||
pixels.y() / imageHeight,
|
||||
pixels.width() / imageWidth,
|
||||
pixels.height() / imageHeight);
|
||||
}
|
||||
|
||||
void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
||||
EGLSurface target = surface->eglSurface();
|
||||
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
|
||||
surface->bufferSize().isEmpty())
|
||||
return;
|
||||
|
||||
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
|
||||
qWarning() << "WallpaperService: the context cannot be made current";
|
||||
return;
|
||||
}
|
||||
|
||||
// Release the context when this scope ends (see FrameService::drawSurface).
|
||||
// Holding it across the draw loop's sleep serializes the two raw-EGL clients on
|
||||
// NVIDIA, which makes eglSwapBuffers fail and starves other processes' contexts.
|
||||
struct ContextRelease {
|
||||
EGLDisplay display;
|
||||
~ContextRelease() {
|
||||
eglMakeCurrent(
|
||||
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
}
|
||||
} contextRelease{m_eglDisplay};
|
||||
|
||||
QVariantMap crops;
|
||||
QSize size;
|
||||
bool upload = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
crops = m_crops;
|
||||
size = surface->bufferSize();
|
||||
|
||||
upload = m_imageChanged;
|
||||
m_imageChanged = false;
|
||||
}
|
||||
|
||||
if (size.isEmpty()) return;
|
||||
|
||||
if (m_program == 0) m_program = createProgram();
|
||||
if (m_texture == 0 || upload) uploadImage();
|
||||
|
||||
if (m_program == 0 || m_texture == 0 || m_image.isNull()) return;
|
||||
|
||||
glUseProgram(m_program);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
|
||||
glUniform2f(
|
||||
GLint(m_sizeUniform), GLfloat(size.width()), GLfloat(size.height()));
|
||||
|
||||
QRectF rect = sourceRect(crops, surface->screen(), size);
|
||||
glUniform4f(
|
||||
GLint(m_cropUniform),
|
||||
GLfloat(rect.x()),
|
||||
GLfloat(rect.y()),
|
||||
GLfloat(rect.width()),
|
||||
GLfloat(rect.height()));
|
||||
|
||||
glViewport(0, 0, size.width(), size.height());
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
|
||||
if (!eglSwapBuffers(m_eglDisplay, target))
|
||||
qWarning()
|
||||
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
|
||||
<< QString::number(unsigned(eglGetError()), 16);
|
||||
}
|
||||
|
||||
void WallpaperService::addSurface(const QString& screen) {
|
||||
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
|
||||
|
||||
QScreen* qscreen = nullptr;
|
||||
for (auto* candidate : QGuiApplication::screens()) {
|
||||
if (candidate->name() == screen) {
|
||||
qscreen = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!qscreen) {
|
||||
qWarning() << "WallpaperService:" << screen << "is not a screen";
|
||||
return;
|
||||
}
|
||||
|
||||
auto* waylandScreen =
|
||||
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
|
||||
if (!waylandScreen || !waylandScreen->output()) {
|
||||
qWarning() << "WallpaperService:" << screen
|
||||
<< "is not a Wayland screen";
|
||||
return;
|
||||
}
|
||||
|
||||
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
|
||||
if (!wlSurface) return;
|
||||
|
||||
auto* surface =
|
||||
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_surfaces.append(surface);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell::internal
|
||||
@@ -0,0 +1,147 @@
|
||||
#pragma once
|
||||
|
||||
#include <EGL/egl.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <QImage>
|
||||
#include <QList>
|
||||
#include <QRectF>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVariantMap>
|
||||
#include <QtQml/qqml.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
||||
#include <wayland-client-protocol.h>
|
||||
|
||||
#include "wallpapersurface.hpp"
|
||||
|
||||
namespace ZShell::internal {
|
||||
|
||||
class WallpaperSurface;
|
||||
|
||||
class WallpaperService : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
||||
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
|
||||
|
||||
public:
|
||||
explicit WallpaperService(QObject* parent = nullptr);
|
||||
~WallpaperService() override;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
void addScreen(const QString& screen);
|
||||
void removeScreen(const QString& screen);
|
||||
|
||||
[[nodiscard]] const QVariantMap& crops() const;
|
||||
void setCrops(const QVariantMap& crops);
|
||||
|
||||
Q_INVOKABLE void redraw();
|
||||
|
||||
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void cropsChanged();
|
||||
|
||||
private:
|
||||
friend class WallpaperSurface;
|
||||
|
||||
void bindGlobals();
|
||||
static void registryGlobal(
|
||||
void* data,
|
||||
struct wl_registry* registry,
|
||||
uint32_t name,
|
||||
const char* interface,
|
||||
uint32_t version);
|
||||
|
||||
void storeGlobal(const char* interface, void* proxy);
|
||||
|
||||
[[nodiscard]] bool setupGl();
|
||||
[[nodiscard]] uint32_t createProgram();
|
||||
|
||||
void uploadImage();
|
||||
|
||||
bool decode(const QString& path);
|
||||
|
||||
bool ensureDecodeSize();
|
||||
|
||||
[[nodiscard]] QSize largestBufferSize();
|
||||
|
||||
void queue(WallpaperSurface* surface);
|
||||
void queueAll();
|
||||
|
||||
void startDrawing();
|
||||
void loop();
|
||||
|
||||
void present();
|
||||
|
||||
void drawSurface(WallpaperSurface* surface);
|
||||
|
||||
void wake();
|
||||
|
||||
[[nodiscard]] wl_event_queue* eventQueue() const { return m_queue; }
|
||||
|
||||
[[nodiscard]] QRectF sourceRect(
|
||||
const QVariantMap& crops,
|
||||
const QString& screen,
|
||||
const QSize& size) const;
|
||||
|
||||
void start();
|
||||
|
||||
void addSurfaces();
|
||||
void addSurface(const QString& screen);
|
||||
|
||||
QString m_path;
|
||||
QStringList m_screens;
|
||||
QVariantMap m_crops;
|
||||
|
||||
QImage m_image;
|
||||
|
||||
QSize m_sourceSize;
|
||||
QSize m_decodeSize;
|
||||
|
||||
wl_display* m_display = nullptr;
|
||||
wl_registry* m_registry = nullptr;
|
||||
wl_compositor* m_compositor = nullptr;
|
||||
QtWayland::zwlr_layer_shell_v1* m_layerShell = nullptr;
|
||||
QtWayland::wp_viewporter* m_viewporter = nullptr;
|
||||
QtWayland::wp_fractional_scale_manager_v1* m_scaleManager = nullptr;
|
||||
|
||||
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
|
||||
EGLConfig m_config = nullptr;
|
||||
EGLContext m_context = EGL_NO_CONTEXT;
|
||||
uint32_t m_texture = 0;
|
||||
uint32_t m_program = 0;
|
||||
int m_sizeUniform = -1;
|
||||
int m_cropUniform = -1;
|
||||
|
||||
QList<WallpaperSurface*> m_surfaces;
|
||||
|
||||
std::thread m_thread;
|
||||
mutable std::recursive_mutex m_mutex;
|
||||
std::vector<WallpaperSurface*> m_pending;
|
||||
wl_event_queue* m_queue = nullptr;
|
||||
|
||||
int m_wake[2] = {-1, -1};
|
||||
|
||||
bool m_stop = false;
|
||||
bool m_drawing = false;
|
||||
|
||||
bool started = false;
|
||||
|
||||
bool m_forceDecode = false;
|
||||
bool m_imageChanged = false;
|
||||
};
|
||||
|
||||
} // namespace ZShell::internal
|
||||
+4
-48
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "wallpaperservice.hpp"
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
namespace ZShell::internal {
|
||||
|
||||
const QString kNamespace = "ZShell-Wallpaper";
|
||||
|
||||
@@ -53,11 +53,6 @@ WallpaperSurface::WallpaperSurface(
|
||||
}
|
||||
|
||||
WallpaperSurface::~WallpaperSurface() {
|
||||
if (m_frameCallback) {
|
||||
wl_callback_destroy(m_frameCallback);
|
||||
m_frameCallback = nullptr;
|
||||
}
|
||||
|
||||
if (m_eglSurface != EGL_NO_SURFACE)
|
||||
eglDestroySurface(m_service->m_eglDisplay, m_eglSurface);
|
||||
|
||||
@@ -73,45 +68,8 @@ WallpaperSurface::~WallpaperSurface() {
|
||||
if (m_surface) wl_surface_destroy(m_surface);
|
||||
}
|
||||
|
||||
void WallpaperSurface::markDirty() {
|
||||
m_dirty.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool WallpaperSurface::needsDraw() const {
|
||||
return m_dirty.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool WallpaperSurface::frameReady() const {
|
||||
return m_frameReady.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void WallpaperSurface::requestFrame() {
|
||||
if (!m_surface || m_frameCallback) return;
|
||||
|
||||
m_frameCallback = wl_surface_frame(m_surface);
|
||||
|
||||
static const struct wl_callback_listener listener = {
|
||||
[](void* data, struct wl_callback*, uint32_t) {
|
||||
static_cast<WallpaperSurface*>(data)->onFrameDone();
|
||||
},
|
||||
};
|
||||
|
||||
wl_callback_add_listener(m_frameCallback, &listener, this);
|
||||
}
|
||||
|
||||
void WallpaperSurface::onFrameDone() {
|
||||
if (m_frameCallback) {
|
||||
wl_callback_destroy(m_frameCallback);
|
||||
m_frameCallback = nullptr;
|
||||
}
|
||||
|
||||
m_frameReady.store(true, std::memory_order_relaxed);
|
||||
m_service->wake();
|
||||
}
|
||||
|
||||
void WallpaperSurface::endDraw() {
|
||||
m_dirty.store(false, std::memory_order_relaxed);
|
||||
m_frameReady.store(false, std::memory_order_relaxed);
|
||||
void WallpaperSurface::redraw() {
|
||||
m_service->queue(this);
|
||||
}
|
||||
|
||||
void WallpaperSurface::zwlr_layer_surface_v1_configure(
|
||||
@@ -183,9 +141,7 @@ void WallpaperSurface::updateBuffer() {
|
||||
if (QtWayland::wp_viewport::isInitialized())
|
||||
set_destination(m_size.width(), m_size.height());
|
||||
|
||||
requestFrame();
|
||||
m_service->queue(this);
|
||||
m_service->notifyBufferSizes();
|
||||
}
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
} // namespace ZShell::internal
|
||||
+3
-18
@@ -3,8 +3,6 @@
|
||||
#include <EGL/egl.h>
|
||||
#include <wayland-egl.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
|
||||
@@ -13,7 +11,7 @@
|
||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
||||
#include <wayland-client-protocol.h>
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
namespace ZShell::internal {
|
||||
|
||||
class WallpaperService;
|
||||
|
||||
@@ -34,14 +32,7 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
||||
[[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; }
|
||||
[[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; }
|
||||
|
||||
void markDirty();
|
||||
[[nodiscard]] bool needsDraw() const;
|
||||
|
||||
[[nodiscard]] bool frameReady() const;
|
||||
|
||||
void requestFrame();
|
||||
|
||||
void endDraw();
|
||||
void redraw();
|
||||
|
||||
private:
|
||||
void zwlr_layer_surface_v1_configure(
|
||||
@@ -51,8 +42,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
||||
|
||||
void updateBuffer();
|
||||
|
||||
void onFrameDone();
|
||||
|
||||
WallpaperService* m_service;
|
||||
QString m_screen;
|
||||
wl_surface* m_surface;
|
||||
@@ -64,10 +53,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
||||
qreal m_scale = 1.0;
|
||||
|
||||
bool configured = false;
|
||||
|
||||
std::atomic<bool> m_dirty{false};
|
||||
std::atomic<bool> m_frameReady{true};
|
||||
struct wl_callback* m_frameCallback = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
} // namespace ZShell::internal
|
||||
@@ -2,12 +2,7 @@ qml_module(ZShell-models
|
||||
URI ZShell.Models
|
||||
SOURCES
|
||||
filesystemmodel.hpp filesystemmodel.cpp
|
||||
wallpaperdb.hpp wallpaperdb.cpp
|
||||
wallpaperutil.hpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Qml
|
||||
Qt::Concurrent
|
||||
Qt::Sql
|
||||
)
|
||||
|
||||
@@ -1,84 +1,11 @@
|
||||
#include "filesystemmodel.hpp"
|
||||
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdatetime.h>
|
||||
#include <qdir.h>
|
||||
#include <qdiriterator.h>
|
||||
#include <qfile.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qimage.h>
|
||||
#include <qpointer.h>
|
||||
#include <qprocess.h>
|
||||
#include <qregularexpression.h>
|
||||
#include <qtemporarydir.h>
|
||||
#include <qtconcurrentrun.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
const QStringList kVideoExtensions = {
|
||||
"mp4",
|
||||
"mkv",
|
||||
"webm",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"m4v",
|
||||
"mpg",
|
||||
"mpeg",
|
||||
"3gp",
|
||||
"ogv",
|
||||
"ts",
|
||||
"vob",
|
||||
};
|
||||
|
||||
bool hasVideoExtension(const QString& path) {
|
||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||
}
|
||||
|
||||
const QStringList kHardwareCodecs = {
|
||||
"av1",
|
||||
"hevc",
|
||||
"h264",
|
||||
"vp9",
|
||||
"vp8",
|
||||
"vc1",
|
||||
"mpeg2",
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"mjpeg",
|
||||
};
|
||||
|
||||
bool probeHardwareCodec(const QString& path) {
|
||||
QProcess process;
|
||||
process.start(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
path,
|
||||
});
|
||||
process.waitForFinished(5000);
|
||||
|
||||
if (process.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString codec =
|
||||
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||
return kHardwareCodecs.contains(codec);
|
||||
}
|
||||
|
||||
FileSystemEntry::FileSystemEntry(
|
||||
const QString& path, const QString& relativePath, QObject* parent)
|
||||
: QObject(parent)
|
||||
@@ -129,14 +56,6 @@ bool FileSystemEntry::isImage() const {
|
||||
return m_isImage;
|
||||
}
|
||||
|
||||
bool FileSystemEntry::isVideo() const {
|
||||
return hasVideoExtension(m_path);
|
||||
}
|
||||
|
||||
QString FileSystemEntry::thumbnailPath() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
QString FileSystemEntry::mimeType() const {
|
||||
if (!m_mimeTypeInitialised) {
|
||||
const QMimeDatabase db;
|
||||
@@ -302,46 +221,6 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::requireHardwareCodec() const {
|
||||
return m_requireHardwareCodec;
|
||||
}
|
||||
|
||||
void FileSystemModel::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_requireHardwareCodec = requireHardwareCodec;
|
||||
emit requireHardwareCodecChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::isHardwareVideo(const QString& path) {
|
||||
const QFileInfo info(path);
|
||||
const QString key =
|
||||
path + '|' + QByteArray::number(info.size()) + '|' +
|
||||
QByteArray::number(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||
|
||||
{
|
||||
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||
|
||||
const auto it = m_codecCache.constFind(key);
|
||||
if (it != m_codecCache.constEnd()) {
|
||||
return it.value();
|
||||
}
|
||||
}
|
||||
|
||||
const bool hardware = probeHardwareCodec(path);
|
||||
|
||||
{
|
||||
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||
m_codecCache.insert(key, hardware);
|
||||
}
|
||||
|
||||
return hardware;
|
||||
}
|
||||
|
||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||
}
|
||||
@@ -372,12 +251,14 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
||||
const auto paths = watcher->result();
|
||||
if (currentDir == m_dir && showHidden == m_showHidden &&
|
||||
!paths.isEmpty()) {
|
||||
// Ignore if dir or showHidden has changed
|
||||
m_watcher.addPaths(paths);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::update() {
|
||||
@@ -424,7 +305,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
const auto showHidden = m_showHidden;
|
||||
const auto filter = m_filter;
|
||||
const auto nameFilters = m_nameFilters;
|
||||
const auto requireHardware = m_requireHardwareCodec;
|
||||
|
||||
QSet<QString> oldPaths;
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
@@ -432,27 +312,18 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
}
|
||||
|
||||
const auto future = QtConcurrent::run(
|
||||
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
const auto flags = recursive ? QDirIterator::Subdirectories
|
||||
: QDirIterator::NoIteratorFlags;
|
||||
|
||||
std::optional<QDirIterator> iter;
|
||||
|
||||
if (filter == Images || filter == Videos || filter == Media) {
|
||||
if (filter == Images) {
|
||||
QStringList extraNameFilters = nameFilters;
|
||||
|
||||
if (filter == Images || filter == Media) {
|
||||
const auto formats = QImageReader::supportedImageFormats();
|
||||
for (const auto& format : formats) {
|
||||
extraNameFilters << "*." + format;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter == Videos || filter == Media) {
|
||||
for (const auto& ext : kVideoExtensions) {
|
||||
extraNameFilters << "*." + ext;
|
||||
}
|
||||
}
|
||||
|
||||
QDir::Filters filters = QDir::Files;
|
||||
if (showHidden) {
|
||||
@@ -490,11 +361,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
|
||||
QString path = iter->next();
|
||||
|
||||
if (hasVideoExtension(path)) {
|
||||
if (requireHardware && !isHardwareVideo(path)) {
|
||||
continue;
|
||||
}
|
||||
} else if (filter == Images || filter == Media) {
|
||||
if (filter == Images) {
|
||||
QImageReader reader(path);
|
||||
if (!reader.canRead()) {
|
||||
continue;
|
||||
@@ -539,6 +406,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
});
|
||||
|
||||
watcher->setFuture(future);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::applyChanges(
|
||||
@@ -551,6 +419,7 @@ void FileSystemModel::applyChanges(
|
||||
}
|
||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||
|
||||
// Batch remove old entries
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
for (int idx : std::as_const(removedIndices)) {
|
||||
@@ -578,11 +447,11 @@ void FileSystemModel::applyChanges(
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
// Create new entries
|
||||
QList<FileSystemEntry*> newEntries;
|
||||
for (const auto& path : addedPaths) {
|
||||
auto* entry =
|
||||
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||
newEntries << entry;
|
||||
newEntries << new FileSystemEntry(
|
||||
path, m_dir.relativeFilePath(path), this);
|
||||
}
|
||||
std::sort(
|
||||
newEntries.begin(),
|
||||
@@ -591,6 +460,7 @@ void FileSystemModel::applyChanges(
|
||||
return compareEntries(a, b);
|
||||
});
|
||||
|
||||
// Batch insert new entries
|
||||
int insertStart = -1;
|
||||
QList<FileSystemEntry*> batchItems;
|
||||
for (const auto& entry : std::as_const(newEntries)) {
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qfuture.h>
|
||||
#include <qhash.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qmutex.h>
|
||||
#include <qmimedatabase.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
@@ -30,8 +28,6 @@ class FileSystemEntry : public QObject {
|
||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||
Q_PROPERTY(QString thumbnailPath READ thumbnailPath CONSTANT)
|
||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||
|
||||
public:
|
||||
@@ -49,8 +45,6 @@ class FileSystemEntry : public QObject {
|
||||
[[nodiscard]] qint64 size() const;
|
||||
[[nodiscard]] bool isDir() const;
|
||||
[[nodiscard]] bool isImage() const;
|
||||
[[nodiscard]] bool isVideo() const;
|
||||
[[nodiscard]] QString thumbnailPath() const;
|
||||
[[nodiscard]] QString mimeType() const;
|
||||
|
||||
void updateRelativePath(const QDir& dir);
|
||||
@@ -91,16 +85,13 @@ class FileSystemModel : public QAbstractListModel {
|
||||
Q_PROPERTY(
|
||||
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
||||
nameFiltersChanged)
|
||||
Q_PROPERTY(
|
||||
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||
|
||||
Q_PROPERTY(
|
||||
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
||||
NOTIFY entriesChanged)
|
||||
|
||||
public:
|
||||
enum Filter { NoFilter, Images, Videos, Media, Files, Dirs };
|
||||
enum Filter { NoFilter, Images, Files, Dirs };
|
||||
Q_ENUM(Filter)
|
||||
|
||||
explicit FileSystemModel(QObject* parent = nullptr);
|
||||
@@ -132,9 +123,6 @@ class FileSystemModel : public QAbstractListModel {
|
||||
[[nodiscard]] QStringList nameFilters() const;
|
||||
void setNameFilters(const QStringList& nameFilters);
|
||||
|
||||
[[nodiscard]] bool requireHardwareCodec() const;
|
||||
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||
|
||||
signals:
|
||||
@@ -145,7 +133,6 @@ class FileSystemModel : public QAbstractListModel {
|
||||
void sortReverseChanged();
|
||||
void filterChanged();
|
||||
void nameFiltersChanged();
|
||||
void requireHardwareCodecChanged();
|
||||
void entriesChanged();
|
||||
|
||||
private:
|
||||
@@ -161,10 +148,6 @@ class FileSystemModel : public QAbstractListModel {
|
||||
bool m_sortReverse;
|
||||
Filter m_filter;
|
||||
QStringList m_nameFilters;
|
||||
bool m_requireHardwareCodec = false;
|
||||
|
||||
QHash<QString, bool> m_codecCache;
|
||||
QMutex m_codecMutex;
|
||||
|
||||
void watchDirIfRecursive(const QString& path);
|
||||
void update();
|
||||
@@ -173,7 +156,6 @@ class FileSystemModel : public QAbstractListModel {
|
||||
void updateEntriesForDir(const QString& dir);
|
||||
void applyChanges(
|
||||
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||
[[nodiscard]] bool isHardwareVideo(const QString& path);
|
||||
[[nodiscard]] bool compareEntries(
|
||||
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||
};
|
||||
|
||||
@@ -1,876 +0,0 @@
|
||||
#include "wallpaperdb.hpp"
|
||||
#include "wallpaperutil.hpp"
|
||||
|
||||
#include <QtQuick/qquickimageprovider.h>
|
||||
#include <qbytearray.h>
|
||||
#include <qdiriterator.h>
|
||||
#include <qelapsedtimer.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qjsondocument.h>
|
||||
#include <qjsonobject.h>
|
||||
#include <qlogging.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qsqldatabase.h>
|
||||
#include <qsqlerror.h>
|
||||
#include <qsqlquery.h>
|
||||
#include <qtconcurrentrun.h>
|
||||
#include <quuid.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
Q_LOGGING_CATEGORY(dbLog, "ZShell.Internal.wallpaper.db", QtWarningMsg)
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
class WallpaperImageProvider : public QQuickImageProvider {
|
||||
public:
|
||||
explicit WallpaperImageProvider(const QString& dbPath)
|
||||
: QQuickImageProvider(QQuickImageProvider::Image), m_dbPath(dbPath) {}
|
||||
|
||||
void setDbPath(const QString& dbPath) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_dbPath == dbPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_dbPath = dbPath;
|
||||
m_generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
QImage requestImage(
|
||||
const QString& id, QSize* size, const QSize& requestedSize) override {
|
||||
const QString path =
|
||||
QString::fromUtf8(QByteArray::fromHex(id.toUtf8()));
|
||||
if (path.isEmpty() || !openThreadDb(currentDbPath())) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_threadUuid));
|
||||
query.prepare("SELECT cache FROM wallpapers WHERE path = ?");
|
||||
query.addBindValue(path);
|
||||
|
||||
if (!query.exec() || !query.next()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QByteArray bytes = query.value(0).toByteArray();
|
||||
if (bytes.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QImage image = QImage::fromData(bytes, "JPEG");
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (size) {
|
||||
*size = image.size();
|
||||
}
|
||||
|
||||
if (requestedSize.isValid() && !requestedSize.isEmpty() &&
|
||||
(image.width() > requestedSize.width() ||
|
||||
image.height() > requestedSize.height())) {
|
||||
image = image.scaled(
|
||||
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private:
|
||||
QString currentDbPath() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_dbPath;
|
||||
}
|
||||
|
||||
bool openThreadDb(const QString& dbPath) {
|
||||
const quint64 generation = m_generation.load(std::memory_order_relaxed);
|
||||
|
||||
if (!m_threadUuid.isEmpty() && m_threadGeneration == generation) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_threadUuid.isEmpty()) {
|
||||
{
|
||||
auto db = QSqlDatabase::database(m_threadUuid, false);
|
||||
if (db.isValid()) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
QSqlDatabase::removeDatabase(m_threadUuid);
|
||||
m_threadUuid.clear();
|
||||
}
|
||||
|
||||
if (dbPath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
auto db = QSqlDatabase::addDatabase("QSQLITE", uuid);
|
||||
db.setConnectOptions("QSQLITE_OPEN_READONLY");
|
||||
db.setDatabaseName(dbPath);
|
||||
|
||||
if (!db.open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("PRAGMA journal_mode=WAL");
|
||||
|
||||
m_threadUuid = uuid;
|
||||
m_threadGeneration = generation;
|
||||
return true;
|
||||
}
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
QString m_dbPath;
|
||||
std::atomic<quint64> m_generation = 0;
|
||||
|
||||
thread_local static QString m_threadUuid;
|
||||
thread_local static quint64 m_threadGeneration;
|
||||
};
|
||||
|
||||
thread_local QString WallpaperImageProvider::m_threadUuid;
|
||||
thread_local quint64 WallpaperImageProvider::m_threadGeneration;
|
||||
|
||||
WallpaperEntry::WallpaperEntry(
|
||||
const QString& path, const QString& relativePath, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_fileInfo(path)
|
||||
, m_path(path)
|
||||
, m_relativePath(relativePath) {}
|
||||
|
||||
QString WallpaperEntry::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
QString WallpaperEntry::relativePath() const {
|
||||
return m_relativePath;
|
||||
}
|
||||
|
||||
QString WallpaperEntry::name() const {
|
||||
return m_fileInfo.fileName();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::baseName() const {
|
||||
return m_fileInfo.baseName();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::parentDir() const {
|
||||
return m_fileInfo.absolutePath();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::suffix() const {
|
||||
return m_fileInfo.completeSuffix();
|
||||
}
|
||||
|
||||
qint64 WallpaperEntry::size() const {
|
||||
return m_fileInfo.size();
|
||||
}
|
||||
|
||||
bool WallpaperEntry::isImage() const {
|
||||
return !hasVideoExtension(m_path);
|
||||
}
|
||||
|
||||
bool WallpaperEntry::isVideo() const {
|
||||
return hasVideoExtension(m_path);
|
||||
}
|
||||
|
||||
bool WallpaperEntry::hardware() const {
|
||||
return m_hardware;
|
||||
}
|
||||
|
||||
bool WallpaperEntry::cached() const {
|
||||
return !m_thumbnailPath.isEmpty();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::thumbnailPath() const {
|
||||
if (!m_thumbnailPath.isEmpty()) {
|
||||
return m_thumbnailPath;
|
||||
}
|
||||
|
||||
return isImage() ? m_path : QString();
|
||||
}
|
||||
|
||||
void WallpaperEntry::setRelativePath(const QString& path) {
|
||||
if (m_relativePath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_relativePath = path;
|
||||
emit relativePathChanged();
|
||||
}
|
||||
|
||||
void WallpaperEntry::setHardware(bool hardware) {
|
||||
if (m_hardware == hardware) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hardware = hardware;
|
||||
emit hardwareChanged();
|
||||
}
|
||||
|
||||
void WallpaperEntry::setThumbnailPath(const QString& path) {
|
||||
if (m_thumbnailPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_thumbnailPath = path;
|
||||
emit thumbnailPathChanged();
|
||||
}
|
||||
|
||||
WallpaperDb::WallpaperDb(QObject* parent) : QObject(parent) {
|
||||
m_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
m_dbPath = QDir::homePath() + "/.local/state/zshell/wallpapers.sqlite";
|
||||
|
||||
m_queueTimer.setInterval(300);
|
||||
m_queueTimer.setTimerType(Qt::CoarseTimer);
|
||||
QObject::connect(
|
||||
&m_queueTimer, &QTimer::timeout, this, &WallpaperDb::processQueue);
|
||||
|
||||
m_watchTimer.setInterval(400);
|
||||
m_watchTimer.setSingleShot(true);
|
||||
QObject::connect(
|
||||
&m_watcher, &QFileSystemWatcher::directoryChanged, this, [this]() {
|
||||
m_watchTimer.start();
|
||||
});
|
||||
QObject::connect(
|
||||
&m_watchTimer, &QTimer::timeout, this, &WallpaperDb::scanDir);
|
||||
}
|
||||
|
||||
WallpaperDb::~WallpaperDb() {
|
||||
if (QSqlDatabase::contains(m_uuid)) {
|
||||
{
|
||||
auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (db.isValid()) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
QSqlDatabase::removeDatabase(m_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
QString WallpaperDb::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void WallpaperDb::setPath(const QString& path) {
|
||||
if (m_path == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = path;
|
||||
m_dir = QDir(path);
|
||||
emit pathChanged();
|
||||
|
||||
for (const auto& dir : m_watcher.directories()) {
|
||||
m_watcher.removePath(dir);
|
||||
}
|
||||
|
||||
scanDir();
|
||||
}
|
||||
|
||||
QString WallpaperDb::dbPath() const {
|
||||
return m_dbPath;
|
||||
}
|
||||
|
||||
void WallpaperDb::setDbPath(const QString& path) {
|
||||
if (m_dbPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_dbPath = path;
|
||||
emit dbPathChanged();
|
||||
|
||||
if (!m_path.isEmpty()) {
|
||||
scanDir();
|
||||
}
|
||||
}
|
||||
|
||||
bool WallpaperDb::requireHardwareCodec() const {
|
||||
return m_requireHardwareCodec;
|
||||
}
|
||||
|
||||
void WallpaperDb::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_requireHardwareCodec = requireHardwareCodec;
|
||||
emit requireHardwareCodecChanged();
|
||||
|
||||
rebuildEntries();
|
||||
}
|
||||
|
||||
QQmlListProperty<WallpaperEntry> WallpaperDb::entries() {
|
||||
return QQmlListProperty<WallpaperEntry>(this, &m_entries);
|
||||
}
|
||||
|
||||
QString WallpaperDb::thumbnailUrl(const QString& path) {
|
||||
return QStringLiteral("image://wallpapers/") +
|
||||
QString::fromUtf8(path.toUtf8().toHex());
|
||||
}
|
||||
|
||||
QSize WallpaperDb::cacheSize() const {
|
||||
return m_cacheSize;
|
||||
}
|
||||
|
||||
void WallpaperDb::setCacheSize(const QSize& cacheSize) {
|
||||
if (m_cacheSize == cacheSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize prev = m_cacheSize;
|
||||
const bool hadPrevious = !prev.isEmpty();
|
||||
|
||||
m_cacheSize = cacheSize;
|
||||
emit cacheSizeChanged();
|
||||
|
||||
if (hadPrevious && qint64(cacheSize.width()) * cacheSize.height() >
|
||||
qint64(prev.width()) * prev.height()) {
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
|
||||
for (auto* entry : std::as_const(m_byPath)) {
|
||||
if (!entry->cached()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL,"
|
||||
" poster = NULL WHERE path = ?");
|
||||
query.addBindValue(entry->path());
|
||||
query.exec();
|
||||
}
|
||||
|
||||
entry->setThumbnailPath(QString());
|
||||
queueMetadata(entry, entry->isVideo());
|
||||
}
|
||||
|
||||
rebuildEntries();
|
||||
}
|
||||
|
||||
if (!m_queue.isEmpty() && !m_queueTimer.isActive()) {
|
||||
m_queueTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void WallpaperDb::registerProvider() {
|
||||
if (m_provider != nullptr) {
|
||||
m_provider->setDbPath(m_dbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
auto* engine = qmlEngine(this);
|
||||
if (engine == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_provider = new WallpaperImageProvider(m_dbPath);
|
||||
engine->addImageProvider(QStringLiteral("wallpapers"), m_provider);
|
||||
}
|
||||
|
||||
void WallpaperDb::openDb() {
|
||||
if (m_dbPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QSqlDatabase::contains(m_uuid)) {
|
||||
{
|
||||
auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (db.isOpen() && db.databaseName() == m_dbPath) {
|
||||
registerProvider();
|
||||
return;
|
||||
}
|
||||
|
||||
if (db.isValid()) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
QSqlDatabase::removeDatabase(m_uuid);
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
|
||||
|
||||
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
|
||||
db.setDatabaseName(m_dbPath);
|
||||
|
||||
if (!db.open()) {
|
||||
qWarning(dbLog) << "WallpaperDb: could not open database" << m_dbPath
|
||||
<< db.lastError();
|
||||
return;
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("PRAGMA journal_mode=WAL");
|
||||
query.exec(
|
||||
"CREATE TABLE IF NOT EXISTS wallpapers("
|
||||
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
|
||||
" poster BLOB, crop TEXT)");
|
||||
|
||||
query.exec("SELECT cache FROM wallpapers LIMIT 1");
|
||||
if (query.lastError().isValid()) {
|
||||
query.exec("DROP TABLE IF EXISTS wallpapers");
|
||||
query.exec(
|
||||
"CREATE TABLE wallpapers("
|
||||
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
|
||||
" poster BLOB, crop TEXT)");
|
||||
} else {
|
||||
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB");
|
||||
query.exec("ALTER TABLE wallpapers ADD COLUMN poster BLOB");
|
||||
query.exec("ALTER TABLE wallpapers ADD COLUMN crop TEXT");
|
||||
}
|
||||
|
||||
registerProvider();
|
||||
}
|
||||
|
||||
void WallpaperDb::scanDir() {
|
||||
openDb();
|
||||
|
||||
watchDir(m_dir.absolutePath());
|
||||
|
||||
if (!m_dir.exists()) {
|
||||
if (!m_entries.isEmpty()) {
|
||||
m_entries.clear();
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto* watcher = new QFutureWatcher<ScanResult>(this);
|
||||
QObject::connect(
|
||||
watcher, &QFutureWatcher<ScanResult>::finished, this, [this, watcher]() {
|
||||
applyScan(watcher->result());
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
const QDir dir = m_dir;
|
||||
|
||||
QHash<QString, QPair<qint64, bool>> knownCodecs;
|
||||
for (const auto* entry : std::as_const(m_byPath)) {
|
||||
if (entry->isVideo()) {
|
||||
knownCodecs.insert(
|
||||
entry->path(), qMakePair(entry->size(), entry->hardware()));
|
||||
}
|
||||
}
|
||||
|
||||
watcher->setFuture(QtConcurrent::run([dir, knownCodecs]() {
|
||||
ScanResult result;
|
||||
result.dir = dir;
|
||||
|
||||
QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
const QString path = it.next();
|
||||
const QFileInfo info(path);
|
||||
|
||||
if (info.isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasVideoExtension(path)) {
|
||||
FileEntry file;
|
||||
file.info = info;
|
||||
|
||||
const auto known = knownCodecs.constFind(path);
|
||||
if (known != knownCodecs.constEnd() &&
|
||||
known.value().first == info.size()) {
|
||||
file.hardware = known.value().second;
|
||||
} else {
|
||||
file.hardware = probeHardwareCodec(path);
|
||||
}
|
||||
|
||||
result.files.append(file);
|
||||
} else {
|
||||
QImageReader reader(path);
|
||||
if (reader.canRead()) {
|
||||
FileEntry file;
|
||||
file.info = info;
|
||||
result.files.append(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
void WallpaperDb::watchDir(const QString& dir) {
|
||||
if (dir.isEmpty() || m_watcher.directories().contains(dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QDir(dir).exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_watcher.addPath(dir);
|
||||
}
|
||||
|
||||
void WallpaperDb::applyScan(const ScanResult& result) {
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: scan of %s found %d files (cache target %dx%d)",
|
||||
m_dir.absolutePath().toUtf8().constData(),
|
||||
int(result.files.size()),
|
||||
m_cacheSize.width(),
|
||||
m_cacheSize.height());
|
||||
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
|
||||
QSet<QString> seen;
|
||||
for (const auto& file : result.files) {
|
||||
seen.insert(file.info.absoluteFilePath());
|
||||
}
|
||||
|
||||
for (auto it = m_byPath.begin(); it != m_byPath.end();) {
|
||||
if (!seen.contains(it.key())) {
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare("DELETE FROM wallpapers WHERE path = ?");
|
||||
query.addBindValue(it.key());
|
||||
query.exec();
|
||||
}
|
||||
|
||||
delete it.value();
|
||||
m_crops.remove(it.key());
|
||||
it = m_byPath.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& file : result.files) {
|
||||
const QFileInfo& info = file.info;
|
||||
const QString path = info.absoluteFilePath();
|
||||
|
||||
const qint64 size = info.size();
|
||||
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
|
||||
const bool isVideo = hasVideoExtension(path);
|
||||
|
||||
watchDir(info.absolutePath());
|
||||
|
||||
const auto existing = m_byPath.constFind(path);
|
||||
if (existing != m_byPath.constEnd()) {
|
||||
if (existing.value()->size() == size) {
|
||||
existing.value()->setHardware(file.hardware);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperEntry* entry = m_byPath.value(path);
|
||||
if (!entry) {
|
||||
entry =
|
||||
new WallpaperEntry(path, m_dir.relativeFilePath(path), this);
|
||||
m_byPath.insert(path, entry);
|
||||
}
|
||||
|
||||
entry->setHardware(file.hardware);
|
||||
|
||||
bool reused = false;
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"SELECT cache, scheme_thumb, poster, size, mtime, crop"
|
||||
" FROM wallpapers WHERE path = ?");
|
||||
query.addBindValue(path);
|
||||
query.exec();
|
||||
|
||||
if (query.next()) {
|
||||
const auto cache = query.value(0).toByteArray();
|
||||
const auto scheme = query.value(1).toByteArray();
|
||||
const auto poster = query.value(2).toByteArray();
|
||||
const auto storedSize = query.value(3).toLongLong();
|
||||
const auto storedMtime = query.value(4).toLongLong();
|
||||
loadCrop(path, query.value(5).toString());
|
||||
|
||||
const bool posterOk = !isVideo || !poster.isEmpty();
|
||||
|
||||
if (storedSize == size && storedMtime == mtime &&
|
||||
!cache.isEmpty()) {
|
||||
reused = true;
|
||||
entry->setThumbnailPath(thumbnailUrl(path));
|
||||
|
||||
if (scheme.isEmpty() || !posterOk) {
|
||||
queueMetadata(entry, isVideo, cache, poster);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!reused) {
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"INSERT INTO wallpapers"
|
||||
" (path, kind, hardware, size, mtime, cache)"
|
||||
" VALUES (?, ?, ?, ?, ?, NULL)"
|
||||
" ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,"
|
||||
" hardware = excluded.hardware, size = excluded.size,"
|
||||
" mtime = excluded.mtime, cache = NULL,"
|
||||
" scheme_thumb = NULL, poster = NULL");
|
||||
query.addBindValue(path);
|
||||
query.addBindValue(isVideo ? "video" : "image");
|
||||
query.addBindValue(
|
||||
isVideo ? (file.hardware ? 1 : 0) : QVariant());
|
||||
query.addBindValue(size);
|
||||
query.addBindValue(mtime);
|
||||
if (!query.exec()) {
|
||||
qWarning(dbLog)
|
||||
<< "WallpaperDb: could not store the row for" << path
|
||||
<< query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
queueMetadata(entry, isVideo);
|
||||
}
|
||||
}
|
||||
|
||||
rebuildEntries();
|
||||
}
|
||||
|
||||
void WallpaperDb::queueMetadata(
|
||||
WallpaperEntry* entry,
|
||||
bool isVideo,
|
||||
const QByteArray& existingCache,
|
||||
const QByteArray& existingPoster) {
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: queued cache job for %s (%s%s)",
|
||||
entry->path().toUtf8().constData(),
|
||||
isVideo ? "video" : "image",
|
||||
existingCache.isEmpty() ? "" : ", existing blob");
|
||||
|
||||
m_queue.append(
|
||||
MetadataJob{entry, entry->path(), isVideo, existingCache, existingPoster});
|
||||
|
||||
if (!m_queueTimer.isActive()) {
|
||||
m_queueTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void WallpaperDb::processQueue() {
|
||||
if (m_jobRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_queue.isEmpty()) {
|
||||
m_queueTimer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_cacheSize.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MetadataJob job = m_queue.takeFirst();
|
||||
m_jobRunning = true;
|
||||
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: caching %s (%s, %d jobs left in queue)",
|
||||
job.path.toUtf8().constData(),
|
||||
job.isVideo ? "video" : "image",
|
||||
int(m_queue.size()));
|
||||
|
||||
runMetadataJob(job);
|
||||
}
|
||||
|
||||
void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||
struct CacheResult {
|
||||
QString path;
|
||||
QByteArray cache;
|
||||
QByteArray poster;
|
||||
QByteArray scheme;
|
||||
};
|
||||
|
||||
const QSize cacheSize = m_cacheSize;
|
||||
const bool isVideoJob = job.isVideo;
|
||||
|
||||
QElapsedTimer clock;
|
||||
clock.start();
|
||||
|
||||
auto* watcher = new QFutureWatcher<CacheResult>(this);
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QFutureWatcher<CacheResult>::finished,
|
||||
this,
|
||||
[this, watcher, clock, isVideoJob]() {
|
||||
const auto result = watcher->result();
|
||||
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: %s produced %d bytes of cache in %dms",
|
||||
result.path.toUtf8().constData(),
|
||||
int(result.cache.size()),
|
||||
int(clock.elapsed()));
|
||||
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: %s poster %d bytes",
|
||||
result.path.toUtf8().constData(),
|
||||
int(result.poster.size()));
|
||||
|
||||
if (isVideoJob && result.poster.isEmpty()) {
|
||||
qWarning(dbLog)
|
||||
<< "WallpaperDb: no poster produced for" << result.path;
|
||||
}
|
||||
|
||||
if (QSqlDatabase::contains(m_uuid)) {
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?,"
|
||||
" poster = ? WHERE path = ?");
|
||||
query.addBindValue(result.cache);
|
||||
query.addBindValue(result.scheme);
|
||||
query.addBindValue(result.poster);
|
||||
query.addBindValue(result.path);
|
||||
if (!query.exec()) {
|
||||
qWarning(dbLog)
|
||||
<< "WallpaperDb: could not store the cache for"
|
||||
<< result.path << query.lastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.cache.isEmpty()) {
|
||||
if (auto* entry = m_byPath.value(result.path)) {
|
||||
entry->setThumbnailPath(thumbnailUrl(result.path));
|
||||
}
|
||||
|
||||
emit cacheStored(result.path);
|
||||
}
|
||||
|
||||
m_jobRunning = false;
|
||||
rebuildEntries();
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
watcher->setFuture(QtConcurrent::run([job, cacheSize]() {
|
||||
CacheResult result;
|
||||
result.path = job.path;
|
||||
|
||||
QByteArray cache = job.existingCache;
|
||||
if (cache.isEmpty()) {
|
||||
cache = job.isVideo ? videoCacheBytes(job.path, cacheSize)
|
||||
: imageCacheBytes(job.path, cacheSize);
|
||||
}
|
||||
|
||||
QByteArray poster = job.existingPoster;
|
||||
if (poster.isEmpty()) {
|
||||
poster =
|
||||
job.isVideo ? videoPosterBytes(job.path, cacheSize) : cache;
|
||||
}
|
||||
|
||||
result.cache = cache;
|
||||
result.poster = poster;
|
||||
result.scheme = cache.isEmpty() ? QByteArray()
|
||||
: schemeThumbBytes(cache, 128);
|
||||
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
void WallpaperDb::rebuildEntries() {
|
||||
QList<WallpaperEntry*> next;
|
||||
next.reserve(m_byPath.size());
|
||||
|
||||
for (auto* entry : std::as_const(m_byPath)) {
|
||||
if (entryAllowed(entry)) {
|
||||
next.append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(
|
||||
next.begin(),
|
||||
next.end(),
|
||||
[](const WallpaperEntry* a, const WallpaperEntry* b) {
|
||||
return a->relativePath() < b->relativePath();
|
||||
});
|
||||
|
||||
if (next == m_entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_entries = next;
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
|
||||
if (!entry->isVideo()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_requireHardwareCodec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return entry->hardware();
|
||||
}
|
||||
|
||||
QVariantMap WallpaperDb::crop(const QString& path, const QString& screen) const {
|
||||
const auto pathCrops = m_crops.constFind(path);
|
||||
if (pathCrops == m_crops.constEnd()) return {};
|
||||
return pathCrops.value().value(screen).toMap();
|
||||
}
|
||||
|
||||
void WallpaperDb::setCrop(
|
||||
const QString& path, const QString& screen, const QVariantMap& crop) {
|
||||
if (path.isEmpty() || screen.isEmpty()) return;
|
||||
|
||||
QVariantMap& pathCrops = m_crops[path];
|
||||
pathCrops[screen] = crop;
|
||||
storeCropJson(path);
|
||||
emit cropsChanged(path);
|
||||
}
|
||||
|
||||
void WallpaperDb::loadCrop(const QString& path, const QString& json) {
|
||||
if (json.isEmpty()) {
|
||||
m_crops.remove(path);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
m_crops.remove(path);
|
||||
return;
|
||||
}
|
||||
|
||||
m_crops[path] = doc.object().toVariantMap();
|
||||
}
|
||||
|
||||
void WallpaperDb::storeCropJson(const QString& path) {
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (!db.isOpen()) return;
|
||||
|
||||
const auto pathCrops = m_crops.value(path);
|
||||
const QByteArray json =
|
||||
pathCrops.isEmpty()
|
||||
? QByteArray()
|
||||
: QJsonDocument(QJsonObject::fromVariantMap(pathCrops))
|
||||
.toJson(QJsonDocument::Compact);
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.prepare("UPDATE wallpapers SET crop = ? WHERE path = ?");
|
||||
query.addBindValue(json.isEmpty() ? QVariant() : QVariant(json));
|
||||
query.addBindValue(path);
|
||||
if (!query.exec()) {
|
||||
qWarning(dbLog) << "WallpaperDb: could not store the crop for" << path
|
||||
<< query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell::models
|
||||
@@ -1,188 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <qbytearray.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qsize.h>
|
||||
#include <qtimer.h>
|
||||
#include <qvariant.h>
|
||||
#include <qvector.h>
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
class WallpaperImageProvider;
|
||||
|
||||
class WallpaperEntry : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE(
|
||||
"WallpaperEntry instances can only be retrieved from a WallpaperDb")
|
||||
|
||||
Q_PROPERTY(QString path READ path CONSTANT)
|
||||
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
||||
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
||||
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||
Q_PROPERTY(bool hardware READ hardware NOTIFY hardwareChanged)
|
||||
Q_PROPERTY(
|
||||
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
|
||||
|
||||
public:
|
||||
explicit WallpaperEntry(
|
||||
const QString& path,
|
||||
const QString& relativePath,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
[[nodiscard]] QString relativePath() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString baseName() const;
|
||||
[[nodiscard]] QString parentDir() const;
|
||||
[[nodiscard]] QString suffix() const;
|
||||
[[nodiscard]] qint64 size() const;
|
||||
[[nodiscard]] bool isImage() const;
|
||||
[[nodiscard]] bool isVideo() const;
|
||||
[[nodiscard]] bool hardware() const;
|
||||
[[nodiscard]] bool cached() const;
|
||||
[[nodiscard]] QString thumbnailPath() const;
|
||||
|
||||
void setRelativePath(const QString& path);
|
||||
void setHardware(bool hardware);
|
||||
void setThumbnailPath(const QString& path);
|
||||
|
||||
signals:
|
||||
void relativePathChanged();
|
||||
void hardwareChanged();
|
||||
void thumbnailPathChanged();
|
||||
|
||||
private:
|
||||
const QFileInfo m_fileInfo;
|
||||
|
||||
const QString m_path;
|
||||
QString m_relativePath;
|
||||
QString m_thumbnailPath;
|
||||
bool m_hardware = false;
|
||||
};
|
||||
|
||||
class WallpaperDb : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
|
||||
Q_PROPERTY(
|
||||
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||
Q_PROPERTY(
|
||||
QQmlListProperty<ZShell::models::WallpaperEntry> entries READ entries
|
||||
NOTIFY entriesChanged)
|
||||
Q_PROPERTY(
|
||||
QSize cacheSize READ cacheSize WRITE setCacheSize NOTIFY
|
||||
cacheSizeChanged)
|
||||
|
||||
public:
|
||||
explicit WallpaperDb(QObject* parent = nullptr);
|
||||
~WallpaperDb() override;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] QString dbPath() const;
|
||||
void setDbPath(const QString& path);
|
||||
|
||||
[[nodiscard]] bool requireHardwareCodec() const;
|
||||
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<WallpaperEntry> entries();
|
||||
|
||||
[[nodiscard]] QSize cacheSize() const;
|
||||
void setCacheSize(const QSize& cacheSize);
|
||||
|
||||
[[nodiscard]] static QString thumbnailUrl(const QString& path);
|
||||
|
||||
Q_INVOKABLE QVariantMap crop(const QString& path, const QString& screen)
|
||||
const;
|
||||
Q_INVOKABLE void setCrop(
|
||||
const QString& path, const QString& screen, const QVariantMap& crop);
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void dbPathChanged();
|
||||
void requireHardwareCodecChanged();
|
||||
void entriesChanged();
|
||||
void cacheSizeChanged();
|
||||
|
||||
void cacheStored(const QString& path);
|
||||
void cropsChanged(const QString& path);
|
||||
|
||||
private:
|
||||
struct FileEntry {
|
||||
QFileInfo info;
|
||||
bool hardware = false;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
QVector<FileEntry> files;
|
||||
QDir dir;
|
||||
};
|
||||
|
||||
struct MetadataJob {
|
||||
WallpaperEntry* entry;
|
||||
QString path;
|
||||
bool isVideo;
|
||||
QByteArray existingCache;
|
||||
QByteArray existingPoster;
|
||||
};
|
||||
|
||||
void openDb();
|
||||
void registerProvider();
|
||||
void scanDir();
|
||||
void watchDir(const QString& dir);
|
||||
void applyScan(const ScanResult& result);
|
||||
void queueMetadata(
|
||||
WallpaperEntry* entry,
|
||||
bool isVideo,
|
||||
const QByteArray& existingCache = {},
|
||||
const QByteArray& existingPoster = {});
|
||||
void processQueue();
|
||||
void runMetadataJob(const MetadataJob& job);
|
||||
void rebuildEntries();
|
||||
|
||||
void loadCrop(const QString& path, const QString& json);
|
||||
void storeCropJson(const QString& path);
|
||||
|
||||
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
|
||||
|
||||
QDir m_dir;
|
||||
QString m_path;
|
||||
QString m_dbPath;
|
||||
QString m_uuid;
|
||||
WallpaperImageProvider* m_provider = nullptr;
|
||||
|
||||
bool m_requireHardwareCodec = false;
|
||||
QSize m_cacheSize;
|
||||
|
||||
QList<WallpaperEntry*> m_entries;
|
||||
QHash<QString, WallpaperEntry*> m_byPath;
|
||||
|
||||
QHash<QString, QVariantMap> m_crops;
|
||||
|
||||
QVector<MetadataJob> m_queue;
|
||||
QTimer m_queueTimer;
|
||||
bool m_jobRunning = false;
|
||||
|
||||
QFileSystemWatcher m_watcher;
|
||||
QTimer m_watchTimer;
|
||||
};
|
||||
|
||||
} // namespace ZShell::models
|
||||
@@ -1,398 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <qbuffer.h>
|
||||
#include <qfile.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qprocess.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringlist.h>
|
||||
#include <qtemporarydir.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ZShell::models {
|
||||
namespace {
|
||||
|
||||
inline const QStringList kVideoExtensions = {
|
||||
"mp4",
|
||||
"mkv",
|
||||
"webm",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"m4v",
|
||||
"mpg",
|
||||
"mpeg",
|
||||
"3gp",
|
||||
"ogv",
|
||||
"ts",
|
||||
"vob",
|
||||
};
|
||||
|
||||
inline bool hasVideoExtension(const QString& path) {
|
||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||
}
|
||||
|
||||
inline const QStringList kHardwareCodecs = {
|
||||
"av1",
|
||||
"hevc",
|
||||
"h264",
|
||||
"vp9",
|
||||
"vp8",
|
||||
"vc1",
|
||||
"mpeg2",
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"mjpeg",
|
||||
};
|
||||
|
||||
inline bool probeHardwareCodec(const QString& path) {
|
||||
QProcess process;
|
||||
process.start(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
path,
|
||||
});
|
||||
process.waitForFinished(5000);
|
||||
|
||||
if (process.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString codec =
|
||||
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||
return kHardwareCodecs.contains(codec);
|
||||
}
|
||||
|
||||
inline QSize coverSizeNoUpscale(const QSize& source, const QSize& fit) {
|
||||
if (!source.isValid() || source.isEmpty() || !fit.isValid() ||
|
||||
fit.isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const QSize cover = source.scaled(fit, Qt::KeepAspectRatioByExpanding);
|
||||
if (cover.width() <= source.width() && cover.height() <= source.height()) {
|
||||
return cover;
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
inline QImage cleanImage(const QImage& src) {
|
||||
QImage out(src.size(), QImage::Format_RGB888);
|
||||
if (out.isNull()) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const auto rowBytes =
|
||||
static_cast<size_t>(std::min(src.bytesPerLine(), out.bytesPerLine()));
|
||||
for (int y = 0; y < src.height(); ++y) {
|
||||
std::memcpy(out.scanLine(y), src.constScanLine(y), rowBytes);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
inline QByteArray imageCacheBytes(const QString& path, const QSize& fit) {
|
||||
QImageReader reader(path);
|
||||
reader.setAutoTransform(true);
|
||||
|
||||
const QSize source = reader.size();
|
||||
const QSize target = coverSizeNoUpscale(source, fit);
|
||||
if (target != source && !target.isEmpty()) {
|
||||
reader.setScaledSize(target);
|
||||
}
|
||||
|
||||
QImage image = reader.read();
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
|
||||
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
if (!buffer.open(QIODevice::WriteOnly) ||
|
||||
!image.save(&buffer, "JPEG", 90)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) {
|
||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
||||
QProcess process;
|
||||
process.start(program, args);
|
||||
if (!process.waitForFinished(15000)) {
|
||||
process.kill();
|
||||
process.waitForFinished(1000);
|
||||
}
|
||||
return qMakePair(process.exitCode(), process.readAllStandardOutput());
|
||||
};
|
||||
|
||||
double duration = 0.0;
|
||||
const auto probe = runProcess(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
src,
|
||||
});
|
||||
if (probe.first == 0) {
|
||||
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
|
||||
}
|
||||
|
||||
QSize source;
|
||||
const auto resProbe = runProcess(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height",
|
||||
"-of",
|
||||
"csv=s=x:p=0",
|
||||
src,
|
||||
});
|
||||
if (resProbe.first == 0) {
|
||||
const auto parts =
|
||||
QString::fromUtf8(resProbe.second).trimmed().split('x');
|
||||
if (parts.size() == 2) {
|
||||
source = QSize(parts[0].toInt(), parts[1].toInt());
|
||||
}
|
||||
}
|
||||
|
||||
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
|
||||
: 8;
|
||||
|
||||
QSize target = coverSizeNoUpscale(source, fit);
|
||||
if (target.isEmpty()) {
|
||||
target = fit;
|
||||
}
|
||||
|
||||
target.setWidth(target.width() & ~1);
|
||||
target.setHeight(target.height() & ~1);
|
||||
|
||||
const QString scale =
|
||||
QStringLiteral(
|
||||
"scale=%1:%2:force_original_aspect_ratio=decrease,"
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
|
||||
.arg(target.width())
|
||||
.arg(target.height());
|
||||
|
||||
QTemporaryDir tmpDir;
|
||||
if (!tmpDir.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int bestIndex = -1;
|
||||
double bestChroma = -1.0;
|
||||
double bestLuma = -1.0;
|
||||
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
const double frac = duration > 0 ? (i + 0.5) / samples : 0.0;
|
||||
const QString seek = duration > 0
|
||||
? QString::number(frac * duration, 'f', 3)
|
||||
: QStringLiteral("0");
|
||||
|
||||
const QString framePath =
|
||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(i));
|
||||
|
||||
runProcess(
|
||||
"ffmpeg",
|
||||
{
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
seek,
|
||||
"-i",
|
||||
src,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
scale,
|
||||
"-pix_fmt",
|
||||
"yuvj420p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-y",
|
||||
framePath,
|
||||
});
|
||||
|
||||
QImage frame(framePath);
|
||||
if (frame.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = frame.convertToFormat(QImage::Format_RGB888);
|
||||
|
||||
double chroma = 0.0;
|
||||
double luma = 0.0;
|
||||
int count = 0;
|
||||
for (int y = 0; y < frame.height(); ++y) {
|
||||
const auto* line = frame.constScanLine(y);
|
||||
for (int x = 0; x < frame.width(); ++x) {
|
||||
const int r = line[x * 3];
|
||||
const int g = line[x * 3 + 1];
|
||||
const int b = line[x * 3 + 2];
|
||||
chroma += std::max({r, g, b}) - std::min({r, g, b});
|
||||
luma += r + g + b;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
const int n = std::max(1, count);
|
||||
chroma /= n;
|
||||
luma /= 3.0 * n;
|
||||
|
||||
if (chroma > bestChroma + 0.5 ||
|
||||
(chroma > bestChroma - 0.5 && luma > bestLuma)) {
|
||||
bestChroma = chroma;
|
||||
bestLuma = luma;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex < 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QFile frame(tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)));
|
||||
if (!frame.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return frame.readAll();
|
||||
}
|
||||
|
||||
inline QByteArray videoPosterBytes(const QString& src, const QSize& fit) {
|
||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
||||
QProcess process;
|
||||
process.start(program, args);
|
||||
if (!process.waitForFinished(15000)) {
|
||||
process.kill();
|
||||
process.waitForFinished(1000);
|
||||
}
|
||||
return qMakePair(process.exitCode(), process.readAllStandardOutput());
|
||||
};
|
||||
|
||||
QSize source;
|
||||
const auto resProbe = runProcess(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height",
|
||||
"-of",
|
||||
"csv=s=x:p=0",
|
||||
src,
|
||||
});
|
||||
if (resProbe.first == 0) {
|
||||
const auto parts =
|
||||
QString::fromUtf8(resProbe.second).trimmed().split('x');
|
||||
if (parts.size() == 2) {
|
||||
source = QSize(parts[0].toInt(), parts[1].toInt());
|
||||
}
|
||||
}
|
||||
|
||||
QSize target = coverSizeNoUpscale(source, fit);
|
||||
if (target.isEmpty()) {
|
||||
target = fit;
|
||||
}
|
||||
|
||||
target.setWidth(target.width() & ~1);
|
||||
target.setHeight(target.height() & ~1);
|
||||
|
||||
const QString scale =
|
||||
QStringLiteral(
|
||||
"scale=%1:%2:force_original_aspect_ratio=decrease,"
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
|
||||
.arg(target.width())
|
||||
.arg(target.height());
|
||||
|
||||
QTemporaryDir tmpDir;
|
||||
if (!tmpDir.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QString framePath = tmpDir.filePath(QStringLiteral("poster.jpg"));
|
||||
|
||||
runProcess(
|
||||
"ffmpeg",
|
||||
{
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
"0",
|
||||
"-i",
|
||||
src,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
scale,
|
||||
"-pix_fmt",
|
||||
"yuvj420p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-y",
|
||||
framePath,
|
||||
});
|
||||
|
||||
QFile frame(framePath);
|
||||
if (!frame.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return frame.readAll();
|
||||
}
|
||||
|
||||
inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) {
|
||||
QImage image = QImage::fromData(source, "JPEG");
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (image.width() > maxSide || image.height() > maxSide) {
|
||||
image = image.scaled(
|
||||
maxSide, maxSide, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
|
||||
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
if (!buffer.open(QIODevice::WriteOnly) ||
|
||||
!image.save(&buffer, "JPEG", 85)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ZShell::models
|
||||
@@ -23,14 +23,11 @@ qml_module(ZShell-services
|
||||
lyriccandidate.hpp lyriccandidate.cpp
|
||||
lyrics.hpp lyrics.cpp
|
||||
dropimporter.hpp dropimporter.cpp
|
||||
notificationstore.hpp notificationstore.cpp
|
||||
LIBRARIES
|
||||
Qt6::Core
|
||||
Qt6::Qml
|
||||
Qt6::Quick
|
||||
Qt6::Concurrent
|
||||
Qt6::Network
|
||||
Qt6::Sql
|
||||
PkgConfig::Pipewire
|
||||
PkgConfig::Aubio
|
||||
PkgConfig::Cava
|
||||
|
||||
@@ -93,7 +93,7 @@ int DropImporter::importAttachments(
|
||||
saveBytes(id, rawImage);
|
||||
} else if (const QString src = firstImgSrc(html); !src.isEmpty()) {
|
||||
if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) {
|
||||
const qsizetype comma = src.indexOf(QLatin1Char(','));
|
||||
const int comma = src.indexOf(QLatin1Char(','));
|
||||
if (comma < 0) {
|
||||
failWith(id, tr("Malformed data URI"));
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,248 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QJSValue>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <qqmlintegration.h>
|
||||
#include <QQmlListProperty>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
namespace ZShell::config {
|
||||
class Notifs;
|
||||
}
|
||||
|
||||
namespace ZShell::Services {
|
||||
|
||||
class NotificationStore;
|
||||
class NotifImageProvider;
|
||||
|
||||
class NotificationActionRow : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE(
|
||||
"NotificationActionRow instances come from a NotificationStore")
|
||||
|
||||
Q_PROPERTY(bool invokable READ invokable NOTIFY invokableChanged)
|
||||
Q_PROPERTY(QString identifier READ identifier CONSTANT)
|
||||
Q_PROPERTY(QString text READ text CONSTANT)
|
||||
|
||||
public:
|
||||
NotificationActionRow(
|
||||
QObject* action,
|
||||
const QString& identifier,
|
||||
const QString& text,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool invokable() const;
|
||||
[[nodiscard]] QString identifier() const;
|
||||
[[nodiscard]] QString text() const;
|
||||
|
||||
Q_INVOKABLE void invoke();
|
||||
|
||||
signals:
|
||||
void invokableChanged();
|
||||
|
||||
private:
|
||||
QPointer<QObject> m_action;
|
||||
QString m_identifier;
|
||||
QString m_text;
|
||||
};
|
||||
|
||||
class NotificationRow : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("NotificationRow instances come from a NotificationStore")
|
||||
|
||||
Q_PROPERTY(QQmlListProperty<NotificationActionRow> actions READ actions NOTIFY actionsChanged)
|
||||
Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged)
|
||||
Q_PROPERTY(QString appName READ appName NOTIFY appNameChanged)
|
||||
Q_PROPERTY(QString body READ body NOTIFY bodyChanged)
|
||||
Q_PROPERTY(bool closed READ closed NOTIFY closedChanged)
|
||||
Q_PROPERTY(bool hasActionIcons READ hasActionIcons NOTIFY hasActionIconsChanged)
|
||||
Q_PROPERTY(QString image READ image NOTIFY imageChanged)
|
||||
Q_PROPERTY(QString imageSource READ imageSource NOTIFY imageSourceChanged)
|
||||
Q_PROPERTY(int notificationId READ notificationId CONSTANT)
|
||||
Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
|
||||
Q_PROPERTY(bool popup READ popup WRITE setPopup NOTIFY popupChanged)
|
||||
Q_PROPERTY(bool resident READ resident NOTIFY residentChanged)
|
||||
Q_PROPERTY(QString summary READ summary NOTIFY summaryChanged)
|
||||
Q_PROPERTY(QDateTime time READ time CONSTANT)
|
||||
Q_PROPERTY(QString timeStr READ timeStr NOTIFY timeStrChanged)
|
||||
Q_PROPERTY(int urgency READ urgency NOTIFY urgencyChanged)
|
||||
|
||||
public:
|
||||
explicit NotificationRow(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<NotificationActionRow> actions();
|
||||
[[nodiscard]] QString appIcon() const;
|
||||
[[nodiscard]] QString appName() const;
|
||||
[[nodiscard]] QString body() const;
|
||||
[[nodiscard]] bool closed() const;
|
||||
[[nodiscard]] bool hasActionIcons() const;
|
||||
[[nodiscard]] QString image() const;
|
||||
[[nodiscard]] QString imageSource() const;
|
||||
[[nodiscard]] int notificationId() const;
|
||||
[[nodiscard]] int progress() const;
|
||||
[[nodiscard]] bool popup() const;
|
||||
void setPopup(bool popup);
|
||||
[[nodiscard]] bool resident() const;
|
||||
[[nodiscard]] QString summary() const;
|
||||
[[nodiscard]] QDateTime time() const;
|
||||
[[nodiscard]] QString timeStr() const;
|
||||
[[nodiscard]] int urgency() const;
|
||||
|
||||
Q_INVOKABLE void close();
|
||||
Q_INVOKABLE void lock(QObject* item);
|
||||
Q_INVOKABLE void pausePopup();
|
||||
Q_INVOKABLE void resumePopup();
|
||||
Q_INVOKABLE void unlock(QObject* item);
|
||||
|
||||
signals:
|
||||
void actionsChanged();
|
||||
void appIconChanged();
|
||||
void appNameChanged();
|
||||
void bodyChanged();
|
||||
void closedChanged();
|
||||
void hasActionIconsChanged();
|
||||
void imageChanged();
|
||||
void imageSourceChanged();
|
||||
void popupChanged();
|
||||
void progressChanged();
|
||||
void residentChanged();
|
||||
void summaryChanged();
|
||||
void timeStrChanged();
|
||||
void urgencyChanged();
|
||||
|
||||
private:
|
||||
friend NotificationStore;
|
||||
|
||||
[[nodiscard]] bool expired(qint64 now) const;
|
||||
void refreshTimeStr();
|
||||
|
||||
QList<NotificationActionRow*> m_actions;
|
||||
NotificationStore* m_store = nullptr;
|
||||
|
||||
QString m_appIcon;
|
||||
QString m_appName;
|
||||
QString m_body;
|
||||
QString m_digest;
|
||||
QString m_image;
|
||||
QString m_imageSource;
|
||||
QString m_summary;
|
||||
QString m_timeStr;
|
||||
|
||||
qint64 m_deadline = 0;
|
||||
qint64 m_rowId = -1;
|
||||
qint64 m_timeMs = 0;
|
||||
|
||||
QPointer<QObject> m_notification;
|
||||
QSet<QObject*> m_locks;
|
||||
QJSValue m_jsNotification;
|
||||
|
||||
bool m_closed = false;
|
||||
bool m_hasActionIcons = false;
|
||||
bool m_liveConnected = false;
|
||||
bool m_popup = false;
|
||||
bool m_popupPaused = false;
|
||||
bool m_resident = false;
|
||||
|
||||
int m_notificationId = 0;
|
||||
int m_progress = 0;
|
||||
int m_urgency = 1;
|
||||
};
|
||||
|
||||
class NotificationStore : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(bool autoExpire READ autoExpire WRITE setAutoExpire NOTIFY autoExpireChanged)
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
||||
Q_PROPERTY(int popupTimeout READ popupTimeout WRITE setPopupTimeout NOTIFY
|
||||
popupTimeoutChanged)
|
||||
Q_PROPERTY(QQmlListProperty<NotificationRow> rows READ rows NOTIFY rowsChanged)
|
||||
|
||||
public:
|
||||
explicit NotificationStore(QObject* parent = nullptr);
|
||||
~NotificationStore() override;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] bool autoExpire() const;
|
||||
void setAutoExpire(bool autoExpire);
|
||||
|
||||
[[nodiscard]] int popupTimeout() const;
|
||||
void setPopupTimeout(int timeout);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<NotificationRow> rows();
|
||||
|
||||
Q_INVOKABLE NotificationRow* insert(const QJSValue& notification, bool popup);
|
||||
Q_INVOKABLE void clear();
|
||||
|
||||
signals:
|
||||
void autoExpireChanged();
|
||||
void pathChanged();
|
||||
void popupTimeoutChanged();
|
||||
void rowsChanged();
|
||||
|
||||
private slots:
|
||||
void handleLiveUpdate();
|
||||
|
||||
private:
|
||||
friend NotificationRow;
|
||||
|
||||
void attachNotification(
|
||||
NotificationRow* row, QObject* notification, const QJSValue& js);
|
||||
void applyNotifConfig(const config::Notifs* notifs);
|
||||
void closeDb();
|
||||
void copyFields(NotificationRow* row, QObject* notification);
|
||||
void handleClosed(NotificationRow* row);
|
||||
void insertDbRow(NotificationRow* row);
|
||||
void loadRows();
|
||||
void migrateLegacyJson();
|
||||
[[nodiscard]] bool openDb();
|
||||
void orphanCleanup();
|
||||
void persistPopup(const NotificationRow* row);
|
||||
void prune();
|
||||
void registerProvider();
|
||||
void removeRow(NotificationRow* row);
|
||||
// Coalesced rowsChanged: a burst of inserts/removes triggers one QML
|
||||
// rebind on the next event loop turn instead of one per notification.
|
||||
void scheduleRowsChanged();
|
||||
[[nodiscard]] NotificationRow* rowForNotificationId(int id) const;
|
||||
void startImageCache(NotificationRow* row);
|
||||
void storeImageBlob(
|
||||
const QPointer<NotificationRow>& row,
|
||||
const QString& digest,
|
||||
const QByteArray& png);
|
||||
void tick();
|
||||
void trimRows();
|
||||
void updateDbRow(NotificationRow* row);
|
||||
|
||||
static QList<NotificationActionRow*> parseActions(
|
||||
const QJSValue& actions, NotificationRow* row);
|
||||
|
||||
int m_popupTimeout = 5000;
|
||||
|
||||
qint64 m_nextPrune = 0;
|
||||
qint64 m_nextTimeStrRefresh = 0;
|
||||
bool m_autoExpire = true;
|
||||
bool m_clearing = false;
|
||||
bool m_rowsSignalPending = false;
|
||||
|
||||
QString m_uuid;
|
||||
QString m_path;
|
||||
|
||||
QList<NotificationRow*> m_rows;
|
||||
QHash<QObject*, NotificationRow*> m_liveRows;
|
||||
|
||||
NotifImageProvider* m_provider = nullptr;
|
||||
QTimer* m_tick = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ZShell::Services
|
||||
@@ -1,70 +0,0 @@
|
||||
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
|
||||
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
|
||||
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
|
||||
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
|
||||
|
||||
qml_module(ZShell-Wallpaper
|
||||
URI ZShell.Wallpaper
|
||||
INCLUDE_PREFIX wallpaper
|
||||
SOURCES
|
||||
wallpaperservice.hpp wallpaperservice.cpp
|
||||
wallpaperobject.hpp wallpaperobject.cpp
|
||||
wallpapersurface.hpp wallpapersurface.cpp
|
||||
wallpaperimage.hpp wallpaperimage.cpp
|
||||
wallpapercache.hpp wallpapercache.cpp
|
||||
videotranscoder.hpp videotranscoder.cpp
|
||||
videodecoder.hpp videodecoder.cpp
|
||||
nvidiaplayer.hpp nvidiaplayer.cpp
|
||||
vaapiplayer.hpp vaapiplayer.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Sql
|
||||
Qt::Core
|
||||
Qt::GuiPrivate
|
||||
Qt::WaylandClient
|
||||
Qt::WaylandClientPrivate
|
||||
wlp-layer-shell
|
||||
wlp-xdg-shell
|
||||
wlp-viewporter
|
||||
wlp-fractional-scale
|
||||
PkgConfig::EGL
|
||||
PkgConfig::WAYLAND_EGL
|
||||
PkgConfig::GL
|
||||
PkgConfig::MPV
|
||||
PkgConfig::AVCODEC
|
||||
PkgConfig::AVFORMAT
|
||||
PkgConfig::AVUTIL
|
||||
ZShell-config
|
||||
)
|
||||
|
||||
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_FFMPEG=1)
|
||||
|
||||
if(LIBVA_FOUND)
|
||||
target_link_libraries(ZShell-Wallpaper PRIVATE PkgConfig::LIBVA)
|
||||
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VAAPI=1)
|
||||
endif()
|
||||
|
||||
if(CUDAToolkit_FOUND)
|
||||
target_link_libraries(ZShell-Wallpaper PRIVATE CUDA::cuda_driver)
|
||||
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VIDEO=1)
|
||||
endif()
|
||||
|
||||
qt_add_resources(ZShell-Wallpaper "wallpaper_shaders"
|
||||
PREFIX "/"
|
||||
FILES
|
||||
shaders/wallpaper.vert
|
||||
shaders/wallpaper.frag
|
||||
shaders/wallpaper_video.frag
|
||||
shaders/wallpaper_trans_bowtie.frag
|
||||
shaders/wallpaper_trans_fade.frag
|
||||
shaders/wallpaper_trans_linear_blur.frag
|
||||
shaders/wallpaper_trans_window_slices.frag
|
||||
shaders/wallpaper_trans_wipe_right.frag
|
||||
shaders/wallpaper_trans_wipe_down.frag
|
||||
shaders/wallpaper_trans_tiles_wave.frag
|
||||
shaders/wallpaper_trans_split_slide_in_horizontal.frag
|
||||
shaders/wallpaper_trans_hexagonalize.frag
|
||||
shaders/wallpaper_trans_doom_melt.frag
|
||||
)
|
||||
@@ -1,567 +0,0 @@
|
||||
#ifdef ZHAVE_VIDEO
|
||||
|
||||
#include "nvidiaplayer.hpp"
|
||||
|
||||
#define EGL_EGLEXT_PROTOTYPES
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
#include <cuda.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
|
||||
#include <QtLogging>
|
||||
|
||||
extern "C" CUresult cuGraphicsGLRegisterImage(
|
||||
CUgraphicsResource* resource,
|
||||
GLuint image,
|
||||
GLenum target,
|
||||
unsigned int flags);
|
||||
|
||||
Q_LOGGING_CATEGORY(videoLog, "ZShell.Internal.wallpaper")
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
|
||||
namespace {
|
||||
|
||||
enum AVPixelFormat chooseFormat(
|
||||
AVCodecContext* ctx, const AVPixelFormat* pix_fmts) {
|
||||
(void)ctx;
|
||||
for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
|
||||
if (*p == AV_PIX_FMT_CUDA) return AV_PIX_FMT_CUDA;
|
||||
return pix_fmts[0];
|
||||
}
|
||||
|
||||
class CudaCtx {
|
||||
public:
|
||||
explicit CudaCtx(CUcontext ctx) : m_ctx(ctx) {
|
||||
if (m_ctx) cuCtxPushCurrent(m_ctx);
|
||||
}
|
||||
~CudaCtx() {
|
||||
if (m_ctx) cuCtxPopCurrent(&m_prev);
|
||||
}
|
||||
|
||||
private:
|
||||
CUcontext m_ctx;
|
||||
CUcontext m_prev = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
NvidiaPlayer::NvidiaPlayer(EGLDisplay display) {
|
||||
(void)display;
|
||||
}
|
||||
|
||||
NvidiaPlayer::~NvidiaPlayer() {
|
||||
stop();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
|
||||
CUcontext prev = nullptr;
|
||||
if (m_cuCtx) cuCtxPushCurrent(m_cuCtx);
|
||||
|
||||
if (m_pending) {
|
||||
av_frame_unref(m_pending);
|
||||
av_frame_free(&m_pending);
|
||||
}
|
||||
if (m_displayed) {
|
||||
av_frame_unref(m_displayed);
|
||||
av_frame_free(&m_displayed);
|
||||
}
|
||||
|
||||
if (m_resY) {
|
||||
cuGraphicsUnregisterResource(m_resY);
|
||||
m_resY = nullptr;
|
||||
}
|
||||
if (m_resUV) {
|
||||
cuGraphicsUnregisterResource(m_resUV);
|
||||
m_resUV = nullptr;
|
||||
}
|
||||
if (m_texY) {
|
||||
glDeleteTextures(1, &m_texY);
|
||||
m_texY = 0;
|
||||
}
|
||||
if (m_texUV) {
|
||||
glDeleteTextures(1, &m_texUV);
|
||||
m_texUV = 0;
|
||||
}
|
||||
|
||||
if (m_cuCtx) cuCtxPopCurrent(&prev);
|
||||
|
||||
if (m_cuCtx) {
|
||||
cuCtxDestroy(m_cuCtx);
|
||||
m_cuCtx = nullptr;
|
||||
}
|
||||
m_texSize = QSize();
|
||||
}
|
||||
|
||||
bool NvidiaPlayer::available() {
|
||||
if (cuInit(0) != CUDA_SUCCESS) return false;
|
||||
CUdevice dev = 0;
|
||||
return cuDeviceGet(&dev, 0) == CUDA_SUCCESS;
|
||||
}
|
||||
|
||||
bool NvidiaPlayer::openDecoder(const QString& path) {
|
||||
if (avformat_open_input(
|
||||
&m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) {
|
||||
qCWarning(
|
||||
videoLog,
|
||||
"VideoPlayer: could not open %s",
|
||||
path.toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
if (avformat_find_stream_info(m_fmt, nullptr) < 0) {
|
||||
qCWarning(videoLog, "VideoPlayer: could not read stream info");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_streamIndex = -1;
|
||||
for (unsigned i = 0; i < m_fmt->nb_streams; ++i)
|
||||
if (m_fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
m_streamIndex = int(i);
|
||||
break;
|
||||
}
|
||||
if (m_streamIndex < 0) {
|
||||
qCWarning(videoLog, "VideoPlayer: no video stream");
|
||||
return false;
|
||||
}
|
||||
|
||||
AVCodecParameters* par = m_fmt->streams[m_streamIndex]->codecpar;
|
||||
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
|
||||
const AVCodec* hwcodec = nullptr;
|
||||
void* it = nullptr;
|
||||
while (const AVCodec* c = av_codec_iterate(&it)) {
|
||||
if (c->id != par->codec_id || !av_codec_is_decoder(c)) continue;
|
||||
for (int i = 0;
|
||||
const AVCodecHWConfig* cfg = avcodec_get_hw_config(c, i);
|
||||
++i)
|
||||
if (cfg->device_type == AV_HWDEVICE_TYPE_CUDA &&
|
||||
(cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
||||
hwcodec = c;
|
||||
break;
|
||||
}
|
||||
if (hwcodec) break;
|
||||
}
|
||||
if (hwcodec) codec = hwcodec;
|
||||
if (!codec) {
|
||||
qCWarning(
|
||||
videoLog,
|
||||
"VideoPlayer: no decoder for codec id %d",
|
||||
int(par->codec_id));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_codec = avcodec_alloc_context3(codec);
|
||||
if (!m_codec) return false;
|
||||
if (avcodec_parameters_to_context(m_codec, par) < 0) {
|
||||
qCWarning(videoLog, "VideoPlayer: could not copy codec params");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_hwDeviceCtx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
|
||||
if (!m_hwDeviceCtx) {
|
||||
qCWarning(
|
||||
videoLog, "VideoPlayer: could not allocate cuda hwdevice context");
|
||||
return false;
|
||||
}
|
||||
AVCUDADeviceContext* cudaHw = reinterpret_cast<AVCUDADeviceContext*>(
|
||||
reinterpret_cast<AVHWDeviceContext*>(m_hwDeviceCtx->data)->hwctx);
|
||||
if (m_cuCtx) cudaHw->cuda_ctx = m_cuCtx;
|
||||
if (av_hwdevice_ctx_init(m_hwDeviceCtx) < 0) {
|
||||
qCWarning(
|
||||
videoLog, "VideoPlayer: could not create cuda hwdevice context");
|
||||
av_buffer_unref(&m_hwDeviceCtx);
|
||||
return false;
|
||||
}
|
||||
m_codec->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx);
|
||||
m_codec->get_format = chooseFormat;
|
||||
|
||||
if (avcodec_open2(m_codec, codec, nullptr) < 0) {
|
||||
qCWarning(videoLog, "VideoPlayer: could not open codec");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NvidiaPlayer::closeDecoder() {
|
||||
if (m_codec) {
|
||||
avcodec_free_context(&m_codec);
|
||||
m_codec = nullptr;
|
||||
}
|
||||
if (m_hwDeviceCtx) {
|
||||
av_buffer_unref(&m_hwDeviceCtx);
|
||||
m_hwDeviceCtx = nullptr;
|
||||
}
|
||||
if (m_fmt) {
|
||||
avformat_close_input(&m_fmt);
|
||||
m_fmt = nullptr;
|
||||
}
|
||||
m_streamIndex = -1;
|
||||
}
|
||||
|
||||
void NvidiaPlayer::decodeLoop(const QString& path, uint32_t generation) {
|
||||
if (!m_threadRunning.load(std::memory_order_acquire) ||
|
||||
m_generation.load(std::memory_order_acquire) != generation)
|
||||
return;
|
||||
|
||||
if (!openDecoder(path)) {
|
||||
m_threadRunning.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
AVRational fr = m_fmt->streams[m_streamIndex]->avg_frame_rate;
|
||||
if (fr.num <= 0 || fr.den <= 0) fr = m_codec->framerate;
|
||||
if (fr.num <= 0 || fr.den <= 0) fr = AVRational{30, 1};
|
||||
const int64_t intervalUs =
|
||||
av_rescale_q(1, AVRational{fr.den, fr.num}, AVRational{1, 1000000});
|
||||
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
if (!frame) {
|
||||
m_threadRunning.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
AVPacket pkt{};
|
||||
|
||||
while (m_threadRunning.load(std::memory_order_acquire) &&
|
||||
m_generation.load(std::memory_order_acquire) == generation) {
|
||||
int ret = av_read_frame(m_fmt, &pkt);
|
||||
if (ret == AVERROR_EOF) {
|
||||
if (avformat_seek_file(
|
||||
m_fmt, m_streamIndex, INT64_MIN, 0, INT64_MAX, 0) < 0) {
|
||||
qCWarning(videoLog, "VideoPlayer: loop seek failed; stopping");
|
||||
break;
|
||||
}
|
||||
avcodec_flush_buffers(m_codec);
|
||||
continue;
|
||||
}
|
||||
if (ret < 0) {
|
||||
qCWarning(videoLog, "VideoPlayer: read error; stopping");
|
||||
break;
|
||||
}
|
||||
if (pkt.stream_index != m_streamIndex) {
|
||||
av_packet_unref(&pkt);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (avcodec_send_packet(m_codec, &pkt) < 0) {
|
||||
av_packet_unref(&pkt);
|
||||
continue;
|
||||
}
|
||||
av_packet_unref(&pkt);
|
||||
|
||||
while (m_threadRunning.load(std::memory_order_acquire) &&
|
||||
m_generation.load(std::memory_order_acquire) == generation &&
|
||||
avcodec_receive_frame(m_codec, frame) == 0) {
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
if (!m_pending) m_pending = av_frame_alloc();
|
||||
av_frame_unref(m_pending);
|
||||
if (av_frame_ref(m_pending, frame) >= 0) {
|
||||
wasEmpty = !m_haveFrame;
|
||||
m_haveFrame = true;
|
||||
}
|
||||
}
|
||||
if (wasEmpty && m_readyCb) m_readyCb();
|
||||
if (m_frameCb) m_frameCb();
|
||||
av_frame_unref(frame);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(intervalUs));
|
||||
}
|
||||
|
||||
av_frame_free(&frame);
|
||||
{
|
||||
CudaCtx ctxGuard(m_cuCtx);
|
||||
closeDecoder();
|
||||
}
|
||||
m_threadRunning.store(false);
|
||||
}
|
||||
|
||||
bool NvidiaPlayer::start(const QString& path) {
|
||||
if (!ensureCudaContext()) return false;
|
||||
|
||||
const uint32_t generation =
|
||||
m_generation.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||
m_threadRunning.store(true);
|
||||
|
||||
std::thread previous = std::move(m_thread);
|
||||
m_thread = std::thread(
|
||||
[this, path, generation, previous = std::move(previous)]() mutable {
|
||||
if (previous.joinable()) previous.join();
|
||||
closeDecoder();
|
||||
decodeLoop(path, generation);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NvidiaPlayer::stop() {
|
||||
m_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
m_threadRunning.store(false);
|
||||
if (m_thread.joinable()) m_thread.join();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
CudaCtx ctxGuard(m_cuCtx);
|
||||
|
||||
closeDecoder();
|
||||
|
||||
if (m_pending) {
|
||||
av_frame_unref(m_pending);
|
||||
m_pending = nullptr;
|
||||
}
|
||||
if (m_displayed) {
|
||||
av_frame_unref(m_displayed);
|
||||
m_displayed = nullptr;
|
||||
}
|
||||
m_haveFrame = false;
|
||||
m_size = QSize();
|
||||
}
|
||||
|
||||
void NvidiaPlayer::deleteTexture() {
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
CUcontext prev = nullptr;
|
||||
if (m_cuCtx) cuCtxPushCurrent(m_cuCtx);
|
||||
if (m_resY) {
|
||||
cuGraphicsUnregisterResource(m_resY);
|
||||
m_resY = nullptr;
|
||||
}
|
||||
if (m_resUV) {
|
||||
cuGraphicsUnregisterResource(m_resUV);
|
||||
m_resUV = nullptr;
|
||||
}
|
||||
if (m_texY) {
|
||||
glDeleteTextures(1, &m_texY);
|
||||
m_texY = 0;
|
||||
}
|
||||
if (m_texUV) {
|
||||
glDeleteTextures(1, &m_texUV);
|
||||
m_texUV = 0;
|
||||
}
|
||||
if (m_cuCtx) cuCtxPopCurrent(&prev);
|
||||
m_texSize = QSize();
|
||||
}
|
||||
|
||||
bool NvidiaPlayer::ensureCudaContext() {
|
||||
if (m_cuCtx) return true;
|
||||
|
||||
if (cuInit(0) != CUDA_SUCCESS) {
|
||||
qCWarning(videoLog, "VideoPlayer: cuInit failed");
|
||||
return false;
|
||||
}
|
||||
CUdevice dev = 0;
|
||||
if (cuDeviceGet(&dev, 0) != CUDA_SUCCESS) {
|
||||
qCWarning(videoLog, "VideoPlayer: cuDeviceGet failed");
|
||||
return false;
|
||||
}
|
||||
if (cuCtxCreate_v4(&m_cuCtx, nullptr, 0, dev) != CUDA_SUCCESS) {
|
||||
m_cuCtx = nullptr;
|
||||
qCWarning(videoLog, "VideoPlayer: cuCtxCreate failed");
|
||||
return false;
|
||||
}
|
||||
CUcontext popped = nullptr;
|
||||
cuCtxPopCurrent(&popped);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NvidiaPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) {
|
||||
if (!ensureCudaContext()) return false;
|
||||
CUcontext prev = nullptr;
|
||||
cuCtxPushCurrent(m_cuCtx);
|
||||
|
||||
const QSize sz = QSize(int(w), int(h));
|
||||
if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) {
|
||||
cuCtxPopCurrent(&prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_resY) cuGraphicsUnregisterResource(m_resY);
|
||||
if (m_resUV) cuGraphicsUnregisterResource(m_resUV);
|
||||
m_resY = m_resUV = nullptr;
|
||||
if (m_texY) glDeleteTextures(1, &m_texY);
|
||||
if (m_texUV) glDeleteTextures(1, &m_texUV);
|
||||
m_texY = m_texUV = 0;
|
||||
|
||||
const bool wide = compBytes == 2;
|
||||
auto makeTex =
|
||||
[&](GLuint& tex, GLint internal, GLenum fmt, unsigned tw, unsigned th) {
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
internal,
|
||||
GLint(tw),
|
||||
GLint(th),
|
||||
0,
|
||||
fmt,
|
||||
wide ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
};
|
||||
makeTex(m_texY, wide ? GL_R16 : GL_R8, GL_RED, w, h);
|
||||
makeTex(m_texUV, wide ? GL_RG16 : GL_RG8, GL_RG, w / 2, h / 2);
|
||||
|
||||
CUresult r = cuGraphicsGLRegisterImage(
|
||||
&m_resY,
|
||||
m_texY,
|
||||
GL_TEXTURE_2D,
|
||||
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
|
||||
if (r != CUDA_SUCCESS) {
|
||||
const char* es = nullptr;
|
||||
cuGetErrorString(r, &es);
|
||||
qCWarning(
|
||||
videoLog,
|
||||
"VideoPlayer: register luma tex failed (%s)",
|
||||
es ? es : "?");
|
||||
m_texSize = QSize();
|
||||
cuCtxPopCurrent(&prev);
|
||||
return false;
|
||||
}
|
||||
r = cuGraphicsGLRegisterImage(
|
||||
&m_resUV,
|
||||
m_texUV,
|
||||
GL_TEXTURE_2D,
|
||||
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
|
||||
if (r != CUDA_SUCCESS) {
|
||||
qCWarning(videoLog, "VideoPlayer: register chroma tex failed");
|
||||
cuGraphicsUnregisterResource(m_resY);
|
||||
m_resY = nullptr;
|
||||
m_texSize = QSize();
|
||||
cuCtxPopCurrent(&prev);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_texSize = sz;
|
||||
m_texComp = compBytes;
|
||||
cuCtxPopCurrent(&prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NvidiaPlayer::importCurrentFrame() {
|
||||
AVFrame* frame = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
if (!m_pending || m_pending->format == AV_PIX_FMT_NONE) return false;
|
||||
frame = m_pending;
|
||||
m_pending = nullptr;
|
||||
}
|
||||
|
||||
if (frame->format != AV_PIX_FMT_CUDA || !frame->hw_frames_ctx) {
|
||||
static int logged = 0;
|
||||
if (logged++ < 3)
|
||||
qCWarning(
|
||||
videoLog,
|
||||
"VideoPlayer: frame not on GPU (fmt=%s hw_frames=%d)",
|
||||
av_get_pix_fmt_name(AVPixelFormat(frame->format)),
|
||||
frame->hw_frames_ctx != nullptr);
|
||||
av_frame_unref(frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
const AVHWFramesContext* hf =
|
||||
reinterpret_cast<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
|
||||
const AVPixelFormat sw = hf->sw_format;
|
||||
const bool tenBit = sw == AV_PIX_FMT_P010LE || sw == AV_PIX_FMT_P010BE ||
|
||||
sw == AV_PIX_FMT_P012LE || sw == AV_PIX_FMT_P016LE;
|
||||
const int compBytes = tenBit ? 2 : 1;
|
||||
const size_t compSize = size_t(compBytes);
|
||||
|
||||
const unsigned w = unsigned(frame->width);
|
||||
const unsigned h = unsigned(frame->height);
|
||||
if (w == 0 || h == 0) {
|
||||
av_frame_unref(frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pitchY = size_t(frame->linesize[0]);
|
||||
size_t pitchUV = frame->linesize[1] > 0 ? size_t(frame->linesize[1])
|
||||
: pitchY;
|
||||
if (pitchY == 0) pitchY = size_t(w) * compSize;
|
||||
|
||||
switch (frame->colorspace) {
|
||||
case AVCOL_SPC_BT470BG:
|
||||
case AVCOL_SPC_SMPTE170M:
|
||||
m_matrix = 0;
|
||||
break;
|
||||
case AVCOL_SPC_BT709:
|
||||
m_matrix = 1;
|
||||
break;
|
||||
default:
|
||||
m_matrix = h >= 720 ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
m_range = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0;
|
||||
m_size = QSize(int(w), int(h));
|
||||
|
||||
if (!ensureTextures(w, h, compBytes)) {
|
||||
av_frame_unref(frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
CUcontext prev = nullptr;
|
||||
cuCtxPushCurrent(m_cuCtx);
|
||||
CUgraphicsResource res[2] = {m_resY, m_resUV};
|
||||
CUresult r = cuGraphicsMapResources(2, res, nullptr);
|
||||
if (r != CUDA_SUCCESS) {
|
||||
cuCtxPopCurrent(&prev);
|
||||
av_frame_unref(frame);
|
||||
qCWarning(videoLog, "VideoPlayer: cuGraphicsMapResources failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
CUarray arrY = nullptr, arrUV = nullptr;
|
||||
cuGraphicsSubResourceGetMappedArray(&arrY, m_resY, 0, 0);
|
||||
cuGraphicsSubResourceGetMappedArray(&arrUV, m_resUV, 0, 0);
|
||||
|
||||
const size_t dstPitch = size_t(w) * compSize;
|
||||
CUDA_MEMCPY2D mc{};
|
||||
mc.srcMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
mc.dstMemoryType = CU_MEMORYTYPE_ARRAY;
|
||||
mc.srcDevice = CUdeviceptr(frame->data[0]);
|
||||
mc.srcPitch = pitchY;
|
||||
mc.dstArray = arrY;
|
||||
mc.dstPitch = dstPitch;
|
||||
mc.WidthInBytes = std::min(pitchY, dstPitch);
|
||||
mc.Height = h;
|
||||
r = cuMemcpy2D(&mc);
|
||||
if (r == CUDA_SUCCESS) {
|
||||
mc.srcDevice = CUdeviceptr(frame->data[1]);
|
||||
mc.srcPitch = pitchUV;
|
||||
mc.dstArray = arrUV;
|
||||
mc.dstPitch = dstPitch;
|
||||
mc.WidthInBytes = std::min(pitchUV, dstPitch);
|
||||
mc.Height = h / 2;
|
||||
r = cuMemcpy2D(&mc);
|
||||
}
|
||||
|
||||
cuGraphicsUnmapResources(2, res, nullptr);
|
||||
cuCtxPopCurrent(&prev);
|
||||
|
||||
if (r != CUDA_SUCCESS) {
|
||||
av_frame_unref(frame);
|
||||
qCWarning(videoLog, "VideoPlayer: cuMemcpy2D failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
if (m_displayed) av_frame_unref(m_displayed);
|
||||
m_displayed = frame;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
|
||||
#endif
|
||||
@@ -1,78 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ZHAVE_VIDEO
|
||||
|
||||
#include "videodecoder.hpp"
|
||||
|
||||
#include <EGL/egl.h>
|
||||
|
||||
#include <cuda.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/buffer.h>
|
||||
#include <libavutil/frame.h>
|
||||
#include <libavutil/hwcontext.h>
|
||||
#include <libavutil/hwcontext_cuda.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
}
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
|
||||
class NvidiaPlayer : public VideoDecoder {
|
||||
public:
|
||||
explicit NvidiaPlayer(EGLDisplay display);
|
||||
~NvidiaPlayer() override;
|
||||
|
||||
[[nodiscard]] static bool available();
|
||||
|
||||
bool start(const QString& path) override;
|
||||
void stop() override;
|
||||
bool importCurrentFrame() override;
|
||||
void deleteTexture() override;
|
||||
|
||||
[[nodiscard]] const char* backendName() const override { return "nvidia"; }
|
||||
|
||||
private:
|
||||
void decodeLoop(const QString& path, uint32_t generation);
|
||||
bool openDecoder(const QString& path);
|
||||
void closeDecoder();
|
||||
|
||||
bool ensureTextures(unsigned w, unsigned h, int compBytes);
|
||||
|
||||
bool ensureCudaContext();
|
||||
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_threadRunning{false};
|
||||
std::atomic<uint32_t> m_generation{0};
|
||||
|
||||
mutable std::mutex m_mtx;
|
||||
AVFrame* m_pending = nullptr;
|
||||
bool m_haveFrame = false;
|
||||
|
||||
AVFrame* m_displayed = nullptr;
|
||||
|
||||
AVFormatContext* m_fmt = nullptr;
|
||||
AVCodecContext* m_codec = nullptr;
|
||||
AVBufferRef* m_hwDeviceCtx = nullptr;
|
||||
int m_streamIndex = -1;
|
||||
|
||||
CUcontext m_cuCtx = nullptr;
|
||||
CUgraphicsResource m_resY = nullptr;
|
||||
CUgraphicsResource m_resUV = nullptr;
|
||||
QSize m_texSize;
|
||||
int m_texComp = 1;
|
||||
};
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
|
||||
#endif // ZHAVE_VIDEO
|
||||
@@ -1,11 +0,0 @@
|
||||
uniform sampler2D source;
|
||||
uniform vec2 size;
|
||||
uniform vec4 crop;
|
||||
uniform float alpha;
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
||||
vec4 c = texture(source, crop.xy + p * crop.zw);
|
||||
outColor = vec4(c.rgb, c.a * alpha);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
void main() {
|
||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Bow-tie wipe transition adapted from gl-transitions.com
|
||||
// Author: huynx (MIT)
|
||||
// Original: two triangles grow from the top and bottom edges into a bow tie
|
||||
// that reveals the incoming image.
|
||||
|
||||
uniform sampler2D from;
|
||||
uniform sampler2D to;
|
||||
uniform vec2 size;
|
||||
uniform vec4 fromCrop;
|
||||
uniform vec4 toCrop;
|
||||
uniform float progress;
|
||||
uniform float fromFlip;
|
||||
uniform float toFlip;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
vec4 getFromColor(vec2 uv) {
|
||||
vec2 s = vec2(uv.x, fromFlip > 0.5 ? 1.0 - uv.y : uv.y);
|
||||
return texture(from, fromCrop.xy + s * fromCrop.zw);
|
||||
}
|
||||
|
||||
vec4 getToColor(vec2 uv) {
|
||||
vec2 s = vec2(uv.x, toFlip > 0.5 ? 1.0 - uv.y : uv.y);
|
||||
return texture(to, toCrop.xy + s * toCrop.zw);
|
||||
}
|
||||
|
||||
float check(vec2 p1, vec2 p2, vec2 p3) {
|
||||
return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
|
||||
}
|
||||
|
||||
bool pointInTriangle(vec2 pt, vec2 p1, vec2 p2, vec2 p3) {
|
||||
bool b1 = check(pt, p1, p2) < 0.0;
|
||||
bool b2 = check(pt, p2, p3) < 0.0;
|
||||
bool b3 = check(pt, p3, p1) < 0.0;
|
||||
return (b1 == b2) && (b2 == b3);
|
||||
}
|
||||
|
||||
bool in_top_triangle(vec2 p) {
|
||||
vec2 vertex1 = vec2(0.5, progress);
|
||||
vec2 vertex2 = vec2(0.5 - progress, 0.0);
|
||||
vec2 vertex3 = vec2(0.5 + progress, 0.0);
|
||||
return pointInTriangle(p, vertex1, vertex2, vertex3);
|
||||
}
|
||||
|
||||
bool in_bottom_triangle(vec2 p) {
|
||||
vec2 vertex1 = vec2(0.5, 1.0 - progress);
|
||||
vec2 vertex2 = vec2(0.5 - progress, 1.0);
|
||||
vec2 vertex3 = vec2(0.5 + progress, 1.0);
|
||||
return pointInTriangle(p, vertex1, vertex2, vertex3);
|
||||
}
|
||||
|
||||
float blur_edge(vec2 bot1, vec2 bot2, vec2 top, vec2 testPt) {
|
||||
vec2 lineDir = bot1 - top;
|
||||
vec2 perpDir = vec2(lineDir.y, -lineDir.x);
|
||||
vec2 dirToPt1 = bot1 - testPt;
|
||||
float dist1 = abs(dot(normalize(perpDir), dirToPt1));
|
||||
|
||||
lineDir = bot2 - top;
|
||||
perpDir = vec2(lineDir.y, -lineDir.x);
|
||||
dirToPt1 = bot2 - testPt;
|
||||
float min_dist = min(abs(dot(normalize(perpDir), dirToPt1)), dist1);
|
||||
|
||||
if (min_dist < 0.005)
|
||||
return min_dist / 0.005;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
if (in_top_triangle(uv)) {
|
||||
if (progress < 0.1)
|
||||
return getFromColor(uv);
|
||||
|
||||
if (uv.y < 0.5) {
|
||||
vec2 vertex1 = vec2(0.5, progress);
|
||||
vec2 vertex2 = vec2(0.5 - progress, 0.0);
|
||||
vec2 vertex3 = vec2(0.5 + progress, 0.0);
|
||||
return mix(
|
||||
getFromColor(uv),
|
||||
getToColor(uv),
|
||||
blur_edge(vertex2, vertex3, vertex1, uv));
|
||||
}
|
||||
if (progress > 0.0)
|
||||
return getToColor(uv);
|
||||
return getFromColor(uv);
|
||||
}
|
||||
|
||||
if (in_bottom_triangle(uv)) {
|
||||
if (uv.y >= 0.5) {
|
||||
vec2 vertex1 = vec2(0.5, 1.0 - progress);
|
||||
vec2 vertex2 = vec2(0.5 - progress, 1.0);
|
||||
vec2 vertex3 = vec2(0.5 + progress, 1.0);
|
||||
return mix(
|
||||
getFromColor(uv),
|
||||
getToColor(uv),
|
||||
blur_edge(vertex2, vertex3, vertex1, uv));
|
||||
}
|
||||
return getFromColor(uv);
|
||||
}
|
||||
|
||||
return getFromColor(uv);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user