noctalia
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: cardContent
|
||||
|
||||
required property int animationDuration
|
||||
required property real centerWidth
|
||||
required property string filePath
|
||||
required property bool isCenter
|
||||
required property bool isVideo
|
||||
required property real radius
|
||||
required property string thumbnailPath
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
Item {
|
||||
id: imgComposite
|
||||
|
||||
height: cardContent.height
|
||||
visible: false
|
||||
width: cardContent.centerWidth
|
||||
x: (cardContent.width - cardContent.centerWidth) / 2
|
||||
|
||||
Image {
|
||||
id: img
|
||||
|
||||
anchors.fill: parent
|
||||
// asynchronous: true // TODO: flickering when going backwards
|
||||
cache: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
source: cardContent.thumbnailPath ? "file://" + cardContent.thumbnailPath : ""
|
||||
sourceSize.height: parent.height
|
||||
sourceSize.width: cardContent.centerWidth
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: border
|
||||
|
||||
anchors.fill: parent
|
||||
border.color: isCenter ? Color.mOutline : Qt.alpha(Color.mOutlineVariant, 0.5)
|
||||
border.width: Style.borderS
|
||||
color: "transparent"
|
||||
opacity: 0.75
|
||||
radius: cardContent.radius
|
||||
z: 20
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
radius: cardContent.radius
|
||||
visible: false
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
anchors.fill: parent
|
||||
maskSource: mask
|
||||
|
||||
source: ShaderEffectSource {
|
||||
sourceItem: imgComposite
|
||||
sourceRect: Qt.rect(-imgComposite.x, 0, cardContent.width, cardContent.height)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: videoLoader
|
||||
|
||||
property bool shouldLoad: false
|
||||
property string videoPath: cardContent.isCenter && cardContent.isVideo ? cardContent.filePath : ""
|
||||
|
||||
active: shouldLoad && videoPath !== ""
|
||||
anchors.fill: parent
|
||||
z: 5
|
||||
|
||||
sourceComponent: Component {
|
||||
Item {
|
||||
id: videoContainer
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
opacity: 0
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
height: videoContainer.height
|
||||
radius: cardContent.radius
|
||||
width: videoContainer.width
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: mediaPlayer
|
||||
|
||||
loops: MediaPlayer.Infinite
|
||||
source: "file://" + videoLoader.videoPath
|
||||
videoOutput: videoOutput
|
||||
|
||||
audioOutput: AudioOutput {
|
||||
volume: 0
|
||||
}
|
||||
|
||||
Component.onCompleted: play()
|
||||
onPlayingChanged: function () {
|
||||
if (mediaPlayer.playing)
|
||||
videoFadeIn.start();
|
||||
}
|
||||
}
|
||||
|
||||
VideoOutput {
|
||||
id: videoOutput
|
||||
|
||||
anchors.fill: parent
|
||||
fillMode: VideoOutput.PreserveAspectCrop
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: videoFadeIn
|
||||
|
||||
duration: cardContent.animationDuration
|
||||
easing.type: Easing.OutCubic
|
||||
from: 0
|
||||
property: "opacity"
|
||||
target: videoContainer
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVideoPathChanged: {
|
||||
shouldLoad = false;
|
||||
if (videoPath !== "")
|
||||
videoDelayTimer.restart();
|
||||
else
|
||||
videoDelayTimer.stop();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: videoDelayTimer
|
||||
|
||||
interval: cardContent.animationDuration
|
||||
|
||||
onTriggered: videoLoader.shouldLoad = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: cardDeck
|
||||
|
||||
required property int animationDuration
|
||||
property real animationIndex: 0
|
||||
required property int cardRadius
|
||||
required property int cardSpacing
|
||||
required property int cardStripWidth
|
||||
required property int cardsShown
|
||||
property real centerWidth: parent.width * centerWidthRatio
|
||||
required property real centerWidthRatio
|
||||
property real centerX: width / 2 - centerWidth / 2
|
||||
property int currentIndex: 0
|
||||
required property int filteredCount
|
||||
required property var filteredModel
|
||||
property int halfVisible: Math.floor(visibleCount / 2)
|
||||
property real runningIndex: 0
|
||||
required property real shearFactor
|
||||
property int sideCount: Math.floor(cardsShown / 2) - 1
|
||||
property int visibleCount: cardsShown
|
||||
|
||||
signal applyRequested(int index)
|
||||
|
||||
function navigateTo(idx) {
|
||||
var maxLead = Math.max(1, halfVisible - 1);
|
||||
if (Math.abs(runningIndex - animationIndex) >= maxLead)
|
||||
return;
|
||||
|
||||
var newIdx = wrappedIndex(idx);
|
||||
var diff = 0;
|
||||
|
||||
if (filteredCount > 0) {
|
||||
diff = newIdx - currentIndex;
|
||||
var half = filteredCount / 2;
|
||||
if (diff > half)
|
||||
diff -= filteredCount;
|
||||
else if (diff < -half)
|
||||
diff += filteredCount;
|
||||
}
|
||||
|
||||
runningIndex += diff;
|
||||
animationIndex = runningIndex;
|
||||
currentIndex = newIdx;
|
||||
}
|
||||
function randomJump() {
|
||||
var rnd = Math.floor(Math.random() * filteredCount);
|
||||
if (rnd === currentIndex)
|
||||
rnd = (rnd + 1) % filteredCount;
|
||||
navigateTo(rnd);
|
||||
}
|
||||
function slotToWidth(slot) {
|
||||
var t = Math.min(Math.abs(slot), 1);
|
||||
return centerWidth + (cardStripWidth - centerWidth) * t;
|
||||
}
|
||||
function slotToX(slot) {
|
||||
var rightEdge = centerX + centerWidth + cardSpacing;
|
||||
var leftEdge = centerX - cardSpacing - cardStripWidth;
|
||||
|
||||
if (slot >= 0 && slot <= 1)
|
||||
return centerX + (rightEdge - centerX) * slot;
|
||||
if (slot > 1)
|
||||
return rightEdge + (slot - 1) * (cardStripWidth + cardSpacing);
|
||||
if (slot >= -1 && slot < 0)
|
||||
return centerX + (leftEdge - centerX) * (-slot);
|
||||
if (slot < -1)
|
||||
return leftEdge + (slot + 1) * (cardStripWidth + cardSpacing);
|
||||
return 0;
|
||||
}
|
||||
function wrappedIndex(idx) {
|
||||
return ((idx % filteredCount) + filteredCount) % filteredCount;
|
||||
}
|
||||
|
||||
function jumpTo(idx) {
|
||||
animBehavior.enabled = false;
|
||||
runningIndex = idx;
|
||||
animationIndex = idx;
|
||||
currentIndex = idx;
|
||||
reenableTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reenableTimer
|
||||
|
||||
interval: 0
|
||||
|
||||
onTriggered: animBehavior.enabled = true
|
||||
}
|
||||
|
||||
width: (cardsShown - 3) * cardStripWidth + (cardsShown - 3) * cardSpacing + centerWidth
|
||||
|
||||
Behavior on animationIndex {
|
||||
id: animBehavior
|
||||
|
||||
NumberAnimation {
|
||||
duration: cardDeck.animationDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
transform: Matrix4x4 {
|
||||
property real s: cardDeck.shearFactor
|
||||
|
||||
matrix: Qt.matrix4x4(1, s, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: cardDeck.filteredCount > 0 ? cardDeck.visibleCount : 0
|
||||
|
||||
delegate: Rectangle {
|
||||
id: card
|
||||
|
||||
property real fractionalSlot: offset + (cardDeck.runningIndex - cardDeck.animationIndex)
|
||||
property bool isCenter: offset === 0
|
||||
property int modelIndex: cardDeck.wrappedIndex(Math.round(cardDeck.runningIndex) + offset)
|
||||
property int offset: index - cardDeck.halfVisible
|
||||
|
||||
signal applyRequested
|
||||
|
||||
border.color: isCenter ? Color.mOutline : Color.mSurface
|
||||
border.width: isCenter ? Style.borderM : Style.borderS
|
||||
color: "transparent"
|
||||
height: cardDeck.height
|
||||
opacity: Math.max(0, Math.min(1, cardDeck.halfVisible - Math.abs(fractionalSlot)))
|
||||
radius: cardDeck.cardRadius
|
||||
visible: (x + width) > 0 && x < cardDeck.width
|
||||
width: cardDeck.slotToWidth(fractionalSlot)
|
||||
x: cardDeck.slotToX(fractionalSlot)
|
||||
y: 0
|
||||
z: isCenter ? 100 : cardDeck.visibleCount - Math.abs(offset)
|
||||
|
||||
Card {
|
||||
animationDuration: cardDeck.animationDuration
|
||||
centerWidth: cardDeck.centerWidth
|
||||
filePath: cardDeck.filteredModel[card.modelIndex]?.filePath ?? ""
|
||||
isCenter: card.isCenter
|
||||
isVideo: cardDeck.filteredModel[card.modelIndex]?.isVideo ?? false
|
||||
radius: cardDeck.cardRadius
|
||||
thumbnailPath: cardDeck.filteredModel[card.modelIndex]?.thumbnail ?? ""
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: card.isCenter ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onClicked: {
|
||||
if (card.isCenter)
|
||||
cardDeck.applyRequested(card.modelIndex);
|
||||
}
|
||||
|
||||
//
|
||||
onWheel: function (wheel) {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
cardDeck.navigateTo(cardDeck.currentIndex - 1);
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
cardDeck.navigateTo(cardDeck.currentIndex + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: loadingBar
|
||||
|
||||
property bool loading: pending > 0
|
||||
required property int pending
|
||||
property real progress: total > 0 ? 1 - (pending / total) : 0
|
||||
required property int total
|
||||
|
||||
color: Qt.alpha(Color.mSurface, 0.9)
|
||||
height: upperRow.height + Style.margin2S + progressBar.height
|
||||
radius: Style.radiusXS
|
||||
visible: loading
|
||||
width: upperRow.width + Style.margin2L
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: upperRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: -progressBar.height / 2
|
||||
|
||||
Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Repeater {
|
||||
model: 3
|
||||
|
||||
Rectangle {
|
||||
property real angle: index * (2 * Math.PI / 3)
|
||||
|
||||
color: Color.mPrimary
|
||||
height: Style.marginXS
|
||||
radius: Style.marginXXS
|
||||
width: Style.marginXS
|
||||
x: Style.marginS + Style.marginS * Math.cos(angle + spinAnimation.value)
|
||||
y: Style.marginS + Style.marginS * Math.sin(angle + spinAnimation.value)
|
||||
|
||||
NumberAnimation on opacity {
|
||||
duration: 600
|
||||
from: 0.3
|
||||
loops: Animation.Infinite
|
||||
running: loadingBar.loading
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
NumberAnimation {
|
||||
id: spinAnimation
|
||||
|
||||
property real value: 0
|
||||
|
||||
duration: 1200
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
property: "value"
|
||||
running: loadingBar.loading
|
||||
target: spinAnimation
|
||||
to: 2 * Math.PI
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${root.pluginApi?.tr("widget.generate-thumbs-message")} ${String(Math.max(loadingBar.total - loadingBar.pending, 0)).padStart(String(loadingBar.total).length, " ")} / ${loadingBar.total}`
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: progressBar
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Qt.alpha(Color.mOnSurface, 0.1)
|
||||
height: Style.marginS
|
||||
radius: loadingBar.radius
|
||||
|
||||
Rectangle {
|
||||
color: Color.mPrimary
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
width: parent.width * loadingBar.progress
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
id: hint
|
||||
|
||||
required property string keys
|
||||
required property string label
|
||||
|
||||
spacing: Style.marginXS
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginXXS
|
||||
|
||||
Repeater {
|
||||
model: hint.keys.split(" / ")
|
||||
|
||||
Row {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginXXS
|
||||
|
||||
// Separator between keys
|
||||
NText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Qt.alpha(Color.mOnSurface, 0.3)
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: root.pluginApi?.tr("shortcuts.key-separator")
|
||||
visible: index > 0
|
||||
}
|
||||
|
||||
// Keycap
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
border.color: Qt.alpha(Color.mOnSurface, 0.2)
|
||||
border.width: Style.borderS
|
||||
color: Qt.alpha(Color.mOnSurface, 0.08)
|
||||
height: Style.marginL + 2
|
||||
radius: Style.radiusXS
|
||||
width: Math.max(Style.marginL + 2, keycapText.width + Style.marginS)
|
||||
|
||||
Rectangle {
|
||||
anchors.bottomMargin: 2
|
||||
anchors.fill: parent
|
||||
border.color: Qt.alpha(Color.mOnSurface, 0.15)
|
||||
border.width: Style.borderS
|
||||
color: Qt.alpha(Color.mOnSurface, 0.05)
|
||||
radius: Style.radiusXS
|
||||
|
||||
NText {
|
||||
id: keycapText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: Qt.alpha(Color.mOnSurface, 0.6)
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: modelData.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Qt.alpha(Color.mOnSurface, 0.4)
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: hint.label
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: sideBar
|
||||
|
||||
property bool expanded: false
|
||||
property bool hideHelp: true
|
||||
|
||||
color: Qt.alpha(Color.mSurface, 0.9)
|
||||
radius: Style.radiusS
|
||||
width: expanded ? shortcutColumn.width + Style.margin2L : !hideHelp ? collapsedColumn.width + Style.margin2L : 0
|
||||
height: expanded ? shortcutColumn.height + Style.margin2L : !hideHelp ? collapsedColumn.height + Style.margin2L : 0
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: collapsedColumn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginS
|
||||
visible: !sideBar.expanded && !sideBar.hideHelp
|
||||
|
||||
ShortcutHint {
|
||||
keys: "?"
|
||||
label: root.pluginApi?.tr("shortcuts.label.help-title")
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: shortcutColumn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
visible: sideBar.expanded
|
||||
|
||||
// Navigation
|
||||
NText {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.35)
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: root.pluginApi?.tr("shortcuts.header.navigation")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "J / K"
|
||||
label: root.pluginApi?.tr("shortcuts.label.navigate")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "H / L"
|
||||
label: root.pluginApi?.tr("shortcuts.label.jump")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "R"
|
||||
label: root.pluginApi?.tr("shortcuts.label.shuffle")
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.1)
|
||||
height: 1
|
||||
width: shortcutColumn.width
|
||||
}
|
||||
|
||||
// Actions
|
||||
NText {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.35)
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: root.pluginApi?.tr("shortcuts.header.actions")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "ENTER"
|
||||
label: root.pluginApi?.tr("shortcuts.label.apply-quit")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "SPACE"
|
||||
label: root.pluginApi?.tr("shortcuts.label.apply")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "ESC / Q"
|
||||
label: root.pluginApi?.tr("shortcuts.label.quit")
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.1)
|
||||
height: 1
|
||||
width: shortcutColumn.width
|
||||
}
|
||||
|
||||
// Filters
|
||||
NText {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.35)
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: root.pluginApi?.tr("shortcuts.header.filters")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "A"
|
||||
label: root.pluginApi?.tr("shortcuts.label.filter-all")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "I"
|
||||
label: root.pluginApi?.tr("shortcuts.label.filter-images")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "V"
|
||||
label: root.pluginApi?.tr("shortcuts.label.filter-videos")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "F"
|
||||
label: root.pluginApi?.tr("shortcuts.label.filter-colors")
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.1)
|
||||
height: 1
|
||||
width: shortcutColumn.width
|
||||
}
|
||||
|
||||
// View
|
||||
NText {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.35)
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: root.pluginApi?.tr("shortcuts.header.view")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "T"
|
||||
label: root.pluginApi?.tr("shortcuts.label.top-bar")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "P"
|
||||
label: root.pluginApi?.tr("shortcuts.label.live-preview")
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.1)
|
||||
height: 1
|
||||
width: shortcutColumn.width
|
||||
}
|
||||
|
||||
// Layout
|
||||
NText {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.35)
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: root.pluginApi?.tr("shortcuts.header.layout")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "SHIFT + H / L"
|
||||
label: root.pluginApi?.tr("shortcuts.label.center-height")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "SHIFT + J / K"
|
||||
label: root.pluginApi?.tr("shortcuts.label.center-width")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "SHIFT + N / P"
|
||||
label: root.pluginApi?.tr("shortcuts.label.cards-shown")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "CTRL + J / K"
|
||||
label: root.pluginApi?.tr("shortcuts.label.cards-spacing")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "CTRL + H / L"
|
||||
label: root.pluginApi?.tr("shortcuts.label.cards-width")
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
color: Qt.alpha(Color.mOnSurface, 0.1)
|
||||
height: 1
|
||||
width: shortcutColumn.width
|
||||
}
|
||||
|
||||
ShortcutHint {
|
||||
keys: "CTRL + S"
|
||||
label: root.pluginApi?.tr("shortcuts.label.save")
|
||||
}
|
||||
ShortcutHint {
|
||||
keys: "?"
|
||||
label: root.pluginApi?.tr("shortcuts.label.hide")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import "Utils.js" as Utils
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import Qt.labs.folderlistmodel
|
||||
|
||||
Item {
|
||||
id: service
|
||||
|
||||
property int maxThumbnailJobs: 4
|
||||
property var pendingJobs: []
|
||||
property int activeJobs: 0
|
||||
required property string cacheDir
|
||||
required property var imageFilter
|
||||
required property var videoFilter
|
||||
required property string wallpaperDir
|
||||
property var colorOrder: ["Red", "Orange", "Green", "Teal", "Blue", "Purple", "Pink", "Monochrome"]
|
||||
property var colorOrderColors: ["#FF4500", "#FFA500", "#32CD32", "#2EC4B6", "#1E90FF", "#8A2BE2", "#FF69B4", "#A9A9A9"]
|
||||
property int fileCount: files.length
|
||||
property var files: []
|
||||
property bool loading: true
|
||||
property int pendingProcesses: 0
|
||||
property int thumbnailRevision: 0
|
||||
|
||||
signal ready
|
||||
|
||||
function toLocalPath(urlStr) {
|
||||
return String(urlStr).replace(/^file:\/\//, "");
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: thumbnailModel
|
||||
|
||||
folder: service.wallpaperDir ? Qt.resolvedUrl("file://" + service.wallpaperDir) : ""
|
||||
nameFilters: Utils.nameFilters(service.imageFilter, service.videoFilter)
|
||||
showDirs: false
|
||||
sortField: FolderListModel.Name
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === FolderListModel.Ready) {
|
||||
service.createThumbnails();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createThumbnails() {
|
||||
var proc = processComponent.createObject(null, {
|
||||
command: ["mkdir", "-p", cacheDir]
|
||||
});
|
||||
proc.running = true;
|
||||
|
||||
var items = [];
|
||||
var jobs = [];
|
||||
|
||||
for (var i = 0; i < thumbnailModel.count; i++) {
|
||||
(function (idx) {
|
||||
var filePath = toLocalPath(thumbnailModel.get(idx, "filePath"));
|
||||
var fileName = thumbnailModel.get(idx, "fileName");
|
||||
var isVid = Utils.isVideo(fileName, service.videoFilter);
|
||||
var thumbName = isVid ? fileName + ".jpg" : fileName;
|
||||
var thumbnailPath = cacheDir + "/" + thumbName;
|
||||
|
||||
var thumbnailCmd = isVid ? videoToThumbnailCmd(filePath, thumbnailPath) : imageToThumbnailCmd(filePath, thumbnailPath);
|
||||
var hexCmd = thumbnailHexValueCmd(thumbnailPath);
|
||||
|
||||
const script = `
|
||||
[ -f "${thumbnailPath}"* ] && exit 0
|
||||
${thumbnailCmd}
|
||||
mv "${thumbnailPath}" "${thumbnailPath}__x$(${hexCmd})"
|
||||
`;
|
||||
|
||||
jobs.push({
|
||||
script: script,
|
||||
thumbnailPath: thumbnailPath
|
||||
});
|
||||
|
||||
items.push({});
|
||||
})(i);
|
||||
}
|
||||
|
||||
pendingJobs = jobs;
|
||||
activeJobs = 0;
|
||||
processNextJob();
|
||||
|
||||
if (thumbnailModel.count === 0) {
|
||||
service.loading = false;
|
||||
}
|
||||
|
||||
files = items;
|
||||
}
|
||||
function processNextJob() {
|
||||
while (activeJobs < maxThumbnailJobs && pendingJobs.length > 0) {
|
||||
var job = pendingJobs.shift();
|
||||
activeJobs++;
|
||||
service.pendingProcesses++;
|
||||
createSingleThumbnail(job, function() {
|
||||
activeJobs--;
|
||||
processNextJob();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createSingleThumbnail(job, onFinished) {
|
||||
var proc = processComponent.createObject(null, {
|
||||
command: ["bash", "-c", job.script]
|
||||
});
|
||||
|
||||
proc.exited.connect(function () {
|
||||
service.pendingProcesses--;
|
||||
service.thumbnailRevision++;
|
||||
|
||||
if (service.pendingProcesses === 0) {
|
||||
filesModel.running = true;
|
||||
}
|
||||
|
||||
proc.destroy();
|
||||
if (onFinished) onFinished();
|
||||
});
|
||||
|
||||
proc.running = true;
|
||||
}
|
||||
function imageToThumbnailCmd(filePath, thumbnailPath) {
|
||||
return `magick "${filePath}" \
|
||||
-resize x500 \
|
||||
-quality 95 \
|
||||
"${thumbnailPath}"
|
||||
`;
|
||||
}
|
||||
|
||||
function videoToThumbnailCmd(filePath, thumbnailPath) {
|
||||
return `ffmpeg -y -i \
|
||||
"${filePath}" \
|
||||
-vf "select=eq(n\\,0),scale=-1:1080" \
|
||||
-frames:v 1 \
|
||||
-q:v 2 \
|
||||
"${thumbnailPath}" </dev/null 2>/dev/null`;
|
||||
}
|
||||
|
||||
function thumbnailHexValueCmd(thumbnailPath) {
|
||||
return `magick "${thumbnailPath}" \
|
||||
-resize "1x1^" \
|
||||
-gravity center \
|
||||
-extent 1x1 \
|
||||
-depth 8 \
|
||||
-format "%[hex:p{0,0}]" info:- 2>/dev/null \
|
||||
| grep -oE '[0-9A-Fa-f]{6}' \
|
||||
| head -n 1`;
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: filesModel
|
||||
|
||||
nameFilters: ["*__x*"]
|
||||
showDirs: false
|
||||
sortField: FolderListModel.Name
|
||||
|
||||
property bool running: false
|
||||
|
||||
onRunningChanged: {
|
||||
if (running)
|
||||
folder = Qt.resolvedUrl("file://" + service.cacheDir);
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === FolderListModel.Ready && running) {
|
||||
service.buildFileList();
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildFileList() {
|
||||
// Ground truth needed to avoid showing thumbnails for files
|
||||
// that are no longer in the wallpaper directory.
|
||||
var existingFiles = new Set();
|
||||
for (let j = 0; j < thumbnailModel.count; j++) {
|
||||
existingFiles.add(thumbnailModel.get(j, "fileName"));
|
||||
}
|
||||
|
||||
var items = [];
|
||||
|
||||
for (let i = 0; i < filesModel.count; i++) {
|
||||
const filePath = toLocalPath(filesModel.get(i, "filePath"));
|
||||
const fileName = filesModel.get(i, "fileName");
|
||||
|
||||
const idx = fileName.lastIndexOf("__x");
|
||||
if (idx === -1)
|
||||
continue;
|
||||
|
||||
const thumbBase = fileName.substring(0, idx);
|
||||
const hexColor = fileName.substring(idx + 3);
|
||||
|
||||
// Video thumbnails are named <original>.<vidext>.jpg__x<hex>
|
||||
// Strip trailing .jpg to recover the original video filename.
|
||||
var isVid = thumbBase.toLowerCase().endsWith(".jpg") && Utils.isVideo(thumbBase.substring(0, thumbBase.lastIndexOf(".")), service.videoFilter);
|
||||
var wallpaperName = isVid ? thumbBase.substring(0, thumbBase.lastIndexOf(".")) : thumbBase;
|
||||
|
||||
if (!existingFiles.has(wallpaperName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
fileName: wallpaperName,
|
||||
filePath: wallpaperDir + "/" + wallpaperName,
|
||||
thumbnail: filePath,
|
||||
hexCode: hexColor,
|
||||
filterColor: getFilterColor(hexColor),
|
||||
isVideo: isVid
|
||||
});
|
||||
}
|
||||
|
||||
items.sort((a, b) => colorOrder.indexOf(a.filterColor) - colorOrder.indexOf(b.filterColor));
|
||||
|
||||
files = items;
|
||||
service.loading = false;
|
||||
service.ready();
|
||||
}
|
||||
|
||||
function getFilterColor(hexColor) {
|
||||
if (!hexColor)
|
||||
return "Monochrome";
|
||||
|
||||
const cleaned = String(hexColor).trim().replace(/x/g, '').substring(0, 6);
|
||||
if (cleaned.length !== 6)
|
||||
return "Monochrome";
|
||||
|
||||
const r = parseInt(cleaned.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(cleaned.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(cleaned.substring(4, 6), 16) / 255;
|
||||
if ([r, g, b].some(isNaN))
|
||||
return "Monochrome";
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const d = max - min;
|
||||
|
||||
let h = 0;
|
||||
let s = max === 0 ? 0 : d / max;
|
||||
let v = max;
|
||||
|
||||
if (d !== 0) {
|
||||
if (max === r)
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
else if (max === g)
|
||||
h = (b - r) / d + 2;
|
||||
else
|
||||
h = (r - g) / d + 4;
|
||||
h = (h / 6) * 360;
|
||||
}
|
||||
|
||||
if (s < 0.15 || v < 0.10)
|
||||
return "Monochrome";
|
||||
if (h >= 345 || h < 15)
|
||||
return "Red";
|
||||
if (h < 50)
|
||||
return "Orange";
|
||||
if (h < 160)
|
||||
return "Green";
|
||||
if (h < 200)
|
||||
return "Teal";
|
||||
if (h < 260)
|
||||
return "Blue";
|
||||
if (h < 315)
|
||||
return "Purple";
|
||||
if (h < 345)
|
||||
return "Pink";
|
||||
|
||||
return "Monochrome";
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processComponent
|
||||
|
||||
Process {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: topBar
|
||||
|
||||
required property int animationDuration
|
||||
required property var availableColors
|
||||
required property var colorOrder
|
||||
required property var colorOrderColors
|
||||
required property string currentCardColor
|
||||
required property int currentIndex
|
||||
property real entryOffset: parent.width / 2
|
||||
required property int filteredCount
|
||||
required property bool livePreview
|
||||
required property var pluginApi
|
||||
required property string selectedColorFilter
|
||||
required property string selectedFilter
|
||||
required property real shearFactor
|
||||
|
||||
signal colorFilterSelected(string key)
|
||||
signal filterSelected(string key)
|
||||
signal livePreviewToggled
|
||||
signal shuffleRequested
|
||||
|
||||
function flashShuffle() {
|
||||
shuffleBtn.flash();
|
||||
}
|
||||
|
||||
color: Color.mSurface
|
||||
|
||||
Behavior on entryOffset {
|
||||
NumberAnimation {
|
||||
duration: topBar.animationDuration
|
||||
easing.overshoot: 1.0
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
transform: Matrix4x4 {
|
||||
property real s: topBar.shearFactor
|
||||
|
||||
matrix: Qt.matrix4x4(1, s, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Component.onCompleted: entryOffset = 0
|
||||
|
||||
// ── Inline Components ──
|
||||
|
||||
component TButton: Rectangle {
|
||||
id: btn
|
||||
|
||||
property color accentColor: active ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
property bool active: false
|
||||
property string hotkey: ""
|
||||
property string icon: ""
|
||||
property string label: ""
|
||||
property bool pulsing: false
|
||||
|
||||
signal clicked
|
||||
|
||||
border.color: active ? Qt.alpha(accentColor, Style.opacityMedium) : Qt.alpha(Color.mOutline, 0.3)
|
||||
border.width: Style.borderS
|
||||
color: active ? Qt.alpha(accentColor, 0.15) : Qt.alpha(Color.mOnSurface, 0.06)
|
||||
height: Style.margin2L
|
||||
radius: Style.radiusM
|
||||
width: btnRow.width + Style.margin2M
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: btnRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginS
|
||||
|
||||
PulsingDot {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
pulsing: btn.pulsing
|
||||
visible: btn.pulsing
|
||||
}
|
||||
NIcon {
|
||||
color: btn.accentColor
|
||||
icon: btn.icon
|
||||
visible: btn.icon !== ""
|
||||
}
|
||||
NText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: btn.accentColor
|
||||
font.pointSize: Style.fontSizeXS
|
||||
text: btn.label
|
||||
visible: btn.label !== ""
|
||||
}
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
border.color: Qt.alpha(btn.accentColor, 0.35)
|
||||
border.width: Style.borderM
|
||||
color: Qt.alpha(btn.accentColor, 0.12)
|
||||
height: Style.marginL + 2
|
||||
opacity: 0.25
|
||||
radius: Style.radiusXS
|
||||
visible: btn.hotkey !== ""
|
||||
width: Style.marginL + 5
|
||||
|
||||
Rectangle {
|
||||
anchors.bottomMargin: 2
|
||||
anchors.fill: parent
|
||||
border.color: Qt.alpha(btn.accentColor, 0.25)
|
||||
border.width: Style.borderS
|
||||
color: Qt.alpha(btn.accentColor, 0.1)
|
||||
radius: Style.radiusXS
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
color: Qt.alpha(btn.accentColor, 0.7)
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeXXS
|
||||
text: btn.hotkey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: btn.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
component CButton: Rectangle {
|
||||
id: colorBtn
|
||||
|
||||
property bool active: false
|
||||
property bool available: true
|
||||
property bool current: false
|
||||
required property color faceColor
|
||||
|
||||
signal clicked
|
||||
|
||||
border.color: active ? Color.mOnSurface : Qt.alpha(Color.mOutline, 0.3)
|
||||
border.width: Style.borderS
|
||||
color: faceColor
|
||||
height: Style.margin2L
|
||||
opacity: active ? 1.0 : available ? 0.4 : 0.12
|
||||
radius: Style.radiusM
|
||||
width: height
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: -height - 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: colorBtn.current ? colorBtn.faceColor : "transparent"
|
||||
height: 3
|
||||
radius: height / 2
|
||||
width: colorBtn.current ? parent.width * 0.6 : 0
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
color: Color.mOnSurface
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeS
|
||||
text: topBar.pluginApi?.tr("buttons.color-na")
|
||||
visible: !colorBtn.available
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: colorBtn.available ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||
enabled: colorBtn.available
|
||||
|
||||
onClicked: colorBtn.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
component SButton: Item {
|
||||
id: shuffleItem
|
||||
|
||||
property string hotkey: ""
|
||||
property string icon: ""
|
||||
property string label: ""
|
||||
|
||||
signal clicked
|
||||
|
||||
function flash() {
|
||||
flashAnim.restart();
|
||||
}
|
||||
|
||||
height: innerBtn.height
|
||||
width: innerBtn.width
|
||||
|
||||
TButton {
|
||||
id: innerBtn
|
||||
|
||||
hotkey: shuffleItem.hotkey
|
||||
icon: shuffleItem.icon
|
||||
label: shuffleItem.label
|
||||
|
||||
onClicked: {
|
||||
shuffleItem.clicked();
|
||||
shuffleItem.flash();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: flashOverlay
|
||||
|
||||
anchors.fill: innerBtn
|
||||
color: Color.mPrimary
|
||||
opacity: 0
|
||||
radius: innerBtn.radius
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: flashAnim
|
||||
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
easing.type: Easing.OutCubic
|
||||
property: "opacity"
|
||||
target: flashOverlay
|
||||
to: 0.3
|
||||
}
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
property: "opacity"
|
||||
target: flashOverlay
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component PulsingDot: Rectangle {
|
||||
id: root
|
||||
|
||||
property color dotColor: Color.mError
|
||||
property bool pulsing: false
|
||||
|
||||
color: dotColor
|
||||
height: Style.marginS
|
||||
radius: Style.marginXXXS
|
||||
width: Style.marginS
|
||||
|
||||
SequentialAnimation {
|
||||
id: pulseAnimation
|
||||
|
||||
loops: Animation.Infinite
|
||||
running: root.pulsing
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running)
|
||||
root.opacity = 1.0;
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.InOutSine
|
||||
from: 1.0
|
||||
property: "opacity"
|
||||
target: root
|
||||
to: 0.3
|
||||
}
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.InOutSine
|
||||
from: 0.3
|
||||
property: "opacity"
|
||||
target: root
|
||||
to: 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Left ──
|
||||
|
||||
NText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: (topBar.currentIndex + 1) + " / " + topBar.filteredCount
|
||||
}
|
||||
|
||||
// ── Center ──
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
key: "all",
|
||||
label: topBar.pluginApi?.tr("buttons.all"),
|
||||
icon: "wallpaper",
|
||||
hotkey: "A"
|
||||
},
|
||||
{
|
||||
key: "images",
|
||||
label: topBar.pluginApi?.tr("buttons.images"),
|
||||
icon: "image",
|
||||
hotkey: "I"
|
||||
},
|
||||
{
|
||||
key: "videos",
|
||||
label: topBar.pluginApi?.tr("buttons.videos"),
|
||||
icon: "video",
|
||||
hotkey: "V"
|
||||
}
|
||||
]
|
||||
|
||||
TButton {
|
||||
required property var modelData
|
||||
|
||||
active: topBar.selectedFilter === modelData.key
|
||||
hotkey: modelData.hotkey
|
||||
icon: modelData.icon
|
||||
label: modelData.label || ""
|
||||
|
||||
onClicked: topBar.filterSelected(modelData.key)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Qt.alpha(Color.mOnSurface, 0.15)
|
||||
height: parent.height * 0.5
|
||||
width: 1
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: topBar.colorOrder
|
||||
|
||||
CButton {
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
active: topBar.selectedColorFilter === modelData
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
available: topBar.availableColors.indexOf(modelData) !== -1
|
||||
current: topBar.selectedColorFilter === "" && topBar.currentCardColor === modelData
|
||||
faceColor: topBar.colorOrderColors[index]
|
||||
|
||||
onClicked: {
|
||||
if (active)
|
||||
topBar.colorFilterSelected("");
|
||||
else
|
||||
topBar.colorFilterSelected(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Right ──
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.marginL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginXS
|
||||
|
||||
SButton {
|
||||
id: shuffleBtn
|
||||
|
||||
hotkey: "R"
|
||||
icon: "arrows-random"
|
||||
label: topBar.pluginApi?.tr("buttons.shuffle")
|
||||
|
||||
onClicked: topBar.shuffleRequested()
|
||||
}
|
||||
|
||||
TButton {
|
||||
accentColor: topBar.livePreview ? Color.mTertiary : Color.mOnSurfaceVariant
|
||||
active: topBar.livePreview
|
||||
hotkey: "P"
|
||||
label: topBar.pluginApi?.tr("buttons.live-preview")
|
||||
pulsing: topBar.livePreview
|
||||
|
||||
onClicked: topBar.livePreviewToggled()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
function getExtension(fileName) {
|
||||
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
|
||||
}
|
||||
|
||||
function isVideo(fileName, filterVideos) {
|
||||
return filterVideos.indexOf(getExtension(fileName)) !== -1;
|
||||
}
|
||||
|
||||
function isImage(fileName, filterImages) {
|
||||
return filterImages.indexOf(getExtension(fileName)) !== -1;
|
||||
}
|
||||
|
||||
function nameFilters(filterImages, filterVideos) {
|
||||
return (filterImages || []).concat(filterVideos || []).map((ext) => "*." + ext);
|
||||
}
|
||||
|
||||
var mpvpaperKill = "killall -9 mpvpaper 2>/dev/null || true";
|
||||
|
||||
var mpvpaperOptions = [
|
||||
"loop",
|
||||
"--no-audio",
|
||||
"--hwdec=auto",
|
||||
"--profile=high-quality",
|
||||
"--video-sync=display-resample",
|
||||
"--interpolation",
|
||||
"--tscale=oversample"
|
||||
].join(" ");
|
||||
|
||||
function mpvpaperRun(filePath) {
|
||||
return "mpvpaper -o '" + mpvpaperOptions + "' '*' \"" + filePath + "\" >/dev/null 2>&1 & disown";
|
||||
}
|
||||
|
||||
function wallpaperCommand(entry) {
|
||||
if (entry.isVideo)
|
||||
return mpvpaperKill + "; " + mpvpaperRun(entry.filePath);
|
||||
return mpvpaperKill;
|
||||
}
|
||||
Reference in New Issue
Block a user