aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/sonix/BootlogoTab.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/sonix/BootlogoTab.qml')
-rw-r--r--desktop/modules/sonix/BootlogoTab.qml338
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) })
+ }
+}