diff options
Diffstat (limited to 'desktop/modules')
| -rw-r--r-- | desktop/modules/sonix/BootlogoTab.qml | 338 |
1 files changed, 338 insertions, 0 deletions
diff --git a/desktop/modules/sonix/BootlogoTab.qml b/desktop/modules/sonix/BootlogoTab.qml new file mode 100644 index 0000000..613a71d --- /dev/null +++ b/desktop/modules/sonix/BootlogoTab.qml @@ -0,0 +1,338 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import "../.." +import "../appearance" +import "Sonix.js" as S + +// The boot logo editor: the canvas on the left, its controls on the right; +// Save and Upload live in the page header (BootlogoModule.qml) and call +// save() and upload() here. +Row { + id: tab + spacing: 24 + + // Edited locally, written to Prefs after a pause, not on every drag step. + property var cfg: Object.assign({}, Prefs.bootlogo) + function set(ch) { tab.cfg = Object.assign({}, tab.cfg, ch); cfgWrite.restart(); } + Timer { id: cfgWrite; interval: 600; onTriggered: Prefs.setBootlogo(tab.cfg) } + + readonly property string folder: tab.cfg.folder || Quickshell.env("HOME") + "/Pictures/sonix-bootlogo" + readonly property string cardDir: "/data/mnt/sd_0/Bootlogo" + + property bool busy: false + property bool waiting: false + property string status: "" + property bool statusError: false + readonly property bool canSave: canvas.ready && !tab.busy + function say(t, err) { tab.status = t; tab.statusError = !!err; } + + readonly property var swatches: ["#ffffff", "#000000", "#3584e4", "#ff7800", "#e5a50a", + "#2ec27e", "#9141ac", "#d56199", "udt"] + readonly property var fontKeys: [["bungee", "Bungee + Inline"], ["mono", "Major Mono"], + ["michroma", "Michroma"], ["audiowide", "Audiowide"]] + + // --- save and upload -------------------------------------------------- + + // then(file) runs after a successful save. + function save(then) { + tab.busy = true; + mk.then = then || null; + mk.command = ["mkdir", "-p", tab.folder]; + mk.running = false; + mk.running = true; + } + Process { + id: mk + property var then: null + onExited: code => { + if (code !== 0) { tab.busy = false; tab.say(`Cannot create ${tab.folder}`, true); return; } + const base = tab.folder + "/" + S.stamp(new Date()); + canvas.save(base, (ok, err) => { + tab.busy = false; + if (!ok) { tab.say(err, true); return; } + tab.say(`Saved ${base}.png`); + if (mk.then) mk.then(base + ".png"); + }); + } + } + + property int tries: 0 + function upload() { tab.save(file => { tab.tries = 0; tab.send(file); }); } + function send(file) { + tab.busy = true; + Sonix.upload(file, tab.cardDir, (code, out) => { + tab.busy = false; + if (code === 3 && tab.tries < 30) { + tab.waiting = true; + tab.tries++; + tab.say("Open Wi-Fi transfer on the player (Control centre) and leave it on screen.", true); + retry.file = file; + retry.restart(); + return; + } + tab.waiting = false; + if (code === 0) + tab.say("Uploaded. Pick it on the player: Settings > Appearance > Boot logo. You can leave Wi-Fi transfer now."); + else if (code === 3) + tab.say("Wi-Fi transfer did not answer for 60s. The copy is saved locally.", true); + else + tab.say(`Upload failed: ${out || "exit " + code}. The copy is saved locally.`, true); + }); + } + Timer { id: retry; property string file: ""; interval: 2000; onTriggered: tab.send(file) } + + // --- canvas ----------------------------------------------------------- + + // 0.75 for display; the canvas itself stays 480x800 for the grab. + Rectangle { + width: 480 * 0.75 + 8 + height: 800 * 0.75 + 8 + radius: 14 + color: "black" + border.width: 2 + border.color: Qt.alpha(Theme.text, 0.25) + + Item { + x: 4; y: 4 + width: 480 * 0.75 + height: 800 * 0.75 + scale: 0.75 + transformOrigin: Item.TopLeft + + BootlogoCanvas { + id: canvas + cfg: tab.cfg + text: tab.cfg.text || Quickshell.env("USER") + moveText: moveTextBtn.chosen + onTextMoved: (x, y) => tab.set({ x: Math.round(x), y: Math.round(y) }) + } + } + + Text { + anchors.centerIn: parent + width: parent.width - 40 + visible: !canvas.ready + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + text: canvas.loadError || "Choose a picture" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: canvas.loadError ? Theme.red : Theme.subtext + } + } + + // --- controls --------------------------------------------------------- + + component Heading: Text { + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.text + } + + component Swatches: Row { + id: sw + property string value: "" + signal picked(string v) + spacing: 6 + Repeater { + model: tab.swatches + delegate: Rectangle { + required property string modelData + width: 24; height: 24; radius: 6 + color: S.colour(modelData, Theme.accent) + border.width: sw.value === modelData ? 3 : 1 + border.color: sw.value === modelData ? Theme.text : Qt.alpha(Theme.text, 0.3) + Text { + visible: modelData === "udt" + anchors.centerIn: parent + text: "U" + font { family: Theme.fontFamily; pixelSize: 11; bold: true } + color: Theme.base + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: sw.picked(modelData) + } + } + } + } + + Column { + width: 380 + spacing: 12 + + Heading { text: "Picture" } + Row { + spacing: 8 + Button { text: "Choose picture…"; onClicked: pick.open() } + Button { text: "Reset"; enabled: canvas.ready; onClicked: canvas.recentre() } + } + Row { + spacing: 8 + Button { + text: "Move picture" + border.color: !moveTextBtn.chosen ? Theme.accent : Qt.alpha(Theme.text, 0.2) + onClicked: moveTextBtn.chosen = false + } + Button { + id: moveTextBtn + property bool chosen: false + text: "Move text" + border.color: chosen ? Theme.accent : Qt.alpha(Theme.text, 0.2) + onClicked: chosen = true + } + } + Text { + text: "Drag on the preview; the wheel zooms the picture." + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } + color: Theme.subtext + } + + Heading { text: "Text" } + Field { + width: 260 + placeholder: Quickshell.env("USER") + Component.onCompleted: value = tab.cfg.text + onEdited: tab.set({ text: value }) + } + Flow { + width: parent.width + spacing: 6 + Repeater { + model: tab.fontKeys + delegate: Button { + required property var modelData + text: modelData[1] + font.family: canvas.families[modelData[0]] + border.color: tab.cfg.font === modelData[0] ? Theme.accent : Qt.alpha(Theme.text, 0.2) + onClicked: tab.set({ font: modelData[0] }) + } + } + } + Row { + spacing: 8 + Button { text: "Top"; onClicked: tab.set({ x: 240, y: 120 }) } + Button { text: "Centre"; onClicked: tab.set({ x: 240, y: 400 }) } + Button { text: "Bottom"; onClicked: tab.set({ x: 240, y: 690 }) } + } + Row { + spacing: 8 + Text { + width: 60 + anchors.verticalCenter: parent.verticalCenter + text: "Size" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.subtext + } + Slider { + width: 260 + from: 40; to: 240; stepSize: 1 + value: tab.cfg.size + onMoved: tab.set({ size: Math.round(value) }) + } + } + + Heading { text: "Colours" } + Row { + spacing: 8 + Text { + width: 60 + anchors.verticalCenter: parent.verticalCenter + text: "Fill" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.subtext + } + Swatches { value: tab.cfg.fill; onPicked: v => tab.set({ fill: v }) } + } + Row { + spacing: 8 + Text { + width: 60 + anchors.verticalCenter: parent.verticalCenter + text: tab.cfg.font === "bungee" ? "Inline" : "Shadow" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.subtext + } + Swatches { value: tab.cfg.second; onPicked: v => tab.set({ second: v }) } + } + Button { text: "Swap colours"; onClicked: tab.set({ fill: tab.cfg.second, second: tab.cfg.fill }) } + Row { + visible: tab.cfg.font !== "bungee" + spacing: 8 + Text { + width: 60 + anchors.verticalCenter: parent.verticalCenter + text: "Offset" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.subtext + } + Slider { + width: 160 + from: 0; to: 30; stepSize: 1 + value: tab.cfg.offset + onMoved: tab.set({ offset: Math.round(value) }) + } + Repeater { + model: [["nw", "↖"], ["ne", "↗"], ["sw", "↙"], ["se", "↘"]] + delegate: Button { + required property var modelData + text: modelData[1] + border.color: tab.cfg.dir === modelData[0] ? Theme.accent : Qt.alpha(Theme.text, 0.2) + onClicked: tab.set({ dir: modelData[0] }) + } + } + } + + Heading { text: "Output" } + Row { + spacing: 8 + Text { + width: 260 + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideMiddle + text: tab.folder + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.text + } + Button { text: "Change…"; onClicked: pickFolder.open() } + } + Text { + width: parent.width + wrapMode: Text.Wrap + text: "Save writes the logo and a transparent mask of the text. Upload also sends the logo to the card's Bootlogo folder; Wi-Fi transfer must be open on the player." + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } + color: Theme.subtext + } + } + + // A FileDialog hands back a file:// URL. + function pathOf(u) { return decodeURIComponent(u.toString().replace(/^file:\/\//, "")); } + + FileDialog { + id: pick + title: "Choose a picture" + fileMode: FileDialog.OpenFile + currentFolder: "file://" + Quickshell.env("HOME") + "/Pictures" + nameFilters: ["Pictures (*.jpg *.jpeg *.png *.JPG *.JPEG *.PNG)"] + onAccepted: canvas.source = selectedFile + } + FolderDialog { + id: pickFolder + title: "Output folder" + currentFolder: "file://" + tab.folder + onAccepted: tab.set({ folder: tab.pathOf(selectedFolder) }) + } +} |
