mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
0033e3f0e0
* feat: add QR Generator modal with auto-generate on input debounce - QRGeneratorModal with dual-buffer sequential fade animation - Auto-generate QR code on typing stop (200ms debounce) - Clear text button on input field - Escape key closes modal - Register modal in PopoutService and AppSearchService - Go backend for text QR code generation * fix: add QR Generator toggle to Launcher DMS settings * qr: also add qrg built in launcher plugin --------- Co-authored-by: bbedward <bbedward@gmail.com>
286 lines
9.0 KiB
QML
286 lines
9.0 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Effects
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Modals.Common
|
|
import qs.Modals.FileBrowser
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
DankModal {
|
|
id: root
|
|
visible: false
|
|
layerNamespace: "dms:qr-generator"
|
|
|
|
property bool disablePopupTransparency: true
|
|
property bool generating: false
|
|
property string themedQrCodePath: ""
|
|
property string normalQrCodePath: ""
|
|
property string initialText: ""
|
|
property string _pendingPayload: ""
|
|
property string _generatingPayload: ""
|
|
property string _displayedPayload: ""
|
|
modalWidth: 420
|
|
modalHeight: 440
|
|
onBackgroundClicked: hide()
|
|
onOpened: {
|
|
Qt.callLater(() => {
|
|
modalFocusScope.forceActiveFocus();
|
|
const item = contentLoader.item;
|
|
if (!item)
|
|
return;
|
|
item.saveBrowserLoader = saveBrowserLoader;
|
|
if (item.textInput) {
|
|
item.textInput.text = initialText;
|
|
item.textInput.forceActiveFocus();
|
|
}
|
|
if (initialText.length > 0) {
|
|
_pendingPayload = initialText;
|
|
generateQR(initialText);
|
|
}
|
|
});
|
|
}
|
|
|
|
function show(text) {
|
|
generating = false;
|
|
initialText = text || "";
|
|
_pendingPayload = "";
|
|
_generatingPayload = "";
|
|
_displayedPayload = "";
|
|
themedQrCodePath = "";
|
|
normalQrCodePath = "";
|
|
open();
|
|
}
|
|
|
|
function hide() {
|
|
deleteQrCodeFiles(themedQrCodePath, normalQrCodePath);
|
|
themedQrCodePath = "";
|
|
normalQrCodePath = "";
|
|
close();
|
|
}
|
|
|
|
function deleteQrCodeFiles(themed, normal) {
|
|
if (themed.length > 0)
|
|
DMSService.sendRequest("network.delete-qrcode", {
|
|
path: themed
|
|
});
|
|
if (normal.length > 0)
|
|
DMSService.sendRequest("network.delete-qrcode", {
|
|
path: normal
|
|
});
|
|
}
|
|
|
|
Timer {
|
|
id: genTimer
|
|
interval: 200
|
|
repeat: false
|
|
onTriggered: root.generateQR(root._pendingPayload)
|
|
}
|
|
|
|
function generateQR(text) {
|
|
const trimmed = (text || "").trim();
|
|
if (trimmed.length === 0 || trimmed === _displayedPayload || generating)
|
|
return;
|
|
|
|
_generatingPayload = trimmed;
|
|
generating = true;
|
|
|
|
DMSService.sendRequest("network.generate-qrcode", {
|
|
text: trimmed
|
|
}, response => {
|
|
root.generating = false;
|
|
if (response.error) {
|
|
ToastService.showError(I18n.tr("Failed to generate QR code: %1").arg(JSON.stringify(response.error)));
|
|
return;
|
|
}
|
|
if (!response.result)
|
|
return;
|
|
if (root._generatingPayload !== root._pendingPayload.trim()) {
|
|
root.deleteQrCodeFiles(response.result[0], response.result[1]);
|
|
genTimer.restart();
|
|
return;
|
|
}
|
|
|
|
const oldThemed = root.themedQrCodePath;
|
|
const oldNormal = root.normalQrCodePath;
|
|
root._displayedPayload = root._generatingPayload;
|
|
root.themedQrCodePath = response.result[0];
|
|
root.normalQrCodePath = response.result[1];
|
|
root.deleteQrCodeFiles(oldThemed, oldNormal);
|
|
});
|
|
}
|
|
|
|
function onTextChanged(text) {
|
|
_pendingPayload = text;
|
|
const trimmed = text.trim();
|
|
if (trimmed.length === 0 || trimmed === _displayedPayload) {
|
|
genTimer.stop();
|
|
return;
|
|
}
|
|
genTimer.restart();
|
|
}
|
|
|
|
LazyLoader {
|
|
id: saveBrowserLoader
|
|
active: false
|
|
|
|
FileBrowserSurfaceModal {
|
|
id: saveBrowser
|
|
|
|
browserTitle: I18n.tr("Save QR Code")
|
|
browserIcon: "qr_code"
|
|
browserType: "default"
|
|
fileExtensions: ["*.png"]
|
|
allowStacking: true
|
|
saveMode: true
|
|
defaultFileName: "qrcode.png"
|
|
onFileSelected: path => {
|
|
const cleanPath = decodeURI(path.toString().replace(/^file:\/\//, ''));
|
|
copyQrCodeProcess.exec(["cp", "-f", root.normalQrCodePath, cleanPath]);
|
|
}
|
|
|
|
Process {
|
|
id: copyQrCodeProcess
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
saveBrowser.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
content: Component {
|
|
Item {
|
|
id: contentItem
|
|
|
|
property alias textInput: textInput
|
|
property var saveBrowserLoader: null
|
|
|
|
anchors.fill: parent
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingL
|
|
|
|
RowLayout {
|
|
id: modalTitle
|
|
width: parent.width
|
|
|
|
StyledText {
|
|
text: I18n.tr("QR Generator")
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Bold
|
|
Layout.alignment: Qt.AlignLeft
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
DankActionButton {
|
|
iconName: "close"
|
|
iconSize: Theme.iconSize - 4
|
|
iconColor: Theme.surfaceText
|
|
onClicked: root.hide()
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
}
|
|
|
|
DankTextField {
|
|
id: textInput
|
|
width: parent.width
|
|
placeholderText: I18n.tr("Enter text to encode")
|
|
showClearButton: true
|
|
focus: true
|
|
onTextEdited: root.onTextChanged(text)
|
|
Keys.onEscapePressed: event => {
|
|
event.accepted = true;
|
|
root.hide();
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: qrContainer
|
|
height: Math.min(parent.height - parent.spacing - modalTitle.height - textInput.height - parent.spacing * 4, 260)
|
|
width: height
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
opacity: 1
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: 80
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
Image {
|
|
id: qrCodeImg
|
|
anchors.fill: parent
|
|
source: root.themedQrCodePath
|
|
fillMode: Image.PreserveAspectFit
|
|
asynchronous: true
|
|
cache: false
|
|
visible: false
|
|
|
|
onSourceChanged: qrContainer.opacity = 0
|
|
onStatusChanged: {
|
|
if (status === Image.Ready)
|
|
qrContainer.opacity = 1;
|
|
}
|
|
}
|
|
|
|
MultiEffect {
|
|
source: qrCodeImg
|
|
anchors.fill: qrCodeImg
|
|
colorization: 1.0
|
|
colorizationColor: Theme.primary
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
width: parent.width
|
|
visible: root.themedQrCodePath.length > 0
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
DankButton {
|
|
text: I18n.tr("Save")
|
|
iconName: "save"
|
|
backgroundColor: Theme.surfaceContainer
|
|
textColor: Theme.surfaceText
|
|
onClicked: {
|
|
contentItem.saveBrowserLoader.active = true;
|
|
if (contentItem.saveBrowserLoader.item) {
|
|
contentItem.saveBrowserLoader.item.open();
|
|
}
|
|
}
|
|
}
|
|
|
|
DankButton {
|
|
text: I18n.tr("Copy")
|
|
iconName: "content_copy"
|
|
backgroundColor: Theme.primary
|
|
textColor: Theme.onPrimary
|
|
onClicked: {
|
|
if (root.normalQrCodePath.length > 0)
|
|
DMSService.sendRequest("clipboard.copyFile", {
|
|
filePath: root.normalQrCodePath
|
|
});
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|