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()
|
||||
}
|
||||
|
||||
+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 {
|
||||
|
||||
@@ -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,19 +75,27 @@ Item {
|
||||
|
||||
implicitHeight: indicator.indicatorThickness
|
||||
implicitWidth: indicator.indicatorThickness
|
||||
radius: Tokens.rounding.full
|
||||
width: root.workspaceButtonWidth
|
||||
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
text: button.modelData?.name ?? ""
|
||||
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}`);
|
||||
}
|
||||
onPressed: {
|
||||
const ws = button.modelData.name;
|
||||
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,10 +104,8 @@ 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 {
|
||||
Layout.fillHeight: true
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
lock: root.lock
|
||||
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
|
||||
|
||||
+26
-31
@@ -7,44 +7,39 @@ import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
ClipRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
layer.enabled: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: Colors.palette.m3surface
|
||||
opacity: 0.7
|
||||
}
|
||||
color: Colors.palette.m3surface
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+101
-91
@@ -11,116 +11,126 @@ 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
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.small
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||
}
|
||||
ClipRect {
|
||||
id: clipRect
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: NotifServer.list.length > 0 ? 0 : 1
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: NotifServer.list.length > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.extraLarge
|
||||
}
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Notifications")
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.extraLarge
|
||||
}
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
CustomListView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
spacing: Tokens.spacing.small
|
||||
Image {
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
layer.enabled: true
|
||||
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
|
||||
sourceSize.width: clipRect.width * 0.8
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
from: 0
|
||||
property: "scale"
|
||||
to: 1
|
||||
layer.effect: Coloriser {
|
||||
brightness: 1
|
||||
colorizationColor: Colors.palette.m3outlineVariant
|
||||
}
|
||||
}
|
||||
delegate: NotifGroup {}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Notifications")
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
||||
return [...new Map(list).keys()];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
CustomListView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 0.6
|
||||
}
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
from: 0
|
||||
property: "scale"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: NotifGroup {}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
||||
return [...new Map(list).keys()];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
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
|
||||
radius: Tokens.rounding.medium
|
||||
visible: opacity > 0
|
||||
clip: true
|
||||
|
||||
CustomRect {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
|
||||
@@ -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,26 +312,17 @@ 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;
|
||||
}
|
||||
const auto formats = QImageReader::supportedImageFormats();
|
||||
for (const auto& format : formats) {
|
||||
extraNameFilters << "*." + format;
|
||||
}
|
||||
|
||||
QDir::Filters filters = QDir::Files;
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Doom-style melt wipe, from gl-transitions.com
|
||||
// Author: Zeh Fernando (MIT)
|
||||
// Description: the outgoing image melts down in uneven columns, like the Doom
|
||||
// screen wipe. Fully procedural (no displacement texture).
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). Optional
|
||||
// gl-transitions params are fixed here: bars = 30, amplitude = 2.0,
|
||||
// noise = 0.1, frequency = 0.5, dripScale = 0.5.
|
||||
|
||||
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;
|
||||
|
||||
const int bars = 30;
|
||||
const float amplitude = 2.0;
|
||||
const float noise = 0.1;
|
||||
const float frequency = 0.5;
|
||||
const float dripScale = 0.5;
|
||||
|
||||
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 rand(int num) {
|
||||
return fract(mod(float(num) * 67123.313, 12.0) * sin(float(num) * 10.3) *
|
||||
cos(float(num)));
|
||||
}
|
||||
|
||||
float wave(int num) {
|
||||
float fn = float(num) * frequency * 0.1 * float(bars);
|
||||
return cos(fn * 0.5) * cos(fn * 0.13) * sin((fn + 10.0) * 0.3) / 2.0 + 0.5;
|
||||
}
|
||||
|
||||
float drip(int num) {
|
||||
return sin(float(num) / float(bars - 1) * 3.141592) * dripScale;
|
||||
}
|
||||
|
||||
float pos(int num) {
|
||||
return (noise == 0.0 ? wave(num)
|
||||
: mix(wave(num), rand(num), noise)) +
|
||||
(dripScale == 0.0 ? 0.0 : drip(num));
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
int bar = int(uv.x * float(bars));
|
||||
float scale = 1.0 + pos(bar) * amplitude;
|
||||
float phase = progress * scale;
|
||||
float posY = uv.y;
|
||||
|
||||
if (phase + posY < 1.0) {
|
||||
return getFromColor(vec2(uv.x, uv.y + phase));
|
||||
}
|
||||
return getToColor(uv);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Fade (crossfade) transition, from gl-transitions.com.
|
||||
//
|
||||
// Ported to ZShell's own GLSL ABI (same contract as wallpaper_trans_bowtie.frag):
|
||||
// version/precision header is prepended by WallpaperService::shaderSource, the
|
||||
// fullscreen triangle comes from shaders/wallpaper.vert, and getFromColor /
|
||||
// getToColor sample with the crop + per-layer flip convention.
|
||||
//
|
||||
// uniforms: samplers `from`/`to`, vec2 size, vec4 fromCrop, vec4 toCrop,
|
||||
// float fromFlip, float toFlip, float progress (0..1).
|
||||
|
||||
uniform sampler2D from;
|
||||
uniform sampler2D to;
|
||||
uniform vec2 size;
|
||||
uniform vec4 fromCrop;
|
||||
uniform vec4 toCrop;
|
||||
uniform float fromFlip;
|
||||
uniform float toFlip;
|
||||
uniform float progress;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
return mix(getFromColor(uv), getToColor(uv), progress);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// Hexagonalize crossfade, from gl-transitions.com
|
||||
// Author: Fernando Kuteken (MIT)
|
||||
// Description: both images turn into hexagons, crossfade, then sharpen again.
|
||||
// Hexagonal math from http://www.redblobgames.com/grids/hexagons/
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag). The
|
||||
// gl-transitions `ratio` contextual value is derived from `size`. Optional
|
||||
// params are fixed here: steps = 50, horizontalHexagons = 20.
|
||||
|
||||
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;
|
||||
|
||||
const int steps = 50;
|
||||
const float horizontalHexagons = 20.0;
|
||||
float ratio;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
struct Hexagon {
|
||||
float q;
|
||||
float r;
|
||||
float s;
|
||||
};
|
||||
|
||||
Hexagon createHexagon(float q, float r) {
|
||||
Hexagon hex;
|
||||
hex.q = q;
|
||||
hex.r = r;
|
||||
hex.s = -q - r;
|
||||
return hex;
|
||||
}
|
||||
|
||||
Hexagon roundHexagon(Hexagon hex) {
|
||||
float q = floor(hex.q + 0.5);
|
||||
float r = floor(hex.r + 0.5);
|
||||
float s = floor(hex.s + 0.5);
|
||||
|
||||
float deltaQ = abs(q - hex.q);
|
||||
float deltaR = abs(r - hex.r);
|
||||
float deltaS = abs(s - hex.s);
|
||||
|
||||
if (deltaQ > deltaR && deltaQ > deltaS)
|
||||
q = -r - s;
|
||||
else if (deltaR > deltaS)
|
||||
r = -q - s;
|
||||
else
|
||||
s = -q - r;
|
||||
|
||||
return createHexagon(q, r);
|
||||
}
|
||||
|
||||
Hexagon hexagonFromPoint(vec2 point, float size) {
|
||||
point.y /= ratio;
|
||||
point = (point - 0.5) / size;
|
||||
|
||||
float q = (sqrt(3.0) / 3.0) * point.x + (-1.0 / 3.0) * point.y;
|
||||
float r = 0.0 * point.x + 2.0 / 3.0 * point.y;
|
||||
|
||||
Hexagon hex = createHexagon(q, r);
|
||||
return roundHexagon(hex);
|
||||
}
|
||||
|
||||
vec2 pointFromHexagon(Hexagon hex, float size) {
|
||||
float x = (sqrt(3.0) * hex.q + (sqrt(3.0) / 2.0) * hex.r) * size + 0.5;
|
||||
float y = (0.0 * hex.q + (3.0 / 2.0) * hex.r) * size + 0.5;
|
||||
return vec2(x, y * ratio);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
float dist = 2.0 * min(progress, 1.0 - progress);
|
||||
dist = steps > 0 ? ceil(dist * float(steps)) / float(steps) : dist;
|
||||
|
||||
float size = (sqrt(3.0) / 3.0) * dist / horizontalHexagons;
|
||||
|
||||
vec2 point = dist > 0.0 ? pointFromHexagon(hexagonFromPoint(uv, size), size)
|
||||
: uv;
|
||||
|
||||
return mix(getFromColor(point), getToColor(point), progress);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
ratio = size.x / size.y;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Linear blur crossfade, from gl-transitions.com
|
||||
// Author: gre (MIT)
|
||||
// Description: a crossfade that passes through a directional blur.
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag): the version
|
||||
// header is prepended by WallpaperService::shaderSource, the fullscreen
|
||||
// triangle comes from wallpaper.vert, and getFromColor/getToColor apply the
|
||||
// crop + flip convention. `uv` is bottom-left origin per gl-transitions.
|
||||
//
|
||||
// Optional gl-transitions params are fixed here: intensity = 0.1.
|
||||
|
||||
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;
|
||||
|
||||
const float intensity = 0.1;
|
||||
const int passes = 6;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
vec4 c1 = vec4(0.0);
|
||||
vec4 c2 = vec4(0.0);
|
||||
|
||||
float disp = intensity * (0.5 - distance(0.5, progress));
|
||||
for (int xi = 0; xi < passes; xi++) {
|
||||
float x = float(xi) / float(passes) - 0.5;
|
||||
for (int yi = 0; yi < passes; yi++) {
|
||||
float y = float(yi) / float(passes) - 0.5;
|
||||
vec2 v = vec2(x, y);
|
||||
c1 += getFromColor(uv + disp * v);
|
||||
c2 += getToColor(uv + disp * v);
|
||||
}
|
||||
}
|
||||
c1 /= float(passes * passes);
|
||||
c2 /= float(passes * passes);
|
||||
return mix(c1, c2, progress);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Split slide-in (horizontal), from gl-transitions.com
|
||||
// Author: OllyOllyOlly (MIT)
|
||||
// Description: the incoming image slides in as two halves moving horizontally
|
||||
// in opposite directions.
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
|
||||
// Optional gl-transitions params are fixed here: reverse = false.
|
||||
|
||||
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;
|
||||
|
||||
const vec2 boundMin = vec2(0.0, 0.0);
|
||||
const vec2 boundMax = vec2(1.0, 1.0);
|
||||
|
||||
bool inBounds(vec2 p) {
|
||||
return all(lessThan(boundMin, p)) && all(lessThan(p, boundMax));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
float modifier = 1.0;
|
||||
vec2 toP = (uv.y > 0.5)
|
||||
? vec2((uv.x - (progress * modifier)) + modifier, uv.y)
|
||||
: vec2((uv.x + (progress * modifier)) - modifier, uv.y);
|
||||
|
||||
return inBounds(toP) ? getToColor(toP) : getFromColor(uv);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Tiles wave reveal, from gl-transitions.com
|
||||
// Author: numb3r23 (MIT)
|
||||
// Description: the incoming image is revealed tile by tile in a diagonal wave.
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
|
||||
// Optional gl-transitions params are fixed here: tileCount = (8, 8),
|
||||
// flipX = true, flipY = false.
|
||||
|
||||
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;
|
||||
|
||||
const ivec2 tileCount = ivec2(8, 8);
|
||||
const bool flipX = true;
|
||||
const bool flipY = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
vec2 tileSize = 1.0 / vec2(tileCount);
|
||||
vec2 posInTile = fract(uv * vec2(tileCount));
|
||||
vec2 tileNum = floor(uv * vec2(tileCount));
|
||||
float countTiles = float(tileCount.x * tileCount.y);
|
||||
|
||||
// Diagonal wave from bottom-left to top-right.
|
||||
float offset = (tileNum.y + tileNum.x * float(tileCount.y)) / countTiles;
|
||||
float timeOffset = clamp((progress - offset) * countTiles, 0.0, 0.5);
|
||||
float sinTime = 1.0 - abs(cos(fract(timeOffset) * 3.1415926));
|
||||
|
||||
vec2 texC = posInTile;
|
||||
|
||||
if (sinTime <= 0.5) {
|
||||
if (flipX) {
|
||||
if (texC.x < sinTime || texC.x > 1.0 - sinTime)
|
||||
return getFromColor(uv);
|
||||
texC.x = texC.x < 0.5
|
||||
? (texC.x - sinTime) * 0.5 / (0.5 - sinTime)
|
||||
: (texC.x - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
|
||||
}
|
||||
if (flipY) {
|
||||
if (texC.y < sinTime || texC.y > 1.0 - sinTime)
|
||||
return getFromColor(uv);
|
||||
texC.y = texC.y < 0.5
|
||||
? (texC.y - sinTime) * 0.5 / (0.5 - sinTime)
|
||||
: (texC.y - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
|
||||
}
|
||||
vec2 globalUV = tileNum * tileSize + texC * tileSize;
|
||||
return getFromColor(globalUV);
|
||||
} else {
|
||||
if (flipX) {
|
||||
if (texC.x > sinTime || texC.x < 1.0 - sinTime)
|
||||
return getToColor(uv);
|
||||
texC.x = texC.x < 0.5
|
||||
? (texC.x - sinTime) * 0.5 / (0.5 - sinTime)
|
||||
: (texC.x - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
|
||||
texC.x = 1.0 - texC.x;
|
||||
}
|
||||
if (flipY) {
|
||||
if (texC.y > sinTime || texC.y < 1.0 - sinTime)
|
||||
return getToColor(uv);
|
||||
texC.y = texC.y < 0.5
|
||||
? (texC.y - sinTime) * 0.5 / (0.5 - sinTime)
|
||||
: (texC.y - 0.5) * 0.5 / (0.5 - sinTime) + 0.5;
|
||||
texC.y = 1.0 - texC.y;
|
||||
}
|
||||
vec2 globalUV = tileNum * tileSize + texC * tileSize;
|
||||
return getToColor(globalUV);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Window slices wipe, from gl-transitions.com
|
||||
// Author: gre (MIT)
|
||||
// Description: the incoming image appears through vertical slices that widen.
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
|
||||
// Optional gl-transitions params are fixed here: count = 10.0,
|
||||
// smoothness = 0.5.
|
||||
|
||||
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;
|
||||
|
||||
const float count = 10.0;
|
||||
const float smoothness = 0.5;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
float pr = smoothstep(-smoothness, 0.0, uv.x - progress * (1.0 + smoothness));
|
||||
float s = step(pr, fract(count * uv.x));
|
||||
return mix(getFromColor(uv), getToColor(uv), s);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Hard-edged wipe from top to bottom, from gl-transitions.com
|
||||
// Author: Jake Nelson (MIT)
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
return mix(getFromColor(uv), getToColor(uv), step(1.0 - uv.y, progress));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Hard-edged wipe from left to right, from gl-transitions.com
|
||||
// Author: Jake Nelson (MIT)
|
||||
//
|
||||
// Ported to ZShell's GLSL ABI (see wallpaper_trans_bowtie.frag).
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
vec4 transition(vec2 uv) {
|
||||
return mix(getFromColor(uv), getToColor(uv), step(0.0 + uv.x, progress));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / size;
|
||||
outColor = transition(uv);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
uniform sampler2D luma;
|
||||
uniform sampler2D chroma;
|
||||
uniform vec2 size;
|
||||
uniform vec2 srcSize;
|
||||
uniform vec4 crop;
|
||||
uniform float alpha;
|
||||
uniform int matrix;
|
||||
uniform int range;
|
||||
uniform vec2 chromaOff;
|
||||
out vec4 outColor;
|
||||
|
||||
float noise(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
const float PI = 3.1415926535893;
|
||||
|
||||
float sinc(float x) {
|
||||
x *= PI;
|
||||
return abs(x) < 1e-6 ? 1.0 : sin(x) / x;
|
||||
}
|
||||
|
||||
float lanczos(float x) {
|
||||
if (abs(x) >= 3.0)
|
||||
return 0.0;
|
||||
return sinc(x) * sinc(x / 3.0);
|
||||
}
|
||||
|
||||
float hermite(float x) {
|
||||
float a = abs(x);
|
||||
if (a >= 1.0)
|
||||
return 0.0;
|
||||
return 1.0 - 3.0 * a * a + 2.0 * a * a * a;
|
||||
}
|
||||
|
||||
vec4 resample(sampler2D tex, vec2 plane, vec2 dest, vec2 uv) {
|
||||
vec2 pt = 1.0 / plane;
|
||||
float wx[6];
|
||||
float wy[6];
|
||||
vec2 base;
|
||||
|
||||
int upx = dest.x >= plane.x ? 1 : 0;
|
||||
int tx = upx == 1 ? 6 : 2;
|
||||
float fsx = max(1.0, plane.x / dest.x);
|
||||
float f = fract(uv.x * plane.x - 0.5);
|
||||
base.x = uv.x - f * pt.x - pt.x * (float(tx) * 0.5 - 1.0);
|
||||
float sx = 0.0;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (i >= tx) { wx[i] = 0.0; continue; }
|
||||
float x = f - (float(i) - float(tx) * 0.5 + 1.0);
|
||||
wx[i] = upx == 1 ? lanczos(x / fsx) : hermite(x / fsx);
|
||||
sx += wx[i];
|
||||
}
|
||||
if (sx != 0.0)
|
||||
for (int i = 0; i < 6; ++i) wx[i] /= sx;
|
||||
|
||||
int upy = dest.y >= plane.y ? 1 : 0;
|
||||
int ty = upy == 1 ? 6 : 2;
|
||||
float fsy = max(1.0, plane.y / dest.y);
|
||||
f = fract(uv.y * plane.y - 0.5);
|
||||
base.y = uv.y - f * pt.y - pt.y * (float(ty) * 0.5 - 1.0);
|
||||
float sy = 0.0;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (i >= ty) { wy[i] = 0.0; continue; }
|
||||
float y = f - (float(i) - float(ty) * 0.5 + 1.0);
|
||||
wy[i] = upy == 1 ? lanczos(y / fsy) : hermite(y / fsy);
|
||||
sy += wy[i];
|
||||
}
|
||||
if (sy != 0.0)
|
||||
for (int i = 0; i < 6; ++i) wy[i] /= sy;
|
||||
|
||||
vec4 col = vec4(0.0);
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (wx[i] == 0.0)
|
||||
continue;
|
||||
for (int j = 0; j < 6; ++j) {
|
||||
if (wy[j] == 0.0)
|
||||
continue;
|
||||
vec2 c = base + vec2(float(i) * pt.x, float(j) * pt.y);
|
||||
col += (wx[i] * wy[j]) * texture(tex, c);
|
||||
}
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
||||
vec2 uv = crop.xy + p * crop.zw;
|
||||
float Yv = resample(luma, srcSize, size, uv).r;
|
||||
vec2 ch = resample(chroma, srcSize * 0.5, size, uv + chromaOff).rg;
|
||||
vec3 yuv;
|
||||
if (range == 0) {
|
||||
yuv = vec3((Yv - 16.0 / 255.0) / (219.0 / 255.0),
|
||||
(ch.r - 128.0 / 255.0) / (224.0 / 255.0),
|
||||
(ch.g - 128.0 / 255.0) / (224.0 / 255.0));
|
||||
} else {
|
||||
yuv = vec3(Yv, ch.r - 0.5, ch.g - 0.5);
|
||||
}
|
||||
mat3 m = matrix == 1
|
||||
? mat3(1.0, 1.0, 1.0, 0.0, -0.187324, 1.855600, 1.574800, -0.468124, 0.0)
|
||||
: mat3(1.0, 1.0, 1.0, 0.0, -0.344136, 1.772000, 1.402000, -0.714136, 0.0);
|
||||
vec3 rgb = m * yuv;
|
||||
rgb += (noise(gl_FragCoord.xy) - 0.5) / 255.0;
|
||||
outColor = vec4(clamp(rgb, 0.0, 1.0), alpha);
|
||||
}
|
||||
@@ -1,662 +0,0 @@
|
||||
#include "vaapiplayer.hpp"
|
||||
|
||||
#ifdef ZHAVE_FFMPEG
|
||||
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
#ifdef ZHAVE_VAAPI
|
||||
#include <unistd.h>
|
||||
extern "C" {
|
||||
#include <va/va.h>
|
||||
#include <va/va_drmcommon.h>
|
||||
#include <libavutil/hwcontext_vaapi.h>
|
||||
}
|
||||
#ifndef DRM_FORMAT_MOD_INVALID
|
||||
#define DRM_FORMAT_MOD_INVALID 0xffffffffffffffffULL
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
|
||||
#include <QtLogging>
|
||||
|
||||
Q_LOGGING_CATEGORY(vaapiLog, "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_VAAPI) return AV_PIX_FMT_VAAPI;
|
||||
return pix_fmts[0];
|
||||
}
|
||||
|
||||
#ifdef ZHAVE_VAAPI
|
||||
using GLEGLImageTargetProc = void (*)(GLenum, EGLImageKHR);
|
||||
|
||||
GLEGLImageTargetProc glEGLImageTarget() {
|
||||
static GLEGLImageTargetProc fn = reinterpret_cast<GLEGLImageTargetProc>(
|
||||
eglGetProcAddress("glEGLImageTargetTexture2DOES"));
|
||||
return fn;
|
||||
}
|
||||
|
||||
// eglCreateImageKHR/eglDestroyImageKHR are EGL extension entry points. Fetch
|
||||
// them through eglGetProcAddress rather than linking them directly, so the
|
||||
// shared library does not carry a hard undefined reference to symbols libEGL
|
||||
// may not export on every platform.
|
||||
using PfnCreateImageKHR = EGLImageKHR (*)(
|
||||
EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
|
||||
using PfnDestroyImageKHR = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
|
||||
|
||||
PfnCreateImageKHR eglCreateImageProc() {
|
||||
static PfnCreateImageKHR fn = reinterpret_cast<PfnCreateImageKHR>(
|
||||
eglGetProcAddress("eglCreateImageKHR"));
|
||||
return fn;
|
||||
}
|
||||
|
||||
PfnDestroyImageKHR eglDestroyImageProc() {
|
||||
static PfnDestroyImageKHR fn = reinterpret_cast<PfnDestroyImageKHR>(
|
||||
eglGetProcAddress("eglDestroyImageKHR"));
|
||||
return fn;
|
||||
}
|
||||
|
||||
void destroyEglImage(EGLDisplay dpy, EGLImageKHR image) {
|
||||
if (image == EGL_NO_IMAGE_KHR) return;
|
||||
PfnDestroyImageKHR fn = eglDestroyImageProc();
|
||||
if (!fn) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
qCWarning(
|
||||
vaapiLog,
|
||||
"VideoPlayer: eglDestroyImageKHR unavailable, EGL images leak");
|
||||
}
|
||||
return;
|
||||
}
|
||||
fn(dpy, image);
|
||||
}
|
||||
|
||||
EGLImageKHR createDmaImage(
|
||||
EGLDisplay dpy,
|
||||
int fd,
|
||||
uint32_t offset,
|
||||
uint32_t pitch,
|
||||
uint32_t fourcc,
|
||||
uint64_t modifier) {
|
||||
PfnCreateImageKHR fn = eglCreateImageProc();
|
||||
if (!fn) return EGL_NO_IMAGE_KHR;
|
||||
|
||||
EGLint attrs[16];
|
||||
int n = 0;
|
||||
attrs[n++] = EGL_LINUX_DRM_FOURCC_EXT;
|
||||
attrs[n++] = EGLint(fourcc);
|
||||
attrs[n++] = EGL_DMA_BUF_PLANE0_FD_EXT;
|
||||
attrs[n++] = fd;
|
||||
attrs[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
|
||||
attrs[n++] = EGLint(offset);
|
||||
attrs[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
|
||||
attrs[n++] = EGLint(pitch);
|
||||
if (modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
|
||||
attrs[n++] = EGLint(modifier & 0xffffffffULL);
|
||||
attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
|
||||
attrs[n++] = EGLint(modifier >> 32);
|
||||
}
|
||||
attrs[n] = EGL_NONE;
|
||||
|
||||
EGLImageKHR image =
|
||||
fn(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs);
|
||||
if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR;
|
||||
return image;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
VaapiPlayer::VaapiPlayer(EGLDisplay display) : m_eglDisplay(display) {}
|
||||
|
||||
VaapiPlayer::~VaapiPlayer() {
|
||||
stop();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
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_hwDeviceCtx) {
|
||||
av_buffer_unref(&m_hwDeviceCtx);
|
||||
m_hwDeviceCtx = nullptr;
|
||||
}
|
||||
|
||||
#ifdef ZHAVE_VAAPI
|
||||
if (m_imageY != EGL_NO_IMAGE_KHR) {
|
||||
destroyEglImage(m_eglDisplay, m_imageY);
|
||||
m_imageY = EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
if (m_imageUV != EGL_NO_IMAGE_KHR) {
|
||||
destroyEglImage(m_eglDisplay, m_imageUV);
|
||||
m_imageUV = EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool VaapiPlayer::available() {
|
||||
AVBufferRef* ref = nullptr;
|
||||
if (av_hwdevice_ctx_create(
|
||||
&ref, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) {
|
||||
if (ref) av_buffer_unref(&ref);
|
||||
return false;
|
||||
}
|
||||
av_buffer_unref(&ref);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VaapiPlayer::ensureDevice() {
|
||||
if (m_hwDeviceCtx) return true;
|
||||
if (av_hwdevice_ctx_create(
|
||||
&m_hwDeviceCtx, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) {
|
||||
if (m_hwDeviceCtx) av_buffer_unref(&m_hwDeviceCtx);
|
||||
m_hwDeviceCtx = nullptr;
|
||||
qCWarning(vaapiLog, "VideoPlayer: could not create vaapi hwdevice");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VaapiPlayer::openDecoder(const QString& path) {
|
||||
if (avformat_open_input(
|
||||
&m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) {
|
||||
qCWarning(
|
||||
vaapiLog,
|
||||
"VideoPlayer: could not open %s",
|
||||
path.toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
if (avformat_find_stream_info(m_fmt, nullptr) < 0) {
|
||||
qCWarning(vaapiLog, "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(vaapiLog, "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_VAAPI &&
|
||||
(cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
||||
hwcodec = c;
|
||||
break;
|
||||
}
|
||||
if (hwcodec) break;
|
||||
}
|
||||
if (hwcodec) codec = hwcodec;
|
||||
if (!codec) {
|
||||
qCWarning(
|
||||
vaapiLog,
|
||||
"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(vaapiLog, "VideoPlayer: could not copy codec params");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ensureDevice()) {
|
||||
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(vaapiLog, "VideoPlayer: could not open codec");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VaapiPlayer::closeDecoder() {
|
||||
if (m_codec) {
|
||||
avcodec_free_context(&m_codec);
|
||||
m_codec = nullptr;
|
||||
}
|
||||
if (m_fmt) {
|
||||
avformat_close_input(&m_fmt);
|
||||
m_fmt = nullptr;
|
||||
}
|
||||
m_streamIndex = -1;
|
||||
}
|
||||
|
||||
void VaapiPlayer::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(vaapiLog, "VideoPlayer: loop seek failed; stopping");
|
||||
break;
|
||||
}
|
||||
avcodec_flush_buffers(m_codec);
|
||||
continue;
|
||||
}
|
||||
if (ret < 0) {
|
||||
qCWarning(vaapiLog, "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);
|
||||
closeDecoder();
|
||||
m_threadRunning.store(false);
|
||||
}
|
||||
|
||||
bool VaapiPlayer::start(const QString& path) {
|
||||
if (!ensureDevice()) 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 VaapiPlayer::stop() {
|
||||
m_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
m_threadRunning.store(false);
|
||||
if (m_thread.joinable()) m_thread.join();
|
||||
|
||||
closeDecoder();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
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 VaapiPlayer::deleteTexture() {
|
||||
#ifdef ZHAVE_VAAPI
|
||||
if (m_imageY != EGL_NO_IMAGE_KHR) {
|
||||
destroyEglImage(m_eglDisplay, m_imageY);
|
||||
m_imageY = EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
if (m_imageUV != EGL_NO_IMAGE_KHR) {
|
||||
destroyEglImage(m_eglDisplay, m_imageUV);
|
||||
m_imageUV = EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
#endif
|
||||
if (m_texY) {
|
||||
glDeleteTextures(1, &m_texY);
|
||||
m_texY = 0;
|
||||
}
|
||||
if (m_texUV) {
|
||||
glDeleteTextures(1, &m_texUV);
|
||||
m_texUV = 0;
|
||||
}
|
||||
m_texSize = QSize();
|
||||
}
|
||||
|
||||
bool VaapiPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) {
|
||||
const QSize sz = QSize(int(w), int(h));
|
||||
if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) return true;
|
||||
|
||||
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);
|
||||
|
||||
m_texSize = sz;
|
||||
m_texComp = compBytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VaapiPlayer::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_VAAPI || !frame->hw_frames_ctx) {
|
||||
static int logged = 0;
|
||||
if (logged++ < 3)
|
||||
qCWarning(
|
||||
vaapiLog,
|
||||
"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;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
#ifdef ZHAVE_VAAPI
|
||||
if (importEglImages(frame)) {
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
if (m_displayed) av_frame_unref(m_displayed);
|
||||
m_displayed = frame;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
AVFrame* tmp = av_frame_alloc();
|
||||
if (!tmp) {
|
||||
av_frame_unref(frame);
|
||||
return false;
|
||||
}
|
||||
if (av_hwframe_transfer_data(tmp, frame, 0) < 0) {
|
||||
av_frame_free(&tmp);
|
||||
av_frame_unref(frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pitchY = size_t(tmp->linesize[0]);
|
||||
size_t pitchUV = tmp->linesize[1] > 0 ? size_t(tmp->linesize[1]) : pitchY;
|
||||
if (pitchY == 0) pitchY = size_t(w) * compSize;
|
||||
|
||||
if (!ensureTextures(w, h, compBytes)) {
|
||||
av_frame_free(&tmp);
|
||||
av_frame_unref(frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLenum type = compBytes == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE;
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_texY);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchY / compSize));
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D, 0, 0, 0, GLint(w), GLint(h), GL_RED, type, tmp->data[0]);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_texUV);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchUV / (compSize * 2)));
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
GLint(w / 2),
|
||||
GLint(h / 2),
|
||||
GL_RG,
|
||||
type,
|
||||
tmp->data[1]);
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
|
||||
av_frame_free(&tmp);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mtx);
|
||||
if (m_displayed) av_frame_unref(m_displayed);
|
||||
m_displayed = frame;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef ZHAVE_VAAPI
|
||||
bool VaapiPlayer::importEglImages(AVFrame* frame) {
|
||||
if (m_eglDisplay == EGL_NO_DISPLAY) return false;
|
||||
GLEGLImageTargetProc target = glEGLImageTarget();
|
||||
if (!target) return false;
|
||||
|
||||
const AVHWFramesContext* hf =
|
||||
reinterpret_cast<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
|
||||
const AVHWDeviceContext* dev = hf->device_ctx;
|
||||
const AVVAAPIDeviceContext* va =
|
||||
reinterpret_cast<const AVVAAPIDeviceContext*>(dev->hwctx);
|
||||
if (!va->display) return false;
|
||||
|
||||
const VASurfaceID surface = VASurfaceID(uintptr_t(frame->data[0]));
|
||||
|
||||
VADRMPRIMESurfaceDescriptor desc{};
|
||||
VAStatus st = vaExportSurfaceHandle(
|
||||
va->display,
|
||||
surface,
|
||||
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
|
||||
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
|
||||
&desc);
|
||||
if (st != VA_STATUS_SUCCESS) {
|
||||
static int logged = 0;
|
||||
if (logged++ < 3)
|
||||
qCWarning(
|
||||
vaapiLog,
|
||||
"VideoPlayer: vaExportSurfaceHandle failed (%d); CPU fallback",
|
||||
int(st));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (desc.num_layers != 2 || desc.num_objects < 2) {
|
||||
for (uint32_t i = 0; i < desc.num_objects; ++i)
|
||||
if (desc.objects[i].fd >= 0) close(desc.objects[i].fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bindLayer = [&](uint32_t layer, GLuint tex) -> EGLImageKHR {
|
||||
const auto& L = desc.layers[layer];
|
||||
if (L.num_planes < 1) return EGL_NO_IMAGE_KHR;
|
||||
const auto& obj = desc.objects[L.object_index[0]];
|
||||
if (obj.fd < 0) return EGL_NO_IMAGE_KHR;
|
||||
EGLImageKHR image = createDmaImage(
|
||||
m_eglDisplay,
|
||||
obj.fd,
|
||||
L.offset[0],
|
||||
L.pitch[0],
|
||||
L.drm_format,
|
||||
obj.drm_format_modifier);
|
||||
if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR;
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
target(GL_TEXTURE_2D, image);
|
||||
return image;
|
||||
};
|
||||
|
||||
const QSize sz = QSize(int(frame->width), int(frame->height));
|
||||
if (m_texSize != sz || m_texY == 0) {
|
||||
if (m_imageY != EGL_NO_IMAGE_KHR) {
|
||||
destroyEglImage(m_eglDisplay, m_imageY);
|
||||
m_imageY = EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
if (m_imageUV != EGL_NO_IMAGE_KHR) {
|
||||
destroyEglImage(m_eglDisplay, m_imageUV);
|
||||
m_imageUV = EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
if (m_texY == 0) glGenTextures(1, &m_texY);
|
||||
if (m_texUV == 0) glGenTextures(1, &m_texUV);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texY);
|
||||
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);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texUV);
|
||||
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);
|
||||
m_texSize = sz;
|
||||
}
|
||||
|
||||
EGLImageKHR imageY = bindLayer(0, m_texY);
|
||||
EGLImageKHR imageUV = bindLayer(1, m_texUV);
|
||||
|
||||
if (imageY == EGL_NO_IMAGE_KHR || imageUV == EGL_NO_IMAGE_KHR) {
|
||||
destroyEglImage(m_eglDisplay, imageY);
|
||||
destroyEglImage(m_eglDisplay, imageUV);
|
||||
for (uint32_t i = 0; i < desc.num_objects; ++i)
|
||||
if (desc.objects[i].fd >= 0) close(desc.objects[i].fd);
|
||||
static int logged = 0;
|
||||
if (logged++ < 3)
|
||||
qCWarning(
|
||||
vaapiLog, "VideoPlayer: EGLImage import failed; CPU fallback");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_imageY != EGL_NO_IMAGE_KHR) destroyEglImage(m_eglDisplay, m_imageY);
|
||||
if (m_imageUV != EGL_NO_IMAGE_KHR) destroyEglImage(m_eglDisplay, m_imageUV);
|
||||
|
||||
m_imageY = imageY;
|
||||
m_imageUV = imageUV;
|
||||
|
||||
for (uint32_t i = 0; i < desc.num_objects; ++i)
|
||||
if (desc.objects[i].fd >= 0) close(desc.objects[i].fd);
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
|
||||
#endif
|
||||
@@ -1,83 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "videodecoder.hpp"
|
||||
|
||||
#include <EGL/egl.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#ifdef ZHAVE_VAAPI
|
||||
#include <EGL/eglext.h>
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/buffer.h>
|
||||
#include <libavutil/frame.h>
|
||||
#include <libavutil/hwcontext.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
}
|
||||
|
||||
namespace ZShell::wallpaper {
|
||||
|
||||
class VaapiPlayer : public VideoDecoder {
|
||||
public:
|
||||
explicit VaapiPlayer(EGLDisplay display);
|
||||
~VaapiPlayer() 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 "vaapi"; }
|
||||
|
||||
private:
|
||||
void decodeLoop(const QString& path, uint32_t generation);
|
||||
bool openDecoder(const QString& path);
|
||||
void closeDecoder();
|
||||
|
||||
bool ensureDevice();
|
||||
bool ensureTextures(unsigned w, unsigned h, int compBytes);
|
||||
|
||||
#ifdef ZHAVE_VAAPI
|
||||
bool importEglImages(AVFrame* frame);
|
||||
#endif
|
||||
|
||||
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
|
||||
|
||||
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;
|
||||
|
||||
QSize m_texSize;
|
||||
int m_texComp = 1;
|
||||
|
||||
#ifdef ZHAVE_VAAPI
|
||||
EGLImageKHR m_imageY = EGL_NO_IMAGE_KHR;
|
||||
EGLImageKHR m_imageUV = EGL_NO_IMAGE_KHR;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace ZShell::wallpaper
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user