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 |
@@ -50,7 +50,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
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(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
|
||||||
|
|
||||||
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
|
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ CustomRect {
|
|||||||
property color activeOnColor
|
property color activeOnColor
|
||||||
property bool checked
|
property bool checked
|
||||||
property real checkedRadius: Tokens.rounding.medium
|
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 disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1)
|
||||||
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
|
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
|
||||||
property bool fillWidth
|
property bool fillWidth
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ BusyIndicator {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
bgColor: root.bgColor
|
bgColor: root.bgColor
|
||||||
fgColor: root.fgColor
|
fgColor: root.fgColor
|
||||||
|
hasEndIndicator: false
|
||||||
padding: root.padding
|
padding: root.padding
|
||||||
rotation: manager.rotation
|
rotation: manager.rotation
|
||||||
startAngle: manager.startFraction * 360
|
startAngle: manager.startFraction * 360
|
||||||
@@ -53,6 +54,7 @@ BusyIndicator {
|
|||||||
Anim {
|
Anim {
|
||||||
duration: manager.completeEndDuration * Tokens.anim.durations.scale
|
duration: manager.completeEndDuration * Tokens.anim.durations.scale
|
||||||
properties: "opacity,internalStrokeWidth"
|
properties: "opacity,internalStrokeWidth"
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ TextFieldBase {
|
|||||||
|
|
||||||
sourceComponent: MaterialIcon {
|
sourceComponent: MaterialIcon {
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Tokens.font.size.larger
|
||||||
text: root.leadingIcon
|
text: root.leadingIcon
|
||||||
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
|
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ Scope {
|
|||||||
hasBar: root.geometry.barOnBottom
|
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 {
|
component ExclusionZone: CustomWindow {
|
||||||
property bool hasBar
|
property bool hasBar
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ZShell.Blobs
|
|||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Modules.Bar
|
import qs.Modules.Bar
|
||||||
|
import qs.Modules.Bar.Popouts.Network as N
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Drawers
|
import qs.Drawers
|
||||||
|
|||||||
@@ -77,16 +77,6 @@ Singleton {
|
|||||||
"99": "thunderstorm"
|
"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 {
|
function getAppIcon(name: string, fallback: string): string {
|
||||||
const icon = DesktopEntries.heuristicLookup(name)?.icon;
|
const icon = DesktopEntries.heuristicLookup(name)?.icon;
|
||||||
if (fallback !== "undefined")
|
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)
|
||||||
|
}
|
||||||
+1
-26
@@ -18,8 +18,6 @@ Searcher {
|
|||||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||||
property alias crops: adapter.monitorCrops
|
property alias crops: adapter.monitorCrops
|
||||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||||
readonly property string currentThumbnail: thumbnailFor(current)
|
|
||||||
readonly property bool currentIsVideo: isVideoPath(current)
|
|
||||||
property alias monitorCrops: monitorCrops
|
property alias monitorCrops: monitorCrops
|
||||||
property bool pendingPreviewClear
|
property bool pendingPreviewClear
|
||||||
property bool previewColorLock
|
property bool previewColorLock
|
||||||
@@ -34,29 +32,6 @@ Searcher {
|
|||||||
return category;
|
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 (!isVideoPath(path))
|
|
||||||
return path;
|
|
||||||
|
|
||||||
for (const entry of root.list) {
|
|
||||||
if (entry.path === path && entry.thumbnailPath)
|
|
||||||
return entry.thumbnailPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCrop(screen: string): var {
|
function getCrop(screen: string): var {
|
||||||
return root.crops[screen];
|
return root.crops[screen];
|
||||||
}
|
}
|
||||||
@@ -150,7 +125,7 @@ Searcher {
|
|||||||
FileSystemModel {
|
FileSystemModel {
|
||||||
id: wallpapers
|
id: wallpapers
|
||||||
|
|
||||||
filter: FileSystemModel.Media
|
filter: FileSystemModel.Images
|
||||||
path: Config.general.wallpaperPath
|
path: Config.general.wallpaperPath
|
||||||
recursive: true
|
recursive: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,15 +206,6 @@ GridLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: "network"
|
|
||||||
|
|
||||||
delegate: EntryWrapper {
|
|
||||||
NetworkWidget {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: "media"
|
roleValue: "media"
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,28 @@ WidgetBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "bluetooth"
|
||||||
|
|
||||||
|
delegate: EntryWrapper {
|
||||||
|
name: "bluetooth"
|
||||||
|
|
||||||
|
BluetoothWidget {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "network"
|
||||||
|
|
||||||
|
delegate: EntryWrapper {
|
||||||
|
name: "network"
|
||||||
|
|
||||||
|
NetworkWidget {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: "power"
|
roleValue: "power"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
anchors.centerIn: parent
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
backer.retainWhileLoading: true
|
|
||||||
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||||
implicitSize: Config.bar.tray.trayIconSize * root.dpr
|
implicitSize: Config.bar.tray.trayIconSize * root.dpr
|
||||||
layer.enabled: Config.bar.tray.recolorIcons
|
layer.enabled: Config.bar.tray.recolorIcons
|
||||||
|
|||||||
@@ -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 {
|
Popout {
|
||||||
name: "updates"
|
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
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Networking
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Components
|
import Quickshell
|
||||||
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Modules
|
import qs.Services
|
||||||
import qs.Helpers as Helpers
|
import qs.Modules.Settings.Common
|
||||||
|
import qs.Modules.Bar.Popouts.Network
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers.Network
|
||||||
|
|
||||||
Item {
|
CustomClippingRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var wrapper
|
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 {
|
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 {
|
CustomText {
|
||||||
model: Helpers.Network.devices
|
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||||
|
Layout.rightMargin: Tokens.padding.extraSmall
|
||||||
|
font.pointSize: Tokens.font.size.large
|
||||||
|
text: qsTr("Wifi")
|
||||||
|
}
|
||||||
|
|
||||||
CustomRadioButton {
|
Toggle {
|
||||||
id: network
|
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
|
ListView {
|
||||||
text: modelData.description
|
id: rows
|
||||||
visible: modelData.name !== "lo"
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
@@ -11,7 +9,7 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property bool isCurrent
|
required property bool isCurrent
|
||||||
required property var modelData
|
required property var modelData // FileSystemEntry-like {path, relativePath}
|
||||||
required property real tileHeight
|
required property real tileHeight
|
||||||
required property real tileWidth
|
required property real tileWidth
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
@@ -31,75 +29,25 @@ Item {
|
|||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
y: Tokens.padding.large
|
y: Tokens.padding.large
|
||||||
|
|
||||||
Loader {
|
MaterialIcon {
|
||||||
active: opacity > 0
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
opacity: img.status === Image.Ready ? 0 : 1
|
color: Colors.tPalette.m3outline
|
||||||
|
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
|
||||||
Behavior on opacity {
|
text: "image"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
CachingImage {
|
||||||
id: img
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
cache: true
|
cache: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
height: root.tileHeight
|
height: root.tileHeight
|
||||||
path: root.modelData ? root.modelData.thumbnailPath : ""
|
path: root.modelData ? root.modelData.path : ""
|
||||||
smooth: root.isCurrent
|
smooth: root.isCurrent
|
||||||
sourceSize.height: root.tileHeight
|
sourceSize.height: root.tileHeight
|
||||||
sourceSize.width: root.tileHeight * (16 / 9)
|
sourceSize.width: root.tileHeight * (16 / 9)
|
||||||
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 {
|
StateLayer {
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ Item {
|
|||||||
for (let i = 0; i < count; i++)
|
for (let i = 0; i < count; i++)
|
||||||
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
|
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
|
||||||
|
|
||||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
|
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard)
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (screenState.dashboard && (panels.dashboard.x + panels.dashboard.width >= panels.notifications.x))
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (screenState.osd) {
|
if (screenState.osd) {
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ Item {
|
|||||||
opacity: img.status === Image.Ready ? 0 : 1
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {}
|
Anim {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sourceComponent: CustomRect {
|
sourceComponent: CustomRect {
|
||||||
color: Colors.palette.m3primaryContainer
|
color: Colors.palette.m3primaryContainer
|
||||||
@@ -63,43 +64,18 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
sourceSize: {
|
sourceSize: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return Qt.size(width * dpr, height * dpr);
|
return Qt.size(width * dpr, height * dpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
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,9 +13,7 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
readonly property bool isVideo: Wallpapers.currentIsVideo
|
property int buttonRowHeight: Tokens.padding.large * 3
|
||||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
|
||||||
readonly property real maxZoom: 5.0
|
|
||||||
readonly property bool changesMade: {
|
readonly property bool changesMade: {
|
||||||
cropRevision;
|
cropRevision;
|
||||||
|
|
||||||
@@ -27,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;
|
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
|
property int cropRevision: 0
|
||||||
|
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||||
|
readonly property real maxZoom: 5.0
|
||||||
property var screens: []
|
property var screens: []
|
||||||
property int selectedScreenIndex: 0
|
property int selectedScreenIndex: 0
|
||||||
property bool shouldBeActive: true
|
property bool shouldBeActive: true
|
||||||
@@ -129,7 +127,7 @@ Item {
|
|||||||
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
||||||
active: wrapper.screens.length > 1 && !wrapper.isVideo
|
active: wrapper.screens.length > 1
|
||||||
|
|
||||||
sourceComponent: ButtonRow {
|
sourceComponent: ButtonRow {
|
||||||
id: screenSelector
|
id: screenSelector
|
||||||
@@ -165,7 +163,6 @@ Item {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
enabled: !wrapper.isVideo
|
|
||||||
|
|
||||||
CustomSlider {
|
CustomSlider {
|
||||||
id: zoomSlider
|
id: zoomSlider
|
||||||
@@ -277,7 +274,7 @@ Item {
|
|||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
source: Wallpapers.currentThumbnail
|
source: Wallpapers.current
|
||||||
sourceSize.height: {
|
sourceSize.height: {
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
return imageSlot.height * dpr;
|
return imageSlot.height * dpr;
|
||||||
@@ -288,37 +285,12 @@ Item {
|
|||||||
type: Anim.SlowEffects
|
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 {
|
Loader {
|
||||||
id: cropRectLoader
|
id: cropRectLoader
|
||||||
|
|
||||||
active: scaledImg.status === Image.Ready && !wrapper.isVideo
|
active: scaledImg.status === Image.Ready
|
||||||
|
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
CustomRect {
|
CustomRect {
|
||||||
@@ -361,6 +333,7 @@ Item {
|
|||||||
y = t.y;
|
y = t.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Where the rect should be according to saved data (same rules as a restore).
|
||||||
function savedTarget() {
|
function savedTarget() {
|
||||||
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
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);
|
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);
|
||||||
@@ -463,10 +436,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
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
|
hoverEnabled: true
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
enabled: !wrapper.isVideo
|
|
||||||
|
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ pragma Singleton
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Components
|
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
import qs.Modules.Settings.Common
|
import qs.Modules.Settings.Common
|
||||||
import qs.Modules.Settings.Pages
|
import qs.Modules.Settings.Pages
|
||||||
import qs.Modules.Settings.Pages.Wallpaper
|
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.Bar
|
||||||
import qs.Modules.Settings.Pages.Panels.Sidebar
|
import qs.Modules.Settings.Pages.Panels.Sidebar
|
||||||
import qs.Modules.Settings.Pages.Services
|
import qs.Modules.Settings.Pages.Services
|
||||||
|
import qs.Modules.Settings.Pages.Network
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
@@ -47,7 +48,27 @@ QtObject {
|
|||||||
|
|
||||||
// Connectivity
|
// Connectivity
|
||||||
Component {
|
Component {
|
||||||
PlaceholderComp {}
|
StackPage {
|
||||||
|
Component {
|
||||||
|
NetworkPage {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
AllNetworks {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
SavedNetworks {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
AddNetwork {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
OldNetwork {}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Component {
|
Component {
|
||||||
PlaceholderComp {}
|
PlaceholderComp {}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Item {
|
|||||||
id: container
|
id: container
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
layer.enabled: opacity < 1
|
||||||
objectName: "PageContainer"
|
objectName: "PageContainer"
|
||||||
|
|
||||||
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
|
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: ({
|
readonly property var builtinIcons: ({
|
||||||
audio: qsTr("Speakers"),
|
audio: qsTr("Speakers"),
|
||||||
microphone: qsTr("Microphone"),
|
microphone: qsTr("Microphone"),
|
||||||
power: qsTr("Battery")
|
power: qsTr("Battery"),
|
||||||
|
network: qsTr("Network"),
|
||||||
|
bluetooth: qsTr("Bluetooth")
|
||||||
})
|
})
|
||||||
|
|
||||||
isSubPage: true
|
isSubPage: true
|
||||||
@@ -93,5 +95,15 @@ PageBase {
|
|||||||
|
|
||||||
onToggled: Config.bar.popouts.statusIcons = checked
|
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 {
|
WallItem {
|
||||||
id: wallItem
|
|
||||||
required property FileSystemEntry modelData
|
required property FileSystemEntry modelData
|
||||||
|
|
||||||
enabled: modelData
|
enabled: modelData
|
||||||
opacity: modelData ? 1 : 0
|
opacity: modelData ? 1 : 0
|
||||||
source: String(modelData?.thumbnailPath ?? "")
|
source: String(modelData?.path ?? "")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Wallpapers.setWallpaper(modelData.path);
|
Wallpapers.setWallpaper(modelData.path);
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
|
|||||||
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
||||||
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
||||||
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
||||||
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
|
|
||||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
|
|||||||
@@ -39,5 +39,4 @@ qml_module(ZShell-internal
|
|||||||
PkgConfig::EGL
|
PkgConfig::EGL
|
||||||
PkgConfig::WAYLAND_EGL
|
PkgConfig::WAYLAND_EGL
|
||||||
PkgConfig::GL
|
PkgConfig::GL
|
||||||
PkgConfig::MPV
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,26 +7,15 @@
|
|||||||
#include <GL/gl.h>
|
#include <GL/gl.h>
|
||||||
#include <GL/glext.h>
|
#include <GL/glext.h>
|
||||||
|
|
||||||
#include <mpv/client.h>
|
|
||||||
#include <mpv/render_gl.h>
|
|
||||||
|
|
||||||
#include <clocale>
|
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QProcess>
|
|
||||||
|
|
||||||
#include <qguiapplication_platform.h>
|
#include <qguiapplication_platform.h>
|
||||||
|
|
||||||
#include <private/qwaylandscreen_p.h>
|
#include <private/qwaylandscreen_p.h>
|
||||||
@@ -40,6 +29,7 @@ namespace {
|
|||||||
const char* kVertexShader = R"(
|
const char* kVertexShader = R"(
|
||||||
#version 300 es
|
#version 300 es
|
||||||
void main() {
|
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));
|
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
@@ -51,36 +41,16 @@ precision highp float;
|
|||||||
uniform sampler2D source;
|
uniform sampler2D source;
|
||||||
uniform vec2 size;
|
uniform vec2 size;
|
||||||
uniform vec4 crop;
|
uniform vec4 crop;
|
||||||
uniform float alpha;
|
|
||||||
out vec4 outColor;
|
out vec4 outColor;
|
||||||
void main() {
|
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;
|
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
||||||
vec4 c = texture(source, crop.xy + p * crop.zw);
|
outColor = texture(source, crop.xy + p * crop.zw);
|
||||||
outColor = vec4(c.rgb, c.a * alpha);
|
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
constexpr int kWaitTimeoutMs = 20;
|
constexpr int kWaitTimeoutMs = 20;
|
||||||
|
|
||||||
constexpr int kFadeDurationMs = 400;
|
|
||||||
|
|
||||||
const QStringList kVideoExtensions = {
|
|
||||||
"mp4",
|
|
||||||
"mkv",
|
|
||||||
"webm",
|
|
||||||
"avi",
|
|
||||||
"mov",
|
|
||||||
"wmv",
|
|
||||||
"flv",
|
|
||||||
"m4v",
|
|
||||||
"mpg",
|
|
||||||
"mpeg",
|
|
||||||
"3gp",
|
|
||||||
"ogv",
|
|
||||||
"ts",
|
|
||||||
"vob",
|
|
||||||
};
|
|
||||||
|
|
||||||
void logShader(uint32_t object, bool program) {
|
void logShader(uint32_t object, bool program) {
|
||||||
int ok = 0;
|
int ok = 0;
|
||||||
if (program)
|
if (program)
|
||||||
@@ -124,16 +94,6 @@ WallpaperService::~WallpaperService() {
|
|||||||
|
|
||||||
if (m_thread.joinable()) m_thread.join();
|
if (m_thread.joinable()) m_thread.join();
|
||||||
|
|
||||||
if (m_mpvRender) {
|
|
||||||
mpv_render_context_free(m_mpvRender);
|
|
||||||
m_mpvRender = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_mpv) {
|
|
||||||
mpv_terminate_destroy(m_mpv);
|
|
||||||
m_mpv = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* surface : m_surfaces)
|
for (auto* surface : m_surfaces)
|
||||||
delete surface;
|
delete surface;
|
||||||
m_surfaces.clear();
|
m_surfaces.clear();
|
||||||
@@ -142,8 +102,6 @@ WallpaperService::~WallpaperService() {
|
|||||||
if (eglMakeCurrent(
|
if (eglMakeCurrent(
|
||||||
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
|
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
|
||||||
if (m_texture) glDeleteTextures(1, &m_texture);
|
if (m_texture) glDeleteTextures(1, &m_texture);
|
||||||
if (m_previousTexture) glDeleteTextures(1, &m_previousTexture);
|
|
||||||
if (m_blackTex) glDeleteTextures(1, &m_blackTex);
|
|
||||||
if (m_program) glDeleteProgram(m_program);
|
if (m_program) glDeleteProgram(m_program);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,210 +127,22 @@ QString WallpaperService::path() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperService::setPath(const QString& path) {
|
void WallpaperService::setPath(const QString& path) {
|
||||||
if (path.isEmpty() || path == this->path()) return;
|
if (m_path == path) return;
|
||||||
|
|
||||||
if (isVideoPath(path) && needsTranscode(path)) {
|
|
||||||
const QString dst = transcodeTarget(path);
|
|
||||||
|
|
||||||
if (QFile::exists(dst)) {
|
|
||||||
applyPath(dst);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyPath(path);
|
|
||||||
transcodeVideo(path, dst);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyPath(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::applyPath(const QString& path) {
|
|
||||||
const bool video = isVideoPath(path);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
m_path = path;
|
m_path = path;
|
||||||
|
|
||||||
const bool wasVideo = m_isVideo && m_mpv != nullptr;
|
|
||||||
const bool wasImage = !m_image.isNull() && m_texture != 0;
|
|
||||||
|
|
||||||
m_previousVideo = wasVideo && !video;
|
|
||||||
|
|
||||||
if (wasImage) {
|
|
||||||
m_previousImage = m_image;
|
|
||||||
m_previousCrops = m_crops;
|
|
||||||
m_previousTexture = 0;
|
|
||||||
} else {
|
|
||||||
m_previousImage = QImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_isVideo = video;
|
|
||||||
|
|
||||||
m_fading = true;
|
|
||||||
m_fadeClockStarted = false;
|
|
||||||
|
|
||||||
m_forceDecode = true;
|
m_forceDecode = true;
|
||||||
m_imageChanged = true;
|
m_imageChanged = true;
|
||||||
m_videoReload = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emit pathChanged();
|
emit pathChanged();
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
|
||||||
queueAll();
|
queueAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WallpaperService::isHardwareCodec(const QString& codec) {
|
|
||||||
static const QStringList hardware{
|
|
||||||
"av1",
|
|
||||||
"hevc",
|
|
||||||
"h264",
|
|
||||||
"vp9",
|
|
||||||
"vp8",
|
|
||||||
"vc1",
|
|
||||||
"mpeg2",
|
|
||||||
"mpeg2video",
|
|
||||||
"mpeg4",
|
|
||||||
"mjpeg",
|
|
||||||
};
|
|
||||||
|
|
||||||
return hardware.contains(codec);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WallpaperService::codecName(const QString& path) const {
|
|
||||||
QProcess probe;
|
|
||||||
probe.start(
|
|
||||||
"ffprobe",
|
|
||||||
{
|
|
||||||
"-v",
|
|
||||||
"error",
|
|
||||||
"-select_streams",
|
|
||||||
"v:0",
|
|
||||||
"-show_entries",
|
|
||||||
"stream=codec_name",
|
|
||||||
"-of",
|
|
||||||
"default=nw=1:nk=1",
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!probe.waitForFinished(2000) || probe.exitCode() != 0) {
|
|
||||||
qWarning() << "WallpaperService: ffprobe failed for" << path << "(exit"
|
|
||||||
<< probe.exitCode() << ")";
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString codec =
|
|
||||||
QString::fromUtf8(probe.readAllStandardOutput()).trimmed().toLower();
|
|
||||||
qWarning() << "WallpaperService: codec of" << path << "is"
|
|
||||||
<< (codec.isEmpty() ? "<empty>" : codec);
|
|
||||||
return codec;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WallpaperService::cacheDir() const {
|
|
||||||
return QDir::homePath() + "/.cache/zshell/wallpaper-videos";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WallpaperService::transcodeTarget(const QString& path) const {
|
|
||||||
const QFileInfo info(path);
|
|
||||||
|
|
||||||
const QByteArray key =
|
|
||||||
path.toUtf8() + '|' + QByteArray::number(info.size()) + '|' +
|
|
||||||
QByteArray::number(info.lastModified().toMSecsSinceEpoch());
|
|
||||||
|
|
||||||
const QString hash = QString::fromUtf8(
|
|
||||||
QCryptographicHash::hash(key, QCryptographicHash::Sha1).toHex());
|
|
||||||
|
|
||||||
return cacheDir() + '/' + hash + ".av1.mkv";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallpaperService::needsTranscode(const QString& path) const {
|
|
||||||
if (path.startsWith(cacheDir())) return false;
|
|
||||||
|
|
||||||
const QString codec = codecName(path);
|
|
||||||
if (codec.isEmpty()) return false;
|
|
||||||
|
|
||||||
return !isHardwareCodec(codec);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::transcodeVideo(const QString& src, const QString& dst) {
|
|
||||||
QDir().mkpath(cacheDir());
|
|
||||||
|
|
||||||
if (m_transcode) {
|
|
||||||
m_transcode->kill();
|
|
||||||
m_transcode->deleteLater();
|
|
||||||
m_transcode = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_transcode = new QProcess(this);
|
|
||||||
auto* proc = m_transcode;
|
|
||||||
|
|
||||||
connect(proc, &QProcess::readyReadStandardError, proc, [proc]() {
|
|
||||||
qWarning().noquote()
|
|
||||||
<< "ffmpeg:" << QString::fromUtf8(proc->readAllStandardError());
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(
|
|
||||||
proc,
|
|
||||||
&QProcess::finished,
|
|
||||||
this,
|
|
||||||
[this, proc, dst](int code, QProcess::ExitStatus status) {
|
|
||||||
const bool ok = code == 0 && status == QProcess::NormalExit &&
|
|
||||||
QFile::exists(dst);
|
|
||||||
|
|
||||||
qWarning() << "WallpaperService: transcode finished, exit" << code
|
|
||||||
<< "status" << int(status) << "exists"
|
|
||||||
<< QFile::exists(dst) << "->" << dst;
|
|
||||||
|
|
||||||
if (proc == m_transcode) m_transcode = nullptr;
|
|
||||||
proc->deleteLater();
|
|
||||||
|
|
||||||
if (ok) applyPath(dst);
|
|
||||||
});
|
|
||||||
|
|
||||||
qWarning() << "WallpaperService: transcoding" << src << "->" << dst;
|
|
||||||
|
|
||||||
proc->setProgram("nice");
|
|
||||||
proc->setArguments({
|
|
||||||
"-n",
|
|
||||||
"19",
|
|
||||||
"ffmpeg",
|
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"error",
|
|
||||||
"-y",
|
|
||||||
"-threads",
|
|
||||||
"2",
|
|
||||||
"-i",
|
|
||||||
src,
|
|
||||||
"-map",
|
|
||||||
"0:v:0",
|
|
||||||
"-vf",
|
|
||||||
"scale=in_range=pc:out_range=tv:out_color_matrix=bt709",
|
|
||||||
"-colorspace",
|
|
||||||
"bt709",
|
|
||||||
"-color_primaries",
|
|
||||||
"bt709",
|
|
||||||
"-color_trc",
|
|
||||||
"bt709",
|
|
||||||
"-color_range",
|
|
||||||
"tv",
|
|
||||||
"-c:v",
|
|
||||||
"libsvtav1",
|
|
||||||
"-preset",
|
|
||||||
"6",
|
|
||||||
"-crf",
|
|
||||||
"20",
|
|
||||||
"-pix_fmt",
|
|
||||||
"yuv420p10le",
|
|
||||||
"-an",
|
|
||||||
dst,
|
|
||||||
});
|
|
||||||
|
|
||||||
proc->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::addScreen(const QString& screen) {
|
void WallpaperService::addScreen(const QString& screen) {
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
@@ -442,178 +212,6 @@ void WallpaperService::redraw() {
|
|||||||
queueAll();
|
queueAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WallpaperService::isVideoPath(const QString& path) {
|
|
||||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::startVideo(const QString& path) {
|
|
||||||
if (path.isEmpty()) return;
|
|
||||||
|
|
||||||
m_videoReady.store(false, std::memory_order_relaxed);
|
|
||||||
m_videoLoaded.store(false, std::memory_order_relaxed);
|
|
||||||
|
|
||||||
if (!m_mpv) {
|
|
||||||
std::setlocale(LC_NUMERIC, "C");
|
|
||||||
|
|
||||||
m_mpv = mpv_create();
|
|
||||||
if (!m_mpv) {
|
|
||||||
qWarning() << "WallpaperService: could not create mpv handle";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mpv_set_option_string(m_mpv, "config", "no");
|
|
||||||
mpv_set_option_string(m_mpv, "vo", "libmpv");
|
|
||||||
mpv_set_option_string(m_mpv, "audio", "no");
|
|
||||||
mpv_set_option_string(m_mpv, "loop", "inf");
|
|
||||||
mpv_set_option_string(m_mpv, "hwdec", "auto-copy");
|
|
||||||
mpv_set_option_string(m_mpv, "video-sync", "display");
|
|
||||||
mpv_set_option_string(m_mpv, "framedrop", "decoder");
|
|
||||||
mpv_set_option_string(m_mpv, "background-color", "#00000000");
|
|
||||||
|
|
||||||
if (mpv_initialize(m_mpv) < 0) {
|
|
||||||
qWarning() << "WallpaperService: mpv_initialize failed";
|
|
||||||
mpv_terminate_destroy(m_mpv);
|
|
||||||
m_mpv = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const QByteArray local = path.toUtf8();
|
|
||||||
|
|
||||||
initMpvRender();
|
|
||||||
|
|
||||||
const char* loadCmd[]{"loadfile", local.constData(), nullptr};
|
|
||||||
if (mpv_command(m_mpv, loadCmd) < 0)
|
|
||||||
qWarning() << "WallpaperService: mpv could not load" << path;
|
|
||||||
|
|
||||||
m_mpvFrameReady.store(true, std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::pumpMpvEvents() {
|
|
||||||
if (!m_mpv) return;
|
|
||||||
|
|
||||||
mpv_event* event = nullptr;
|
|
||||||
while ((event = mpv_wait_event(m_mpv, 0)) != nullptr &&
|
|
||||||
event->event_id != MPV_EVENT_NONE) {
|
|
||||||
if (event->event_id == MPV_EVENT_LOG_MESSAGE) {
|
|
||||||
auto* data = static_cast<mpv_event_log_message*>(event->data);
|
|
||||||
if (data && data->text)
|
|
||||||
qWarning().noquote() << "mpv:" << data->prefix << data->text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->event_id == MPV_EVENT_FILE_LOADED) {
|
|
||||||
m_videoLoaded.store(true, std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->event_id == MPV_EVENT_END_FILE) {
|
|
||||||
m_videoLoaded.store(false, std::memory_order_relaxed);
|
|
||||||
m_videoReady.store(false, std::memory_order_relaxed);
|
|
||||||
|
|
||||||
auto* data = static_cast<mpv_event_end_file*>(event->data);
|
|
||||||
if (data && data->error != MPV_END_FILE_REASON_EOF)
|
|
||||||
qWarning() << "WallpaperService: mpv ended the file, error"
|
|
||||||
<< data->error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::stopVideo() {
|
|
||||||
teardownMpv();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::teardownMpv() {
|
|
||||||
if (m_mpvRender) {
|
|
||||||
mpv_render_context_free(m_mpvRender);
|
|
||||||
m_mpvRender = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_mpv) {
|
|
||||||
mpv_terminate_destroy(m_mpv);
|
|
||||||
m_mpv = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_mpvFrameReady.store(false, std::memory_order_relaxed);
|
|
||||||
m_videoReady.store(false, std::memory_order_relaxed);
|
|
||||||
m_videoLoaded.store(false, std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
void* WallpaperService::getMpvProcAddress(void* ctx, const char* name) {
|
|
||||||
(void)ctx;
|
|
||||||
|
|
||||||
if (strcmp(name, "glFenceSync") == 0)
|
|
||||||
return reinterpret_cast<void*>(
|
|
||||||
+[](GLenum, GLbitfield) -> GLsync { return nullptr; });
|
|
||||||
|
|
||||||
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::mpvUpdate(void* ctx) {
|
|
||||||
auto* self = static_cast<WallpaperService*>(ctx);
|
|
||||||
|
|
||||||
self->m_mpvFrameReady.store(true, std::memory_order_relaxed);
|
|
||||||
|
|
||||||
if (self->m_videoLoaded.load(std::memory_order_relaxed))
|
|
||||||
self->m_videoReady.store(true, std::memory_order_relaxed);
|
|
||||||
|
|
||||||
self->wake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::initMpvRender() {
|
|
||||||
if (m_mpvRender || !m_mpv) return;
|
|
||||||
|
|
||||||
mpv_opengl_init_params gl_init{getMpvProcAddress, nullptr};
|
|
||||||
|
|
||||||
mpv_render_param params[]{
|
|
||||||
{MPV_RENDER_PARAM_API_TYPE,
|
|
||||||
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
|
||||||
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init},
|
|
||||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mpv_render_context_create(&m_mpvRender, m_mpv, params) < 0) {
|
|
||||||
qWarning() << "WallpaperService: could not create mpv render context";
|
|
||||||
m_mpvRender = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mpv_render_context_set_update_callback(m_mpvRender, mpvUpdate, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::renderMpvToSurface(const QSize& size) {
|
|
||||||
if (!m_mpvRender) initMpvRender();
|
|
||||||
if (!m_mpvRender) return;
|
|
||||||
|
|
||||||
glViewport(0, 0, size.width(), size.height());
|
|
||||||
|
|
||||||
mpv_opengl_fbo fbo{0, size.width(), size.height(), GL_RGBA};
|
|
||||||
|
|
||||||
int flip = 1;
|
|
||||||
int block = 0;
|
|
||||||
mpv_render_param params[]{
|
|
||||||
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
|
|
||||||
{MPV_RENDER_PARAM_FLIP_Y, &flip},
|
|
||||||
{MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block},
|
|
||||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
mpv_render_context_render(m_mpvRender, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::createBlackTexture(uint32_t& texture) {
|
|
||||||
if (texture != 0) return;
|
|
||||||
|
|
||||||
const unsigned char pixel[4] = {0, 0, 0, 255};
|
|
||||||
|
|
||||||
glGenTextures(1, &texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
||||||
glTexImage2D(
|
|
||||||
GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::start() {
|
void WallpaperService::start() {
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
@@ -662,7 +260,11 @@ void WallpaperService::wake() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperService::queue(WallpaperSurface* surface) {
|
void WallpaperService::queue(WallpaperSurface* surface) {
|
||||||
surface->markDirty();
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
m_pending.push_back(surface);
|
||||||
|
}
|
||||||
|
|
||||||
wake();
|
wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,7 +273,7 @@ void WallpaperService::queueAll() {
|
|||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
|
||||||
for (auto* surface : m_surfaces)
|
for (auto* surface : m_surfaces)
|
||||||
surface->markDirty();
|
m_pending.push_back(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
wake();
|
wake();
|
||||||
@@ -721,50 +323,13 @@ void WallpaperService::present() {
|
|||||||
|
|
||||||
ensureDecodeSize();
|
ensureDecodeSize();
|
||||||
|
|
||||||
QList<WallpaperSurface*> surfaces;
|
std::vector<WallpaperSurface*> pending;
|
||||||
bool fading = false;
|
|
||||||
bool videoReload = false;
|
|
||||||
QString path;
|
|
||||||
bool isVideo = false;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
pending.swap(m_pending);
|
||||||
if (m_videoReload) {
|
|
||||||
m_videoReload = false;
|
|
||||||
videoReload = true;
|
|
||||||
path = m_path;
|
|
||||||
isVideo = m_isVideo;
|
|
||||||
}
|
|
||||||
|
|
||||||
fading = m_fading;
|
|
||||||
const bool newVideoFrame =
|
|
||||||
m_isVideo &&
|
|
||||||
m_mpvFrameReady.exchange(false, std::memory_order_relaxed);
|
|
||||||
|
|
||||||
if (fading || newVideoFrame)
|
|
||||||
for (auto* surface : m_surfaces)
|
|
||||||
surface->markDirty();
|
|
||||||
|
|
||||||
surfaces = m_surfaces;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_context == EGL_NO_CONTEXT) return;
|
for (auto* surface : pending)
|
||||||
|
|
||||||
eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context);
|
|
||||||
|
|
||||||
if (videoReload) {
|
|
||||||
if (isVideo)
|
|
||||||
startVideo(path);
|
|
||||||
else if (m_mpv && !m_previousVideo)
|
|
||||||
teardownMpv();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool videoActive = (m_isVideo || m_previousVideo) && m_mpv != nullptr;
|
|
||||||
|
|
||||||
if (videoActive) pumpMpvEvents();
|
|
||||||
|
|
||||||
for (auto* surface : surfaces)
|
|
||||||
drawSurface(surface);
|
drawSurface(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,8 +494,7 @@ uint32_t WallpaperService::createProgram() {
|
|||||||
logShader(program, true);
|
logShader(program, true);
|
||||||
|
|
||||||
if (glGetUniformLocation(program, "size") < 0 ||
|
if (glGetUniformLocation(program, "size") < 0 ||
|
||||||
glGetUniformLocation(program, "crop") < 0 ||
|
glGetUniformLocation(program, "crop") < 0) {
|
||||||
glGetUniformLocation(program, "alpha") < 0) {
|
|
||||||
qWarning() << "WallpaperService: the program does not have the "
|
qWarning() << "WallpaperService: the program does not have the "
|
||||||
"uniforms it needs";
|
"uniforms it needs";
|
||||||
glDeleteProgram(program);
|
glDeleteProgram(program);
|
||||||
@@ -939,23 +503,22 @@ uint32_t WallpaperService::createProgram() {
|
|||||||
|
|
||||||
m_sizeUniform = glGetUniformLocation(program, "size");
|
m_sizeUniform = glGetUniformLocation(program, "size");
|
||||||
m_cropUniform = glGetUniformLocation(program, "crop");
|
m_cropUniform = glGetUniformLocation(program, "crop");
|
||||||
m_alphaUniform = glGetUniformLocation(program, "alpha");
|
|
||||||
|
|
||||||
glDeleteShader(vertex);
|
glDeleteShader(vertex);
|
||||||
glDeleteShader(fragment);
|
glDeleteShader(fragment);
|
||||||
return program;
|
return program;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperService::uploadTexture(uint32_t& texture, const QImage& image) {
|
void WallpaperService::uploadImage() {
|
||||||
if (image.isNull()) return;
|
if (m_image.isNull()) return;
|
||||||
|
|
||||||
if (texture == 0) {
|
if (m_texture == 0) {
|
||||||
glGenTextures(1, &texture);
|
glGenTextures(1, &m_texture);
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
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_MAG_FILTER, GL_LINEAR);
|
||||||
} else {
|
} else {
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
@@ -963,35 +526,12 @@ void WallpaperService::uploadTexture(uint32_t& texture, const QImage& image) {
|
|||||||
GL_TEXTURE_2D,
|
GL_TEXTURE_2D,
|
||||||
0,
|
0,
|
||||||
GL_RGBA8,
|
GL_RGBA8,
|
||||||
image.width(),
|
m_image.width(),
|
||||||
image.height(),
|
m_image.height(),
|
||||||
0,
|
0,
|
||||||
GL_RGBA,
|
GL_RGBA,
|
||||||
GL_UNSIGNED_BYTE,
|
GL_UNSIGNED_BYTE,
|
||||||
image.constBits());
|
m_image.constBits());
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::uploadImage() {
|
|
||||||
uploadTexture(m_texture, m_image);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallpaperService::drawTexture(
|
|
||||||
uint32_t texture,
|
|
||||||
const QRectF& rect,
|
|
||||||
float sizeW,
|
|
||||||
float sizeH,
|
|
||||||
float alpha) {
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glUniform2f(GLint(m_sizeUniform), sizeW, sizeH);
|
|
||||||
glUniform4f(
|
|
||||||
GLint(m_cropUniform),
|
|
||||||
GLfloat(rect.x()),
|
|
||||||
GLfloat(rect.y()),
|
|
||||||
GLfloat(rect.width()),
|
|
||||||
GLfloat(rect.height()));
|
|
||||||
glUniform1f(GLint(m_alphaUniform), alpha);
|
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WallpaperService::decode(const QString& path) {
|
bool WallpaperService::decode(const QString& path) {
|
||||||
@@ -1024,8 +564,6 @@ bool WallpaperService::ensureDecodeSize() {
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
|
||||||
if (m_isVideo) return false;
|
|
||||||
|
|
||||||
largest = largestBufferSize();
|
largest = largestBufferSize();
|
||||||
if (largest.isEmpty()) return false;
|
if (largest.isEmpty()) return false;
|
||||||
|
|
||||||
@@ -1073,10 +611,7 @@ QSize WallpaperService::largestBufferSize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QRectF WallpaperService::sourceRect(
|
QRectF WallpaperService::sourceRect(
|
||||||
const QVariantMap& crops,
|
const QVariantMap& crops, const QString& screen, const QSize& size) const {
|
||||||
const QString& screen,
|
|
||||||
const QSize& size,
|
|
||||||
const QSize& image) const {
|
|
||||||
QVariantMap crop = cropFor(crops, screen);
|
QVariantMap crop = cropFor(crops, screen);
|
||||||
|
|
||||||
QRectF rect(
|
QRectF rect(
|
||||||
@@ -1085,10 +620,10 @@ QRectF WallpaperService::sourceRect(
|
|||||||
crop.value("width", 1.0).toDouble(),
|
crop.value("width", 1.0).toDouble(),
|
||||||
crop.value("height", 1.0).toDouble());
|
crop.value("height", 1.0).toDouble());
|
||||||
|
|
||||||
if (size.isEmpty() || rect.isEmpty() || image.isEmpty()) return rect;
|
if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect;
|
||||||
|
|
||||||
qreal imageWidth = qreal(image.width());
|
qreal imageWidth = qreal(m_image.width());
|
||||||
qreal imageHeight = qreal(image.height());
|
qreal imageHeight = qreal(m_image.height());
|
||||||
|
|
||||||
QRectF pixels(
|
QRectF pixels(
|
||||||
rect.x() * imageWidth,
|
rect.x() * imageWidth,
|
||||||
@@ -1117,8 +652,6 @@ QRectF WallpaperService::sourceRect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
||||||
if (!surface->needsDraw() || !surface->frameReady()) return;
|
|
||||||
|
|
||||||
EGLSurface target = surface->eglSurface();
|
EGLSurface target = surface->eglSurface();
|
||||||
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
|
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
|
||||||
surface->bufferSize().isEmpty())
|
surface->bufferSize().isEmpty())
|
||||||
@@ -1129,8 +662,9 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
eglSwapInterval(m_eglDisplay, 0);
|
// 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 {
|
struct ContextRelease {
|
||||||
EGLDisplay display;
|
EGLDisplay display;
|
||||||
~ContextRelease() {
|
~ContextRelease() {
|
||||||
@@ -1141,6 +675,7 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
|||||||
|
|
||||||
QVariantMap crops;
|
QVariantMap crops;
|
||||||
QSize size;
|
QSize size;
|
||||||
|
bool upload = false;
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
@@ -1148,153 +683,38 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) {
|
|||||||
crops = m_crops;
|
crops = m_crops;
|
||||||
size = surface->bufferSize();
|
size = surface->bufferSize();
|
||||||
|
|
||||||
if (size.isEmpty()) return;
|
upload = m_imageChanged;
|
||||||
|
m_imageChanged = false;
|
||||||
if (m_program == 0) m_program = createProgram();
|
|
||||||
if (m_program == 0) return;
|
|
||||||
|
|
||||||
const bool videoNow = m_isVideo;
|
|
||||||
|
|
||||||
if (videoNow && !m_videoReady.load(std::memory_order_relaxed)) return;
|
|
||||||
|
|
||||||
const bool videoAsBackground =
|
|
||||||
(videoNow || (m_previousVideo && m_fading)) && m_mpv != nullptr;
|
|
||||||
|
|
||||||
if (videoAsBackground) {
|
|
||||||
if (!m_mpvRender) initMpvRender();
|
|
||||||
if (!m_mpvRender) return;
|
|
||||||
|
|
||||||
glDisable(GL_BLEND);
|
|
||||||
|
|
||||||
glViewport(0, 0, size.width(), size.height());
|
|
||||||
renderMpvToSurface(size);
|
|
||||||
} else {
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glViewport(0, 0, size.width(), size.height());
|
|
||||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!videoNow) {
|
|
||||||
const bool upload = m_imageChanged;
|
|
||||||
m_imageChanged = false;
|
|
||||||
|
|
||||||
if (m_texture == 0 || upload) uploadImage();
|
|
||||||
|
|
||||||
if (m_texture == 0 || m_image.isNull()) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glEnable(GL_BLEND);
|
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
||||||
|
|
||||||
if (m_fading && !m_fadeClockStarted) {
|
|
||||||
m_fadeClockStarted = true;
|
|
||||||
m_fadeTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool fade = false;
|
|
||||||
bool fadeDone = false;
|
|
||||||
qreal progress = 1.0;
|
|
||||||
|
|
||||||
if (m_fading) {
|
|
||||||
progress = qreal(m_fadeTimer.elapsed()) / qreal(kFadeDurationMs);
|
|
||||||
if (progress >= 1.0)
|
|
||||||
fadeDone = true;
|
|
||||||
else
|
|
||||||
fade = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float w = float(size.width());
|
|
||||||
const float h = float(size.height());
|
|
||||||
|
|
||||||
if (videoNow) {
|
|
||||||
if (fade && !m_previousImage.isNull()) {
|
|
||||||
if (m_previousTexture == 0)
|
|
||||||
uploadTexture(m_previousTexture, m_previousImage);
|
|
||||||
|
|
||||||
if (m_previousTexture != 0) {
|
|
||||||
const QRectF previousRect = sourceRect(
|
|
||||||
m_previousCrops,
|
|
||||||
surface->screen(),
|
|
||||||
size,
|
|
||||||
m_previousImage.size());
|
|
||||||
|
|
||||||
drawTexture(
|
|
||||||
m_previousTexture,
|
|
||||||
previousRect,
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
1.0f - float(progress));
|
|
||||||
}
|
|
||||||
} else if (fade && !m_firstFadeDone) {
|
|
||||||
if (m_blackTex == 0) createBlackTexture(m_blackTex);
|
|
||||||
|
|
||||||
if (m_blackTex != 0)
|
|
||||||
drawTexture(
|
|
||||||
m_blackTex,
|
|
||||||
QRectF(0.0, 0.0, 1.0, 1.0),
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
1.0f - float(progress));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const QRectF rect =
|
|
||||||
sourceRect(crops, surface->screen(), size, m_image.size());
|
|
||||||
|
|
||||||
if (fade) {
|
|
||||||
if (!m_previousVideo && !m_previousImage.isNull() &&
|
|
||||||
m_previousTexture == 0) {
|
|
||||||
uploadTexture(m_previousTexture, m_previousImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_previousVideo && m_previousTexture != 0) {
|
|
||||||
const QRectF previousRect = sourceRect(
|
|
||||||
m_previousCrops,
|
|
||||||
surface->screen(),
|
|
||||||
size,
|
|
||||||
m_previousImage.size());
|
|
||||||
|
|
||||||
drawTexture(m_previousTexture, previousRect, w, h, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTexture(m_texture, rect, w, h, float(progress));
|
|
||||||
} else {
|
|
||||||
drawTexture(m_texture, rect, w, h, 1.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fadeDone) {
|
|
||||||
m_fading = false;
|
|
||||||
m_firstFadeDone = true;
|
|
||||||
|
|
||||||
if (m_previousVideo) {
|
|
||||||
teardownMpv();
|
|
||||||
m_previousVideo = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_previousTexture != 0) {
|
|
||||||
glDeleteTextures(1, &m_previousTexture);
|
|
||||||
m_previousTexture = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_previousImage = QImage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
surface->requestFrame();
|
if (size.isEmpty()) return;
|
||||||
|
|
||||||
if (!eglSwapBuffers(m_eglDisplay, target)) {
|
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()
|
qWarning()
|
||||||
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
|
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
|
||||||
<< QString::number(unsigned(eglGetError()), 16);
|
<< QString::number(unsigned(eglGetError()), 16);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((m_isVideo || m_previousVideo) && m_mpvRender)
|
|
||||||
mpv_render_context_report_swap(m_mpvRender);
|
|
||||||
|
|
||||||
surface->endDraw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperService::addSurface(const QString& screen) {
|
void WallpaperService::addSurface(const QString& screen) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QElapsedTimer>
|
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
@@ -17,18 +16,12 @@
|
|||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
#include <QtQml/qqml.h>
|
#include <QtQml/qqml.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
||||||
#include <wayland-client-protocol.h>
|
#include <wayland-client-protocol.h>
|
||||||
|
|
||||||
#include "wallpapersurface.hpp"
|
#include "wallpapersurface.hpp"
|
||||||
|
|
||||||
struct mpv_handle;
|
|
||||||
struct mpv_render_context;
|
|
||||||
|
|
||||||
class QProcess;
|
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::internal {
|
||||||
|
|
||||||
class WallpaperSurface;
|
class WallpaperSurface;
|
||||||
@@ -64,29 +57,6 @@ class WallpaperService : public QObject {
|
|||||||
private:
|
private:
|
||||||
friend class WallpaperSurface;
|
friend class WallpaperSurface;
|
||||||
|
|
||||||
static bool isVideoPath(const QString& path);
|
|
||||||
|
|
||||||
static bool isHardwareCodec(const QString& codec);
|
|
||||||
|
|
||||||
[[nodiscard]] QString codecName(const QString& path) const;
|
|
||||||
[[nodiscard]] QString cacheDir() const;
|
|
||||||
[[nodiscard]] QString transcodeTarget(const QString& path) const;
|
|
||||||
[[nodiscard]] bool needsTranscode(const QString& path) const;
|
|
||||||
void transcodeVideo(const QString& src, const QString& dst);
|
|
||||||
|
|
||||||
void applyPath(const QString& path);
|
|
||||||
|
|
||||||
void startVideo(const QString& path);
|
|
||||||
void stopVideo();
|
|
||||||
void teardownMpv();
|
|
||||||
|
|
||||||
void initMpvRender();
|
|
||||||
void renderMpvToSurface(const QSize& size);
|
|
||||||
void pumpMpvEvents();
|
|
||||||
|
|
||||||
static void* getMpvProcAddress(void* ctx, const char* name);
|
|
||||||
static void mpvUpdate(void* ctx);
|
|
||||||
|
|
||||||
void bindGlobals();
|
void bindGlobals();
|
||||||
static void registryGlobal(
|
static void registryGlobal(
|
||||||
void* data,
|
void* data,
|
||||||
@@ -100,16 +70,7 @@ class WallpaperService : public QObject {
|
|||||||
[[nodiscard]] bool setupGl();
|
[[nodiscard]] bool setupGl();
|
||||||
[[nodiscard]] uint32_t createProgram();
|
[[nodiscard]] uint32_t createProgram();
|
||||||
|
|
||||||
void uploadTexture(uint32_t& texture, const QImage& image);
|
|
||||||
void uploadImage();
|
void uploadImage();
|
||||||
void createBlackTexture(uint32_t& texture);
|
|
||||||
|
|
||||||
void drawTexture(
|
|
||||||
uint32_t texture,
|
|
||||||
const QRectF& rect,
|
|
||||||
float sizeW,
|
|
||||||
float sizeH,
|
|
||||||
float alpha);
|
|
||||||
|
|
||||||
bool decode(const QString& path);
|
bool decode(const QString& path);
|
||||||
|
|
||||||
@@ -134,8 +95,7 @@ class WallpaperService : public QObject {
|
|||||||
[[nodiscard]] QRectF sourceRect(
|
[[nodiscard]] QRectF sourceRect(
|
||||||
const QVariantMap& crops,
|
const QVariantMap& crops,
|
||||||
const QString& screen,
|
const QString& screen,
|
||||||
const QSize& size,
|
const QSize& size) const;
|
||||||
const QSize& image) const;
|
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
|
|
||||||
@@ -148,18 +108,6 @@ class WallpaperService : public QObject {
|
|||||||
|
|
||||||
QImage m_image;
|
QImage m_image;
|
||||||
|
|
||||||
bool m_isVideo = false;
|
|
||||||
bool m_videoReload = false;
|
|
||||||
bool m_previousVideo = false;
|
|
||||||
mpv_handle* m_mpv = nullptr;
|
|
||||||
mpv_render_context* m_mpvRender = nullptr;
|
|
||||||
uint32_t m_blackTex = 0;
|
|
||||||
std::atomic<bool> m_mpvFrameReady{false};
|
|
||||||
std::atomic<bool> m_videoReady{false};
|
|
||||||
std::atomic<bool> m_videoLoaded{false};
|
|
||||||
|
|
||||||
QProcess* m_transcode = nullptr;
|
|
||||||
|
|
||||||
QSize m_sourceSize;
|
QSize m_sourceSize;
|
||||||
QSize m_decodeSize;
|
QSize m_decodeSize;
|
||||||
|
|
||||||
@@ -177,20 +125,12 @@ class WallpaperService : public QObject {
|
|||||||
uint32_t m_program = 0;
|
uint32_t m_program = 0;
|
||||||
int m_sizeUniform = -1;
|
int m_sizeUniform = -1;
|
||||||
int m_cropUniform = -1;
|
int m_cropUniform = -1;
|
||||||
int m_alphaUniform = -1;
|
|
||||||
|
|
||||||
QImage m_previousImage;
|
|
||||||
QVariantMap m_previousCrops;
|
|
||||||
uint32_t m_previousTexture = 0;
|
|
||||||
bool m_fading = false;
|
|
||||||
bool m_fadeClockStarted = false;
|
|
||||||
bool m_firstFadeDone = false;
|
|
||||||
QElapsedTimer m_fadeTimer;
|
|
||||||
|
|
||||||
QList<WallpaperSurface*> m_surfaces;
|
QList<WallpaperSurface*> m_surfaces;
|
||||||
|
|
||||||
std::thread m_thread;
|
std::thread m_thread;
|
||||||
mutable std::recursive_mutex m_mutex;
|
mutable std::recursive_mutex m_mutex;
|
||||||
|
std::vector<WallpaperSurface*> m_pending;
|
||||||
wl_event_queue* m_queue = nullptr;
|
wl_event_queue* m_queue = nullptr;
|
||||||
|
|
||||||
int m_wake[2] = {-1, -1};
|
int m_wake[2] = {-1, -1};
|
||||||
|
|||||||
@@ -53,11 +53,6 @@ WallpaperSurface::WallpaperSurface(
|
|||||||
}
|
}
|
||||||
|
|
||||||
WallpaperSurface::~WallpaperSurface() {
|
WallpaperSurface::~WallpaperSurface() {
|
||||||
if (m_frameCallback) {
|
|
||||||
wl_callback_destroy(m_frameCallback);
|
|
||||||
m_frameCallback = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_eglSurface != EGL_NO_SURFACE)
|
if (m_eglSurface != EGL_NO_SURFACE)
|
||||||
eglDestroySurface(m_service->m_eglDisplay, m_eglSurface);
|
eglDestroySurface(m_service->m_eglDisplay, m_eglSurface);
|
||||||
|
|
||||||
@@ -73,45 +68,8 @@ WallpaperSurface::~WallpaperSurface() {
|
|||||||
if (m_surface) wl_surface_destroy(m_surface);
|
if (m_surface) wl_surface_destroy(m_surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallpaperSurface::markDirty() {
|
void WallpaperSurface::redraw() {
|
||||||
m_dirty.store(true, std::memory_order_relaxed);
|
m_service->queue(this);
|
||||||
}
|
|
||||||
|
|
||||||
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::zwlr_layer_surface_v1_configure(
|
void WallpaperSurface::zwlr_layer_surface_v1_configure(
|
||||||
@@ -183,7 +141,6 @@ void WallpaperSurface::updateBuffer() {
|
|||||||
if (QtWayland::wp_viewport::isInitialized())
|
if (QtWayland::wp_viewport::isInitialized())
|
||||||
set_destination(m_size.width(), m_size.height());
|
set_destination(m_size.width(), m_size.height());
|
||||||
|
|
||||||
requestFrame();
|
|
||||||
m_service->queue(this);
|
m_service->queue(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
#include <EGL/egl.h>
|
#include <EGL/egl.h>
|
||||||
#include <wayland-egl.h>
|
#include <wayland-egl.h>
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
@@ -34,14 +32,7 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
|||||||
[[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; }
|
[[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; }
|
||||||
[[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; }
|
[[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; }
|
||||||
|
|
||||||
void markDirty();
|
void redraw();
|
||||||
[[nodiscard]] bool needsDraw() const;
|
|
||||||
|
|
||||||
[[nodiscard]] bool frameReady() const;
|
|
||||||
|
|
||||||
void requestFrame();
|
|
||||||
|
|
||||||
void endDraw();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void zwlr_layer_surface_v1_configure(
|
void zwlr_layer_surface_v1_configure(
|
||||||
@@ -51,8 +42,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
|||||||
|
|
||||||
void updateBuffer();
|
void updateBuffer();
|
||||||
|
|
||||||
void onFrameDone();
|
|
||||||
|
|
||||||
WallpaperService* m_service;
|
WallpaperService* m_service;
|
||||||
QString m_screen;
|
QString m_screen;
|
||||||
wl_surface* m_surface;
|
wl_surface* m_surface;
|
||||||
@@ -64,10 +53,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
|
|||||||
qreal m_scale = 1.0;
|
qreal m_scale = 1.0;
|
||||||
|
|
||||||
bool configured = false;
|
bool configured = false;
|
||||||
|
|
||||||
std::atomic<bool> m_dirty{false};
|
|
||||||
std::atomic<bool> m_frameReady{true};
|
|
||||||
struct wl_callback* m_frameCallback = nullptr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::internal
|
||||||
|
|||||||
@@ -1,183 +1,17 @@
|
|||||||
#include "filesystemmodel.hpp"
|
#include "filesystemmodel.hpp"
|
||||||
|
|
||||||
#include <qcoreapplication.h>
|
|
||||||
#include <qdatetime.h>
|
|
||||||
#include <qdir.h>
|
|
||||||
#include <qdiriterator.h>
|
#include <qdiriterator.h>
|
||||||
#include <qfile.h>
|
|
||||||
#include <qfileinfo.h>
|
|
||||||
#include <qfuturewatcher.h>
|
#include <qfuturewatcher.h>
|
||||||
#include <qimage.h>
|
|
||||||
#include <qpointer.h>
|
|
||||||
#include <qprocess.h>
|
|
||||||
#include <qregularexpression.h>
|
|
||||||
#include <qtemporarydir.h>
|
|
||||||
#include <qtconcurrentrun.h>
|
#include <qtconcurrentrun.h>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace ZShell::models {
|
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString videoThumbnailPath(const QString& path) {
|
|
||||||
const QFileInfo info(path);
|
|
||||||
|
|
||||||
QString key = QStringLiteral("%1_%2_%3")
|
|
||||||
.arg(info.baseName())
|
|
||||||
.arg(info.size())
|
|
||||||
.arg(info.lastModified().toMSecsSinceEpoch() / 1000);
|
|
||||||
key.replace(QRegularExpression("[^A-Za-z0-9._-]"), "_");
|
|
||||||
|
|
||||||
return QDir::homePath() + "/.cache/zshell/imagecache/video-previews/" +
|
|
||||||
key + "_preview.jpg";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool generateVideoThumbnail(const QString& src, const QString& cachePath) {
|
|
||||||
if (QFile::exists(cachePath)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString scale = QStringLiteral(
|
|
||||||
"scale=1920:1080:force_original_aspect_ratio=decrease,"
|
|
||||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2");
|
|
||||||
|
|
||||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
|
||||||
QProcess process;
|
|
||||||
process.start(program, args);
|
|
||||||
process.waitForFinished(15000);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
|
|
||||||
: 8;
|
|
||||||
|
|
||||||
QTemporaryDir tmpDir;
|
|
||||||
if (!tmpDir.isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir().mkpath(QFileInfo(cachePath).absolutePath());
|
|
||||||
return QFile::copy(
|
|
||||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)),
|
|
||||||
cachePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSystemEntry::FileSystemEntry(
|
FileSystemEntry::FileSystemEntry(
|
||||||
const QString& path, const QString& relativePath, QObject* parent)
|
const QString& path, const QString& relativePath, QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_fileInfo(path)
|
, m_fileInfo(path)
|
||||||
, m_path(path)
|
, m_path(path)
|
||||||
, m_relativePath(relativePath)
|
, m_relativePath(relativePath)
|
||||||
, m_thumbnailPath(path)
|
|
||||||
, m_isImageInitialised(false)
|
, m_isImageInitialised(false)
|
||||||
, m_mimeTypeInitialised(false) {}
|
, m_mimeTypeInitialised(false) {}
|
||||||
|
|
||||||
@@ -222,23 +56,6 @@ bool FileSystemEntry::isImage() const {
|
|||||||
return m_isImage;
|
return m_isImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileSystemEntry::isVideo() const {
|
|
||||||
return hasVideoExtension(m_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FileSystemEntry::thumbnailPath() const {
|
|
||||||
return m_thumbnailPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileSystemEntry::setThumbnailPath(const QString& path) {
|
|
||||||
if (m_thumbnailPath == path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_thumbnailPath = path;
|
|
||||||
emit thumbnailPathChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FileSystemEntry::mimeType() const {
|
QString FileSystemEntry::mimeType() const {
|
||||||
if (!m_mimeTypeInitialised) {
|
if (!m_mimeTypeInitialised) {
|
||||||
const QMimeDatabase db;
|
const QMimeDatabase db;
|
||||||
@@ -501,20 +318,11 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
|
|
||||||
std::optional<QDirIterator> iter;
|
std::optional<QDirIterator> iter;
|
||||||
|
|
||||||
if (filter == Images || filter == Videos || filter == Media) {
|
if (filter == Images) {
|
||||||
QStringList extraNameFilters = nameFilters;
|
QStringList extraNameFilters = nameFilters;
|
||||||
|
const auto formats = QImageReader::supportedImageFormats();
|
||||||
if (filter == Images || filter == Media) {
|
for (const auto& format : formats) {
|
||||||
const auto formats = QImageReader::supportedImageFormats();
|
extraNameFilters << "*." + format;
|
||||||
for (const auto& format : formats) {
|
|
||||||
extraNameFilters << "*." + format;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter == Videos || filter == Media) {
|
|
||||||
for (const auto& ext : kVideoExtensions) {
|
|
||||||
extraNameFilters << "*." + ext;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir::Filters filters = QDir::Files;
|
QDir::Filters filters = QDir::Files;
|
||||||
@@ -558,13 +366,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (filter == Media) {
|
|
||||||
if (!hasVideoExtension(path)) {
|
|
||||||
QImageReader reader(path);
|
|
||||||
if (!reader.canRead()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newPaths.insert(path);
|
newPaths.insert(path);
|
||||||
@@ -608,30 +409,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::startThumbnailGeneration(FileSystemEntry* entry) {
|
|
||||||
const QPointer<FileSystemEntry> guard(entry);
|
|
||||||
const QString source = entry->path();
|
|
||||||
const QString cachePath = videoThumbnailPath(source);
|
|
||||||
|
|
||||||
const auto future = QtConcurrent::run([source, cachePath]() {
|
|
||||||
return generateVideoThumbnail(source, cachePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
const auto watcher = new QFutureWatcher<bool>(this);
|
|
||||||
connect(
|
|
||||||
watcher,
|
|
||||||
&QFutureWatcher<bool>::finished,
|
|
||||||
this,
|
|
||||||
[guard, cachePath, watcher]() {
|
|
||||||
if (guard && watcher->result()) {
|
|
||||||
guard->setThumbnailPath(cachePath);
|
|
||||||
}
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileSystemModel::applyChanges(
|
void FileSystemModel::applyChanges(
|
||||||
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
|
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
|
||||||
QList<int> removedIndices;
|
QList<int> removedIndices;
|
||||||
@@ -673,12 +450,8 @@ void FileSystemModel::applyChanges(
|
|||||||
// Create new entries
|
// Create new entries
|
||||||
QList<FileSystemEntry*> newEntries;
|
QList<FileSystemEntry*> newEntries;
|
||||||
for (const auto& path : addedPaths) {
|
for (const auto& path : addedPaths) {
|
||||||
auto* entry =
|
newEntries << new FileSystemEntry(
|
||||||
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
path, m_dir.relativeFilePath(path), this);
|
||||||
if (entry->isVideo()) {
|
|
||||||
startThumbnailGeneration(entry);
|
|
||||||
}
|
|
||||||
newEntries << entry;
|
|
||||||
}
|
}
|
||||||
std::sort(
|
std::sort(
|
||||||
newEntries.begin(),
|
newEntries.begin(),
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ class FileSystemEntry : public QObject {
|
|||||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||||
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
|
||||||
Q_PROPERTY(
|
|
||||||
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
|
|
||||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -48,24 +45,18 @@ class FileSystemEntry : public QObject {
|
|||||||
[[nodiscard]] qint64 size() const;
|
[[nodiscard]] qint64 size() const;
|
||||||
[[nodiscard]] bool isDir() const;
|
[[nodiscard]] bool isDir() const;
|
||||||
[[nodiscard]] bool isImage() const;
|
[[nodiscard]] bool isImage() const;
|
||||||
[[nodiscard]] bool isVideo() const;
|
|
||||||
[[nodiscard]] QString thumbnailPath() const;
|
|
||||||
[[nodiscard]] QString mimeType() const;
|
[[nodiscard]] QString mimeType() const;
|
||||||
|
|
||||||
void setThumbnailPath(const QString& path);
|
|
||||||
|
|
||||||
void updateRelativePath(const QDir& dir);
|
void updateRelativePath(const QDir& dir);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void relativePathChanged();
|
void relativePathChanged();
|
||||||
void thumbnailPathChanged();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const QFileInfo m_fileInfo;
|
const QFileInfo m_fileInfo;
|
||||||
|
|
||||||
const QString m_path;
|
const QString m_path;
|
||||||
QString m_relativePath;
|
QString m_relativePath;
|
||||||
QString m_thumbnailPath;
|
|
||||||
|
|
||||||
mutable bool m_isImage;
|
mutable bool m_isImage;
|
||||||
mutable bool m_isImageInitialised;
|
mutable bool m_isImageInitialised;
|
||||||
@@ -100,7 +91,7 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
NOTIFY entriesChanged)
|
NOTIFY entriesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Filter { NoFilter, Images, Videos, Media, Files, Dirs };
|
enum Filter { NoFilter, Images, Files, Dirs };
|
||||||
Q_ENUM(Filter)
|
Q_ENUM(Filter)
|
||||||
|
|
||||||
explicit FileSystemModel(QObject* parent = nullptr);
|
explicit FileSystemModel(QObject* parent = nullptr);
|
||||||
@@ -165,7 +156,6 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
void updateEntriesForDir(const QString& dir);
|
void updateEntriesForDir(const QString& dir);
|
||||||
void applyChanges(
|
void applyChanges(
|
||||||
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||||
void startThumbnailGeneration(FileSystemEntry* entry);
|
|
||||||
[[nodiscard]] bool compareEntries(
|
[[nodiscard]] bool compareEntries(
|
||||||
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||||
};
|
};
|
||||||
|
|||||||
+1834
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -32,23 +31,6 @@ from zshell.utils.schemepalettes import (
|
|||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
VIDEO_EXTENSIONS = {
|
|
||||||
".mp4",
|
|
||||||
".mkv",
|
|
||||||
".webm",
|
|
||||||
".avi",
|
|
||||||
".mov",
|
|
||||||
".wmv",
|
|
||||||
".flv",
|
|
||||||
".m4v",
|
|
||||||
".mpg",
|
|
||||||
".mpeg",
|
|
||||||
".3gp",
|
|
||||||
".ogv",
|
|
||||||
".ts",
|
|
||||||
".vob",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _complete_scheme_name(incomplete):
|
def _complete_scheme_name(incomplete):
|
||||||
schemes = [
|
schemes = [
|
||||||
@@ -369,109 +351,6 @@ def generate(
|
|||||||
safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key)
|
safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key)
|
||||||
return thumb_dir / f"{safe_key}_thumbnail.jpg"
|
return thumb_dir / f"{safe_key}_thumbnail.jpg"
|
||||||
|
|
||||||
def is_video(path: Path) -> bool:
|
|
||||||
return path.suffix.lower() in VIDEO_EXTENSIONS
|
|
||||||
|
|
||||||
def video_duration(path: Path) -> float:
|
|
||||||
try:
|
|
||||||
out = subprocess.run(
|
|
||||||
[
|
|
||||||
"ffprobe",
|
|
||||||
"-v",
|
|
||||||
"error",
|
|
||||||
"-show_entries",
|
|
||||||
"format=duration",
|
|
||||||
"-of",
|
|
||||||
"default=nw=1:nk=1",
|
|
||||||
str(path),
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
return float(out.stdout.strip())
|
|
||||||
except (ValueError, OSError):
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def video_frame_chroma(path: Path) -> tuple[float, float]:
|
|
||||||
"""Return (mean chroma, mean luminance) of a frame.
|
|
||||||
|
|
||||||
Mean chroma (mean of max-min across RGB channels) is ~0 for black,
|
|
||||||
white and grey frames, and rises with saturation - so it identifies a
|
|
||||||
"good", colorful frame and rejects black or fade frames.
|
|
||||||
"""
|
|
||||||
with Image.open(path) as im:
|
|
||||||
im = im.convert("RGB")
|
|
||||||
pixels = list(im.getdata())
|
|
||||||
|
|
||||||
chroma = 0
|
|
||||||
luma = 0
|
|
||||||
for r, g, b in pixels:
|
|
||||||
chroma += max(r, g, b) - min(r, g, b)
|
|
||||||
luma += r + g + b
|
|
||||||
|
|
||||||
n = max(1, len(pixels))
|
|
||||||
return chroma / n, luma / (3 * n)
|
|
||||||
|
|
||||||
def generate_video_thumbnail(
|
|
||||||
video_path: Path, cache_path: Path, size=(128, 128)
|
|
||||||
) -> None:
|
|
||||||
w, h = size
|
|
||||||
duration = video_duration(video_path)
|
|
||||||
|
|
||||||
samples = 8 if duration <= 0 else min(10, max(4, int(duration)))
|
|
||||||
|
|
||||||
tmp_dir = Path(tempfile.mkdtemp(prefix="zshell-vthumb-"))
|
|
||||||
try:
|
|
||||||
best: Path | None = None
|
|
||||||
best_key: tuple[float, float] = (-1.0, -1.0)
|
|
||||||
|
|
||||||
for i in range(samples):
|
|
||||||
frac = (i + 0.5) / samples if duration > 0 else 0.0
|
|
||||||
seek = f"{frac * duration:.3f}" if duration > 0 else "0"
|
|
||||||
frame = tmp_dir / f"frame_{i}.jpg"
|
|
||||||
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
"ffmpeg",
|
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"error",
|
|
||||||
"-ss",
|
|
||||||
seek,
|
|
||||||
"-i",
|
|
||||||
str(video_path),
|
|
||||||
"-frames:v",
|
|
||||||
"1",
|
|
||||||
"-vf",
|
|
||||||
f"scale={w}:{h}",
|
|
||||||
"-pix_fmt",
|
|
||||||
"yuvj420p",
|
|
||||||
"-q:v",
|
|
||||||
"2",
|
|
||||||
"-y",
|
|
||||||
str(frame),
|
|
||||||
],
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not frame.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = video_frame_chroma(frame)
|
|
||||||
if key > best_key:
|
|
||||||
best_key = key
|
|
||||||
best = frame
|
|
||||||
|
|
||||||
if best is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Could not extract a frame from {video_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
shutil.copy(best, cache_path)
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def generate_thumbnail(
|
def generate_thumbnail(
|
||||||
image_path: Path, thumb_dir: Path, size=(128, 128)
|
image_path: Path, thumb_dir: Path, size=(128, 128)
|
||||||
) -> Path:
|
) -> Path:
|
||||||
@@ -481,10 +360,6 @@ def generate(
|
|||||||
if cache_path.exists():
|
if cache_path.exists():
|
||||||
return cache_path
|
return cache_path
|
||||||
|
|
||||||
if is_video(image_path):
|
|
||||||
generate_video_thumbnail(image_path, cache_path, size)
|
|
||||||
return cache_path
|
|
||||||
|
|
||||||
image = Image.open(image_path)
|
image = Image.open(image_path)
|
||||||
image.draft("RGB", size)
|
image.draft("RGB", size)
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -9,16 +8,6 @@ args = ["qs", "-c", "zshell"]
|
|||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
qt_logging_rules = (
|
|
||||||
"qt.qml.warning=false;"
|
|
||||||
"qt.qpa.wayland.*=false;"
|
|
||||||
"scene=false;"
|
|
||||||
"quickshell.service.sni.watcher=false;"
|
|
||||||
"default.warning=false;"
|
|
||||||
"default.info=false;"
|
|
||||||
"qt.sql.qsqldatabase"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def kill():
|
def kill():
|
||||||
@@ -30,13 +19,8 @@ def kill():
|
|||||||
|
|
||||||
|
|
||||||
def start_instance(no_daemon: bool = False) -> None:
|
def start_instance(no_daemon: bool = False) -> None:
|
||||||
env = os.environ.copy()
|
|
||||||
env["QT_LOGGING_RULES"] = qt_logging_rules
|
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
args + ["-n"] + ([] if no_daemon else ["-d"]),
|
args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True
|
||||||
capture_output=True,
|
|
||||||
env=env,
|
|
||||||
)
|
)
|
||||||
stdout = result.stdout.decode().strip()
|
stdout = result.stdout.decode().strip()
|
||||||
if stdout and "already running" in stdout.lower():
|
if stdout and "already running" in stdout.lower():
|
||||||
|
|||||||
Reference in New Issue
Block a user