summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock188
-rw-r--r--Cargo.toml13
-rw-r--r--README.md44
-rw-r--r--crates/flasher-gtk/Cargo.toml10
-rw-r--r--crates/flasher-gtk/src/main.rs (renamed from src/main.rs)2
-rw-r--r--crates/flasher-qt/Cargo.toml9
-rw-r--r--crates/flasher-qt/qml/Main.qml394
-rw-r--r--crates/flasher-qt/src/main.rs69
8 files changed, 717 insertions, 12 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4af0b09..cb7eb08 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,15 @@
version = 4
[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -15,6 +24,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
name = "cairo-rs"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -38,6 +53,16 @@ dependencies = [
]
[[package]]
+name = "cc"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
name = "cfg-expr"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -48,6 +73,53 @@ dependencies = [
]
[[package]]
+name = "cpp"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1f6422bf20bc0654eac481a29a03e6d9121ad9f12345a03773b8a76a8701915"
+dependencies = [
+ "cpp_macros",
+]
+
+[[package]]
+name = "cpp_build"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f6fed3200ba0708c2adca5f6ed5ae202edd824bd4cbac7935a85edac9bcddce"
+dependencies = [
+ "cc",
+ "cpp_common",
+ "proc-macro2",
+ "regex",
+ "syn 2.0.119",
+ "unicode-xid",
+]
+
+[[package]]
+name = "cpp_common"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7280a73ef92e18d27d2ec3005b57fe0043b51d1b506be86b0bf66f588f9857b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "cpp_macros"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffc7fd49a5b246251229ea4d4bf94c8d418689a1f9986ef96e00b64992922169"
+dependencies = [
+ "aho-corasick",
+ "byteorder",
+ "cpp_common",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -64,7 +136,13 @@ dependencies = [
]
[[package]]
-name = "flasher"
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flasher-gtk"
version = "0.1.0"
dependencies = [
"gtk4",
@@ -72,6 +150,13 @@ dependencies = [
]
[[package]]
+name = "flasher-qt"
+version = "0.1.0"
+dependencies = [
+ "qmetaobject",
+]
+
+[[package]]
name = "futures-channel"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -404,6 +489,12 @@ dependencies = [
]
[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
name = "libadwaita"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -441,6 +532,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -510,6 +607,43 @@ dependencies = [
]
[[package]]
+name = "qmetaobject"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "426a57e85d36f055a0c82cb0a8a261d49ba051ab2a2ef5471835f69d477816cd"
+dependencies = [
+ "cpp",
+ "cpp_build",
+ "lazy_static",
+ "log",
+ "qmetaobject_impl",
+ "qttypes",
+ "semver",
+]
+
+[[package]]
+name = "qmetaobject_impl"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afc24897c707dcd6963e359e7f2b123857c508f129bed8ac4d3bd575c1a47627"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "qttypes"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7edf5b38c97ad8900ad2a8418ee44b4adceaa866a4a3405e2f1c909871d7ebd"
+dependencies = [
+ "cpp",
+ "cpp_build",
+ "semver",
+]
+
+[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -519,6 +653,35 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -563,6 +726,12 @@ dependencies = [
]
[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -576,6 +745,17 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
@@ -673,6 +853,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 75f7dc3..01db450 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,10 +1,3 @@
-[package]
-name = "flasher"
-version = "0.1.0"
-edition = "2024"
-description = "A GTK interface for preparing ALT Linux images for single-board computers"
-license = "GPL-2.0-or-later"
-
-[dependencies]
-adw = { package = "libadwaita", version = "0.8", features = ["v1_5"] }
-gtk = { package = "gtk4", version = "0.10", features = ["v4_10"] }
+[workspace]
+members = ["crates/flasher-gtk", "crates/flasher-qt"]
+resolver = "3"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c2fd06d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
+# ALT Image Writer GUI prototypes
+
+This workspace contains two independent interfaces for `alt-rootfs-installer`:
+
+- `crates/flasher-gtk` — GTK 4/libadwaita
+- `crates/flasher-qt` — Qt 5/Qt Quick Controls 2
+
+Both are interface previews. They inspect supported boards and removable drives, but do not write to any device.
+
+## System dependencies
+
+On ALT Linux:
+
+```sh
+sudo apt-get install gcc-c++ libgtk4-devel libadwaita-devel \
+ qt5-base-devel qt5-declarative-devel qt5-quickcontrols2-devel \
+ qt5-quickcontrols2
+```
+
+The Qt bindings discover Qt through `qmake`. If several Qt versions are installed, select Qt 5 explicitly:
+
+```sh
+export QMAKE=/usr/lib64/qt5/bin/qmake
+```
+
+(The exact `qmake` path can be found with `rpm -ql qt5-base-devel | grep '/qmake$'`.)
+
+## Build
+
+Build both applications:
+
+```sh
+cargo build
+```
+
+Build or run one interface:
+
+```sh
+cargo build -p flasher-gtk
+cargo build -p flasher-qt
+
+cargo run -p flasher-gtk
+cargo run -p flasher-qt
+```
diff --git a/crates/flasher-gtk/Cargo.toml b/crates/flasher-gtk/Cargo.toml
new file mode 100644
index 0000000..c7d8c5b
--- /dev/null
+++ b/crates/flasher-gtk/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "flasher-gtk"
+version = "0.1.0"
+edition = "2024"
+description = "An Adwaita GTK interface for preparing ALT Linux SBC images"
+license = "GPL-2.0-or-later"
+
+[dependencies]
+adw = { package = "libadwaita", version = "0.8", features = ["v1_5"] }
+gtk = { package = "gtk4", version = "0.10", features = ["v4_10"] }
diff --git a/src/main.rs b/crates/flasher-gtk/src/main.rs
index b82def3..462b498 100644
--- a/src/main.rs
+++ b/crates/flasher-gtk/src/main.rs
@@ -3,7 +3,7 @@ use gtk::glib;
use std::{cell::RefCell, fs, path::Path, rc::Rc};
const APP_ID: &str = "org.altlinux.Flasher";
-const BOARDS: &str = include_str!("../alt-rootfs-installer/SUPPORTED-BOARDS");
+const BOARDS: &str = include_str!("../../../alt-rootfs-installer/SUPPORTED-BOARDS");
#[derive(Default)]
struct State {
diff --git a/crates/flasher-qt/Cargo.toml b/crates/flasher-qt/Cargo.toml
new file mode 100644
index 0000000..5bbf918
--- /dev/null
+++ b/crates/flasher-qt/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "flasher-qt"
+version = "0.1.0"
+edition = "2024"
+description = "A Qt interface for preparing ALT Linux SBC images"
+license = "GPL-2.0-or-later"
+
+[dependencies]
+qmetaobject = "0.2.10"
diff --git a/crates/flasher-qt/qml/Main.qml b/crates/flasher-qt/qml/Main.qml
new file mode 100644
index 0000000..a41064d
--- /dev/null
+++ b/crates/flasher-qt/qml/Main.qml
@@ -0,0 +1,394 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import Qt.labs.platform 1.1 as Platform
+
+ApplicationWindow {
+ id: window
+ visible: true
+ width: 760
+ height: 820
+ minimumWidth: 620
+ minimumHeight: 620
+ title: qsTr("ALT Image Writer — Qt")
+
+ property bool sourceIsImage: false
+ property bool outputIsFile: false
+ property string sourcePath: ""
+ property string outputPath: ""
+ property string selectedBoard: ""
+ property string selectedPlatform: ""
+ property var driveNames: availableDrivesText.length ? availableDrivesText.split("\n") : []
+
+ function localPath(url) {
+ var value = url.toString()
+ return value.indexOf("file://") === 0 ? decodeURIComponent(value.substring(7)) : value
+ }
+
+ function fileName(path) {
+ var parts = path.split("/")
+ return parts[parts.length - 1]
+ }
+
+ function ready() {
+ return sourcePath.length > 0 && selectedBoard.length > 0
+ && (outputIsFile ? outputPath.length > 0 : driveBox.currentIndex >= 0)
+ }
+
+ header: ToolBar {
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 16
+ anchors.rightMargin: 8
+ Label {
+ text: qsTr("ALT Image Writer")
+ font.pixelSize: 18
+ font.bold: true
+ Layout.fillWidth: true
+ }
+ ToolButton {
+ text: "ⓘ"
+ font.pixelSize: 18
+ Accessible.name: qsTr("About")
+ onClicked: aboutDialog.open()
+ }
+ }
+ }
+
+ ScrollView {
+ id: scroll
+ anchors.fill: parent
+ contentWidth: availableWidth
+ clip: true
+
+ ColumnLayout {
+ width: Math.min(scroll.availableWidth - 40, 700)
+ x: Math.max(20, (scroll.availableWidth - width) / 2)
+ spacing: 16
+
+ Item { Layout.preferredHeight: 8 }
+ Label {
+ text: qsTr("Create Installation Media")
+ font.pixelSize: 26
+ font.bold: true
+ Layout.alignment: Qt.AlignHCenter
+ }
+ Label {
+ text: qsTr("Prepare an ALT Linux root filesystem or disk image for your board.")
+ color: palette.mid
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ Layout.fillWidth: true
+ }
+
+ Frame {
+ Layout.fillWidth: true
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 12
+ Label { text: qsTr("Installation Source"); font.bold: true; font.pixelSize: 17 }
+ Label { text: qsTr("Choose a root filesystem archive or a ready-made disk image."); color: palette.mid }
+ ButtonGroup { id: sourceGroup }
+ RowLayout {
+ Label { text: qsTr("Source type"); Layout.fillWidth: true }
+ RadioButton {
+ text: qsTr("Root filesystem")
+ checked: true
+ ButtonGroup.group: sourceGroup
+ onCheckedChanged: if (checked) sourceIsImage = false
+ }
+ RadioButton {
+ text: qsTr("Disk image")
+ ButtonGroup.group: sourceGroup
+ onCheckedChanged: if (checked) {
+ sourceIsImage = true
+ outputIsFile = false
+ driveOutput.checked = true
+ }
+ }
+ }
+ RowLayout {
+ spacing: 12
+ Label {
+ text: sourcePath.length ? fileName(sourcePath) : qsTr("No source selected")
+ font.bold: sourcePath.length > 0
+ elide: Text.ElideMiddle
+ Layout.fillWidth: true
+ }
+ Button { text: qsTr("Choose…"); onClicked: sourceDialog.open() }
+ }
+ Label {
+ text: sourcePath.length ? sourcePath
+ : sourceIsImage ? qsTr("Supported: .img, .img.xz")
+ : qsTr("Supported: .tar, .tar.gz, .tar.xz")
+ color: palette.mid
+ elide: Text.ElideMiddle
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ Frame {
+ Layout.fillWidth: true
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 10
+ Label { text: qsTr("Target Hardware"); font.bold: true; font.pixelSize: 17 }
+ Label { text: qsTr("Boards are grouped by platform, SoC family, and architecture."); color: palette.mid }
+ RowLayout {
+ ColumnLayout {
+ Layout.fillWidth: true
+ Label {
+ text: selectedBoard.length ? selectedBoard : qsTr("No board selected")
+ font.bold: selectedBoard.length > 0
+ }
+ Label {
+ text: selectedBoard.length ? selectedPlatform : qsTr("Choose from the supported board list")
+ color: palette.mid
+ }
+ }
+ Button { text: qsTr("Select Board…"); onClicked: boardDialog.open() }
+ }
+ }
+ }
+
+ Frame {
+ Layout.fillWidth: true
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 12
+ Label { text: qsTr("Destination"); font.bold: true; font.pixelSize: 17 }
+ Label { text: qsTr("Write to removable media or create an image file."); color: palette.mid }
+ ButtonGroup { id: outputGroup }
+ RowLayout {
+ Label { text: qsTr("Output type"); Layout.fillWidth: true }
+ RadioButton {
+ id: driveOutput
+ text: qsTr("Drive")
+ checked: true
+ ButtonGroup.group: outputGroup
+ onCheckedChanged: if (checked) outputIsFile = false
+ }
+ RadioButton {
+ text: qsTr("Image file")
+ enabled: !sourceIsImage
+ ButtonGroup.group: outputGroup
+ onCheckedChanged: if (checked) outputIsFile = true
+ }
+ }
+ RowLayout {
+ visible: !outputIsFile
+ Label { text: qsTr("Storage device"); Layout.preferredWidth: 150 }
+ ComboBox {
+ id: driveBox
+ Layout.fillWidth: true
+ model: driveNames
+ displayText: currentIndex < 0 ? qsTr("No removable drives detected") : currentText
+ }
+ }
+ RowLayout {
+ visible: outputIsFile
+ Label {
+ text: outputPath.length ? outputPath : qsTr("Choose where to save the image")
+ elide: Text.ElideMiddle
+ Layout.fillWidth: true
+ }
+ Button { text: qsTr("Choose…"); onClicked: outputDialog.open() }
+ }
+ }
+ }
+
+ Frame {
+ Layout.fillWidth: true
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 8
+ Label { text: qsTr("Options"); font.bold: true; font.pixelSize: 17 }
+ RowLayout {
+ Label { text: qsTr("Root filesystem"); Layout.fillWidth: true }
+ ComboBox { model: ["ext4", "f2fs"] }
+ }
+ CheckBox { text: qsTr("Resize root partition to fill the destination"); checked: true }
+ CheckBox { text: qsTr("Create a separate 512 MiB boot partition") }
+ CheckBox { text: qsTr("Encrypt the root partition with LUKS") }
+ CheckBox { text: qsTr("Add the system serial console") }
+ RowLayout {
+ Label { text: qsTr("EFI system partition"); Layout.fillWidth: true }
+ ComboBox { model: [qsTr("None"), qsTr("EFI (GPT)"), qsTr("EFI (MBR)")] }
+ }
+ RowLayout {
+ Label { text: qsTr("First-boot VNC"); Layout.fillWidth: true }
+ ComboBox { model: [qsTr("Use image default"), qsTr("Enabled"), qsTr("Disabled")] }
+ }
+ }
+ }
+ Item { Layout.preferredHeight: 80 }
+ }
+ }
+
+ footer: ToolBar {
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: 10
+ Label { text: qsTr("Nothing will be written until you confirm."); color: palette.mid; Layout.fillWidth: true }
+ Button {
+ text: qsTr("Review and Write")
+ highlighted: true
+ enabled: ready()
+ onClicked: reviewDialog.open()
+ }
+ }
+ }
+
+ Dialog {
+ id: boardDialog
+ parent: Overlay.overlay
+ anchors.centerIn: parent
+ width: Math.min(window.width - 48, 620)
+ height: Math.min(window.height - 80, 650)
+ modal: true
+ title: qsTr("Select Target Board")
+ standardButtons: Dialog.Cancel
+
+ ListModel { id: allBoards }
+ ListModel { id: filteredBoards }
+
+ function populate() {
+ if (allBoards.count > 0) return
+ var platform = "Other"
+ var lines = supportedBoardsText.split("\n")
+ for (var i = 0; i < lines.length; ++i) {
+ var line = lines[i].trim()
+ if (!line.length) continue
+ if (line.charAt(line.length - 1) === ":") {
+ platform = line.substring(0, line.length - 1)
+ } else {
+ var names = line.split(/\s+/)
+ for (var j = 0; j < names.length; ++j)
+ allBoards.append({ boardName: names[j], platformName: platform })
+ }
+ }
+ filter("")
+ }
+
+ function filter(query) {
+ filteredBoards.clear()
+ var needle = query.toLowerCase()
+ for (var i = 0; i < allBoards.count; ++i) {
+ var item = allBoards.get(i)
+ if (!needle.length || item.boardName.toLowerCase().indexOf(needle) >= 0
+ || item.platformName.toLowerCase().indexOf(needle) >= 0)
+ filteredBoards.append({ boardName: item.boardName, platformName: item.platformName })
+ }
+ }
+
+ onOpened: { populate(); searchField.forceActiveFocus() }
+
+ ColumnLayout {
+ anchors.fill: parent
+ TextField {
+ id: searchField
+ Layout.fillWidth: true
+ placeholderText: qsTr("Search boards or platforms…")
+ selectByMouse: true
+ onTextChanged: boardDialog.filter(text)
+ }
+ Label {
+ text: qsTr("%1 matching boards").arg(filteredBoards.count)
+ color: palette.mid
+ }
+ ListView {
+ id: boardList
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ model: filteredBoards
+ spacing: 2
+ section.property: "platformName"
+ section.criteria: ViewSection.FullString
+ section.delegate: Rectangle {
+ width: boardList.width
+ height: 42
+ color: window.palette.alternateBase
+ Label {
+ anchors.left: parent.left
+ anchors.leftMargin: 12
+ anchors.verticalCenter: parent.verticalCenter
+ text: section
+ font.bold: true
+ color: window.palette.highlight
+ }
+ }
+ delegate: ItemDelegate {
+ width: boardList.width
+ text: boardName
+ onClicked: {
+ selectedBoard = boardName
+ selectedPlatform = platformName
+ boardDialog.close()
+ }
+ }
+ ScrollBar.vertical: ScrollBar {}
+ }
+ }
+ }
+
+ Dialog {
+ id: reviewDialog
+ parent: Overlay.overlay
+ anchors.centerIn: parent
+ modal: true
+ title: qsTr("Ready to write?")
+ standardButtons: Dialog.Cancel | Dialog.Ok
+ onAccepted: previewNotice.open()
+ ColumnLayout {
+ Label { text: qsTr("Board"); font.bold: true }
+ Label { text: selectedBoard + " — " + selectedPlatform }
+ Label { text: qsTr("Destination"); font.bold: true }
+ Label { text: outputIsFile ? outputPath : driveBox.currentText; elide: Text.ElideMiddle; Layout.maximumWidth: 450 }
+ Label { text: qsTr("This is an interface preview. No data will be written."); color: palette.mid }
+ }
+ }
+
+ Dialog {
+ id: aboutDialog
+ parent: Overlay.overlay
+ anchors.centerIn: parent
+ modal: true
+ title: qsTr("About ALT Image Writer")
+ standardButtons: Dialog.Ok
+ Label {
+ text: qsTr("ALT Image Writer — Qt\nVersion 0.1.0\n\nPrepare ALT Linux installation media for supported SBCs.")
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ Popup {
+ id: previewNotice
+ parent: Overlay.overlay
+ x: (parent.width - width) / 2
+ y: parent.height - height - 24
+ padding: 16
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ Label { text: qsTr("Flashing is not implemented in this preview") }
+ Timer { interval: 3500; running: previewNotice.visible; onTriggered: previewNotice.close() }
+ }
+
+ Platform.FileDialog {
+ id: sourceDialog
+ title: qsTr("Choose Installation Source")
+ fileMode: Platform.FileDialog.OpenFile
+ nameFilters: sourceIsImage ? [qsTr("Disk images (*.img *.img.xz)"), qsTr("All files (*)")]
+ : [qsTr("Root filesystem archives (*.tar *.tar.gz *.tar.xz)"), qsTr("All files (*)")]
+ onAccepted: sourcePath = localPath(file)
+ }
+
+ Platform.FileDialog {
+ id: outputDialog
+ title: qsTr("Create Disk Image")
+ fileMode: Platform.FileDialog.SaveFile
+ defaultSuffix: "img"
+ nameFilters: [qsTr("Raw disk images (*.img)"), qsTr("All files (*)")]
+ onAccepted: outputPath = localPath(file)
+ }
+}
diff --git a/crates/flasher-qt/src/main.rs b/crates/flasher-qt/src/main.rs
new file mode 100644
index 0000000..f131bc5
--- /dev/null
+++ b/crates/flasher-qt/src/main.rs
@@ -0,0 +1,69 @@
+use qmetaobject::{QmlEngine, QString, QVariant};
+use std::fs;
+
+const BOARDS: &str = include_str!("../../../alt-rootfs-installer/SUPPORTED-BOARDS");
+const QML: &str = include_str!("../qml/Main.qml");
+
+fn main() {
+ let mut engine = QmlEngine::new();
+ engine.set_property(
+ QString::from("supportedBoardsText"),
+ QVariant::from(QString::from(BOARDS)),
+ );
+ engine.set_property(
+ QString::from("availableDrivesText"),
+ QVariant::from(QString::from(available_drives().join("\n"))),
+ );
+ engine.load_data(QML.into());
+ engine.exec();
+}
+
+fn available_drives() -> Vec<String> {
+ let mut drives = Vec::new();
+ let Ok(entries) = fs::read_dir("/sys/block") else {
+ return drives;
+ };
+
+ for entry in entries.flatten() {
+ let name = entry.file_name().to_string_lossy().into_owned();
+ if name.starts_with("loop") || name.starts_with("ram") || name.starts_with("zram") {
+ continue;
+ }
+ let removable = fs::read_to_string(entry.path().join("removable"))
+ .map(|value| value.trim() == "1")
+ .unwrap_or(false);
+ if !removable && !name.starts_with("mmcblk") {
+ continue;
+ }
+
+ let sectors = fs::read_to_string(entry.path().join("size"))
+ .ok()
+ .and_then(|value| value.trim().parse::<u64>().ok())
+ .unwrap_or(0);
+ let model = fs::read_to_string(entry.path().join("device/model"))
+ .unwrap_or_default()
+ .trim()
+ .to_owned();
+ let description = if model.is_empty() {
+ format!("/dev/{name}")
+ } else {
+ model
+ };
+ drives.push(format!(
+ "{description} · {} · /dev/{name}",
+ format_size(sectors.saturating_mul(512))
+ ));
+ }
+ drives.sort();
+ drives
+}
+
+fn format_size(bytes: u64) -> String {
+ const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
+ const MIB: f64 = 1024.0 * 1024.0;
+ if bytes as f64 >= GIB {
+ format!("{:.1} GB", bytes as f64 / GIB)
+ } else {
+ format!("{:.0} MB", bytes as f64 / MIB)
+ }
+}