Compare commits
4
Commits
v0.4.0
...
ec80adf0a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec80adf0a5 | ||
|
|
88a2808a47 | ||
|
|
a13daf27a6 | ||
|
|
75de349ff4 |
Executable
+338
@@ -0,0 +1,338 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Modules.Launcher.Items
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var arrangement: {
|
||||||
|
const W = width;
|
||||||
|
const F = focalWidth;
|
||||||
|
const M = minEdgeWidth;
|
||||||
|
const sp = itemSpacing;
|
||||||
|
|
||||||
|
if (W <= 0 || F <= 0)
|
||||||
|
return {
|
||||||
|
n: 0,
|
||||||
|
sizes: [F],
|
||||||
|
effectiveExtraWidth: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let bestN = 0;
|
||||||
|
let bestSizes = [F];
|
||||||
|
let bestExtraWidth = 0;
|
||||||
|
|
||||||
|
for (let n = 1; ; n++) {
|
||||||
|
const ratio = Math.pow(M / F, 1 / n);
|
||||||
|
const sizes = [F];
|
||||||
|
|
||||||
|
for (let i = 1; i <= n; i++) {
|
||||||
|
sizes.push(i === n ? M : F * Math.pow(ratio, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
let decreasedWidth = 0;
|
||||||
|
|
||||||
|
for (let i = 1; i <= n; i++)
|
||||||
|
decreasedWidth += pitch - sizes[i];
|
||||||
|
|
||||||
|
const effectiveWidth = W + decreasedWidth * 2;
|
||||||
|
const requiredWidth = (2 * n + 1) * pitch;
|
||||||
|
|
||||||
|
if (requiredWidth > effectiveWidth)
|
||||||
|
break;
|
||||||
|
|
||||||
|
bestN = n;
|
||||||
|
bestSizes = sizes;
|
||||||
|
bestExtraWidth = decreasedWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
n: bestN,
|
||||||
|
sizes: bestSizes,
|
||||||
|
effectiveExtraWidth: bestExtraWidth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
readonly property int centerBlockIndex: Math.floor(loopMultiplier / 2)
|
||||||
|
readonly property int centerBlockStart: centerBlockIndex * realCount
|
||||||
|
readonly property int currentRealIndex: realIndexOf(view.currentIndex)
|
||||||
|
readonly property real focalWidth: Config.launcher.sizes.wallpaperWidth
|
||||||
|
readonly property real itemSpacing: Tokens.spacing.small
|
||||||
|
readonly property var keylines: {
|
||||||
|
const sp = itemSpacing;
|
||||||
|
const sizes = arrangement.sizes;
|
||||||
|
const n = arrangement.n;
|
||||||
|
|
||||||
|
const right = [
|
||||||
|
{
|
||||||
|
io: 0,
|
||||||
|
lo: 0,
|
||||||
|
sz: sizes[0]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let lo = sizes[0] / 2;
|
||||||
|
|
||||||
|
for (let i = 1; i <= n; i++) {
|
||||||
|
const size = sizes[i];
|
||||||
|
|
||||||
|
lo += sp + size / 2;
|
||||||
|
|
||||||
|
right.push({
|
||||||
|
io: i,
|
||||||
|
lo: lo,
|
||||||
|
sz: size
|
||||||
|
});
|
||||||
|
|
||||||
|
lo += size / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
right.push({
|
||||||
|
io: n + 1,
|
||||||
|
lo: width / 2 - sp,
|
||||||
|
sz: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const k = [];
|
||||||
|
|
||||||
|
for (let i = right.length - 1; i >= 1; i--) {
|
||||||
|
k.push({
|
||||||
|
io: -right[i].io,
|
||||||
|
lo: -right[i].lo,
|
||||||
|
sz: right[i].sz
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of right)
|
||||||
|
k.push(item);
|
||||||
|
|
||||||
|
for (let i = 0; i < k.length; i++)
|
||||||
|
k[i].loc = k[i].io * pitch;
|
||||||
|
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
readonly property int loopMultiplier: 401
|
||||||
|
readonly property real minEdgeWidth: 56
|
||||||
|
required property var panels
|
||||||
|
readonly property real pitch: focalWidth + itemSpacing
|
||||||
|
readonly property int realCount: values.length
|
||||||
|
readonly property var screen: QsWindow.window?.screen
|
||||||
|
required property SearchBar search
|
||||||
|
readonly property real shrinkRatio: 0.63
|
||||||
|
readonly property real tileHeight: focalWidth / 16 * 9
|
||||||
|
readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" "))
|
||||||
|
readonly property int virtualCount: realCount > 0 ? realCount * loopMultiplier : 0
|
||||||
|
required property var visibilities
|
||||||
|
|
||||||
|
function contentXForIndex(index) {
|
||||||
|
return index * pitch + pitch / 2 - width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementCurrentIndex() {
|
||||||
|
if (realCount === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
goTo(view.currentIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function glideTo(dest) {
|
||||||
|
if (Math.abs(dest - view.contentX) < 0.5)
|
||||||
|
return;
|
||||||
|
view.cancelFlick();
|
||||||
|
snapAnim.stop();
|
||||||
|
snapAnim.from = view.contentX;
|
||||||
|
snapAnim.to = dest;
|
||||||
|
snapAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goTo(virtualIndex) {
|
||||||
|
glideTo(contentXForIndex(virtualIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementCurrentIndex() {
|
||||||
|
if (realCount === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
goTo(view.currentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexForContentX(x) {
|
||||||
|
return Math.round((x + width / 2 - pitch / 2) / pitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function realIndexOf(virtualIndex) {
|
||||||
|
if (realCount === 0)
|
||||||
|
return -1;
|
||||||
|
return ((virtualIndex % realCount) + realCount) % realCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sample(childLoc) {
|
||||||
|
const k = keylines;
|
||||||
|
|
||||||
|
if (childLoc <= k[0].loc)
|
||||||
|
return {
|
||||||
|
c: k[0].lo,
|
||||||
|
s: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (childLoc >= k[k.length - 1].loc)
|
||||||
|
return {
|
||||||
|
c: k[k.length - 1].lo,
|
||||||
|
s: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < k.length - 1; i++) {
|
||||||
|
const a = k[i];
|
||||||
|
const b = k[i + 1];
|
||||||
|
|
||||||
|
if (childLoc > b.loc)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const span = b.loc - a.loc;
|
||||||
|
const t = span > 0 ? (childLoc - a.loc) / span : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
c: a.lo + (b.lo - a.lo) * t,
|
||||||
|
s: a.sz + (b.sz - a.sz) * t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
c: 0,
|
||||||
|
s: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectReal(realIdx) {
|
||||||
|
if (realCount === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const curBlock = Math.floor(view.currentIndex / realCount);
|
||||||
|
const current = view.currentIndex;
|
||||||
|
|
||||||
|
let target = curBlock * realCount + realIdx;
|
||||||
|
|
||||||
|
if (target - current > realCount / 2)
|
||||||
|
target -= realCount;
|
||||||
|
else if (current - target > realCount / 2)
|
||||||
|
target += realCount;
|
||||||
|
|
||||||
|
goTo(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapTarget(x) {
|
||||||
|
return contentXForIndex(indexForContentX(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapToNearest() {
|
||||||
|
glideTo(snapTarget(view.contentX));
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
|
||||||
|
implicitWidth: {
|
||||||
|
if (!screen)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const barMargins = Config.bar.border * 2;
|
||||||
|
let margins = 0;
|
||||||
|
if (visibilities.utilities || visibilities.sidebar)
|
||||||
|
margins = panels.utilities.implicitWidth;
|
||||||
|
const maxWidth = screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4;
|
||||||
|
|
||||||
|
return maxWidth;
|
||||||
|
}
|
||||||
|
width: implicitWidth
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
|
||||||
|
|
||||||
|
if (idx >= 0)
|
||||||
|
view.contentX = contentXForIndex(centerBlockStart + idx);
|
||||||
|
}
|
||||||
|
Component.onDestruction: Wallpapers.stopPreview()
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: view
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
cacheBuffer: root.width
|
||||||
|
clip: false
|
||||||
|
currentIndex: root.centerBlockStart
|
||||||
|
displayMarginBeginning: root.arrangement.effectiveExtraWidth
|
||||||
|
displayMarginEnd: root.arrangement.effectiveExtraWidth
|
||||||
|
interactive: true
|
||||||
|
model: root.virtualCount
|
||||||
|
orientation: ListView.Horizontal
|
||||||
|
snapMode: ListView.NoSnap
|
||||||
|
|
||||||
|
Anim on contentX {
|
||||||
|
id: snapAnim
|
||||||
|
|
||||||
|
running: false
|
||||||
|
type: Anim.Standard
|
||||||
|
}
|
||||||
|
delegate: Item {
|
||||||
|
id: slot
|
||||||
|
|
||||||
|
readonly property real childLoc: index * root.pitch + root.pitch / 2 - view.contentX - view.width / 2
|
||||||
|
required property int index
|
||||||
|
readonly property bool isCurrent: index === view.currentIndex
|
||||||
|
readonly property var keyline: root.sample(childLoc)
|
||||||
|
readonly property var modelData: realIdx >= 0 ? root.values[realIdx] : null
|
||||||
|
readonly property int realIdx: root.realIndexOf(index)
|
||||||
|
readonly property real tileWidth: keyline.s
|
||||||
|
|
||||||
|
height: view.height
|
||||||
|
visible: modelData !== null
|
||||||
|
width: root.pitch
|
||||||
|
|
||||||
|
WallpaperTile {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
isCurrent: slot.isCurrent
|
||||||
|
modelData: slot.modelData
|
||||||
|
tileHeight: root.tileHeight
|
||||||
|
tileWidth: slot.tileWidth
|
||||||
|
visibilities: root.visibilities
|
||||||
|
x: (slot.keyline.c - slot.childLoc) + (slot.width - width) / 2
|
||||||
|
|
||||||
|
onRequestActivate: {
|
||||||
|
if (slot.isCurrent) {
|
||||||
|
Wallpapers.setWallpaper(slot.modelData.path);
|
||||||
|
root.visibilities.launcher = false;
|
||||||
|
} else {
|
||||||
|
root.goTo(slot.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentXChanged: {
|
||||||
|
if (root.realCount === 0)
|
||||||
|
return;
|
||||||
|
const blockWidth = root.realCount * root.pitch;
|
||||||
|
const centerX = root.contentXForIndex(root.centerBlockStart);
|
||||||
|
const blocksFromCenter = Math.round((contentX - centerX) / blockWidth);
|
||||||
|
if (Math.abs(blocksFromCenter) >= 2) {
|
||||||
|
const shift = blocksFromCenter * blockWidth;
|
||||||
|
|
||||||
|
contentX -= shift;
|
||||||
|
|
||||||
|
if (snapAnim.running)
|
||||||
|
snapAnim.to -= shift;
|
||||||
|
}
|
||||||
|
currentIndex = Math.round((contentX + width / 2 - root.pitch / 2) / root.pitch);
|
||||||
|
}
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
const realIdx = root.realIndexOf(currentIndex);
|
||||||
|
if (realIdx >= 0)
|
||||||
|
Wallpapers.preview(root.values[realIdx].path);
|
||||||
|
}
|
||||||
|
onFlickEnded: root.snapToNearest()
|
||||||
|
onMovementEnded: root.snapToNearest()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,8 +111,8 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
||||||
sourceComponent: WallpaperList {
|
sourceComponent: CarouselView {
|
||||||
content: root.content
|
// content: root.content
|
||||||
panels: root.panels
|
panels: root.panels
|
||||||
search: root.search
|
search: root.search
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import ZShell.Models
|
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
|
||||||
import qs.Components
|
|
||||||
import qs.Helpers
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Modules
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property FileSystemEntry modelData
|
|
||||||
required property PersistentProperties visibilities
|
|
||||||
|
|
||||||
implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
|
|
||||||
implicitWidth: image.width + Tokens.padding.larger * 2
|
|
||||||
opacity: 0
|
|
||||||
scale: 0.5
|
|
||||||
z: PathView.z ?? 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on scale {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0);
|
|
||||||
opacity = Qt.binding(() => PathView.onPath ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
radius: Tokens.rounding.normal
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
Wallpapers.setWallpaper(root.modelData.path);
|
|
||||||
root.visibilities.launcher = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Elevation {
|
|
||||||
anchors.fill: image
|
|
||||||
level: 4
|
|
||||||
opacity: root.PathView.isCurrentItem ? 1 : 0
|
|
||||||
radius: image.radius
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomClippingRect {
|
|
||||||
id: image
|
|
||||||
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: Colors.tPalette.m3surfaceContainer
|
|
||||||
implicitHeight: implicitWidth / 16 * 9
|
|
||||||
implicitWidth: Config.launcher.sizes.wallpaperWidth
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
y: Tokens.padding.large
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: Colors.tPalette.m3outline
|
|
||||||
font.pointSize: Tokens.font.size.extraLarge * 2
|
|
||||||
font.weight: 600
|
|
||||||
text: "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
CachingImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
cache: true
|
|
||||||
path: root.modelData.path
|
|
||||||
smooth: !root.PathView.view.moving
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: label
|
|
||||||
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: image.bottom
|
|
||||||
anchors.topMargin: Tokens.spacing.small / 2
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font.pointSize: Tokens.font.size.normal
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
renderType: Text.QtRendering
|
|
||||||
text: root.modelData.relativePath
|
|
||||||
width: image.width - Tokens.padding.normal * 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property bool isCurrent
|
||||||
|
required property var modelData // FileSystemEntry-like {path, relativePath}
|
||||||
|
required property real tileHeight
|
||||||
|
required property real tileWidth
|
||||||
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
|
// Emitted on click: caller decides whether that means "activate" (if
|
||||||
|
// already current) or "bring to center" (if not) - see CarouselView.
|
||||||
|
signal requestActivate
|
||||||
|
|
||||||
|
implicitHeight: tileHeight + Tokens.padding.large * 2
|
||||||
|
implicitWidth: tileWidth
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
id: image
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
|
implicitHeight: root.tileHeight
|
||||||
|
implicitWidth: root.tileWidth
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
y: Tokens.padding.large
|
||||||
|
|
||||||
|
Behavior on implicitWidth {
|
||||||
|
enabled: !root.visibilities.launcher // avoid fighting drag-driven updates
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: Colors.tPalette.m3outline
|
||||||
|
font.pointSize: Tokens.font.size.extraLarge * 2
|
||||||
|
font.weight: 600
|
||||||
|
text: "image"
|
||||||
|
}
|
||||||
|
|
||||||
|
CachingImage {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
cache: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
height: root.tileHeight
|
||||||
|
path: root.modelData ? root.modelData.path : ""
|
||||||
|
smooth: root.isCurrent
|
||||||
|
sourceSize.height: root.tileHeight
|
||||||
|
sourceSize.width: root.tileHeight * (16 / 9)
|
||||||
|
width: root.tileHeight * (16 / 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
onClicked: root.requestActivate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
|
||||||
import qs.Components
|
|
||||||
import qs.Helpers
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Modules.Launcher.Items
|
|
||||||
|
|
||||||
PathView {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property var content
|
|
||||||
readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.9 + Tokens.padding.larger * 2
|
|
||||||
readonly property int numItems: {
|
|
||||||
const screen = QsWindow.window?.screen;
|
|
||||||
if (!screen)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
// Screen width - 4x outer rounding - 2x max side thickness (cause centered)
|
|
||||||
const barMargins = panels.bar.implicitWidth;
|
|
||||||
let outerMargins = 0;
|
|
||||||
if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins)
|
|
||||||
outerMargins = panels.utilities.implicitWidth;
|
|
||||||
const maxWidth = screen.width - Config.bar.rounding * 4 - (barMargins + outerMargins) * 2;
|
|
||||||
|
|
||||||
if (maxWidth <= 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const maxItemsOnScreen = Math.floor(maxWidth / itemWidth);
|
|
||||||
const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length);
|
|
||||||
|
|
||||||
if (visible === 2)
|
|
||||||
return 1;
|
|
||||||
if (visible > 1 && visible % 2 === 0)
|
|
||||||
return visible - 1;
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
required property var panels
|
|
||||||
required property SearchBar search
|
|
||||||
required property var visibilities
|
|
||||||
|
|
||||||
cacheItemCount: 4
|
|
||||||
highlightRangeMode: PathView.StrictlyEnforceRange
|
|
||||||
implicitWidth: Math.min(numItems, count) * itemWidth
|
|
||||||
pathItemCount: numItems
|
|
||||||
preferredHighlightBegin: 0.5
|
|
||||||
preferredHighlightEnd: 0.5
|
|
||||||
snapMode: PathView.SnapToItem
|
|
||||||
|
|
||||||
delegate: WallpaperItem {
|
|
||||||
visibilities: root.visibilities
|
|
||||||
}
|
|
||||||
model: ScriptModel {
|
|
||||||
id: scriptModel
|
|
||||||
|
|
||||||
readonly property string search: root.search.text.split(" ").slice(1).join(" ")
|
|
||||||
|
|
||||||
values: Wallpapers.query(search)
|
|
||||||
|
|
||||||
onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent)
|
|
||||||
}
|
|
||||||
path: Path {
|
|
||||||
startY: root.height / 2
|
|
||||||
|
|
||||||
PathAttribute {
|
|
||||||
name: "z"
|
|
||||||
value: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
relativeY: 0
|
|
||||||
x: root.width / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
PathAttribute {
|
|
||||||
name: "z"
|
|
||||||
value: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
relativeY: 0
|
|
||||||
x: root.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent)
|
|
||||||
Component.onDestruction: Wallpapers.stopPreview()
|
|
||||||
onCurrentItemChanged: {
|
|
||||||
if (currentItem)
|
|
||||||
Wallpapers.preview(currentItem.modelData.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "cachingimagemanager.hpp"
|
#include "cachingimagemanager.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include <QtQuick/qquickwindow.h>
|
#include <QtQuick/qquickwindow.h>
|
||||||
#include <qcryptographichash.h>
|
#include <qcryptographichash.h>
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
@@ -7,10 +9,27 @@
|
|||||||
#include <qfuturewatcher.h>
|
#include <qfuturewatcher.h>
|
||||||
#include <qimagereader.h>
|
#include <qimagereader.h>
|
||||||
#include <qpainter.h>
|
#include <qpainter.h>
|
||||||
|
#include <qthread.h>
|
||||||
|
#include <qthreadpool.h>
|
||||||
#include <qtconcurrentrun.h>
|
#include <qtconcurrentrun.h>
|
||||||
|
|
||||||
namespace ZShell::internal {
|
namespace ZShell::internal {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QThreadPool* imageIoPool() {
|
||||||
|
static QThreadPool pool;
|
||||||
|
static const bool init = [] {
|
||||||
|
pool.setMaxThreadCount(std::max(2, QThread::idealThreadCount() / 2));
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
|
(void)init;
|
||||||
|
|
||||||
|
return &pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
qreal CachingImageManager::effectiveScale() const {
|
qreal CachingImageManager::effectiveScale() const {
|
||||||
if (m_item && m_item->window()) {
|
if (m_item && m_item->window()) {
|
||||||
return m_item->window()->devicePixelRatio();
|
return m_item->window()->devicePixelRatio();
|
||||||
@@ -101,14 +120,13 @@ void CachingImageManager::updateSource() {
|
|||||||
|
|
||||||
void CachingImageManager::updateSource(const QString& path) {
|
void CachingImageManager::updateSource(const QString& path) {
|
||||||
if (path.isEmpty() || path == m_shaPath) {
|
if (path.isEmpty() || path == m_shaPath) {
|
||||||
// Path is empty or already calculating sha for path
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_shaPath = path;
|
m_shaPath = path;
|
||||||
|
|
||||||
const auto future =
|
const auto future =
|
||||||
QtConcurrent::run(&CachingImageManager::sha256sum, path);
|
QtConcurrent::run(imageIoPool(), &CachingImageManager::sha256sum, path);
|
||||||
|
|
||||||
const auto watcher = new QFutureWatcher<QString>(this);
|
const auto watcher = new QFutureWatcher<QString>(this);
|
||||||
|
|
||||||
@@ -118,7 +136,16 @@ void CachingImageManager::updateSource(const QString& path) {
|
|||||||
this,
|
this,
|
||||||
[watcher, path, this]() {
|
[watcher, path, this]() {
|
||||||
if (m_path != path) {
|
if (m_path != path) {
|
||||||
// Object is destroyed or path has changed, ignore
|
watcher->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString key = watcher->result();
|
||||||
|
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
if (m_shaPath == path) {
|
||||||
|
m_shaPath = QString();
|
||||||
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -126,19 +153,29 @@ void CachingImageManager::updateSource(const QString& path) {
|
|||||||
const QSize size = effectiveSize();
|
const QSize size = effectiveSize();
|
||||||
|
|
||||||
if (!m_item || !size.width() || !size.height()) {
|
if (!m_item || !size.width() || !size.height()) {
|
||||||
|
if (m_shaPath == path) {
|
||||||
|
m_shaPath = QString();
|
||||||
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString fillMode = m_item->property("fillMode").toString();
|
const QString fillMode = m_item->property("fillMode").toString();
|
||||||
// clang-format off
|
const QString filename =
|
||||||
const QString filename = QString("%1@%2x%3-%4.png")
|
QString("%1@%2x%3-%4.png")
|
||||||
.arg(watcher->result()).arg(size.width()).arg(size.height())
|
.arg(key)
|
||||||
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch");
|
.arg(size.width())
|
||||||
// clang-format on
|
.arg(size.height())
|
||||||
|
.arg(
|
||||||
|
fillMode == "PreserveAspectCrop" ? "crop"
|
||||||
|
: fillMode == "PreserveAspectFit" ? "fit"
|
||||||
|
: "stretch");
|
||||||
|
|
||||||
const QUrl cache = m_cacheDir.resolved(QUrl(filename));
|
const QUrl cache = m_cacheDir.resolved(QUrl(filename));
|
||||||
if (m_cachePath == cache) {
|
if (m_cachePath == cache) {
|
||||||
|
if (m_shaPath == path) {
|
||||||
|
m_shaPath = QString();
|
||||||
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,6 +186,9 @@ void CachingImageManager::updateSource(const QString& path) {
|
|||||||
if (!cache.isLocalFile()) {
|
if (!cache.isLocalFile()) {
|
||||||
qWarning() << "CachingImageManager::updateSource: cachePath"
|
qWarning() << "CachingImageManager::updateSource: cachePath"
|
||||||
<< cache << "is not a local file";
|
<< cache << "is not a local file";
|
||||||
|
if (m_shaPath == path) {
|
||||||
|
m_shaPath = QString();
|
||||||
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,7 +201,6 @@ void CachingImageManager::updateSource(const QString& path) {
|
|||||||
createCache(path, cache.toLocalFile(), fillMode, size);
|
createCache(path, cache.toLocalFile(), fillMode, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear current running sha if same
|
|
||||||
if (m_shaPath == path) {
|
if (m_shaPath == path) {
|
||||||
m_shaPath = QString();
|
m_shaPath = QString();
|
||||||
}
|
}
|
||||||
@@ -170,7 +209,6 @@ void CachingImageManager::updateSource(const QString& path) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl CachingImageManager::cachePath() const {
|
QUrl CachingImageManager::cachePath() const {
|
||||||
@@ -182,61 +220,105 @@ void CachingImageManager::createCache(
|
|||||||
const QString& cache,
|
const QString& cache,
|
||||||
const QString& fillMode,
|
const QString& fillMode,
|
||||||
const QSize& size) const {
|
const QSize& size) const {
|
||||||
QThreadPool::globalInstance()->start([path, cache, fillMode, size] {
|
auto* watcher =
|
||||||
QImage image(path);
|
new QFutureWatcher<bool>(const_cast<CachingImageManager*>(this));
|
||||||
|
|
||||||
if (image.isNull()) {
|
connect(
|
||||||
qWarning() << "CachingImageManager::createCache: failed to read"
|
watcher,
|
||||||
<< path;
|
&QFutureWatcher<bool>::finished,
|
||||||
return;
|
this,
|
||||||
}
|
[this, watcher, cache, path]() {
|
||||||
|
if (watcher->result() && m_item && m_path == path &&
|
||||||
|
m_cachePath.toLocalFile() == cache) {
|
||||||
|
m_item->setProperty("source", QUrl::fromLocalFile(cache));
|
||||||
|
}
|
||||||
|
|
||||||
image.convertTo(QImage::Format_ARGB32);
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
if (fillMode == "PreserveAspectCrop") {
|
const auto future =
|
||||||
image = image.scaled(
|
QtConcurrent::run(
|
||||||
size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
imageIoPool(), [path, cache, fillMode, size]() -> bool {
|
||||||
} else if (fillMode == "PreserveAspectFit") {
|
QImageReader reader(path);
|
||||||
image = image.scaled(
|
|
||||||
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
||||||
} else {
|
|
||||||
image = image.scaled(
|
|
||||||
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fillMode == "PreserveAspectCrop" ||
|
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
|
||||||
fillMode == "PreserveAspectFit") {
|
const QSize full = reader.size();
|
||||||
QImage canvas(size, QImage::Format_ARGB32);
|
if (full.isValid() && !full.isEmpty()) {
|
||||||
canvas.fill(Qt::transparent);
|
const QSize hint = size * 2;
|
||||||
|
QSize scaled = full;
|
||||||
|
scaled.scale(hint, Qt::KeepAspectRatioByExpanding);
|
||||||
|
scaled = QSize(
|
||||||
|
std::min(scaled.width(), full.width()),
|
||||||
|
std::min(scaled.height(), full.height()));
|
||||||
|
reader.setScaledSize(scaled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QPainter painter(&canvas);
|
QImage image = reader.read();
|
||||||
painter.drawImage(
|
|
||||||
(size.width() - image.width()) / 2,
|
|
||||||
(size.height() - image.height()) / 2,
|
|
||||||
image);
|
|
||||||
painter.end();
|
|
||||||
|
|
||||||
image = canvas;
|
if (image.isNull()) {
|
||||||
}
|
qWarning()
|
||||||
|
<< "CachingImageManager::createCache: failed to read"
|
||||||
|
<< path << reader.errorString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const QString parent = QFileInfo(cache).absolutePath();
|
image.convertTo(QImage::Format_ARGB32);
|
||||||
if (!QDir().mkpath(parent) || !image.save(cache)) {
|
|
||||||
qWarning() << "CachingImageManager::createCache: failed to save to"
|
if (fillMode == "PreserveAspectCrop") {
|
||||||
<< cache;
|
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;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CachingImageManager::sha256sum(const QString& path) {
|
QString CachingImageManager::sha256sum(const QString& path) {
|
||||||
QFile file(path);
|
const QFileInfo info(path);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
|
||||||
qWarning() << "CachingImageManager::sha256sum: failed to open" << path;
|
if (!info.exists() || !info.isFile()) {
|
||||||
return "";
|
qWarning() << "CachingImageManager::sha256sum: failed to stat" << path;
|
||||||
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||||
hash.addData(&file);
|
hash.addData(path.toUtf8());
|
||||||
file.close();
|
hash.addData(QByteArray::number(info.size()));
|
||||||
|
hash.addData(QByteArray::number(info.lastModified().toMSecsSinceEpoch()));
|
||||||
|
|
||||||
return hash.result().toHex();
|
return hash.result().toHex();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user