lots of changes
This commit is contained in:
@@ -14,6 +14,7 @@ Singleton {
|
||||
property alias wallust: adapter.wallust
|
||||
property alias workspaceWidget: adapter.workspaceWidget
|
||||
property alias colors: adapter.colors
|
||||
property alias gpuType: adapter.gpuType
|
||||
|
||||
FileView {
|
||||
id: root
|
||||
@@ -37,6 +38,7 @@ Singleton {
|
||||
property bool wallust: false
|
||||
property WorkspaceWidget workspaceWidget: WorkspaceWidget {}
|
||||
property Colors colors: Colors {}
|
||||
property string gpuType: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
-1
@@ -5,8 +5,10 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import ZShell
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Paths
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -81,7 +83,7 @@ Singleton {
|
||||
|
||||
FileView {
|
||||
id: storage
|
||||
path: NotifPath.notifPath
|
||||
path: `${Paths.state}/notifs.json`
|
||||
|
||||
onLoaded: {
|
||||
const data = JSON.parse(text());
|
||||
@@ -90,6 +92,7 @@ Singleton {
|
||||
root.list.sort((a, b) => b.time - a.time);
|
||||
root.loaded = true;
|
||||
}
|
||||
|
||||
onLoadFailed: err => {
|
||||
if (err === FileViewError.FileNotFound) {
|
||||
root.loaded = true;
|
||||
@@ -144,6 +147,50 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property LazyLoader dummyImageLoader: LazyLoader {
|
||||
active: false
|
||||
|
||||
PanelWindow {
|
||||
implicitWidth: 48
|
||||
implicitHeight: 48
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: Qt.resolvedUrl(notif.image)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
asynchronous: true
|
||||
opacity: 0
|
||||
|
||||
onStatusChanged: {
|
||||
if (status !== Image.Ready)
|
||||
return;
|
||||
|
||||
const cacheKey = notif.appName + notif.summary + notif.id;
|
||||
let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch;
|
||||
for (let i = 0; i < cacheKey.length; i++) {
|
||||
ch = cacheKey.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
|
||||
|
||||
const cache = `${Paths.notifimagecache}/${hash}.png`;
|
||||
ZShellIo.saveItem(this, Qt.resolvedUrl(cache), () => {
|
||||
notif.image = cache;
|
||||
notif.dummyImageLoader.active = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Connections conn: Connections {
|
||||
target: notif.notification
|
||||
|
||||
@@ -169,6 +216,8 @@ Singleton {
|
||||
|
||||
function onImageChanged(): void {
|
||||
notif.image = notif.notification.image;
|
||||
if (notif.notification?.image)
|
||||
notif.dummyImageLoader.active = true;
|
||||
}
|
||||
|
||||
function onExpireTimeoutChanged(): void {
|
||||
@@ -225,6 +274,8 @@ Singleton {
|
||||
appIcon = notification.appIcon;
|
||||
appName = notification.appName;
|
||||
image = notification.image;
|
||||
if (notification?.image)
|
||||
dummyImageLoader.active = true;
|
||||
expireTimeout = notification.expireTimeout;
|
||||
urgency = notification.urgency;
|
||||
resident = notification.resident;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import ZShell.Internal
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.Paths
|
||||
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property alias path: manager.path
|
||||
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
Connections {
|
||||
target: QsWindow.window
|
||||
|
||||
function onDevicePixelRatioChanged(): void {
|
||||
manager.updateSource();
|
||||
}
|
||||
}
|
||||
|
||||
CachingImageManager {
|
||||
id: manager
|
||||
|
||||
item: root
|
||||
cacheDir: Qt.resolvedUrl(Paths.imagecache)
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,33 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Config
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import ZShell.Models
|
||||
|
||||
Searcher {
|
||||
id: root
|
||||
|
||||
readonly property string currentNamePath: WallpaperPath.currentWallpaperPath
|
||||
|
||||
property bool showPreview: false
|
||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||
property string previewPath
|
||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
WallpaperPath.currentWallpaperPath = path;
|
||||
}
|
||||
|
||||
function preview(path: string): void {
|
||||
previewPath = path;
|
||||
showPreview = true;
|
||||
}
|
||||
|
||||
function stopPreview(): void {
|
||||
showPreview = false;
|
||||
}
|
||||
|
||||
list: wallpapers.entries
|
||||
key: "relativePath"
|
||||
useFuzzy: true
|
||||
@@ -16,6 +38,15 @@ Searcher {
|
||||
forward: false
|
||||
})
|
||||
|
||||
FileView {
|
||||
path: root.currentNamePath
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
root.actualCurrent = this.text;
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemModel {
|
||||
id: wallpapers
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string source: SearchWallpapers.current
|
||||
property Image current: one
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onSourceChanged: {
|
||||
if (!source) {
|
||||
current = null;
|
||||
} else if (current === one) {
|
||||
two.update();
|
||||
} else {
|
||||
one.update();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log(root.source)
|
||||
if (source)
|
||||
Qt.callLater(() => one.update());
|
||||
}
|
||||
|
||||
Img {
|
||||
id: one
|
||||
}
|
||||
|
||||
Img {
|
||||
id: two
|
||||
}
|
||||
|
||||
component Img: CachingImage {
|
||||
id: img
|
||||
|
||||
function update(): void {
|
||||
if (path === root.source) {
|
||||
root.current = this;
|
||||
} else {
|
||||
path = root.source;
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 0
|
||||
scale: SearchWallpapers.showPreview ? 1 : 0.8
|
||||
asynchronous: true
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready) {
|
||||
root.current = this;
|
||||
}
|
||||
}
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.current === img
|
||||
|
||||
PropertyChanges {
|
||||
img.opacity: 1
|
||||
img.scale: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
target: img
|
||||
properties: "opacity,scale"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,13 +83,16 @@ TextField {
|
||||
Search.launch(appListLoader.item.currentItem.modelData);
|
||||
launcherWindow.visible = false;
|
||||
} else if ( wallpaperPickerLoader.active ) {
|
||||
WallpaperPath.currentWallpaperPath = wallpaperPickerLoader.item.currentItem.modelData.path;
|
||||
SearchWallpapers.setWallpaper(wallpaperPickerLoader.item.currentItem.modelData.path)
|
||||
if ( Config.wallust ) {
|
||||
Wallust.generateColors(WallpaperPath.currentWallpaperPath);
|
||||
}
|
||||
closeAnim.start();
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if ( event.key === Qt.Key_Escape ) {
|
||||
if ( wallpaperPickerLoader.active )
|
||||
SearchWallpapers.stopPreview();
|
||||
closeAnim.start();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ Repeater {
|
||||
root.flagChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: groupColumn
|
||||
required property string modelData
|
||||
@@ -28,6 +29,7 @@ Repeater {
|
||||
|
||||
property bool shouldShow: false
|
||||
property bool isExpanded: false
|
||||
property bool collapseAnimRunning: false
|
||||
|
||||
function closeAll(): void {
|
||||
for ( const n of NotifServer.notClosed.filter( n => n.appName === modelData ))
|
||||
@@ -49,7 +51,7 @@ Repeater {
|
||||
id: addTrans
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 50
|
||||
duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 30
|
||||
}
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
@@ -77,6 +79,16 @@ Repeater {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: addTrans.ViewTransition.targetIndexes.length * 30 + 100
|
||||
running: groupColumn.isExpanded
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
groupColumn.shouldShow = true;
|
||||
console.log("ran timer");
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
id: moveTrans
|
||||
NumberAnimation {
|
||||
@@ -84,6 +96,11 @@ Repeater {
|
||||
duration: 100;
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
properties: "opacity, scale";
|
||||
to: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -120,11 +137,11 @@ Repeater {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
groupColumn.shouldShow = false;
|
||||
groupColumn.collapseAnimRunning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NotifGroupRepeater { }
|
||||
NotifGroupRepeater { id: groupRepeater }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,12 @@ Scope {
|
||||
}
|
||||
|
||||
Component.onCompleted: currentIndex = SearchWallpapers.list.findIndex( w => w.path === WallpaperPath.currentWallpaperPath )
|
||||
Component.onDestruction: SearchWallpapers.stopPreview()
|
||||
|
||||
onCurrentItemChanged: {
|
||||
if ( currentItem )
|
||||
SearchWallpapers.preview( currentItem.modelData.path );
|
||||
}
|
||||
|
||||
cacheItemCount: 5
|
||||
snapMode: PathView.SnapToItem
|
||||
|
||||
@@ -4,21 +4,21 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
|
||||
Repeater {
|
||||
id: groupListView
|
||||
model: ScriptModel {
|
||||
id: groupModel
|
||||
values: groupColumn.isExpanded ? groupColumn.notifications : groupColumn.notifications.slice( 0, 1 )
|
||||
values: groupColumn.isExpanded || groupColumn.shouldShow ? groupColumn.notifications : groupColumn.notifications.slice( 0, 1 )
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: groupHeader
|
||||
required property int index
|
||||
required property NotifServer.Notif modelData
|
||||
property alias notifHeight: groupHeader.height
|
||||
|
||||
property bool previewHidden: groupColumn.shouldShow && index > 0
|
||||
property bool previewHidden: !groupColumn.shouldShow && index > 0
|
||||
|
||||
width: parent.width
|
||||
height: contentColumn.height + 15
|
||||
@@ -26,10 +26,12 @@ Repeater {
|
||||
border.color: "#555555"
|
||||
border.width: 1
|
||||
radius: 8
|
||||
opacity: previewHidden ? 0 : 1.0
|
||||
opacity: previewHidden ? 0 : 1
|
||||
scale: previewHidden ? 0.7 : 1.0
|
||||
|
||||
Component.onCompleted: modelData.lock(this);
|
||||
Component.onCompleted: {
|
||||
modelData.lock(this);
|
||||
}
|
||||
Component.onDestruction: modelData.unlock(this);
|
||||
|
||||
MouseArea {
|
||||
@@ -40,7 +42,6 @@ Repeater {
|
||||
groupHeader.modelData.actions[0].invoke();
|
||||
}
|
||||
} else {
|
||||
groupColumn.shouldShow = true;
|
||||
groupColumn.isExpanded = true;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +49,7 @@ Repeater {
|
||||
|
||||
ParallelAnimation {
|
||||
id: collapseAnim
|
||||
running: !groupColumn.shouldShow && index > 0
|
||||
running: groupColumn.collapseAnimRunning
|
||||
|
||||
Anim {
|
||||
target: groupHeader
|
||||
@@ -66,6 +67,8 @@ Repeater {
|
||||
}
|
||||
onFinished: {
|
||||
groupColumn.isExpanded = false;
|
||||
groupColumn.shouldShow = false;
|
||||
groupColumn.collapseAnimRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,13 +110,6 @@ Repeater {
|
||||
}
|
||||
}
|
||||
|
||||
// Behavior on height {
|
||||
// Anim {
|
||||
// duration: MaterialEasing.expressiveDefaultSpatialTime
|
||||
// easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial
|
||||
// }
|
||||
// }
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.top: parent.top
|
||||
@@ -122,7 +118,6 @@ Repeater {
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
anchors.topMargin: 5
|
||||
// width: parent.width - 20
|
||||
spacing: 10
|
||||
RowLayout {
|
||||
id: infoRow
|
||||
@@ -130,19 +125,19 @@ Repeater {
|
||||
spacing: 10
|
||||
|
||||
IconImage {
|
||||
source: groupHeader.modelData.image
|
||||
source: groupHeader.modelData.image === "" ? Qt.resolvedUrl(groupHeader.modelData.appIcon) : Qt.resolvedUrl(groupHeader.modelData.image)
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
Layout.topMargin: 5
|
||||
visible: groupHeader.modelData.image !== ""
|
||||
visible: source !== ""
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Text {
|
||||
TextRender {
|
||||
text: groupHeader.modelData.summary
|
||||
color: "white"
|
||||
font.bold: true
|
||||
@@ -152,21 +147,26 @@ Repeater {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
Text {
|
||||
TextRender {
|
||||
text: groupHeader.modelData.body
|
||||
font.pointSize: 12
|
||||
color: "#dddddd"
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 20
|
||||
textFormat: Text.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 5
|
||||
linkColor: Config.accentColor.accents.primaryAlt
|
||||
|
||||
onLinkActivated: link => {
|
||||
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
TextRender {
|
||||
text: groupHeader.modelData.timeStr
|
||||
font.pointSize: 10
|
||||
color: "#666666"
|
||||
@@ -191,7 +191,7 @@ Repeater {
|
||||
required property var modelData
|
||||
color: buttonArea.containsMouse ? "#15FFFFFF" : "#09FFFFFF"
|
||||
radius: 4
|
||||
Text {
|
||||
TextRender {
|
||||
anchors.centerIn: parent
|
||||
text: actionButton.modelData.text
|
||||
color: "white"
|
||||
@@ -220,7 +220,7 @@ Repeater {
|
||||
color: closeArea.containsMouse ? "#FF6077" : "transparent"
|
||||
radius: 9
|
||||
|
||||
Text {
|
||||
TextRender {
|
||||
anchors.centerIn: parent
|
||||
text: "✕"
|
||||
color: closeArea.containsMouse ? "white" : "#888888"
|
||||
|
||||
@@ -71,7 +71,7 @@ PanelWindow {
|
||||
id: showAnimation
|
||||
target: backgroundRect
|
||||
property: "x"
|
||||
to: root.bar.screen.width - backgroundRect.implicitWidth - 10
|
||||
to: Math.round(root.bar.screen.width - backgroundRect.implicitWidth - 10)
|
||||
from: root.bar.screen.width
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
|
||||
@@ -4,8 +4,10 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property double memoryTotal: 1
|
||||
property double memoryFree: 1
|
||||
property double memoryUsed: memoryTotal - memoryFree
|
||||
@@ -19,6 +21,8 @@ Singleton {
|
||||
property double gpuUsage: 0
|
||||
property double gpuMemUsage: 0
|
||||
property double totalMem: 0
|
||||
readonly property string gpuType: Config.gpuType.toUpperCase() || autoGpuType
|
||||
property string autoGpuType: "NONE"
|
||||
|
||||
Timer {
|
||||
interval: 1
|
||||
@@ -52,7 +56,9 @@ Singleton {
|
||||
|
||||
previousCpuStats = { total, idle }
|
||||
}
|
||||
processGpu.running = true
|
||||
if ( root.gpuType === "NVIDIA" ) {
|
||||
processGpu.running = true
|
||||
}
|
||||
|
||||
interval = 1000
|
||||
}
|
||||
@@ -61,10 +67,20 @@ Singleton {
|
||||
FileView { id: fileMeminfo; path: "/proc/meminfo" }
|
||||
FileView { id: fileStat; path: "/proc/stat" }
|
||||
|
||||
Process {
|
||||
id: gpuTypeCheck
|
||||
|
||||
running: !Config.gpuType
|
||||
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.autoGpuType = text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: oneshotMem
|
||||
command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
||||
running: true
|
||||
running: root.gpuType === "NVIDIA" && totalMem === 0
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
totalMem = Number(this.text.trim())
|
||||
|
||||
@@ -146,11 +146,11 @@ PanelWindow {
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
IconImage {
|
||||
source: rootItem.modelData.image
|
||||
source: rootItem.modelData.image === "" ? Qt.resolvedUrl(rootItem.modelData.appIcon) : Qt.resolvedUrl(rootItem.modelData.image)
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignLeft
|
||||
visible: rootItem.modelData.image !== ""
|
||||
// visible: rootItem.modelData.image !== ""
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -185,10 +185,16 @@ PanelWindow {
|
||||
text: rootItem.modelData.body
|
||||
color: "#dddddd"
|
||||
font.pointSize: 14
|
||||
textFormat: Text.MarkdownText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 4
|
||||
width: parent.width
|
||||
linkColor: Config.accentColor.accents.primaryAlt
|
||||
|
||||
onLinkActivated: link => {
|
||||
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ Singleton {
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
updatesProc.running = true
|
||||
interval = 60000
|
||||
interval = 5000
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
pragma Singleton
|
||||
|
||||
import ZShell
|
||||
import Quickshell
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string home: Quickshell.env("HOME")
|
||||
readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures`
|
||||
readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos`
|
||||
|
||||
readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell`
|
||||
readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell`
|
||||
readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell`
|
||||
readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell`
|
||||
|
||||
readonly property string imagecache: `${cache}/imagecache`
|
||||
readonly property string notifimagecache: `${imagecache}/notifs`
|
||||
readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.wallpaperPath)
|
||||
readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings`
|
||||
readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell"
|
||||
|
||||
function toLocalFile(path: url): string {
|
||||
path = Qt.resolvedUrl(path);
|
||||
return path.toString() ? ZShellIo.toLocalFile(path) : "";
|
||||
}
|
||||
|
||||
function absolutePath(path: string): string {
|
||||
return toLocalFile(path.replace("~", home));
|
||||
}
|
||||
|
||||
function shortenHome(path: string): string {
|
||||
return path.replace(home, "~");
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,10 @@ qml_module(ZShell-internal
|
||||
SOURCES
|
||||
hyprextras.hpp hyprextras.cpp
|
||||
hyprdevices.hpp hyprdevices.cpp
|
||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Core
|
||||
)
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
#include "cachingimagemanager.hpp"
|
||||
|
||||
#include <QtQuick/qquickwindow.h>
|
||||
#include <qcryptographichash.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qpainter.h>
|
||||
#include <qtconcurrentrun.h>
|
||||
|
||||
namespace ZShell::internal {
|
||||
|
||||
qreal CachingImageManager::effectiveScale() const {
|
||||
if (m_item && m_item->window()) {
|
||||
return m_item->window()->devicePixelRatio();
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
QSize CachingImageManager::effectiveSize() const {
|
||||
if (!m_item) {
|
||||
return QSize();
|
||||
}
|
||||
|
||||
const qreal scale = effectiveScale();
|
||||
const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize();
|
||||
m_item->setProperty("sourceSize", size);
|
||||
return size;
|
||||
}
|
||||
|
||||
QQuickItem* CachingImageManager::item() const {
|
||||
return m_item;
|
||||
}
|
||||
|
||||
void CachingImageManager::setItem(QQuickItem* item) {
|
||||
if (m_item == item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_widthConn) {
|
||||
disconnect(m_widthConn);
|
||||
}
|
||||
if (m_heightConn) {
|
||||
disconnect(m_heightConn);
|
||||
}
|
||||
|
||||
m_item = item;
|
||||
emit itemChanged();
|
||||
|
||||
if (item) {
|
||||
m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() {
|
||||
updateSource();
|
||||
});
|
||||
m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() {
|
||||
updateSource();
|
||||
});
|
||||
updateSource();
|
||||
}
|
||||
}
|
||||
|
||||
QUrl CachingImageManager::cacheDir() const {
|
||||
return m_cacheDir;
|
||||
}
|
||||
|
||||
void CachingImageManager::setCacheDir(const QUrl& cacheDir) {
|
||||
if (m_cacheDir == cacheDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_cacheDir = cacheDir;
|
||||
if (!m_cacheDir.path().endsWith("/")) {
|
||||
m_cacheDir.setPath(m_cacheDir.path() + "/");
|
||||
}
|
||||
emit cacheDirChanged();
|
||||
}
|
||||
|
||||
QString CachingImageManager::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void CachingImageManager::setPath(const QString& path) {
|
||||
if (m_path == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = path;
|
||||
emit pathChanged();
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
updateSource(path);
|
||||
}
|
||||
}
|
||||
|
||||
void CachingImageManager::updateSource() {
|
||||
updateSource(m_path);
|
||||
}
|
||||
|
||||
void CachingImageManager::updateSource(const QString& path) {
|
||||
if (path.isEmpty() || path == m_shaPath) {
|
||||
// Path is empty or already calculating sha for path
|
||||
return;
|
||||
}
|
||||
|
||||
m_shaPath = path;
|
||||
|
||||
const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path);
|
||||
|
||||
const auto watcher = new QFutureWatcher<QString>(this);
|
||||
|
||||
connect(watcher, &QFutureWatcher<QString>::finished, this, [watcher, path, this]() {
|
||||
if (m_path != path) {
|
||||
// Object is destroyed or path has changed, ignore
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize size = effectiveSize();
|
||||
|
||||
if (!m_item || !size.width() || !size.height()) {
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString fillMode = m_item->property("fillMode").toString();
|
||||
// clang-format off
|
||||
const QString filename = QString("%1@%2x%3-%4.png")
|
||||
.arg(watcher->result()).arg(size.width()).arg(size.height())
|
||||
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch");
|
||||
// clang-format on
|
||||
|
||||
const QUrl cache = m_cacheDir.resolved(QUrl(filename));
|
||||
if (m_cachePath == cache) {
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cachePath = cache;
|
||||
emit cachePathChanged();
|
||||
|
||||
if (!cache.isLocalFile()) {
|
||||
qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file";
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const QImageReader reader(cache.toLocalFile());
|
||||
if (reader.canRead()) {
|
||||
m_item->setProperty("source", cache);
|
||||
} else {
|
||||
m_item->setProperty("source", QUrl::fromLocalFile(path));
|
||||
createCache(path, cache.toLocalFile(), fillMode, size);
|
||||
}
|
||||
|
||||
// Clear current running sha if same
|
||||
if (m_shaPath == path) {
|
||||
m_shaPath = QString();
|
||||
}
|
||||
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
QUrl CachingImageManager::cachePath() const {
|
||||
return m_cachePath;
|
||||
}
|
||||
|
||||
void CachingImageManager::createCache(
|
||||
const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const {
|
||||
QThreadPool::globalInstance()->start([path, cache, fillMode, size] {
|
||||
QImage image(path);
|
||||
|
||||
if (image.isNull()) {
|
||||
qWarning() << "CachingImageManager::createCache: failed to read" << path;
|
||||
return;
|
||||
}
|
||||
|
||||
image.convertTo(QImage::Format_ARGB32);
|
||||
|
||||
if (fillMode == "PreserveAspectCrop") {
|
||||
image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||
} else if (fillMode == "PreserveAspectFit") {
|
||||
image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
} else {
|
||||
image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") {
|
||||
QImage canvas(size, QImage::Format_ARGB32);
|
||||
canvas.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&canvas);
|
||||
painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image);
|
||||
painter.end();
|
||||
|
||||
image = canvas;
|
||||
}
|
||||
|
||||
const QString parent = QFileInfo(cache).absolutePath();
|
||||
if (!QDir().mkpath(parent) || !image.save(cache)) {
|
||||
qWarning() << "CachingImageManager::createCache: failed to save to" << cache;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QString CachingImageManager::sha256sum(const QString& path) {
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "CachingImageManager::sha256sum: failed to open" << path;
|
||||
return "";
|
||||
}
|
||||
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
hash.addData(&file);
|
||||
file.close();
|
||||
|
||||
return hash.result().toHex();
|
||||
}
|
||||
|
||||
} // namespace ZShell::internal
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtQuick/qquickitem.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::internal {
|
||||
|
||||
class CachingImageManager : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED)
|
||||
Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED)
|
||||
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||
Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged)
|
||||
|
||||
public:
|
||||
explicit CachingImageManager(QObject* parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_item(nullptr) {}
|
||||
|
||||
[[nodiscard]] QQuickItem* item() const;
|
||||
void setItem(QQuickItem* item);
|
||||
|
||||
[[nodiscard]] QUrl cacheDir() const;
|
||||
void setCacheDir(const QUrl& cacheDir);
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] QUrl cachePath() const;
|
||||
|
||||
Q_INVOKABLE void updateSource();
|
||||
Q_INVOKABLE void updateSource(const QString& path);
|
||||
|
||||
signals:
|
||||
void itemChanged();
|
||||
void cacheDirChanged();
|
||||
|
||||
void pathChanged();
|
||||
void cachePathChanged();
|
||||
void usingCacheChanged();
|
||||
|
||||
private:
|
||||
QString m_shaPath;
|
||||
|
||||
QQuickItem* m_item;
|
||||
QUrl m_cacheDir;
|
||||
|
||||
QString m_path;
|
||||
QUrl m_cachePath;
|
||||
|
||||
QMetaObject::Connection m_widthConn;
|
||||
QMetaObject::Connection m_heightConn;
|
||||
|
||||
[[nodiscard]] qreal effectiveScale() const;
|
||||
[[nodiscard]] QSize effectiveSize() const;
|
||||
|
||||
void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const;
|
||||
[[nodiscard]] static QString sha256sum(const QString& path);
|
||||
};
|
||||
|
||||
} // namespace ZShell::internal
|
||||
+1
-17
@@ -21,23 +21,7 @@ Scope {
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
Image {
|
||||
id: wallpaperImage
|
||||
anchors.fill: parent
|
||||
source: WallpaperPath.currentWallpaperPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
|
||||
// Behavior on source {
|
||||
// Anim {
|
||||
// properties: "opacity"
|
||||
// duration: 500
|
||||
// }
|
||||
// }
|
||||
}
|
||||
Background {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user