diff options
Diffstat (limited to 'crates/flasher-qt/qml/Main.qml')
| -rw-r--r-- | crates/flasher-qt/qml/Main.qml | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/crates/flasher-qt/qml/Main.qml b/crates/flasher-qt/qml/Main.qml new file mode 100644 index 0000000..805c6ed --- /dev/null +++ b/crates/flasher-qt/qml/Main.qml @@ -0,0 +1,437 @@ +import QtQuick +import QtQuick.Controls as Controls +import QtQuick.Layouts +import QtQuick.Dialogs +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + visible: true + width: 760 + height: 820 + minimumWidth: 600 + minimumHeight: 580 + title: qsTr("ALT Image Writer") + + readonly property string supportedBoardsText: @SUPPORTED_BOARDS@ + readonly property string availableDrivesText: @AVAILABLE_DRIVES@ + 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.startsWith("file://") ? 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 && driveNames.length > 0) + } + + globalDrawer: Kirigami.GlobalDrawer { + isMenu: true + actions: [ + Kirigami.Action { + text: qsTr("About ALT Image Writer") + icon.name: "help-about" + onTriggered: aboutDialog.open() + } + ] + } + + pageStack.initialPage: Kirigami.ScrollablePage { + title: qsTr("Create Installation Media") + actions: [ + Kirigami.Action { + text: qsTr("About") + icon.name: "help-about" + displayHint: Kirigami.DisplayHint.IconOnly + onTriggered: aboutDialog.open() + } + ] + + ColumnLayout { + width: Math.min(parent.width, Kirigami.Units.gridUnit * 42) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Kirigami.Units.largeSpacing + + Controls.Label { + text: qsTr("Prepare an ALT Linux root filesystem or disk image for your board.") + color: Kirigami.Theme.disabledTextColor + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.Heading { text: qsTr("Installation Source"); level: 3 } + Controls.Label { + text: qsTr("Choose a root filesystem archive or a ready-made disk image.") + color: Kirigami.Theme.disabledTextColor + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + Kirigami.FormLayout { + Layout.fillWidth: true + RowLayout { + Kirigami.FormData.label: qsTr("Source type:") + Controls.RadioButton { + text: qsTr("Root filesystem") + checked: true + onCheckedChanged: if (checked) sourceIsImage = false + } + Controls.RadioButton { + text: qsTr("Disk image") + onCheckedChanged: if (checked) { + sourceIsImage = true + driveOutput.checked = true + } + } + } + RowLayout { + Kirigami.FormData.label: qsTr("Source:") + Controls.Label { + text: sourcePath.length ? fileName(sourcePath) : qsTr("No source selected") + elide: Text.ElideMiddle + Layout.fillWidth: true + } + Controls.Button { + text: qsTr("Choose…") + icon.name: "document-open" + onClicked: sourceDialog.open() + } + } + } + Controls.Label { + text: sourcePath.length ? sourcePath + : sourceIsImage ? qsTr("Supported: .img, .img.xz") + : qsTr("Supported: .tar, .tar.gz, .tar.xz") + color: Kirigami.Theme.disabledTextColor + elide: Text.ElideMiddle + Layout.fillWidth: true + } + } + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + Kirigami.Heading { text: qsTr("Target Hardware"); level: 3 } + Controls.Label { + text: qsTr("Boards are grouped by platform, SoC family, and architecture.") + color: Kirigami.Theme.disabledTextColor + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + RowLayout { + Layout.fillWidth: true + Kirigami.Icon { + source: "computer-symbolic" + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: implicitWidth + } + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Controls.Label { + text: selectedBoard.length ? selectedBoard : qsTr("No board selected") + font.bold: selectedBoard.length > 0 + } + Controls.Label { + text: selectedBoard.length ? selectedPlatform : qsTr("Choose a supported target board") + color: Kirigami.Theme.disabledTextColor + } + } + Controls.Button { + text: qsTr("Select Board…") + onClicked: boardDialog.open() + } + } + } + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + Kirigami.Heading { text: qsTr("Destination"); level: 3 } + Controls.Label { + text: qsTr("Write to removable media or create an image file.") + color: Kirigami.Theme.disabledTextColor + } + Kirigami.FormLayout { + Layout.fillWidth: true + RowLayout { + Kirigami.FormData.label: qsTr("Output type:") + Controls.RadioButton { + id: driveOutput + text: qsTr("Drive") + checked: true + onCheckedChanged: if (checked) outputIsFile = false + } + Controls.RadioButton { + text: qsTr("Image file") + enabled: !sourceIsImage + onCheckedChanged: if (checked) outputIsFile = true + } + } + Controls.ComboBox { + id: driveBox + visible: !outputIsFile + Kirigami.FormData.label: qsTr("Storage device:") + Layout.fillWidth: true + model: driveNames + displayText: currentIndex < 0 || driveNames.length === 0 + ? qsTr("No removable drives detected") : currentText + enabled: driveNames.length > 0 + } + RowLayout { + visible: outputIsFile + Kirigami.FormData.label: qsTr("Image file:") + Controls.Label { + text: outputPath.length ? outputPath : qsTr("Choose where to save the image") + elide: Text.ElideMiddle + Layout.fillWidth: true + } + Controls.Button { + text: qsTr("Choose…") + icon.name: "document-save" + onClicked: outputDialog.open() + } + } + } + } + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + Kirigami.Heading { text: qsTr("Options"); level: 3 } + Kirigami.FormLayout { + Layout.fillWidth: true + Controls.ComboBox { + Kirigami.FormData.label: qsTr("Root filesystem:") + model: ["ext4", "f2fs"] + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Resize root partition:") + text: qsTr("Grow to fill the destination") + checked: true + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Boot partition:") + text: qsTr("Create a separate 512 MiB partition") + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Encryption:") + text: qsTr("Encrypt root partition with LUKS") + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Serial console:") + text: qsTr("Add to boot arguments") + } + Controls.ComboBox { + Kirigami.FormData.label: qsTr("EFI system partition:") + model: [qsTr("None"), qsTr("EFI (GPT)"), qsTr("EFI (MBR)")] + } + Controls.ComboBox { + Kirigami.FormData.label: qsTr("First-boot VNC:") + model: [qsTr("Use image default"), qsTr("Enabled"), qsTr("Disabled")] + } + } + } + } + + Kirigami.InlineMessage { + id: previewMessage + Layout.fillWidth: true + type: Kirigami.MessageType.Information + text: qsTr("Flashing is not implemented in this interface preview.") + showCloseButton: true + } + } + } + + footer: Controls.ToolBar { + contentItem: RowLayout { + Controls.Label { + text: qsTr("Nothing will be written until you confirm.") + color: Kirigami.Theme.disabledTextColor + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + } + Controls.Button { + text: qsTr("Review and Write") + icon.name: "media-flash-sd-mmc" + highlighted: true + enabled: root.ready() + Layout.rightMargin: Kirigami.Units.largeSpacing + onClicked: reviewDialog.open() + } + } + } + + Controls.Dialog { + id: boardDialog + parent: Controls.Overlay.overlay + anchors.centerIn: parent + width: Math.min(root.width - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 34) + height: Math.min(root.height - Kirigami.Units.gridUnit * 4, Kirigami.Units.gridUnit * 34) + modal: true + title: qsTr("Select Target Board") + standardButtons: Controls.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.endsWith(":")) { + 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().includes(needle) + || item.platformName.toLowerCase().includes(needle)) + filteredBoards.append({ boardName: item.boardName, platformName: item.platformName }) + } + } + + onOpened: { + populate() + searchField.forceActiveFocus() + } + + contentItem: ColumnLayout { + Controls.TextField { + id: searchField + Layout.fillWidth: true + placeholderText: qsTr("Search boards or platforms…") + onTextChanged: boardDialog.filter(text) + } + Controls.Label { + text: qsTr("%1 matching boards").arg(filteredBoards.count) + color: Kirigami.Theme.disabledTextColor + } + ListView { + id: boardList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: filteredBoards + section.property: "platformName" + section.criteria: ViewSection.FullString + section.delegate: Kirigami.ListSectionHeader { text: section } + delegate: Controls.ItemDelegate { + required property string boardName + required property string platformName + width: boardList.width + text: boardName + icon.name: "computer-symbolic" + onClicked: { + selectedBoard = boardName + selectedPlatform = platformName + boardDialog.close() + } + } + Controls.ScrollBar.vertical: Controls.ScrollBar {} + } + } + } + + Controls.Dialog { + id: reviewDialog + parent: Controls.Overlay.overlay + anchors.centerIn: parent + modal: true + title: qsTr("Ready to write?") + standardButtons: Controls.Dialog.Cancel | Controls.Dialog.Ok + onAccepted: previewMessage.visible = true + contentItem: Kirigami.FormLayout { + Controls.Label { Kirigami.FormData.label: qsTr("Board:"); text: selectedBoard } + Controls.Label { Kirigami.FormData.label: qsTr("Platform:"); text: selectedPlatform } + Controls.Label { + Kirigami.FormData.label: qsTr("Destination:") + text: outputIsFile ? outputPath : driveBox.currentText + elide: Text.ElideMiddle + } + Controls.Label { + text: qsTr("This is an interface preview. No data will be written.") + color: Kirigami.Theme.disabledTextColor + wrapMode: Text.WordWrap + } + } + } + + Controls.Dialog { + id: aboutDialog + parent: Controls.Overlay.overlay + anchors.centerIn: parent + modal: true + title: qsTr("About ALT Image Writer") + standardButtons: Controls.Dialog.Ok + contentItem: ColumnLayout { + Kirigami.Icon { + source: "media-flash-sd-mmc" + implicitWidth: Kirigami.Units.iconSizes.huge + implicitHeight: implicitWidth + Layout.alignment: Qt.AlignHCenter + } + Kirigami.Heading { text: qsTr("ALT Image Writer"); level: 2; Layout.alignment: Qt.AlignHCenter } + Controls.Label { text: qsTr("Version 0.1.0"); Layout.alignment: Qt.AlignHCenter } + Controls.Label { + text: qsTr("Prepare ALT Linux installation media for supported single-board computers.") + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: Kirigami.Units.gridUnit * 22 + } + } + } + + FileDialog { + id: sourceDialog + title: qsTr("Choose Installation Source") + fileMode: 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(selectedFile) + } + + FileDialog { + id: outputDialog + title: qsTr("Create Disk Image") + fileMode: FileDialog.SaveFile + defaultSuffix: "img" + nameFilters: [qsTr("Raw disk images (*.img)"), qsTr("All files (*)")] + onAccepted: outputPath = localPath(selectedFile) + } +} |