commit 8c8c61f4de673a375587ad2002f7409828ad1003 Author: Matteo Benedetto Date: Sun Aug 16 16:34:00 2026 +0200 feat: aggiungi tema SDDM costruttivista diff --git a/Combo.qml b/Combo.qml new file mode 100644 index 0000000..c373ec7 --- /dev/null +++ b/Combo.qml @@ -0,0 +1,88 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 + +// Selettore a tendina etichettato in stile costruttivista. +Column { + id: comboWrap + property string label: "" + property alias model: combo.model + property alias textRole: combo.textRole + property alias currentIndex: combo.currentIndex + property alias currentText: combo.currentText + signal activated(int index) + width: parent.width + spacing: 4 + + Text { + text: comboWrap.label + color: "#f0dfba" + font.pixelSize: 12 + font.bold: true + font.letterSpacing: 2 + } + + QQC2.ComboBox { + id: combo + width: comboWrap.width + height: 40 + font.pixelSize: 16 + font.bold: true + onActivated: comboWrap.activated(index) + + background: Rectangle { + color: "#cc101012" + border.color: combo.hovered ? "#d83a28" : "#66f0dfba" + border.width: 1 + } + + contentItem: Text { + leftPadding: 10 + rightPadding: 10 + text: combo.displayText + color: "#f0dfba" + font: combo.font + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Item { width: 0; height: 0 } + + delegate: QQC2.ItemDelegate { + width: combo.width + height: 34 + highlighted: combo.highlightedIndex === index + + contentItem: Text { + text: combo.textRole ? model[combo.textRole] : modelData + color: combo.highlightedIndex === index ? "#f6f1e5" : "#f0dfba" + font: combo.font + verticalAlignment: Text.AlignVCenter + leftPadding: 10 + } + + background: Rectangle { + color: combo.highlightedIndex === index ? "#d83a28" : "#e6101012" + } + } + + popup: QQC2.Popup { + y: combo.height + 2 + width: combo.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: combo.popup.visible ? combo.delegateModel : null + currentIndex: combo.highlightedIndex + } + + background: Rectangle { + color: "#e6101012" + border.color: "#66f0dfba" + border.width: 1 + } + } + } +} diff --git a/Field.qml b/Field.qml new file mode 100644 index 0000000..7e01603 --- /dev/null +++ b/Field.qml @@ -0,0 +1,47 @@ +import QtQuick 2.15 + +// Campo di input etichettato in stile costruttivista. +Column { + id: field + property string label: "" + property alias text: input.text + property alias input: input + property bool password: false + property bool focusOnStart: false + signal accepted() + width: parent.width + spacing: 4 + + Text { + text: field.label + color: "#f0dfba" + font.pixelSize: 12 + font.bold: true + font.letterSpacing: 2 + } + + Rectangle { + width: field.width + height: 40 + color: "#cc101012" + border.color: input.activeFocus ? "#d83a28" : "#66f0dfba" + border.width: input.activeFocus ? 2 : 1 + + Behavior on border.color { ColorAnimation { duration: 150 } } + + TextInput { + id: input + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + verticalAlignment: Text.AlignVCenter + color: "#f0dfba" + font.pixelSize: 16 + font.bold: true + echoMode: field.password ? TextInput.Password : TextInput.Normal + selectByMouse: true + focus: field.focusOnStart + onAccepted: field.accepted() + } + } +} diff --git a/Main.qml b/Main.qml new file mode 100644 index 0000000..d699488 --- /dev/null +++ b/Main.qml @@ -0,0 +1,391 @@ +/* + Costruttivista — tema SDDM + Schermata di accesso in stile costruttivista sovietico per SDDM. + Autosufficiente: solo QtQuick 2.15 e QtQuick.Controls 2.15, nessuna dipendenza esterna. +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +Rectangle { + id: root + color: "#101012" + width: 1920 + height: 1080 + + readonly property color red: "#d83a28" + readonly property color cream: "#f0dfba" + readonly property color dark: "#181719" + readonly property color panelBg: "#d9151517" + readonly property color paper: "#f6f1e5" + + // Utente selezionato dalla lista (quando il modello espone utenti). + property string selectedUser: userModel.lastUser || "" + + // Data in italiano senza dipendere dalla locale del greeter. + function italianDate(d) { + var days = ["DOMENICA", "LUNEDÌ", "MARTEDÌ", "MERCOLEDÌ", "GIOVEDÌ", "VENERDÌ", "SABATO"] + var months = ["GENNAIO", "FEBBRAIO", "MARZO", "APRILE", "MAGGIO", "GIUGNO", "LUGLIO", "AGOSTO", "SETTEMBRE", "OTTOBRE", "NOVEMBRE", "DICEMBRE"] + return days[d.getDay()] + " " + d.getDate() + " " + months[d.getMonth()] + " " + d.getFullYear() + } + + function tryLogin() { + var username = userModel.count > 0 ? root.selectedUser : userField.text + if (username.length === 0) { + errorText.text = "INSERISCI L'UTENTE" + shake.start() + return + } + errorText.text = "" + sddm.login(username, passField.text, sessionCombo.currentIndex) + } + + Item { + id: ui + anchors.fill: parent + opacity: 0 + + // ---------- sfondo ---------- + Image { + id: background + anchors.fill: parent + source: "assets/sfondo_costruttivista_fhd.png" + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + + Rectangle { + anchors.fill: parent + color: "#12000000" + opacity: overlayFade.value + } + + // ---------- accenti geometrici ---------- + Item { + id: accents + anchors.fill: parent + + Rectangle { + id: redBar + x: parent.width * 0.07 + y: parent.height * 0.15 + width: Math.max(5, parent.width * 0.008) + height: parent.height * 0.27 + color: root.red + opacity: 0.9 + transform: Translate { id: redShift; y: -root.height * 0.10 } + } + + Rectangle { + id: creamLine + x: parent.width * 0.07 + y: parent.height * 0.44 + width: parent.width * 0.20 + height: Math.max(2, parent.height * 0.004) + color: root.cream + opacity: 0.85 + transform: Translate { id: lineShift; x: -root.width * 0.12 } + } + + Rectangle { + id: darkBar + x: parent.width * 0.82 + y: parent.height * 0.60 + width: Math.max(4, parent.width * 0.005) + height: parent.height * 0.20 + color: root.dark + opacity: 0.75 + transform: Translate { id: darkShift; y: root.height * 0.09 } + } + } + + // ---------- orologio ---------- + Rectangle { + id: clockBackdrop + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Math.max(36, root.height * 0.055) + width: clockBlock.width + Math.max(36, root.width * 0.02) + height: clockBlock.height + Math.max(22, root.height * 0.022) + color: "#66101012" + border.color: "#33f0dfba" + border.width: 1 + + Column { + id: clockBlock + anchors.centerIn: parent + spacing: Math.max(2, root.height * 0.004) + + Text { + id: clockText + anchors.horizontalCenter: parent.horizontalCenter + text: Qt.formatTime(new Date(), "HH:mm") + color: root.cream + font.pixelSize: Math.max(44, Math.min(root.width * 0.05, 88)) + font.bold: true + font.letterSpacing: 4 + } + + Text { + id: dateText + anchors.horizontalCenter: parent.horizontalCenter + text: root.italianDate(new Date()) + color: "#ef6a55" + font.pixelSize: Math.max(13, Math.min(root.width * 0.014, 22)) + font.bold: true + font.letterSpacing: 2 + } + + Timer { + interval: 1000 + repeat: true + running: true + onTriggered: { + clockText.text = Qt.formatTime(new Date(), "HH:mm") + dateText.text = root.italianDate(new Date()) + } + } + } + } + + // ---------- pannello di accesso ---------- + Item { + id: panelHost + anchors.centerIn: parent + width: panel.width + height: panel.height + transform: Translate { id: panelShift; x: 0 } + + Rectangle { + id: panel + width: Math.min(root.width * 0.40, 500) + height: panelCol.implicitHeight + Math.max(52, root.height * 0.056) + color: root.panelBg + border.color: "#66f0dfba" + border.width: Math.max(1, root.height * 0.002) + + Rectangle { + width: Math.max(8, panel.width * 0.02) + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + color: root.red + } + + Column { + id: panelCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Math.max(26, root.height * 0.028) + anchors.rightMargin: Math.max(26, root.height * 0.028) + anchors.topMargin: Math.max(26, root.height * 0.028) + spacing: Math.max(14, root.height * 0.016) + + // intestazione: marca + titolo + RowLayout { + width: parent.width + spacing: Math.max(16, root.width * 0.011) + + Mark { + Layout.preferredWidth: Math.max(58, Math.min(panel.width * 0.15, 96)) + Layout.preferredHeight: Layout.preferredWidth + Layout.alignment: Qt.AlignVCenter + } + + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 2 + + Text { + text: "ACCESSO" + color: root.cream + font.pixelSize: Math.max(20, Math.min(root.width * 0.024, 34)) + font.bold: true + font.letterSpacing: 3 + } + + Text { + text: "INSERISCI LE CREDENZIALI" + color: "#e85a48" + font.pixelSize: Math.max(10, Math.min(root.width * 0.011, 15)) + font.bold: true + font.letterSpacing: 1 + } + } + } + + // lista utenti con avatar (solo se il modello espone utenti) + Item { + width: parent.width + height: userListRow.height + + UserList { + id: userListRow + anchors.horizontalCenter: parent.horizontalCenter + onUserSelected: root.selectedUser = name + } + } + + // campo utente (solo se non ci sono utenti da selezionare) + Field { + id: userField + visible: userModel.count === 0 + label: "UTENTE" + text: userModel.lastUser || "" + } + + // campo password + Field { + id: passField + label: "PASSWORD" + password: true + onAccepted: root.tryLogin() + } + + // selettore sessione + Combo { + id: sessionCombo + label: "SESSIONE" + model: sessionModel + textRole: "name" + currentIndex: sessionModel.lastIndex >= 0 ? sessionModel.lastIndex : 0 + } + + // messaggio di errore + Text { + id: errorText + width: parent.width + text: "" + color: root.red + font.bold: true + font.letterSpacing: 1 + font.pixelSize: Math.max(11, Math.min(root.width * 0.012, 16)) + visible: text.length > 0 + wrapMode: Text.WordWrap + } + + // pulsante di accesso + Rectangle { + id: loginButton + width: parent.width + height: Math.max(42, root.height * 0.05) + color: root.red + border.color: root.cream + border.width: 1 + + Text { + anchors.centerIn: parent + text: "ENTRA" + color: root.paper + font.bold: true + font.letterSpacing: 3 + font.pixelSize: Math.max(14, Math.min(root.width * 0.015, 20)) + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: root.tryLogin() + onPressed: loginButton.color = Qt.darker(root.red, 1.3) + onReleased: loginButton.color = root.red + } + } + + // separatore + Rectangle { + width: parent.width + height: 2 + color: "#66f0dfba" + } + + // pulsanti di sistema + RowLayout { + width: parent.width + spacing: Math.max(8, root.width * 0.006) + + PowerButton { + Layout.fillWidth: true + label: "SOSPENDI" + enabled: sddm.canSuspend + onClicked: sddm.suspend() + } + PowerButton { + Layout.fillWidth: true + label: "RIAVVIA" + enabled: sddm.canReboot + onClicked: sddm.reboot() + } + PowerButton { + Layout.fillWidth: true + label: "SPEGNI" + enabled: sddm.canPowerOff + onClicked: sddm.powerOff() + } + } + } + } + } + } + + // ---------- animazioni ---------- + QtObject { id: overlayFade; property real value: 0.0 } + + ParallelAnimation { + id: intro + NumberAnimation { target: ui; property: "opacity"; from: 0; to: 1; duration: 700; easing.type: Easing.OutCubic } + NumberAnimation { target: overlayFade; property: "value"; from: 0; to: 1; duration: 900 } + NumberAnimation { target: redShift; property: "y"; to: 0; duration: 850; easing.type: Easing.OutCubic } + NumberAnimation { target: lineShift; property: "x"; to: 0; duration: 950; easing.type: Easing.OutCubic } + NumberAnimation { target: darkShift; property: "y"; to: 0; duration: 1050; easing.type: Easing.OutCubic } + } + + SequentialAnimation { + id: shake + NumberAnimation { target: panelShift; property: "x"; to: -14; duration: 45 } + NumberAnimation { target: panelShift; property: "x"; to: 14; duration: 45 } + NumberAnimation { target: panelShift; property: "x"; to: -10; duration: 45 } + NumberAnimation { target: panelShift; property: "x"; to: 10; duration: 45 } + NumberAnimation { target: panelShift; property: "x"; to: 0; duration: 45 } + } + + NumberAnimation { + id: leave + target: root + property: "opacity" + to: 0 + duration: 500 + easing.type: Easing.InOutQuad + } + + Component.onCompleted: { + intro.start() + focusTimer.start() + } + + Timer { + id: focusTimer + interval: 200 + onTriggered: { + if (userModel.count > 0) { + passField.input.forceActiveFocus() + } else { + userField.input.forceActiveFocus() + } + } + } + + Connections { + target: sddm + function onLoginFailed() { + errorText.text = "ACCESSO NEGATO" + passField.text = "" + shake.start() + } + function onLoginSucceeded() { + leave.start() + } + } +} diff --git a/Mark.qml b/Mark.qml new file mode 100644 index 0000000..8429297 --- /dev/null +++ b/Mark.qml @@ -0,0 +1,55 @@ +import QtQuick 2.15 + +// Marca geometrica rotante — stessa identità visiva dello splash costruttivista. +Item { + id: mark + + Rectangle { + anchors.fill: parent + anchors.margins: mark.width * 0.09 + radius: width / 2 + color: "transparent" + border.color: "#f0dfba" + border.width: Math.max(4, mark.width * 0.075) + opacity: 0.92 + } + + Item { + id: rotor + anchors.fill: parent + + Rectangle { + width: parent.width * 0.48 + height: Math.max(10, parent.height * 0.16) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: "#d83a28" + } + + Rectangle { + width: Math.max(9, parent.width * 0.14) + height: parent.height * 0.42 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + color: "#f0dfba" + } + + RotationAnimator on rotation { + from: 0 + to: 360 + duration: 1200 + loops: Animation.Infinite + running: true + } + } + + Rectangle { + width: parent.width * 0.25 + height: width + anchors.centerIn: parent + color: "#171719" + border.color: "#d83a28" + border.width: Math.max(3, width * 0.15) + rotation: 45 + } +} diff --git a/PowerButton.qml b/PowerButton.qml new file mode 100644 index 0000000..d0ede2d --- /dev/null +++ b/PowerButton.qml @@ -0,0 +1,32 @@ +import QtQuick 2.15 + +// Pulsante di sistema (sospendi / riavvia / spegni). +Rectangle { + id: pbtn + property string label: "" + property bool enabled: true + property bool hovered: false + signal clicked() + height: 34 + color: !pbtn.enabled ? "#77101012" : (pbtn.hovered ? "#33d83a28" : "#cc101012") + border.color: pbtn.enabled ? "#99f0dfba" : "#55f0dfba" + border.width: 1 + + Text { + anchors.centerIn: parent + text: pbtn.label + color: pbtn.enabled ? "#f0dfba" : "#aaf0dfba" + font.pixelSize: 13 + font.bold: true + font.letterSpacing: 1 + } + + MouseArea { + anchors.fill: parent + enabled: pbtn.enabled + hoverEnabled: true + onClicked: pbtn.clicked() + onEntered: pbtn.hovered = true + onExited: pbtn.hovered = false + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3327f44 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Costruttivista — tema SDDM + +Schermata di accesso in stile costruttivista sovietico per SDDM, gemella del tema splash KDE `org.enne2.costruttivista.splash`. Stessa identità visiva: palette rosso/crema/nero, accenti geometrici, marca rotante e pannello con bordo crema. + +Autosufficiente: solo QtQuick 2.15 e QtQuick.Controls 2.15, nessuna dipendenza esterna (niente Kirigami, Plasma components o rete). L'immagine di sfondo è inclusa nel pacchetto. + +## Funzionalità + +- Lista utenti con avatar (dal modello `userModel` di SDDM, con fallback geometrico se manca l'avatar) +- Campo password con focus automatico +- Selettore sessione +- Orologio e data in italiano su pannello scuro +- Pulsanti di sistema: sospendi / riavvia / spegni +- Animazione di ingresso, scossa su errore, dissolvenza al successo + +## Struttura + +``` +org.enne2.costruttivista.sddm/ +├── theme.conf # metadati del tema +├── metadata.desktop # metadati (compatibilità) +├── Main.qml # schermata principale +├── UserList.qml # lista utenti con avatar +├── Field.qml # campo di input etichettato +├── Combo.qml # selettore a tendina etichettato +├── PowerButton.qml # pulsante di sistema +├── Mark.qml # marca geometrica rotante +├── assets/ +│ └── sfondo_costruttivista_fhd.png +└── dist/ + └── org.enne2.costruttivista.sddm.tar.gz +``` + +## Installazione + +Per l'utente corrente (consigliato per la prova): + +```sh +mkdir -p ~/.local/share/sddm/themes +cp -r org.enne2.costruttivista.sddm ~/.local/share/sddm/themes/ +``` + +Per tutti gli utenti: + +```sh +sudo cp -r org.enne2.costruttivista.sddm /usr/share/sddm/themes/ +``` + +## Attivazione + +Impostare il tema in `/etc/sddm.conf` (o in un drop-in in `/etc/sddm.conf.d/`): + +```ini +[Theme] +Current=org.enne2.costruttivista.sddm +``` + +In alternativa, dal KCM **Impostazioni di sistema → Sessione → Schermata di accesso (SDDM)** selezionare **Costruttivista**. + +Il pacchetto non viene installato né attivato automaticamente. diff --git a/UserList.qml b/UserList.qml new file mode 100644 index 0000000..9803726 --- /dev/null +++ b/UserList.qml @@ -0,0 +1,97 @@ +import QtQuick 2.15 + +// Lista orizzontale di utenti con avatar, in stile costruttivista. +// Usa il modello `userModel` esposto da SDDM (ruoli: name, realName, icon, isCurrent). +Row { + id: userList + property int currentIndex: userModel.lastIndex >= 0 ? userModel.lastIndex : 0 + signal userSelected(string name) + spacing: Math.max(8, parent.width * 0.02) + + Repeater { + id: repeater + model: userModel + + delegate: Rectangle { + id: card + required property int index + required property string name + required property string icon + + width: 80 + height: 100 + color: userList.currentIndex === index ? "#33d83a28" : "#cc101012" + border.color: userList.currentIndex === index ? "#d83a28" : "#66f0dfba" + border.width: userList.currentIndex === index ? 2 : 1 + + // cornice circolare con avatar + Rectangle { + id: avatarFrame + width: 54 + height: 54 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 7 + radius: width / 2 + color: "#181719" + border.color: "#f0dfba" + border.width: 2 + clip: true + + Image { + id: avatarImage + anchors.fill: parent + anchors.margins: 2 + source: card.icon + fillMode: Image.PreserveAspectCrop + sourceSize: Qt.size(108, 108) + asynchronous: true + } + + // fallback geometrico se l'avatar non è disponibile + Text { + anchors.centerIn: parent + text: card.name.length > 0 ? card.name.charAt(0).toUpperCase() : "?" + color: "#f0dfba" + font.pixelSize: 22 + font.bold: true + visible: avatarImage.status !== Image.Ready + } + } + + // nome utente + Text { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 6 + width: card.width - 6 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + text: card.name.toUpperCase() + color: userList.currentIndex === index ? "#f6f1e5" : "#f0dfba" + font.pixelSize: 11 + font.bold: true + font.letterSpacing: 1 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: userList.select(index) + } + } + } + + // Seleziona l'utente all'indice i (click o tastiera). + function select(i) { + if (i < 0 || i >= userModel.count) return + currentIndex = i + var item = repeater.itemAt(i) + if (item) { + userSelected(item.name) + } + } + + Keys.onLeftPressed: select(currentIndex - 1) + Keys.onRightPressed: select(currentIndex + 1) +} diff --git a/assets/sfondo_costruttivista_fhd.png b/assets/sfondo_costruttivista_fhd.png new file mode 100644 index 0000000..e3bbb98 Binary files /dev/null and b/assets/sfondo_costruttivista_fhd.png differ diff --git a/dist/org.enne2.costruttivista.sddm.tar.gz b/dist/org.enne2.costruttivista.sddm.tar.gz new file mode 100644 index 0000000..401ffe6 Binary files /dev/null and b/dist/org.enne2.costruttivista.sddm.tar.gz differ diff --git a/metadata.desktop b/metadata.desktop new file mode 100644 index 0000000..8dd9418 --- /dev/null +++ b/metadata.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Type=X-SDDM-Theme +Name=Costruttivista +Comment=Schermata di accesso costruttivista per SDDM diff --git a/preview.png b/preview.png new file mode 100644 index 0000000..c1c22c8 Binary files /dev/null and b/preview.png differ diff --git a/theme.conf b/theme.conf new file mode 100644 index 0000000..8aa0507 --- /dev/null +++ b/theme.conf @@ -0,0 +1,3 @@ +[General] +Name=Costruttivista +Comment=Schermata di accesso costruttivista per SDDM