Téléverser les fichiers vers "first-login-ui/src"

This commit is contained in:
2026-08-18 15:49:10 +02:00
parent dfddaefca2
commit e2d803d0d2
3 changed files with 920 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
qt_add_executable(nixos-first-login-ui
main.cpp
)
qt_add_qml_module(nixos-first-login-ui
URI org.raspot.nixosfirstlogin
VERSION 1.0
QML_FILES
Main.qml
)
target_link_libraries(nixos-first-login-ui
PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Qml
Qt6::Quick
Qt6::QuickControls2
)
install(
TARGETS nixos-first-login-ui
RUNTIME DESTINATION bin
)
+864
View File
@@ -0,0 +1,864 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami
Kirigami.ApplicationWindow {
id: root
title: "Configuration initiale du poste"
visibility: Window.FullScreen
color: "#eef2f7"
property int currentStep: 0
property var steps: [
{ "title": "Bienvenue", "subtitle": "Présentation" },
{ "title": "Mot de passe", "subtitle": "Accès de secours" },
{ "title": "YubiKey", "subtitle": "PIN personnel" },
{ "title": "Terminé", "subtitle": "Poste prêt" }
]
function goNext() {
if (currentStep < steps.length - 1) {
currentStep += 1
}
}
function goBack() {
if (currentStep > 0) {
currentStep -= 1
}
}
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
component PrimaryButton: Controls.Button {
id: control
implicitHeight: 48
implicitWidth: 170
leftPadding: 24
rightPadding: 24
contentItem: Controls.Label {
text: control.text
font.pixelSize: 15
font.weight: Font.DemiBold
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: 12
color: control.down ? "#1e40af" : control.hovered ? "#2563eb" : "#1d4ed8"
Behavior on color { ColorAnimation { duration: 120 } }
}
}
component SecondaryButton: Controls.Button {
id: control
implicitHeight: 48
implicitWidth: 140
leftPadding: 22
rightPadding: 22
contentItem: Controls.Label {
text: control.text
font.pixelSize: 15
font.weight: Font.Medium
color: "#334155"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: 12
color: control.down ? "#dbe3ee" : control.hovered ? "#edf2f7" : "transparent"
border.width: 1
border.color: "#cbd5e1"
}
}
component SecretField: Controls.TextField {
id: field
Layout.fillWidth: true
implicitHeight: 50
echoMode: TextInput.Password
passwordCharacter: "●"
font.pixelSize: 15
leftPadding: 16
rightPadding: 16
selectByMouse: true
background: Rectangle {
radius: 10
color: field.activeFocus ? "#ffffff" : "#f8fafc"
border.width: field.activeFocus ? 2 : 1
border.color: field.activeFocus ? "#2563eb" : "#cbd5e1"
}
}
component StepDot: Rectangle {
required property int stepIndex
width: 34
height: 34
radius: 17
color: stepIndex < root.currentStep ? "#22c55e"
: stepIndex === root.currentStep ? "#ffffff"
: "#17345f"
border.width: stepIndex === root.currentStep ? 2 : 0
border.color: "#93c5fd"
Controls.Label {
anchors.centerIn: parent
text: parent.stepIndex < root.currentStep ? "✓" : (parent.stepIndex + 1)
color: parent.stepIndex === root.currentStep ? "#0f2b50" : "white"
font.pixelSize: 14
font.weight: Font.Bold
}
}
RowLayout {
anchors.fill: parent
spacing: 0
// -----------------------------------------------------
// Colonne gauche : identité + progression
// -----------------------------------------------------
Rectangle {
Layout.preferredWidth: Math.max(330, root.width * 0.29)
Layout.fillHeight: true
gradient: Gradient {
GradientStop { position: 0.0; color: "#081a33" }
GradientStop { position: 0.55; color: "#0b2a50" }
GradientStop { position: 1.0; color: "#123d70" }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 44
spacing: 0
RowLayout {
spacing: 14
Rectangle {
width: 54
height: 54
radius: 16
color: "#1d4ed8"
Controls.Label {
anchors.centerIn: parent
text: "N"
color: "white"
font.pixelSize: 25
font.weight: Font.Bold
}
}
ColumnLayout {
spacing: 2
Controls.Label {
text: "NixOS Workstations"
color: "white"
font.pixelSize: 19
font.weight: Font.DemiBold
}
Controls.Label {
text: "Configuration du poste"
color: "#a8c1e1"
font.pixelSize: 13
}
}
}
Item { Layout.preferredHeight: 70 }
Controls.Label {
text: "VOTRE CONFIGURATION"
color: "#7fa4d1"
font.pixelSize: 11
font.weight: Font.Bold
font.letterSpacing: 1.5
}
Item { Layout.preferredHeight: 26 }
Repeater {
model: root.steps
delegate: Item {
required property int index
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: 78
RowLayout {
anchors.fill: parent
spacing: 16
ColumnLayout {
Layout.alignment: Qt.AlignTop
spacing: 0
StepDot { stepIndex: index }
Rectangle {
visible: index < root.steps.length - 1
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 2
Layout.preferredHeight: 36
color: index < root.currentStep ? "#22c55e" : "#28517e"
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 3
Controls.Label {
text: modelData.title
color: index === root.currentStep ? "white" : "#c1d3e8"
font.pixelSize: 15
font.weight: index === root.currentStep ? Font.DemiBold : Font.Medium
}
Controls.Label {
text: modelData.subtitle
color: "#7699c2"
font.pixelSize: 12
}
}
}
}
}
Item { Layout.fillHeight: true }
Rectangle {
Layout.fillWidth: true
implicitHeight: 86
radius: 14
color: "#102f55"
border.width: 1
border.color: "#214b78"
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Controls.Label {
text: "🛡"
font.pixelSize: 23
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Controls.Label {
text: "Configuration locale"
color: "white"
font.pixelSize: 13
font.weight: Font.DemiBold
}
Controls.Label {
Layout.fillWidth: true
text: "Les secrets définitifs ne seront pas enregistrés dans Git."
wrapMode: Text.WordWrap
color: "#91add0"
font.pixelSize: 11
}
}
}
}
Item { Layout.preferredHeight: 20 }
Controls.Label {
text: "Ctrl+Q : quitter l'aperçu"
color: "#6289b6"
font.pixelSize: 11
Layout.alignment: Qt.AlignHCenter
}
}
}
// -----------------------------------------------------
// Partie droite
// -----------------------------------------------------
Rectangle {
id: contentArea
Layout.fillWidth: true
Layout.fillHeight: true
color: "#eef2f7"
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: Math.max(48, contentArea.width * 0.07)
anchors.rightMargin: Math.max(48, contentArea.width * 0.07)
anchors.topMargin: 34
anchors.bottomMargin: 34
spacing: 20
RowLayout {
Layout.fillWidth: true
Rectangle {
implicitWidth: previewLabel.implicitWidth + 28
implicitHeight: 32
radius: 16
color: "#fff7ed"
border.width: 1
border.color: "#fed7aa"
Controls.Label {
id: previewLabel
anchors.centerIn: parent
text: "MODE APERÇU — aucune modification système"
color: "#c2410c"
font.pixelSize: 11
font.weight: Font.Bold
}
}
Item { Layout.fillWidth: true }
SecondaryButton {
text: "Quitter l'aperçu"
onClicked: Qt.quit()
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 24
color: "white"
border.width: 1
border.color: "#dce3ec"
ColumnLayout {
anchors.fill: parent
anchors.margins: Math.max(34, Math.min(64, parent.width * 0.055))
spacing: 24
// Petit indicateur de progression supérieur
RowLayout {
Layout.fillWidth: true
spacing: 10
Repeater {
model: root.steps.length
Rectangle {
required property int index
Layout.fillWidth: true
implicitHeight: 5
radius: 3
color: index <= root.currentStep ? "#2563eb" : "#e2e8f0"
Behavior on color { ColorAnimation { duration: 180 } }
}
}
}
StackLayout {
id: wizard
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: root.currentStep
// ---------------------------------
// 0 - Bienvenue
// ---------------------------------
Item {
ColumnLayout {
anchors.fill: parent
spacing: 20
Item { Layout.fillHeight: true }
Rectangle {
Layout.preferredWidth: 86
Layout.preferredHeight: 86
Layout.alignment: Qt.AlignHCenter
radius: 24
color: "#eff6ff"
border.width: 1
border.color: "#bfdbfe"
Controls.Label {
anchors.centerIn: parent
text: "✦"
color: "#2563eb"
font.pixelSize: 42
font.weight: Font.Bold
}
}
Controls.Label {
Layout.fillWidth: true
text: "Bienvenue sur votre nouveau poste"
color: "#0f172a"
font.pixelSize: 32
font.weight: Font.Bold
horizontalAlignment: Text.AlignHCenter
}
Controls.Label {
Layout.maximumWidth: 720
Layout.alignment: Qt.AlignHCenter
text: "Quelques étapes rapides vont personnaliser vos moyens d'authentification et finaliser la préparation de votre environnement de travail."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 16
lineHeight: 1.25
horizontalAlignment: Text.AlignHCenter
}
Item { Layout.preferredHeight: 12 }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 14
Repeater {
model: [
{ "icon": "🔑", "title": "Mot de passe", "text": "Définir votre accès local de secours" },
{ "icon": "🔐", "title": "YubiKey", "text": "Personnaliser votre PIN FIDO2" }
]
Rectangle {
required property var modelData
Layout.preferredWidth: 290
Layout.preferredHeight: 128
radius: 16
color: "#f8fafc"
border.width: 1
border.color: "#e2e8f0"
RowLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 14
Controls.Label {
text: modelData.icon
font.pixelSize: 28
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Controls.Label {
text: modelData.title
color: "#0f172a"
font.pixelSize: 15
font.weight: Font.DemiBold
}
Controls.Label {
Layout.fillWidth: true
text: modelData.text
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 12
}
}
}
}
}
}
Item { Layout.fillHeight: true }
PrimaryButton {
Layout.alignment: Qt.AlignHCenter
text: "Commencer"
onClicked: root.goNext()
}
}
}
// ---------------------------------
// 1 - Mot de passe
// ---------------------------------
Item {
ColumnLayout {
anchors.fill: parent
spacing: 18
Controls.Label {
text: "Mot de passe local"
color: "#0f172a"
font.pixelSize: 29
font.weight: Font.Bold
}
Controls.Label {
Layout.maximumWidth: 720
text: "Ce mot de passe reste disponible comme solution de secours si votre YubiKey est momentanément indisponible."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 15
lineHeight: 1.2
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 70
radius: 12
color: "#eff6ff"
border.width: 1
border.color: "#bfdbfe"
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Controls.Label {
text: ""
color: "#2563eb"
font.pixelSize: 22
font.weight: Font.Bold
}
Controls.Label {
Layout.fillWidth: true
text: "Aperçu uniquement : les champs ci-dessous ne modifient actuellement aucun mot de passe."
wrapMode: Text.WordWrap
color: "#1e40af"
font.pixelSize: 13
}
}
}
Item { Layout.preferredHeight: 4 }
Controls.Label {
text: "Mot de passe temporaire actuel"
color: "#334155"
font.pixelSize: 13
font.weight: Font.DemiBold
}
SecretField {
id: currentPassword
placeholderText: "Saisissez le mot de passe temporaire"
}
Controls.Label {
text: "Nouveau mot de passe"
color: "#334155"
font.pixelSize: 13
font.weight: Font.DemiBold
}
SecretField {
id: newPassword
placeholderText: "Choisissez votre mot de passe personnel"
}
Controls.Label {
text: "Confirmation"
color: "#334155"
font.pixelSize: 13
font.weight: Font.DemiBold
}
SecretField {
id: confirmPassword
placeholderText: "Confirmez votre nouveau mot de passe"
}
Controls.Label {
visible: confirmPassword.text.length > 0 && newPassword.text !== confirmPassword.text
text: "Les deux nouveaux mots de passe ne correspondent pas."
color: "#dc2626"
font.pixelSize: 12
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.fillWidth: true
SecondaryButton {
text: "Retour"
onClicked: root.goBack()
}
Item { Layout.fillWidth: true }
PrimaryButton {
text: "Continuer"
enabled: newPassword.text.length > 0 && newPassword.text === confirmPassword.text
opacity: enabled ? 1.0 : 0.45
onClicked: root.goNext()
}
}
}
}
// ---------------------------------
// 2 - YubiKey
// ---------------------------------
Item {
ColumnLayout {
anchors.fill: parent
spacing: 18
RowLayout {
Layout.fillWidth: true
spacing: 20
Rectangle {
Layout.preferredWidth: 80
Layout.preferredHeight: 80
radius: 22
color: "#f0fdf4"
border.width: 1
border.color: "#bbf7d0"
Controls.Label {
anchors.centerIn: parent
text: "🔐"
font.pixelSize: 34
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Controls.Label {
text: "PIN de votre YubiKey"
color: "#0f172a"
font.pixelSize: 29
font.weight: Font.Bold
}
Controls.Label {
Layout.fillWidth: true
text: "Remplacez le PIN temporaire de préparation par un PIN personnel que vous serez seul à connaître."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 15
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 78
radius: 12
color: "#fffbeb"
border.width: 1
border.color: "#fde68a"
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Controls.Label {
text: "⌨"
color: "#b45309"
font.pixelSize: 23
}
Controls.Label {
Layout.fillWidth: true
text: "Vérifiez Verr Num avant d'utiliser le pavé numérique. En cas de doute, utilisez les chiffres situés au-dessus des lettres."
wrapMode: Text.WordWrap
color: "#92400e"
font.pixelSize: 13
}
}
}
Controls.Label {
text: "PIN temporaire actuel"
color: "#334155"
font.pixelSize: 13
font.weight: Font.DemiBold
}
SecretField {
id: currentPin
placeholderText: "PIN temporaire"
inputMethodHints: Qt.ImhDigitsOnly
}
Controls.Label {
text: "Nouveau PIN"
color: "#334155"
font.pixelSize: 13
font.weight: Font.DemiBold
}
SecretField {
id: newPin
placeholderText: "Nouveau PIN personnel"
inputMethodHints: Qt.ImhDigitsOnly
}
Controls.Label {
text: "Confirmation du nouveau PIN"
color: "#334155"
font.pixelSize: 13
font.weight: Font.DemiBold
}
SecretField {
id: confirmPin
placeholderText: "Confirmez le nouveau PIN"
inputMethodHints: Qt.ImhDigitsOnly
}
Controls.Label {
visible: confirmPin.text.length > 0 && newPin.text !== confirmPin.text
text: "Les deux nouveaux PIN ne correspondent pas."
color: "#dc2626"
font.pixelSize: 12
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.fillWidth: true
SecondaryButton {
text: "Retour"
onClicked: root.goBack()
}
Item { Layout.fillWidth: true }
PrimaryButton {
text: "Finaliser"
enabled: newPin.text.length >= 4 && newPin.text === confirmPin.text
opacity: enabled ? 1.0 : 0.45
onClicked: root.goNext()
}
}
}
}
// ---------------------------------
// 3 - Terminé
// ---------------------------------
Item {
ColumnLayout {
anchors.fill: parent
spacing: 18
Item { Layout.fillHeight: true }
Rectangle {
Layout.preferredWidth: 96
Layout.preferredHeight: 96
Layout.alignment: Qt.AlignHCenter
radius: 48
color: "#dcfce7"
border.width: 1
border.color: "#86efac"
Controls.Label {
anchors.centerIn: parent
text: "✓"
color: "#16a34a"
font.pixelSize: 50
font.weight: Font.Bold
}
}
Controls.Label {
Layout.fillWidth: true
text: "Votre poste est prêt"
color: "#0f172a"
font.pixelSize: 32
font.weight: Font.Bold
horizontalAlignment: Text.AlignHCenter
}
Controls.Label {
Layout.maximumWidth: 680
Layout.alignment: Qt.AlignHCenter
text: "Dans la version finale, cet écran confirmera que votre mot de passe local et le PIN de votre YubiKey ont été personnalisés avec succès."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 15
lineHeight: 1.25
horizontalAlignment: Text.AlignHCenter
}
Item { Layout.preferredHeight: 12 }
Rectangle {
Layout.preferredWidth: 520
Layout.preferredHeight: 112
Layout.alignment: Qt.AlignHCenter
radius: 16
color: "#f8fafc"
border.width: 1
border.color: "#e2e8f0"
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 10
RowLayout {
Controls.Label { text: "✓"; color: "#16a34a"; font.pixelSize: 17; font.weight: Font.Bold }
Controls.Label { text: "Mot de passe personnel configuré"; color: "#334155"; font.pixelSize: 14 }
}
RowLayout {
Controls.Label { text: "✓"; color: "#16a34a"; font.pixelSize: 17; font.weight: Font.Bold }
Controls.Label { text: "PIN personnel YubiKey configuré"; color: "#334155"; font.pixelSize: 14 }
}
}
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 12
SecondaryButton {
text: "Revoir"
onClicked: root.currentStep = 0
}
PrimaryButton {
text: "Fermer l'aperçu"
onClicked: Qt.quit()
}
}
}
}
}
}
}
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
#include <QGuiApplication>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQuickStyle>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QGuiApplication::setOrganizationName(QStringLiteral("Raspot"));
QGuiApplication::setOrganizationDomain(QStringLiteral("raspot.in"));
QGuiApplication::setApplicationName(QStringLiteral("NixOS First Login"));
QGuiApplication::setDesktopFileName(QStringLiteral("org.raspot.nixosfirstlogin"));
QIcon::setThemeName(QStringLiteral("breeze"));
// L'UI est volontairement dessinée par l'application afin d'avoir
// un rendu stable et homogène, tout en restant une application Qt/QML.
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(QStringLiteral("Basic"));
}
QQmlApplicationEngine engine;
engine.loadFromModule(QStringLiteral("org.raspot.nixosfirstlogin"),
QStringLiteral("Main"));
if (engine.rootObjects().isEmpty()) {
return 1;
}
return app.exec();
}