From e6d06f3fa3d8671e62d87b78d2e393f7cbef07e8 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Thu, 19 Mar 2026 09:57:44 +0000 Subject: [PATCH] =?UTF-8?q?refactor(qt6):=20component=20extraction=20batch?= =?UTF-8?q?=203=20=E2=80=94=20WorkflowEditor,=20editors,=20remaining=20spl?= =?UTF-8?q?its?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorkflowEditor (1432→631): CWorkflowCanvas, CNodePalette, CNodePropertiesPanel, CConnectionLayer, CWorkflowToolbar + CssClassManager, DatabaseManager, DropdownConfigManager, MediaServicePanel, PageRoutesManager, UserManagement split into extracted components + Theme editor: ThemeLivePreview, ThemeSpacingRadius, ThemeTypography + SMTP editor: CSmtpTemplateEditor, CSmtpTemplateList, CSmtpTestEmailForm Net: -2,549 lines from view files into reusable components. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontends/qt6/CssClassManager.qml | 665 ++---------- frontends/qt6/DatabaseManager.qml | 506 ++------- frontends/qt6/DropdownConfigManager.qml | 706 ++----------- frontends/qt6/MediaServicePanel.qml | 971 +----------------- frontends/qt6/PageRoutesManager.qml | 530 +--------- frontends/qt6/UserManagement.qml | 720 ++----------- .../MetaBuilder/CSmtpTemplateEditor.qml | 119 +++ .../qmllib/MetaBuilder/CSmtpTemplateList.qml | 42 + .../qmllib/MetaBuilder/CSmtpTestEmailForm.qml | 98 ++ .../qmllib/MetaBuilder/ThemeLivePreview.qml | 256 +++++ .../qmllib/MetaBuilder/ThemeSpacingRadius.qml | 182 ++++ .../qmllib/MetaBuilder/ThemeTypography.qml | 94 ++ 12 files changed, 1170 insertions(+), 3719 deletions(-) create mode 100644 frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateEditor.qml create mode 100644 frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateList.qml create mode 100644 frontends/qt6/qmllib/MetaBuilder/CSmtpTestEmailForm.qml create mode 100644 frontends/qt6/qmllib/MetaBuilder/ThemeLivePreview.qml create mode 100644 frontends/qt6/qmllib/MetaBuilder/ThemeSpacingRadius.qml create mode 100644 frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml diff --git a/frontends/qt6/CssClassManager.qml b/frontends/qt6/CssClassManager.qml index 8c7f04537..8a01e7eba 100644 --- a/frontends/qt6/CssClassManager.qml +++ b/frontends/qt6/CssClassManager.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QmlComponents 1.0 import "qmllib/dbal" +import "qmllib/MetaBuilder" Rectangle { id: root @@ -12,87 +13,21 @@ Rectangle { DBALProvider { id: dbal } property bool useLiveData: dbal.connected - // ── Mock data ────────────────────────────────────────────────────── + // ── State ────────────────────────────────────────────────────────── property var cssClasses: [ - { - name: "card-primary", - usageCount: 14, - properties: [ - { prop: "background-color", value: "#1976d2" }, - { prop: "border-radius", value: "12px" }, - { prop: "padding", value: "16px" }, - { prop: "color", value: "#ffffff" } - ] - }, - { - name: "card-dark", - usageCount: 9, - properties: [ - { prop: "background-color", value: "#1e1e2e" }, - { prop: "border-radius", value: "8px" }, - { prop: "color", value: "#cdd6f4" }, - { prop: "padding", value: "20px" } - ] - }, - { - name: "text-accent", - usageCount: 22, - properties: [ - { prop: "color", value: "#f59e0b" }, - { prop: "font-size", value: "14px" }, - { prop: "opacity", value: "0.95" } - ] - }, - { - name: "btn-glow", - usageCount: 6, - properties: [ - { prop: "background-color", value: "#7c3aed" }, - { prop: "border-radius", value: "24px" }, - { prop: "box-shadow", value: "0 0 12px rgba(124,58,237,0.5)" }, - { prop: "color", value: "#ffffff" }, - { prop: "padding", value: "10px 24px" } - ] - }, - { - name: "surface-elevated", - usageCount: 17, - properties: [ - { prop: "background-color", value: "#2a2a3c" }, - { prop: "border-radius", value: "6px" }, - { prop: "box-shadow", value: "0 4px 12px rgba(0,0,0,0.3)" }, - { prop: "padding", value: "12px" } - ] - }, - { - name: "badge-live", - usageCount: 3, - properties: [ - { prop: "background-color", value: "#ef4444" }, - { prop: "border-radius", value: "999px" }, - { prop: "color", value: "#ffffff" }, - { prop: "font-size", value: "11px" }, - { prop: "padding", value: "2px 8px" } - ] - }, - { - name: "panel-glass", - usageCount: 8, - properties: [ - { prop: "background-color", value: "rgba(255,255,255,0.06)" }, - { prop: "border-radius", value: "16px" }, - { prop: "opacity", value: "0.9" }, - { prop: "padding", value: "24px" } - ] - } + { name: "card-primary", usageCount: 14, properties: [{ prop: "background-color", value: "#1976d2" }, { prop: "border-radius", value: "12px" }, { prop: "padding", value: "16px" }, { prop: "color", value: "#ffffff" }] }, + { name: "card-dark", usageCount: 9, properties: [{ prop: "background-color", value: "#1e1e2e" }, { prop: "border-radius", value: "8px" }, { prop: "color", value: "#cdd6f4" }, { prop: "padding", value: "20px" }] }, + { name: "text-accent", usageCount: 22, properties: [{ prop: "color", value: "#f59e0b" }, { prop: "font-size", value: "14px" }, { prop: "opacity", value: "0.95" }] }, + { name: "btn-glow", usageCount: 6, properties: [{ prop: "background-color", value: "#7c3aed" }, { prop: "border-radius", value: "24px" }, { prop: "box-shadow", value: "0 0 12px rgba(124,58,237,0.5)" }, { prop: "color", value: "#ffffff" }, { prop: "padding", value: "10px 24px" }] }, + { name: "surface-elevated", usageCount: 17, properties: [{ prop: "background-color", value: "#2a2a3c" }, { prop: "border-radius", value: "6px" }, { prop: "box-shadow", value: "0 4px 12px rgba(0,0,0,0.3)" }, { prop: "padding", value: "12px" }] }, + { name: "badge-live", usageCount: 3, properties: [{ prop: "background-color", value: "#ef4444" }, { prop: "border-radius", value: "999px" }, { prop: "color", value: "#ffffff" }, { prop: "font-size", value: "11px" }, { prop: "padding", value: "2px 8px" }] }, + { name: "panel-glass", usageCount: 8, properties: [{ prop: "background-color", value: "rgba(255,255,255,0.06)" }, { prop: "border-radius", value: "16px" }, { prop: "opacity", value: "0.9" }, { prop: "padding", value: "24px" }] } ] property int selectedClassIndex: 0 property bool showAddClassDialog: false property bool showDeleteConfirm: false property string newClassName: "" - property bool showSuggestions: false - property int editingPropertyIndex: -1 property var propertySuggestions: [ "color", "background-color", "border-radius", "padding", @@ -101,117 +36,53 @@ Rectangle { ] // ── Helpers ───────────────────────────────────────────────────────── - function selectedClass() { - if (selectedClassIndex >= 0 && selectedClassIndex < cssClasses.length) - return cssClasses[selectedClassIndex] + if (selectedClassIndex >= 0 && selectedClassIndex < cssClasses.length) return cssClasses[selectedClassIndex] return null } - function updateClasses(arr) { - cssClasses = arr - if (useLiveData) saveCssClass(selectedClassIndex) - } + function updateClasses(arr) { cssClasses = arr; if (useLiveData) saveCssClass(selectedClassIndex) } function addPropertyToSelected() { - var cls = cssClasses.slice() - var entry = Object.assign({}, cls[selectedClassIndex]) - var props = entry.properties.slice() - props.push({ prop: "property", value: "value" }) - entry.properties = props - cls[selectedClassIndex] = entry - updateClasses(cls) + var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex]) + var props = entry.properties.slice(); props.push({ prop: "property", value: "value" }) + entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls) } function removePropertyFromSelected(propIndex) { - var cls = cssClasses.slice() - var entry = Object.assign({}, cls[selectedClassIndex]) - var props = entry.properties.slice() - props.splice(propIndex, 1) - entry.properties = props - cls[selectedClassIndex] = entry - updateClasses(cls) + var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex]) + var props = entry.properties.slice(); props.splice(propIndex, 1) + entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls) } function setPropertyName(propIndex, name) { - var cls = cssClasses.slice() - var entry = Object.assign({}, cls[selectedClassIndex]) - var props = entry.properties.slice() - props[propIndex] = { prop: name, value: props[propIndex].value } - entry.properties = props - cls[selectedClassIndex] = entry - updateClasses(cls) + var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex]) + var props = entry.properties.slice(); props[propIndex] = { prop: name, value: props[propIndex].value } + entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls) } function setPropertyValue(propIndex, val) { - var cls = cssClasses.slice() - var entry = Object.assign({}, cls[selectedClassIndex]) - var props = entry.properties.slice() - props[propIndex] = { prop: props[propIndex].prop, value: val } - entry.properties = props - cls[selectedClassIndex] = entry - updateClasses(cls) + var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex]) + var props = entry.properties.slice(); props[propIndex] = { prop: props[propIndex].prop, value: val } + entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls) } function setClassName(name) { - var cls = cssClasses.slice() - var entry = Object.assign({}, cls[selectedClassIndex]) - entry.name = name - cls[selectedClassIndex] = entry - updateClasses(cls) + var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex]) + entry.name = name; cls[selectedClassIndex] = entry; updateClasses(cls) } function addClass(name) { var newClass = { name: name, usageCount: 0, properties: [{ prop: "color", value: "#ffffff" }] } - if (useLiveData) { - dbal.execute("core/css-classes/create", { data: newClass }, function(r, e) { if (!e) loadCssClasses() }) - } - var cls = cssClasses.slice() - cls.push(newClass) - cssClasses = cls - selectedClassIndex = cls.length - 1 + if (useLiveData) dbal.execute("core/css-classes/create", { data: newClass }, function(r, e) { if (!e) loadCssClasses() }) + var cls = cssClasses.slice(); cls.push(newClass); cssClasses = cls; selectedClassIndex = cls.length - 1 } function deleteSelectedClass() { if (cssClasses.length <= 1) return - if (useLiveData && cssClasses[selectedClassIndex].id) { - dbal.execute("core/css-classes/delete", { id: cssClasses[selectedClassIndex].id }, function(r, e) { if (!e) loadCssClasses() }) - } - var cls = cssClasses.slice() - cls.splice(selectedClassIndex, 1) - cssClasses = cls - if (selectedClassIndex >= cls.length) - selectedClassIndex = cls.length - 1 - } - - function resolvePreviewColor(properties, name, fallback) { - for (var i = 0; i < properties.length; i++) { - if (properties[i].prop === name) - return properties[i].value - } - return fallback - } - - function resolvePreviewRadius(properties) { - for (var i = 0; i < properties.length; i++) { - if (properties[i].prop === "border-radius") - return parseInt(properties[i].value) || 0 - } - return 0 - } - - function resolvePreviewOpacity(properties) { - for (var i = 0; i < properties.length; i++) { - if (properties[i].prop === "opacity") - return parseFloat(properties[i].value) || 1.0 - } - return 1.0 - } - - function swatchColor(properties) { - var bg = resolvePreviewColor(properties, "background-color", "") - if (bg) return bg - return resolvePreviewColor(properties, "color", Theme.surface) + if (useLiveData && cssClasses[selectedClassIndex].id) dbal.execute("core/css-classes/delete", { id: cssClasses[selectedClassIndex].id }, function(r, e) { if (!e) loadCssClasses() }) + var cls = cssClasses.slice(); cls.splice(selectedClassIndex, 1); cssClasses = cls + if (selectedClassIndex >= cls.length) selectedClassIndex = cls.length - 1 } // ── DBAL Integration ───────────────────────────────────────────── @@ -240,486 +111,82 @@ Rectangle { } // ── Layout ────────────────────────────────────────────────────────── - ColumnLayout { anchors.fill: parent spacing: 16 anchors.margins: 20 - // Header FlexRow { - Layout.fillWidth: true - spacing: 12 - + Layout.fillWidth: true; spacing: 12 CText { variant: "h3"; text: "CSS Class Manager" } - Item { Layout.fillWidth: true } - CBadge { text: cssClasses.length + " classes" } - - CButton { - text: "Add Class" - variant: "primary" - size: "sm" - onClicked: { - newClassName = "" - showAddClassDialog = true - } - } + CButton { text: "Add Class"; variant: "primary"; size: "sm"; onClicked: { newClassName = ""; showAddClassDialog = true } } } CDivider { Layout.fillWidth: true } - // Main split RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 16 + Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16 - // ── Left: class list ──────────────────────────────────────── - CCard { - Layout.preferredWidth: 280 - Layout.fillHeight: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 10 - - CText { variant: "h4"; text: "Classes" } - - ListView { - id: classListView - Layout.fillWidth: true - Layout.fillHeight: true - model: cssClasses - spacing: 4 - clip: true - - delegate: CListItem { - width: classListView.width - title: modelData.name - subtitle: modelData.properties.length + " properties" - selected: index === selectedClassIndex - - // Swatch + usage badge row - Row { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - spacing: 8 - - Rectangle { - width: 16; height: 16 - radius: 3 - color: swatchColor(modelData.properties) - border.color: Theme.border - border.width: 1 - } - - CBadge { text: modelData.usageCount.toString() } - } - - onClicked: { - selectedClassIndex = index - showSuggestions = false - editingPropertyIndex = -1 - } - } - } - } + CssClassSidebar { + Layout.preferredWidth: 280; Layout.fillHeight: true + cssClasses: root.cssClasses + selectedIndex: root.selectedClassIndex + onItemClicked: function(idx) { root.selectedClassIndex = idx } } - // ── Right: editor + preview ───────────────────────────────── ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 16 + Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16 - // Editor card - CCard { - Layout.fillWidth: true - Layout.fillHeight: true + CssPropertyEditor { + Layout.fillWidth: true; Layout.fillHeight: true visible: selectedClass() !== null - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - // Class name + delete - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CTextField { - id: classNameField - Layout.preferredWidth: 260 - label: "Class Name" - placeholderText: ".class-name" - text: selectedClass() ? selectedClass().name : "" - onTextChanged: { - if (selectedClass() && text !== selectedClass().name) - setClassName(text) - } - } - - Item { Layout.fillWidth: true } - - CButton { - text: "Delete Class" - variant: "danger" - size: "sm" - onClicked: showDeleteConfirm = true - } - } - - CDivider { Layout.fillWidth: true } - - // Properties header - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { variant: "h4"; text: "Properties" } - - Item { Layout.fillWidth: true } - - CButton { - text: "Add Property" - variant: "primary" - size: "sm" - onClicked: addPropertyToSelected() - } - } - - // Property list - ListView { - id: propertyListView - Layout.fillWidth: true - Layout.fillHeight: true - model: selectedClass() ? selectedClass().properties : [] - spacing: 6 - clip: true - - delegate: Item { - width: propertyListView.width - height: 48 - - RowLayout { - anchors.fill: parent - spacing: 8 - - // Property name field with suggestions - Item { - Layout.preferredWidth: 200 - Layout.fillHeight: true - - CTextField { - id: propNameField - anchors.fill: parent - label: index === 0 ? "Property" : "" - placeholderText: "property-name" - text: modelData.prop - onTextChanged: { - if (text !== modelData.prop) - setPropertyName(index, text) - } - onActiveFocusChanged: { - if (activeFocus) { - editingPropertyIndex = index - showSuggestions = true - } else { - // Delay hide so clicks on suggestions register - suggestHideTimer.start() - } - } - } - - // Suggestions dropdown - Rectangle { - visible: showSuggestions && editingPropertyIndex === index - anchors.top: propNameField.bottom - anchors.left: propNameField.left - width: propNameField.width - height: Math.min(suggestCol.implicitHeight + 8, 180) - z: 100 - color: Theme.paper - border.color: Theme.border - border.width: 1 - radius: 6 - clip: true - - Flickable { - anchors.fill: parent - anchors.margins: 4 - contentHeight: suggestCol.implicitHeight - clip: true - - Column { - id: suggestCol - width: parent.width - spacing: 2 - - Repeater { - model: propertySuggestions - - Rectangle { - width: suggestCol.width - height: 26 - radius: 4 - color: suggestMa.containsMouse ? Theme.surface : "transparent" - - CText { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 8 - variant: "body2" - text: modelData - } - - MouseArea { - id: suggestMa - anchors.fill: parent - hoverEnabled: true - onClicked: { - setPropertyName(editingPropertyIndex, modelData) - showSuggestions = false - } - } - } - } - } - } - } - } - - // Value field - CTextField { - Layout.fillWidth: true - Layout.fillHeight: true - label: index === 0 ? "Value" : "" - placeholderText: "#000000" - text: modelData.value - onTextChanged: { - if (text !== modelData.value) - setPropertyValue(index, text) - } - } - - // Color swatch for color-like values - Rectangle { - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 - Layout.alignment: Qt.AlignVCenter - radius: 4 - border.color: Theme.border - border.width: 1 - color: { - var v = modelData.value - if (v.charAt(0) === "#" || v.indexOf("rgb") === 0) - return v - return "transparent" - } - visible: { - var p = modelData.prop - return p === "color" || p === "background-color" - } - } - - // Remove property button - CButton { - text: "x" - variant: "ghost" - size: "sm" - onClicked: removePropertyFromSelected(index) - } - } - } - } - } + selectedClass: root.selectedClass() + propertySuggestions: root.propertySuggestions + onClassNameChanged: function(name) { setClassName(name) } + onDeleteClassClicked: showDeleteConfirm = true + onAddPropertyClicked: addPropertyToSelected() + onRemovePropertyClicked: function(idx) { removePropertyFromSelected(idx) } + onPropertyNameChanged: function(idx, name) { setPropertyName(idx, name) } + onPropertyValueChanged: function(idx, val) { setPropertyValue(idx, val) } } - // ── Preview card ──────────────────────────────────────── - CCard { - Layout.fillWidth: true - Layout.preferredHeight: 160 + CssClassPreview { + Layout.fillWidth: true; Layout.preferredHeight: 160 visible: selectedClass() !== null - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 10 - - CText { variant: "h4"; text: "Preview" } - - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: Theme.surface - radius: 6 - border.color: Theme.border - border.width: 1 - - // Checkerboard background for transparency - Grid { - anchors.fill: parent - rows: Math.ceil(height / 12) - columns: Math.ceil(width / 12) - clip: true - Repeater { - model: Math.ceil(parent.parent.width / 12) * Math.ceil(parent.parent.height / 12) - Rectangle { - width: 12; height: 12 - color: (Math.floor(index / Math.ceil(parent.parent.width / 12)) + index) % 2 === 0 - ? "#2a2a2a" : "#333333" - } - } - } - - // Rendered preview element - Rectangle { - id: previewRect - anchors.centerIn: parent - width: Math.min(parent.width - 40, 280) - height: Math.min(parent.height - 20, 72) - radius: selectedClass() ? resolvePreviewRadius(selectedClass().properties) : 0 - opacity: selectedClass() ? resolvePreviewOpacity(selectedClass().properties) : 1.0 - color: selectedClass() - ? resolvePreviewColor(selectedClass().properties, "background-color", Theme.surface) - : Theme.surface - - CText { - anchors.centerIn: parent - variant: "body1" - text: selectedClass() ? "." + selectedClass().name : "" - color: selectedClass() - ? resolvePreviewColor(selectedClass().properties, "color", Theme.text) - : Theme.text - font.pixelSize: { - if (!selectedClass()) return 14 - var props = selectedClass().properties - for (var i = 0; i < props.length; i++) { - if (props[i].prop === "font-size") - return parseInt(props[i].value) || 14 - } - return 14 - } - } - } - } - } + selectedClass: root.selectedClass() } } } } - // ── Timer to delay hiding suggestions ─────────────────────────────── - Timer { - id: suggestHideTimer - interval: 200 - onTriggered: showSuggestions = false - } - // ── Add Class Dialog ──────────────────────────────────────────────── CDialog { - id: addClassDialog - visible: showAddClassDialog - title: "Add New Class" - + visible: showAddClassDialog; title: "Add New Class" ColumnLayout { - spacing: 16 - width: 320 - - CText { - variant: "body1" - text: "Enter a name for the new CSS class." - } - - CTextField { - id: newClassNameField - Layout.fillWidth: true - label: "Class Name" - placeholderText: "my-class" - text: newClassName - onTextChanged: newClassName = text - } - + spacing: 16; width: 320 + CText { variant: "body1"; text: "Enter a name for the new CSS class." } + CTextField { Layout.fillWidth: true; label: "Class Name"; placeholderText: "my-class"; text: newClassName; onTextChanged: newClassName = text } FlexRow { - Layout.fillWidth: true - spacing: 8 - - Item { Layout.fillWidth: true } - - CButton { - text: "Cancel" - variant: "ghost" - size: "sm" - onClicked: showAddClassDialog = false - } - - CButton { - text: "Create" - variant: "primary" - size: "sm" - enabled: newClassName.trim().length > 0 - onClicked: { - addClass(newClassName.trim()) - showAddClassDialog = false - } - } + Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true } + CButton { text: "Cancel"; variant: "ghost"; size: "sm"; onClicked: showAddClassDialog = false } + CButton { text: "Create"; variant: "primary"; size: "sm"; enabled: newClassName.trim().length > 0; onClicked: { addClass(newClassName.trim()); showAddClassDialog = false } } } } } // ── Delete Confirmation Dialog ────────────────────────────────────── CDialog { - id: deleteConfirmDialog - visible: showDeleteConfirm - title: "Delete Class" - + visible: showDeleteConfirm; title: "Delete Class" ColumnLayout { - spacing: 16 - width: 320 - - CAlert { - Layout.fillWidth: true - severity: "warning" - text: selectedClass() - ? "Are you sure you want to delete \"." + selectedClass().name + "\"? This action cannot be undone." - : "No class selected." - } - - CText { - variant: "body2" - text: selectedClass() - ? "This class is used in " + selectedClass().usageCount + " place(s)." - : "" - visible: selectedClass() !== null && selectedClass().usageCount > 0 - } - + spacing: 16; width: 320 + CAlert { Layout.fillWidth: true; severity: "warning"; text: selectedClass() ? "Are you sure you want to delete \"." + selectedClass().name + "\"? This action cannot be undone." : "No class selected." } + CText { variant: "body2"; text: selectedClass() ? "This class is used in " + selectedClass().usageCount + " place(s)." : ""; visible: selectedClass() !== null && selectedClass().usageCount > 0 } FlexRow { - Layout.fillWidth: true - spacing: 8 - - Item { Layout.fillWidth: true } - - CButton { - text: "Cancel" - variant: "ghost" - size: "sm" - onClicked: showDeleteConfirm = false - } - - CButton { - text: "Delete" - variant: "danger" - size: "sm" - onClicked: { - deleteSelectedClass() - showDeleteConfirm = false - } - } + Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true } + CButton { text: "Cancel"; variant: "ghost"; size: "sm"; onClicked: showDeleteConfirm = false } + CButton { text: "Delete"; variant: "danger"; size: "sm"; onClicked: { deleteSelectedClass(); showDeleteConfirm = false } } } } } diff --git a/frontends/qt6/DatabaseManager.qml b/frontends/qt6/DatabaseManager.qml index 4dcd992c0..4b661ce60 100644 --- a/frontends/qt6/DatabaseManager.qml +++ b/frontends/qt6/DatabaseManager.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QmlComponents 1.0 import "qmllib/dbal" +import "qmllib/MetaBuilder" Rectangle { id: root @@ -10,73 +11,8 @@ Rectangle { // ── DBAL connection ────────────────────────────────────────── DBALProvider { id: dbal } - property bool useLiveData: dbal.connected - property var mockBackends: JSON.parse(JSON.stringify(backends)) - - function loadAdapterStatus() { - dbal.execute("core/adapters", {}, function(result, error) { - if (!error && result && result.adapters && result.adapters.length > 0) { - var liveBackends = [] - for (var i = 0; i < result.adapters.length; i++) { - var a = result.adapters[i] - liveBackends.push({ - name: a.name || "", - key: a.key || "", - status: a.status || "disconnected", - description: a.description || "", - connectionString: a.connectionString || "", - records: a.records || 0, - sizeKb: a.sizeKb || 0, - lastBackup: a.lastBackup || "Never" - }) - } - backends = liveBackends - if (selectedBackendIndex >= liveBackends.length) - selectedBackendIndex = 0 - if (activeBackendIndex >= liveBackends.length) - activeBackendIndex = 0 - } - // On error or empty result, keep existing mock backends as fallback - }) - } - - function testConnectionLive(index) { - var backend = backends[index] - testingIndex = index - if (useLiveData) { - dbal.execute("core/test-connection", { adapter: backend.key }, function(result, error) { - var newResults = Object.assign({}, testResults) - if (!error && result && result.success) { - newResults[index] = "success" - } else { - newResults[index] = "error" - } - testResults = newResults - testingIndex = -1 - }) - } else { - // Fall back to mock timer - testTimer.targetIndex = index - testTimer.start() - } - } - - function checkHealth() { - if (useLiveData) { - dbal.ping() - } - } - - onUseLiveDataChanged: { - if (useLiveData) loadAdapterStatus() - } - - Component.onCompleted: { - loadAdapterStatus() - } - // ── State ────────────────────────────────────────────────────────── property int selectedBackendIndex: 2 property int activeBackendIndex: 2 @@ -86,7 +22,6 @@ Rectangle { property int adapterPattern: 0 property bool showExportDialog: false property bool showImportDialog: false - property var adapterPatterns: ["read-through", "write-through", "cache-aside", "dual-write"] property var backends: [ @@ -106,18 +41,10 @@ Rectangle { { name: "Generic", key: "generic", status: "disconnected", description: "Custom adapter via DATABASE_URL with driver query params", connectionString: "generic://localhost:9999/custom?driver=mydriver", records: 0, sizeKb: 0, lastBackup: "Never" } ] - // ── Mock testing state ───────────────────────────────────────────── + // ── Testing state ───────────────────────────────────────────── property var testingIndex: -1 property var testResults: ({}) - function testConnection(index) { - testingIndex = index - testResults = Object.assign({}, testResults) - delete testResults[index] - testTimer.targetIndex = index - testTimer.start() - } - Timer { id: testTimer property int targetIndex: -1 @@ -125,75 +52,63 @@ Rectangle { onTriggered: { var backend = backends[targetIndex] var result = (backend.status === "connected") ? "success" : (backend.status === "error" ? "error" : "warning") - var newResults = Object.assign({}, testResults) - newResults[targetIndex] = result - testResults = newResults - testingIndex = -1 + var newResults = Object.assign({}, testResults); newResults[targetIndex] = result + testResults = newResults; testingIndex = -1 } } - function formatSize(kb) { - if (kb < 1024) return kb + " KB" - return (kb / 1024).toFixed(1) + " MB" + function formatSize(kb) { return kb < 1024 ? kb + " KB" : (kb / 1024).toFixed(1) + " MB" } + function totalRecords() { var s = 0; for (var i = 0; i < backends.length; i++) s += backends[i].records; return s } + function totalSize() { var s = 0; for (var i = 0; i < backends.length; i++) s += backends[i].sizeKb; return s } + function connectedCount() { var c = 0; for (var i = 0; i < backends.length; i++) if (backends[i].status === "connected") c++; return c } + + function testConnectionLive(index) { + testingIndex = index + if (useLiveData) { + dbal.execute("core/test-connection", { adapter: backends[index].key }, function(result, error) { + var newResults = Object.assign({}, testResults) + newResults[index] = (!error && result && result.success) ? "success" : "error" + testResults = newResults; testingIndex = -1 + }) + } else { testTimer.targetIndex = index; testTimer.start() } } - function totalRecords() { - var sum = 0 - for (var i = 0; i < backends.length; i++) sum += backends[i].records - return sum + function loadAdapterStatus() { + dbal.execute("core/adapters", {}, function(result, error) { + if (!error && result && result.adapters && result.adapters.length > 0) { + var liveBackends = [] + for (var i = 0; i < result.adapters.length; i++) { + var a = result.adapters[i] + liveBackends.push({ name: a.name || "", key: a.key || "", status: a.status || "disconnected", description: a.description || "", + connectionString: a.connectionString || "", records: a.records || 0, sizeKb: a.sizeKb || 0, lastBackup: a.lastBackup || "Never" }) + } + backends = liveBackends + if (selectedBackendIndex >= liveBackends.length) selectedBackendIndex = 0 + if (activeBackendIndex >= liveBackends.length) activeBackendIndex = 0 + } + }) } - function totalSize() { - var sum = 0 - for (var i = 0; i < backends.length; i++) sum += backends[i].sizeKb - return sum - } + onUseLiveDataChanged: { if (useLiveData) loadAdapterStatus() } + Component.onCompleted: { loadAdapterStatus() } - function connectedCount() { - var count = 0 - for (var i = 0; i < backends.length; i++) - if (backends[i].status === "connected") count++ - return count - } - - // ── Export Dialog ────────────────────────────────────────────────── + // ── Export/Import Dialogs ──────────────────────────────────────── Dialog { - id: exportDialog - visible: showExportDialog - title: "Export Database" - modal: true - anchors.centerIn: parent - width: 420 - standardButtons: Dialog.Ok | Dialog.Cancel - onAccepted: showExportDialog = false - onRejected: showExportDialog = false - + id: exportDialog; visible: showExportDialog; title: "Export Database"; modal: true; anchors.centerIn: parent; width: 420 + standardButtons: Dialog.Ok | Dialog.Cancel; onAccepted: showExportDialog = false; onRejected: showExportDialog = false ColumnLayout { - spacing: 12 - width: parent.width - + spacing: 12; width: parent.width CText { variant: "body1"; text: "Export the active database (" + backends[activeBackendIndex].name + ") to a JSON dump file." } CTextField { label: "Output path"; text: "/tmp/dbal-export-" + backends[activeBackendIndex].key + ".json"; Layout.fillWidth: true } CAlert { severity: "success"; text: "Export includes all tenants and entity data (" + backends[activeBackendIndex].records + " records)." } } } - // ── Import Dialog ────────────────────────────────────────────────── Dialog { - id: importDialog - visible: showImportDialog - title: "Import Database" - modal: true - anchors.centerIn: parent - width: 420 - standardButtons: Dialog.Ok | Dialog.Cancel - onAccepted: showImportDialog = false - onRejected: showImportDialog = false - + id: importDialog; visible: showImportDialog; title: "Import Database"; modal: true; anchors.centerIn: parent; width: 420 + standardButtons: Dialog.Ok | Dialog.Cancel; onAccepted: showImportDialog = false; onRejected: showImportDialog = false ColumnLayout { - spacing: 12 - width: parent.width - + spacing: 12; width: parent.width CText { variant: "body1"; text: "Import a JSON dump into the active backend (" + backends[activeBackendIndex].name + ")." } CTextField { label: "Import file"; placeholderText: "/path/to/dbal-export.json"; Layout.fillWidth: true } CAlert { severity: "warning"; text: "Existing records with matching IDs will be overwritten." } @@ -202,337 +117,66 @@ Rectangle { // ── Main Layout ──────────────────────────────────────────────────── ColumnLayout { - anchors.fill: parent - anchors.margins: 20 - spacing: 16 + anchors.fill: parent; anchors.margins: 20; spacing: 16 - // ── Header ──────────────────────────────────────────────────── FlexRow { - Layout.fillWidth: true - spacing: 12 - + Layout.fillWidth: true; spacing: 12 CText { variant: "h3"; text: "Database Manager" } CStatusBadge { status: "success"; text: connectedCount() + " / " + backends.length + " connected" } - - CBadge { - text: useLiveData ? "Connected to DBAL" : "Mock Data" - color: useLiveData ? Theme.success : Theme.warning - } - + CBadge { text: useLiveData ? "Connected to DBAL" : "Mock Data"; color: useLiveData ? Theme.success : Theme.warning } Item { Layout.fillWidth: true } - CButton { text: "Export"; variant: "ghost"; onClicked: showExportDialog = true } CButton { text: "Import"; variant: "ghost"; onClicked: showImportDialog = true } } - // ── Stats Row ───────────────────────────────────────────────── - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CCard { - Layout.fillWidth: true - implicitHeight: 72 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 4 - CText { variant: "caption"; text: "Total Records" } - CText { variant: "h4"; text: totalRecords().toLocaleString() } - } - } - - CCard { - Layout.fillWidth: true - implicitHeight: 72 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 4 - CText { variant: "caption"; text: "Total Size" } - CText { variant: "h4"; text: formatSize(totalSize()) } - } - } - - CCard { - Layout.fillWidth: true - implicitHeight: 72 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 4 - CText { variant: "caption"; text: "Active Backend" } - CText { variant: "h4"; text: backends[activeBackendIndex].name } - } - } - - CCard { - Layout.fillWidth: true - implicitHeight: 72 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 4 - CText { variant: "caption"; text: "Adapter Pattern" } - CText { variant: "h4"; text: adapterPatterns[adapterPattern] } - } - } + CDatabaseStatsRow { + totalRecords: root.totalRecords().toLocaleString() + totalSize: root.formatSize(root.totalSize()) + activeBackend: backends[activeBackendIndex].name + adapterPattern: adapterPatterns[root.adapterPattern] } - // ── Body: Sidebar + Detail ──────────────────────────────────── RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 16 + Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16 - // ── Backend List (sidebar) ──────────────────────────────── - CCard { - Layout.preferredWidth: 300 - Layout.fillHeight: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 8 - - CText { variant: "subtitle1"; text: "Backends (14)" } - CDivider { Layout.fillWidth: true } - - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - model: backends - spacing: 4 - clip: true - - delegate: CListItem { - width: parent ? parent.width : 268 - title: modelData.name - subtitle: modelData.key - selected: index === selectedBackendIndex - leadingIcon: modelData.status === "connected" ? "check_circle" : (modelData.status === "error" ? "error" : "radio_button_unchecked") - - onClicked: selectedBackendIndex = index - - CStatusBadge { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - status: modelData.status === "connected" ? "success" : (modelData.status === "error" ? "error" : "warning") - text: modelData.status - } - } - } - } + CBackendListSidebar { + backends: root.backends + selectedIndex: selectedBackendIndex + onBackendSelected: function(index) { selectedBackendIndex = index } } - // ── Detail Panel ────────────────────────────────────────── - CCard { - Layout.fillWidth: true - Layout.fillHeight: true + ColumnLayout { + Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16 - Flickable { - anchors.fill: parent - anchors.margins: 16 - contentHeight: detailColumn.implicitHeight - clip: true + CBackendDetailPanel { + backend: backends[selectedBackendIndex] + isActive: selectedBackendIndex === activeBackendIndex + testingIndex: root.testingIndex + backendIndex: selectedBackendIndex + testResult: testResults[selectedBackendIndex] + onTestConnectionRequested: testConnectionLive(selectedBackendIndex) + onSetActiveRequested: activeBackendIndex = selectedBackendIndex + } + + CCard { + Layout.fillWidth: true ColumnLayout { - id: detailColumn - width: parent.width - spacing: 16 + anchors.fill: parent; anchors.margins: 16; spacing: 12 - // ── Backend Header ──────────────────────────── - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { variant: "h4"; text: backends[selectedBackendIndex].name } - CStatusBadge { - status: backends[selectedBackendIndex].status === "connected" ? "success" : (backends[selectedBackendIndex].status === "error" ? "error" : "warning") - text: backends[selectedBackendIndex].status - } - - Item { Layout.fillWidth: true } - - CBadge { - text: selectedBackendIndex === activeBackendIndex ? "ACTIVE" : "INACTIVE" - accent: selectedBackendIndex === activeBackendIndex - } - } - - CText { - variant: "body1" - text: backends[selectedBackendIndex].description - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - CDivider { Layout.fillWidth: true } - - // ── Connection ──────────────────────────────── - CText { variant: "subtitle1"; text: "Connection" } - - CTextField { - label: "Connection String" - text: backends[selectedBackendIndex].connectionString - Layout.fillWidth: true - } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CButton { - text: testingIndex === selectedBackendIndex ? "Testing..." : "Test Connection" - variant: "primary" - enabled: testingIndex === -1 - onClicked: testConnectionLive(selectedBackendIndex) - } - - CButton { - text: selectedBackendIndex === activeBackendIndex ? "Active Backend" : "Set as Active" - variant: selectedBackendIndex === activeBackendIndex ? "ghost" : "primary" - enabled: selectedBackendIndex !== activeBackendIndex && backends[selectedBackendIndex].status === "connected" - onClicked: activeBackendIndex = selectedBackendIndex - } - - Item { Layout.fillWidth: true } - - Loader { - active: testResults[selectedBackendIndex] !== undefined - sourceComponent: CStatusBadge { - status: testResults[selectedBackendIndex] === "success" ? "success" : "error" - text: testResults[selectedBackendIndex] === "success" ? "Connection OK" : "Connection Failed" - } - } - } - - CDivider { Layout.fillWidth: true } - - // ── Storage Stats ───────────────────────────── - CText { variant: "subtitle1"; text: "Storage Statistics" } - - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Records" } - CText { variant: "h4"; text: backends[selectedBackendIndex].records.toLocaleString() } - } - } - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Size" } - CText { variant: "h4"; text: formatSize(backends[selectedBackendIndex].sizeKb) } - } - } - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Last Backup" } - CText { variant: "body2"; text: backends[selectedBackendIndex].lastBackup } - } - } - } - - CDivider { Layout.fillWidth: true } - - // ── Configuration ───────────────────────────── CText { variant: "subtitle1"; text: "Environment Configuration" } - CTextField { - label: "DATABASE_URL" - text: databaseUrl - onTextChanged: databaseUrl = text - placeholderText: "sqlite:///path/to/db or postgres://user:pass@host/db" - Layout.fillWidth: true - } - - CTextField { - label: "DBAL_CACHE_URL (Redis)" - text: cacheUrl - onTextChanged: cacheUrl = text - placeholderText: "redis://localhost:6379/0?ttl=300&pattern=read-through" - Layout.fillWidth: true - } - - CTextField { - label: "DBAL_SEARCH_URL (Elasticsearch)" - text: searchUrl - onTextChanged: searchUrl = text - placeholderText: "http://localhost:9200?index=dbal_search&refresh=true" - Layout.fillWidth: true - } + CTextField { label: "DATABASE_URL"; text: databaseUrl; onTextChanged: databaseUrl = text; placeholderText: "sqlite:///path/to/db or postgres://user:pass@host/db"; Layout.fillWidth: true } + CTextField { label: "DBAL_CACHE_URL (Redis)"; text: cacheUrl; onTextChanged: cacheUrl = text; placeholderText: "redis://localhost:6379/0?ttl=300&pattern=read-through"; Layout.fillWidth: true } + CTextField { label: "DBAL_SEARCH_URL (Elasticsearch)"; text: searchUrl; onTextChanged: searchUrl = text; placeholderText: "http://localhost:9200?index=dbal_search&refresh=true"; Layout.fillWidth: true } CDivider { Layout.fillWidth: true } - // ── Multi-Adapter Pattern ───────────────────── - CText { variant: "subtitle1"; text: "Multi-Adapter Pattern" } - CText { variant: "body2"; text: "Select how the primary, cache, and search adapters coordinate data flow." } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - Repeater { - model: adapterPatterns - delegate: CButton { - text: modelData - variant: index === adapterPattern ? "primary" : "ghost" - onClicked: adapterPattern = index - } - } + CAdapterPatternSelector { + selectedPattern: root.adapterPattern + onPatternChanged: function(index) { root.adapterPattern = index } } - CPaper { - Layout.fillWidth: true - implicitHeight: patternDesc.implicitHeight + 24 - - CText { - id: patternDesc - anchors.fill: parent - anchors.margins: 12 - variant: "body2" - wrapMode: Text.Wrap - text: { - var descriptions = [ - "Read-through: Reads check cache first. On miss, the cache fetches from the primary DB, stores the result, and returns it. Best for read-heavy workloads.", - "Write-through: Every write goes to both cache and primary DB synchronously. Guarantees consistency at the cost of write latency.", - "Cache-aside: Application manages cache explicitly. Reads check cache, fetch from DB on miss and populate cache. Writes go directly to DB and invalidate cache.", - "Dual-write: Writes are sent to two backends simultaneously (e.g., primary DB + search index). Requires conflict resolution strategy." - ] - return descriptions[adapterPattern] - } - } - } - - // ── Spacer at bottom ────────────────────────── Item { height: 16 } } } diff --git a/frontends/qt6/DropdownConfigManager.qml b/frontends/qt6/DropdownConfigManager.qml index 46f039f4a..a2d14c746 100644 --- a/frontends/qt6/DropdownConfigManager.qml +++ b/frontends/qt6/DropdownConfigManager.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QmlComponents 1.0 import "qmllib/dbal" +import "qmllib/MetaBuilder" Rectangle { id: root @@ -19,103 +20,13 @@ Rectangle { property string newDropdownDescription: "" property var dropdowns: [ - { - name: "user_roles", - description: "Assignable user roles for access control", - allowCustom: false, - required: true, - options: [ - { label: "Administrator", value: "admin" }, - { label: "Moderator", value: "moderator" }, - { label: "Editor", value: "editor" }, - { label: "Viewer", value: "viewer" }, - { label: "Guest", value: "guest" } - ] - }, - { - name: "content_status", - description: "Publication lifecycle status for content items", - allowCustom: false, - required: true, - options: [ - { label: "Draft", value: "draft" }, - { label: "In Review", value: "in_review" }, - { label: "Published", value: "published" }, - { label: "Archived", value: "archived" } - ] - }, - { - name: "priority_levels", - description: "Task and issue priority classifications", - allowCustom: false, - required: true, - options: [ - { label: "Critical", value: "critical" }, - { label: "High", value: "high" }, - { label: "Medium", value: "medium" }, - { label: "Low", value: "low" }, - { label: "None", value: "none" } - ] - }, - { - name: "categories", - description: "General-purpose content categorization tags", - allowCustom: true, - required: false, - options: [ - { label: "Technology", value: "technology" }, - { label: "Design", value: "design" }, - { label: "Business", value: "business" }, - { label: "Science", value: "science" }, - { label: "Education", value: "education" }, - { label: "Entertainment", value: "entertainment" } - ] - }, - { - name: "languages", - description: "Supported interface and content languages", - allowCustom: false, - required: true, - options: [ - { label: "English", value: "en" }, - { label: "Spanish", value: "es" }, - { label: "French", value: "fr" }, - { label: "German", value: "de" }, - { label: "Japanese", value: "ja" }, - { label: "Chinese", value: "zh" }, - { label: "Portuguese", value: "pt" } - ] - }, - { - name: "themes", - description: "Available UI theme presets", - allowCustom: true, - required: false, - options: [ - { label: "Light", value: "light" }, - { label: "Dark", value: "dark" }, - { label: "System Default", value: "system" }, - { label: "High Contrast", value: "high_contrast" } - ] - }, - { - name: "database_backends", - description: "Supported DBAL database adapter backends", - allowCustom: false, - required: true, - options: [ - { label: "SQLite", value: "sqlite" }, - { label: "PostgreSQL", value: "postgres" }, - { label: "MySQL", value: "mysql" }, - { label: "MariaDB", value: "mariadb" }, - { label: "MongoDB", value: "mongodb" }, - { label: "Redis", value: "redis" }, - { label: "CockroachDB", value: "cockroachdb" }, - { label: "SurrealDB", value: "surrealdb" }, - { label: "Supabase", value: "supabase" }, - { label: "In-Memory", value: "memory" } - ] - } + { name: "user_roles", description: "Assignable user roles for access control", allowCustom: false, required: true, options: [{ label: "Administrator", value: "admin" }, { label: "Moderator", value: "moderator" }, { label: "Editor", value: "editor" }, { label: "Viewer", value: "viewer" }, { label: "Guest", value: "guest" }] }, + { name: "content_status", description: "Publication lifecycle status for content items", allowCustom: false, required: true, options: [{ label: "Draft", value: "draft" }, { label: "In Review", value: "in_review" }, { label: "Published", value: "published" }, { label: "Archived", value: "archived" }] }, + { name: "priority_levels", description: "Task and issue priority classifications", allowCustom: false, required: true, options: [{ label: "Critical", value: "critical" }, { label: "High", value: "high" }, { label: "Medium", value: "medium" }, { label: "Low", value: "low" }, { label: "None", value: "none" }] }, + { name: "categories", description: "General-purpose content categorization tags", allowCustom: true, required: false, options: [{ label: "Technology", value: "technology" }, { label: "Design", value: "design" }, { label: "Business", value: "business" }, { label: "Science", value: "science" }, { label: "Education", value: "education" }, { label: "Entertainment", value: "entertainment" }] }, + { name: "languages", description: "Supported interface and content languages", allowCustom: false, required: true, options: [{ label: "English", value: "en" }, { label: "Spanish", value: "es" }, { label: "French", value: "fr" }, { label: "German", value: "de" }, { label: "Japanese", value: "ja" }, { label: "Chinese", value: "zh" }, { label: "Portuguese", value: "pt" }] }, + { name: "themes", description: "Available UI theme presets", allowCustom: true, required: false, options: [{ label: "Light", value: "light" }, { label: "Dark", value: "dark" }, { label: "System Default", value: "system" }, { label: "High Contrast", value: "high_contrast" }] }, + { name: "database_backends", description: "Supported DBAL database adapter backends", allowCustom: false, required: true, options: [{ label: "SQLite", value: "sqlite" }, { label: "PostgreSQL", value: "postgres" }, { label: "MySQL", value: "mysql" }, { label: "MariaDB", value: "mariadb" }, { label: "MongoDB", value: "mongodb" }, { label: "Redis", value: "redis" }, { label: "CockroachDB", value: "cockroachdb" }, { label: "SurrealDB", value: "surrealdb" }, { label: "Supabase", value: "supabase" }, { label: "In-Memory", value: "memory" }] } ] function selectedDropdown() { @@ -124,82 +35,51 @@ Rectangle { } function updateDropdown(index, updated) { - var copy = dropdowns.slice() - copy[index] = updated - dropdowns = copy + var copy = dropdowns.slice(); copy[index] = updated; dropdowns = copy if (useLiveData) saveDropdown(index) } function updateSelectedField(field, value) { if (selectedIndex < 0) return - var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])) - dd[field] = value - updateDropdown(selectedIndex, dd) + var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])); dd[field] = value; updateDropdown(selectedIndex, dd) } function addOption() { if (selectedIndex < 0) return var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])) - dd.options.push({ label: "New Option", value: "new_option_" + dd.options.length }) - updateDropdown(selectedIndex, dd) + dd.options.push({ label: "New Option", value: "new_option_" + dd.options.length }); updateDropdown(selectedIndex, dd) } function removeOption(optIndex) { if (selectedIndex < 0) return - var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])) - dd.options.splice(optIndex, 1) - updateDropdown(selectedIndex, dd) + var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])); dd.options.splice(optIndex, 1); updateDropdown(selectedIndex, dd) } function moveOption(optIndex, direction) { if (selectedIndex < 0) return var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])) - var targetIndex = optIndex + direction - if (targetIndex < 0 || targetIndex >= dd.options.length) return - var temp = dd.options[optIndex] - dd.options[optIndex] = dd.options[targetIndex] - dd.options[targetIndex] = temp - updateDropdown(selectedIndex, dd) + var t = optIndex + direction; if (t < 0 || t >= dd.options.length) return + var tmp = dd.options[optIndex]; dd.options[optIndex] = dd.options[t]; dd.options[t] = tmp; updateDropdown(selectedIndex, dd) } function updateOptionField(optIndex, field, value) { if (selectedIndex < 0) return - var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])) - dd.options[optIndex][field] = value - updateDropdown(selectedIndex, dd) + var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])); dd.options[optIndex][field] = value; updateDropdown(selectedIndex, dd) } function addDropdown() { if (newDropdownName.trim() === "") return - var newDd = { - name: newDropdownName.trim().toLowerCase().replace(/ /g, "_"), - description: newDropdownDescription.trim() || "No description", - allowCustom: false, - required: false, - options: [{ label: "Option 1", value: "option_1" }] - } - if (useLiveData) { - dbal.execute("core/dropdown-configs/create", { data: newDd }, function(r, e) { if (!e) loadDropdowns() }) - } - var copy = dropdowns.slice() - copy.push(newDd) - dropdowns = copy - selectedIndex = dropdowns.length - 1 - newDropdownName = "" - newDropdownDescription = "" - addDialogOpen = false + var newDd = { name: newDropdownName.trim().toLowerCase().replace(/ /g, "_"), description: newDropdownDescription.trim() || "No description", allowCustom: false, required: false, options: [{ label: "Option 1", value: "option_1" }] } + if (useLiveData) dbal.execute("core/dropdown-configs/create", { data: newDd }, function(r, e) { if (!e) loadDropdowns() }) + var copy = dropdowns.slice(); copy.push(newDd); dropdowns = copy + selectedIndex = dropdowns.length - 1; newDropdownName = ""; newDropdownDescription = ""; addDialogOpen = false } function deleteSelectedDropdown() { if (selectedIndex < 0) return - if (useLiveData && dropdowns[selectedIndex].id) { - dbal.execute("core/dropdown-configs/delete", { id: dropdowns[selectedIndex].id }, function(r, e) { if (!e) loadDropdowns() }) - } - var copy = dropdowns.slice() - copy.splice(selectedIndex, 1) - dropdowns = copy - selectedIndex = copy.length > 0 ? Math.min(selectedIndex, copy.length - 1) : -1 - deleteDialogOpen = false + if (useLiveData && dropdowns[selectedIndex].id) dbal.execute("core/dropdown-configs/delete", { id: dropdowns[selectedIndex].id }, function(r, e) { if (!e) loadDropdowns() }) + var copy = dropdowns.slice(); copy.splice(selectedIndex, 1); dropdowns = copy + selectedIndex = copy.length > 0 ? Math.min(selectedIndex, copy.length - 1) : -1; deleteDialogOpen = false } // ── DBAL Integration ───────────────────────────────────────────── @@ -232,83 +112,31 @@ Rectangle { anchors.margins: 20 spacing: 16 - // Header FlexRow { Layout.fillWidth: true spacing: 12 - CText { variant: "h3"; text: "Dropdown Configuration Manager" } Item { Layout.fillWidth: true } CBadge { text: dropdowns.length + " dropdowns" } } - CText { - variant: "body2" - text: "Configure dropdown/select field options used across all packages and entities." - color: Theme.text - opacity: 0.7 - } - + CText { variant: "body2"; text: "Configure dropdown/select field options used across all packages and entities."; color: Theme.text; opacity: 0.7 } CDivider { Layout.fillWidth: true } - // Main content: sidebar + editor RowLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 16 - // Left sidebar: dropdown list - CCard { + DropdownSidebar { Layout.preferredWidth: 320 Layout.fillHeight: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - FlexRow { - Layout.fillWidth: true - spacing: 8 - CText { variant: "h4"; text: "Dropdowns" } - Item { Layout.fillWidth: true } - CButton { - text: "+ Add" - variant: "primary" - size: "sm" - onClicked: addDialogOpen = true - } - } - - CDivider { Layout.fillWidth: true } - - ListView { - id: dropdownList - Layout.fillWidth: true - Layout.fillHeight: true - model: dropdowns - spacing: 4 - clip: true - - delegate: CListItem { - width: dropdownList.width - title: modelData.name - subtitle: modelData.description - selected: index === selectedIndex - onClicked: selectedIndex = index - - CBadge { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.options.length + "" - } - } - } - } + dropdowns: root.dropdowns + selectedIndex: root.selectedIndex + onItemClicked: function(idx) { root.selectedIndex = idx } + onAddClicked: addDialogOpen = true } - // Right panel: editor + preview CCard { Layout.fillWidth: true Layout.fillHeight: true @@ -318,396 +146,58 @@ Rectangle { anchors.margins: 16 spacing: 12 - // No selection state Item { - Layout.fillWidth: true - Layout.fillHeight: true - visible: selectedIndex < 0 - - CText { - anchors.centerIn: parent - variant: "body1" - text: "Select a dropdown from the list to edit its configuration." - color: Theme.text - opacity: 0.5 - } + Layout.fillWidth: true; Layout.fillHeight: true; visible: selectedIndex < 0 + CText { anchors.centerIn: parent; variant: "body1"; text: "Select a dropdown from the list to edit its configuration."; color: Theme.text; opacity: 0.5 } } - // Editor content (visible when a dropdown is selected) ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 12 - visible: selectedIndex >= 0 + Layout.fillWidth: true; Layout.fillHeight: true; spacing: 12; visible: selectedIndex >= 0 - // Editor header FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { - variant: "h4" - text: selectedDropdown() ? selectedDropdown().name : "" - } + Layout.fillWidth: true; spacing: 12 + CText { variant: "h4"; text: selectedDropdown() ? selectedDropdown().name : "" } Item { Layout.fillWidth: true } - CButton { - text: "Delete" - variant: "danger" - size: "sm" - onClicked: deleteDialogOpen = true - } + CButton { text: "Delete"; variant: "danger"; size: "sm"; onClicked: deleteDialogOpen = true } } CDivider { Layout.fillWidth: true } - // Scrollable editor area Flickable { - Layout.fillWidth: true - Layout.fillHeight: true - contentHeight: editorColumn.implicitHeight - clip: true - boundsBehavior: Flickable.StopAtBounds + Layout.fillWidth: true; Layout.fillHeight: true + contentHeight: editorColumn.implicitHeight; clip: true; boundsBehavior: Flickable.StopAtBounds ColumnLayout { id: editorColumn width: parent.width spacing: 16 - // Name and description fields - CText { variant: "body2"; text: "General"; font.bold: true } - - RowLayout { + DropdownGeneralForm { Layout.fillWidth: true - spacing: 12 - - CTextField { - label: "Name" - placeholderText: "dropdown_name" - text: selectedDropdown() ? selectedDropdown().name : "" - Layout.fillWidth: true - onTextChanged: { - if (selectedDropdown() && text !== selectedDropdown().name) { - updateSelectedField("name", text) - } - } - } - - CTextField { - label: "Description" - placeholderText: "What is this dropdown for?" - text: selectedDropdown() ? selectedDropdown().description : "" - Layout.fillWidth: true - onTextChanged: { - if (selectedDropdown() && text !== selectedDropdown().description) { - updateSelectedField("description", text) - } - } - } - } - - // Toggles - RowLayout { - Layout.fillWidth: true - spacing: 24 - - RowLayout { - spacing: 8 - Switch { - id: allowCustomSwitch - checked: selectedDropdown() ? selectedDropdown().allowCustom : false - onCheckedChanged: { - if (selectedDropdown() && checked !== selectedDropdown().allowCustom) { - updateSelectedField("allowCustom", checked) - } - } - } - CText { - variant: "body2" - text: "Allow custom values" - } - } - - RowLayout { - spacing: 8 - Switch { - id: requiredSwitch - checked: selectedDropdown() ? selectedDropdown().required : false - onCheckedChanged: { - if (selectedDropdown() && checked !== selectedDropdown().required) { - updateSelectedField("required", checked) - } - } - } - CText { - variant: "body2" - text: "Required" - } - } - - Item { Layout.fillWidth: true } - - CBadge { - text: (selectedDropdown() && selectedDropdown().required ? "Required" : "Optional") - accent: selectedDropdown() ? selectedDropdown().required : false - } - CBadge { - text: (selectedDropdown() && selectedDropdown().allowCustom ? "Custom allowed" : "Fixed options") - visible: true - } + dropdown: selectedDropdown() + onFieldChanged: function(field, value) { updateSelectedField(field, value) } } CDivider { Layout.fillWidth: true } - // Options header - FlexRow { + DropdownOptionsEditor { Layout.fillWidth: true - spacing: 8 - - CText { variant: "body2"; text: "Options"; font.bold: true } - CBadge { text: selectedDropdown() ? selectedDropdown().options.length + " items" : "0 items" } - Item { Layout.fillWidth: true } - CButton { - text: "+ Add Option" - variant: "primary" - size: "sm" - onClicked: addOption() - } - } - - // Options list - Repeater { - id: optionsRepeater - model: selectedDropdown() ? selectedDropdown().options : [] - - CPaper { - Layout.fillWidth: true - implicitHeight: optionRow.implicitHeight + 24 - - RowLayout { - id: optionRow - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - // Index indicator - CText { - variant: "caption" - text: (index + 1) + "." - Layout.preferredWidth: 24 - color: Theme.text - opacity: 0.5 - } - - CTextField { - label: "Label" - placeholderText: "Display label" - text: modelData.label - Layout.fillWidth: true - onTextChanged: { - if (text !== modelData.label) { - updateOptionField(index, "label", text) - } - } - } - - CTextField { - label: "Value" - placeholderText: "stored_value" - text: modelData.value - Layout.fillWidth: true - onTextChanged: { - if (text !== modelData.value) { - updateOptionField(index, "value", text) - } - } - } - - // Reorder buttons - ColumnLayout { - spacing: 2 - CButton { - text: "\u25B2" - variant: "ghost" - size: "sm" - enabled: index > 0 - onClicked: moveOption(index, -1) - } - CButton { - text: "\u25BC" - variant: "ghost" - size: "sm" - enabled: selectedDropdown() ? index < selectedDropdown().options.length - 1 : false - onClicked: moveOption(index, 1) - } - } - - CButton { - text: "X" - variant: "danger" - size: "sm" - onClicked: removeOption(index) - } - } - } + dropdown: selectedDropdown() + onAddOptionClicked: addOption() + onRemoveOptionClicked: function(idx) { removeOption(idx) } + onMoveOptionClicked: function(idx, dir) { moveOption(idx, dir) } + onOptionFieldChanged: function(idx, field, val) { updateOptionField(idx, field, val) } } CDivider { Layout.fillWidth: true } - // Preview section CText { variant: "body2"; text: "Preview"; font.bold: true } - CPaper { + DropdownPreview { Layout.fillWidth: true - implicitHeight: previewColumn.implicitHeight + 32 - - ColumnLayout { - id: previewColumn - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - CText { - variant: "caption" - text: "This is how the dropdown will render in forms:" - color: Theme.text - opacity: 0.6 - } - - // Simulated dropdown label - ColumnLayout { - Layout.fillWidth: true - spacing: 4 - - CText { - variant: "body2" - text: (selectedDropdown() ? selectedDropdown().name : "") + (selectedDropdown() && selectedDropdown().required ? " *" : "") - font.bold: true - } - - // Simulated select box - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - color: Theme.surface - border.color: Theme.border - border.width: 1 - radius: 4 - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 - - CText { - variant: "body1" - text: selectedDropdown() && selectedDropdown().options.length > 0 - ? selectedDropdown().options[0].label - : "No options" - Layout.fillWidth: true - color: Theme.text - } - CText { - variant: "body2" - text: "\u25BE" - color: Theme.text - opacity: 0.5 - } - } - } - - CText { - variant: "caption" - text: selectedDropdown() ? selectedDropdown().description : "" - color: Theme.text - opacity: 0.5 - } - } - - CDivider { Layout.fillWidth: true } - - // Simulated expanded dropdown list - CText { - variant: "caption" - text: "Expanded view:" - color: Theme.text - opacity: 0.6 - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: previewOptionsList.implicitHeight + 8 - color: Theme.surface - border.color: Theme.border - border.width: 1 - radius: 4 - - ColumnLayout { - id: previewOptionsList - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 4 - spacing: 0 - - Repeater { - model: selectedDropdown() ? selectedDropdown().options : [] - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - color: previewOptionMouse.containsMouse ? Theme.primary : "transparent" - opacity: previewOptionMouse.containsMouse ? 0.12 : 1.0 - radius: 2 - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 - - CText { - variant: "body1" - text: modelData.label - Layout.fillWidth: true - } - CText { - variant: "caption" - text: modelData.value - color: Theme.text - opacity: 0.4 - } - } - - MouseArea { - id: previewOptionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - } - } - } - } - } - - // Metadata summary - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CBadge { text: selectedDropdown() ? selectedDropdown().options.length + " options" : "0 options" } - CBadge { - text: selectedDropdown() && selectedDropdown().required ? "Required" : "Optional" - accent: selectedDropdown() ? selectedDropdown().required : false - } - CBadge { - text: selectedDropdown() && selectedDropdown().allowCustom ? "Custom values OK" : "Fixed options only" - } - } - } + dropdown: selectedDropdown() } - // Bottom spacer Item { Layout.preferredHeight: 16 } } } @@ -719,102 +209,32 @@ Rectangle { // Add Dropdown Dialog CDialog { - id: addDialog - visible: addDialogOpen - title: "Add New Dropdown" - + visible: addDialogOpen; title: "Add New Dropdown" ColumnLayout { - spacing: 16 - width: 400 - - CText { - variant: "body2" - text: "Create a new dropdown configuration. The name will be normalized to snake_case." - } - - CTextField { - label: "Dropdown Name" - placeholderText: "e.g. ticket_types" - text: newDropdownName - Layout.fillWidth: true - onTextChanged: newDropdownName = text - } - - CTextField { - label: "Description" - placeholderText: "What will this dropdown be used for?" - text: newDropdownDescription - Layout.fillWidth: true - onTextChanged: newDropdownDescription = text - } - - CAlert { - severity: "info" - text: "A default option will be added. You can configure options after creation." - Layout.fillWidth: true - } - + spacing: 16; width: 400 + CText { variant: "body2"; text: "Create a new dropdown configuration. The name will be normalized to snake_case." } + CTextField { label: "Dropdown Name"; placeholderText: "e.g. ticket_types"; text: newDropdownName; Layout.fillWidth: true; onTextChanged: newDropdownName = text } + CTextField { label: "Description"; placeholderText: "What will this dropdown be used for?"; text: newDropdownDescription; Layout.fillWidth: true; onTextChanged: newDropdownDescription = text } + CAlert { severity: "info"; text: "A default option will be added. You can configure options after creation."; Layout.fillWidth: true } FlexRow { - Layout.fillWidth: true - spacing: 8 - Item { Layout.fillWidth: true } - CButton { - text: "Cancel" - variant: "ghost" - onClicked: { - addDialogOpen = false - newDropdownName = "" - newDropdownDescription = "" - } - } - CButton { - text: "Create" - variant: "primary" - enabled: newDropdownName.trim() !== "" - onClicked: addDropdown() - } + Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true } + CButton { text: "Cancel"; variant: "ghost"; onClicked: { addDialogOpen = false; newDropdownName = ""; newDropdownDescription = "" } } + CButton { text: "Create"; variant: "primary"; enabled: newDropdownName.trim() !== ""; onClicked: addDropdown() } } } } // Delete Confirmation Dialog CDialog { - id: deleteDialog - visible: deleteDialogOpen - title: "Delete Dropdown" - + visible: deleteDialogOpen; title: "Delete Dropdown" ColumnLayout { - spacing: 16 - width: 400 - - CAlert { - severity: "warning" - text: "This action cannot be undone. Any forms referencing this dropdown will need to be updated." - Layout.fillWidth: true - } - - CText { - variant: "body1" - text: selectedDropdown() - ? "Are you sure you want to delete \"" + selectedDropdown().name + "\" with " + selectedDropdown().options.length + " options?" - : "" - wrapMode: Text.WordWrap - } - + spacing: 16; width: 400 + CAlert { severity: "warning"; text: "This action cannot be undone. Any forms referencing this dropdown will need to be updated."; Layout.fillWidth: true } + CText { variant: "body1"; text: selectedDropdown() ? "Are you sure you want to delete \"" + selectedDropdown().name + "\" with " + selectedDropdown().options.length + " options?" : ""; wrapMode: Text.WordWrap } FlexRow { - Layout.fillWidth: true - spacing: 8 - Item { Layout.fillWidth: true } - CButton { - text: "Cancel" - variant: "ghost" - onClicked: deleteDialogOpen = false - } - CButton { - text: "Delete" - variant: "danger" - onClicked: deleteSelectedDropdown() - } + Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true } + CButton { text: "Cancel"; variant: "ghost"; onClicked: deleteDialogOpen = false } + CButton { text: "Delete"; variant: "danger"; onClicked: deleteSelectedDropdown() } } } } diff --git a/frontends/qt6/MediaServicePanel.qml b/frontends/qt6/MediaServicePanel.qml index 193a88188..60a2ef7e7 100644 --- a/frontends/qt6/MediaServicePanel.qml +++ b/frontends/qt6/MediaServicePanel.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QmlComponents 1.0 import "qmllib/dbal" +import "qmllib/MetaBuilder" Rectangle { id: mediaPanel @@ -10,20 +11,12 @@ Rectangle { property int currentTab: 0 - // ── Service Health ─────────────────────────────────────────────────── - property string serviceStatus: "unknown" // unknown, online, offline + // Service Health + property string serviceStatus: "unknown" property string serviceVersion: "" property string lastHealthCheck: "" - // ── Job Submission Form ────────────────────────────────────────────── - property int jobTypeIndex: 0 - property var jobTypes: ["video", "audio", "document", "image"] - property string jobInputPath: "" - property string jobOutputPath: "" - property int jobPriorityIndex: 2 - property var jobPriorities: ["urgent", "high", "normal", "low"] - - // ── Jobs Data ──────────────────────────────────────────────────────── + // Jobs Data property var jobs: [ { id: "job-001", type: "video", status: "completed", progress: 100, created: "2026-03-19 08:12:34" }, { id: "job-002", type: "audio", status: "processing", progress: 67, created: "2026-03-19 09:45:01" }, @@ -32,43 +25,26 @@ Rectangle { { id: "job-005", type: "video", status: "processing", progress: 34, created: "2026-03-19 10:30:55" } ] - // ── Radio Data ─────────────────────────────────────────────────────── - property int selectedRadioIndex: 0 + // Radio Data property var radioChannels: [ { name: "MetaBuilder FM", status: "live", listeners: 142, currentTrack: "Synthwave Dreams - NeonCoder", bitrate: "320 kbps", - playlist: [ - "Synthwave Dreams - NeonCoder", - "Digital Horizon - ByteRunner", - "Midnight Protocol - CipherAce", - "Neon Streets - RetroVolt", - "Electric Soul - WaveForm" - ] + playlist: ["Synthwave Dreams - NeonCoder", "Digital Horizon - ByteRunner", "Midnight Protocol - CipherAce", "Neon Streets - RetroVolt", "Electric Soul - WaveForm"] }, { name: "Chiptune Radio", status: "live", listeners: 87, currentTrack: "8-Bit Adventure - PixelMaster", bitrate: "192 kbps", - playlist: [ - "8-Bit Adventure - PixelMaster", - "Game Over Theme - ChipTuner", - "Level Up! - BitCrafter", - "Boss Battle - NEStalgia" - ] + playlist: ["8-Bit Adventure - PixelMaster", "Game Over Theme - ChipTuner", "Level Up! - BitCrafter", "Boss Battle - NEStalgia"] }, { name: "Ambient Lounge", status: "offline", listeners: 0, currentTrack: "---", bitrate: "256 kbps", - playlist: [ - "Deep Focus - AmbientWave", - "Ocean Drift - CalmCode", - "Forest Rain - NatureByte" - ] + playlist: ["Deep Focus - AmbientWave", "Ocean Drift - CalmCode", "Forest Rain - NatureByte"] } ] - // ── TV Data ────────────────────────────────────────────────────────── - property int selectedTvIndex: 0 + // TV Data property var tvChannels: [ { name: "MetaBuilder TV", status: "broadcasting", resolution: "1080p", @@ -92,7 +68,7 @@ Rectangle { } ] - // ── Plugins Data ───────────────────────────────────────────────────── + // Plugins Data property var plugins: [ { name: "FFmpeg", version: "8.0.1", status: "active", capabilities: ["H.264", "H.265", "VP9", "AV1", "AAC", "FLAC", "Opus"] }, { name: "ImageMagick", version: "7.1.1", status: "active", capabilities: ["JPEG", "PNG", "WebP", "AVIF", "SVG", "TIFF", "Resize", "Crop"] }, @@ -108,7 +84,7 @@ Rectangle { { label: "Plugins" } ] - // ── Media Service HTTP Client ──────────────────────────────────────── + // Media Service HTTP Client QtObject { id: mediaService property string baseUrl: "http://localhost:8090" @@ -132,43 +108,26 @@ Rectangle { var url = baseUrl + endpoint xhr.open(method, url) xhr.setRequestHeader("Content-Type", "application/json") - if (body) { - xhr.send(JSON.stringify(body)) - } else { - xhr.send() - } + if (body) { xhr.send(JSON.stringify(body)) } else { xhr.send() } } function healthCheck() { serviceStatus = "unknown" request("GET", "/health", null, function(result, error) { - if (error) { - serviceStatus = "offline" - serviceVersion = "" - } else { - serviceStatus = "online" - serviceVersion = result.version || "" - } + if (error) { serviceStatus = "offline"; serviceVersion = "" } + else { serviceStatus = "online"; serviceVersion = result.version || "" } lastHealthCheck = Qt.formatDateTime(new Date(), "hh:mm:ss") }) - // Fallback: if no response after 3 seconds, mark offline healthTimeout.start() } function submitJob(type, input, output, priority) { - request("POST", "/api/jobs", { - type: type, - input: input, - output: output, - priority: priority - }, function(result, error) { + request("POST", "/api/jobs", { type: type, input: input, output: output, priority: priority }, function(result, error) { if (!error && result) { var updated = jobs.slice() updated.unshift({ id: result.id || ("job-" + (jobs.length + 1).toString().padStart(3, "0")), - type: type, - status: "queued", - progress: 0, + type: type, status: "queued", progress: 0, created: Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss") }) jobs = updated @@ -176,90 +135,33 @@ Rectangle { }) } - function cancelJob(jobId) { - request("DELETE", "/api/jobs/" + jobId, null, null) - } + function cancelJob(jobId) { request("DELETE", "/api/jobs/" + jobId, null, null) } } - Timer { - id: healthTimeout - interval: 3000 - repeat: false - onTriggered: { - if (serviceStatus === "unknown") { - serviceStatus = "offline" - lastHealthCheck = Qt.formatDateTime(new Date(), "hh:mm:ss") - } - } - } - - Timer { - id: healthPollTimer - interval: 30000 - repeat: true - running: true - onTriggered: mediaService.healthCheck() - } - - Component.onCompleted: { - mediaService.healthCheck() - } - - // ── Helper Functions ───────────────────────────────────────────────── - function jobStatusColor(status) { - switch (status) { - case "completed": return "success" - case "processing": return "warning" - case "queued": return "info" - case "failed": return "error" - default: return "info" - } - } + Timer { id: healthTimeout; interval: 3000; repeat: false; onTriggered: { if (serviceStatus === "unknown") { serviceStatus = "offline"; lastHealthCheck = Qt.formatDateTime(new Date(), "hh:mm:ss") } } } + Timer { id: healthPollTimer; interval: 30000; repeat: true; running: true; onTriggered: mediaService.healthCheck() } + Component.onCompleted: mediaService.healthCheck() function cancelJobById(jobId) { mediaService.cancelJob(jobId) - var updated = jobs.map(function(j) { - return j.id === jobId ? Object.assign({}, j, { status: "failed", progress: j.progress }) : j - }) + var updated = jobs.map(function(j) { return j.id === jobId ? Object.assign({}, j, { status: "failed", progress: j.progress }) : j }) jobs = updated } - function submitNewJob() { - if (jobInputPath.length === 0 || jobOutputPath.length === 0) return - var type = jobTypes[jobTypeIndex] - var priority = jobPriorities[jobPriorityIndex] - - // Try live service first - mediaService.submitJob(type, jobInputPath, jobOutputPath, priority) - - // Optimistic local update (mock fallback) - var newJob = { - id: "job-" + (jobs.length + 1).toString().padStart(3, "0"), - type: type, - status: "queued", - progress: 0, - created: Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss") - } + function submitNewJob(type, input, output, priority) { + if (input.length === 0 || output.length === 0) return + mediaService.submitJob(type, input, output, priority) + var newJob = { id: "job-" + (jobs.length + 1).toString().padStart(3, "0"), type: type, status: "queued", progress: 0, created: Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss") } var updated = jobs.slice() updated.unshift(newJob) jobs = updated - - jobInputPath = "" - jobOutputPath = "" } function toggleRadioStream(index) { var updated = radioChannels.slice() var ch = Object.assign({}, updated[index]) - if (ch.status === "live") { - ch.status = "offline" - ch.listeners = 0 - ch.currentTrack = "---" - } else { - ch.status = "live" - ch.listeners = Math.floor(Math.random() * 200) + 10 - ch.currentTrack = ch.playlist[0] - } + if (ch.status === "live") { ch.status = "offline"; ch.listeners = 0; ch.currentTrack = "---" } + else { ch.status = "live"; ch.listeners = Math.floor(Math.random() * 200) + 10; ch.currentTrack = ch.playlist[0] } updated[index] = ch radioChannels = updated } @@ -267,40 +169,21 @@ Rectangle { function toggleTvBroadcast(index) { var updated = tvChannels.slice() var ch = Object.assign({}, updated[index]) - if (ch.status === "broadcasting") { - ch.status = "offline" - ch.viewers = 0 - ch.uptime = "0m" - } else { - ch.status = "broadcasting" - ch.viewers = Math.floor(Math.random() * 500) + 20 - ch.uptime = "0m" - } + if (ch.status === "broadcasting") { ch.status = "offline"; ch.viewers = 0; ch.uptime = "0m" } + else { ch.status = "broadcasting"; ch.viewers = Math.floor(Math.random() * 500) + 20; ch.uptime = "0m" } updated[index] = ch tvChannels = updated } - function resolutionColor(res) { - switch (res) { - case "1080p": return Theme.success - case "720p": return Theme.warning - case "480p": return Theme.error - default: return Theme.textSecondary - } - } - - // ── Main Layout ────────────────────────────────────────────────────── + // Main Layout ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 16 - // ══════════════════════════════════════════ // Header - // ══════════════════════════════════════════ CCard { Layout.fillWidth: true - ColumnLayout { anchors.fill: parent anchors.margins: 20 @@ -309,821 +192,73 @@ Rectangle { FlexRow { Layout.fillWidth: true spacing: 12 - CText { variant: "h2"; text: "Media Service" } CBadge { text: "God Panel" } CStatusBadge { - status: serviceStatus === "online" ? "success" - : serviceStatus === "offline" ? "error" - : "warning" - text: serviceStatus === "online" ? "Online" - : serviceStatus === "offline" ? "Offline" - : "Checking..." + status: serviceStatus === "online" ? "success" : serviceStatus === "offline" ? "error" : "warning" + text: serviceStatus === "online" ? "Online" : serviceStatus === "offline" ? "Offline" : "Checking..." } - Item { Layout.fillWidth: true } - - CButton { - text: "Refresh" - variant: "ghost" - size: "sm" - onClicked: mediaService.healthCheck() - } + CButton { text: "Refresh"; variant: "ghost"; size: "sm"; onClicked: mediaService.healthCheck() } } - CDivider { Layout.fillWidth: true } - FlexRow { Layout.fillWidth: true spacing: 8 - CChip { text: jobs.length + " Jobs" } CChip { text: radioChannels.length + " Radio Channels" } CChip { text: tvChannels.length + " TV Channels" } CChip { text: plugins.length + " Plugins" } - Item { Layout.fillWidth: true } - - CText { - visible: lastHealthCheck.length > 0 - variant: "caption" - text: "Last check: " + lastHealthCheck - color: Theme.textSecondary - } - CText { - visible: serviceVersion.length > 0 - variant: "caption" - text: "v" + serviceVersion - color: Theme.textSecondary - } + CText { visible: lastHealthCheck.length > 0; variant: "caption"; text: "Last check: " + lastHealthCheck; color: Theme.textSecondary } + CText { visible: serviceVersion.length > 0; variant: "caption"; text: "v" + serviceVersion; color: Theme.textSecondary } } } } - // ── Tab bar ── - CTabBar { - id: tabBar - Layout.fillWidth: true - currentIndex: currentTab - onCurrentIndexChanged: currentTab = currentIndex - tabs: tabModel - } + CTabBar { id: tabBar; Layout.fillWidth: true; currentIndex: currentTab; onCurrentIndexChanged: currentTab = currentIndex; tabs: tabModel } - // ── Tab content ── StackLayout { Layout.fillWidth: true Layout.fillHeight: true currentIndex: currentTab - // ══════════════════════════════════════════ - // 0 - JOBS - // ══════════════════════════════════════════ + // Jobs tab Rectangle { color: "transparent" - ScrollView { anchors.fill: parent clip: true - ColumnLayout { width: parent.width spacing: 16 - // ── Job Submission Form ── - CCard { - Layout.fillWidth: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - CText { variant: "h4"; text: "Submit Job" } - CDivider { Layout.fillWidth: true } - - RowLayout { - Layout.fillWidth: true - spacing: 12 - - // Type selector - ColumnLayout { - Layout.preferredWidth: 200 - spacing: 4 - - CText { variant: "caption"; text: "Type" } - - RowLayout { - spacing: 4 - - Repeater { - model: jobTypes - delegate: CButton { - text: modelData - variant: jobTypeIndex === index ? "primary" : "ghost" - size: "sm" - onClicked: jobTypeIndex = index - } - } - } - } - - CTextField { - Layout.fillWidth: true - label: "Input Path" - placeholderText: "/media/input/video.mp4" - text: jobInputPath - onTextChanged: jobInputPath = text - } - - CTextField { - Layout.fillWidth: true - label: "Output Path" - placeholderText: "/media/output/video.webm" - text: jobOutputPath - onTextChanged: jobOutputPath = text - } - } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - // Priority selector - CText { variant: "caption"; text: "Priority:" } - - Repeater { - model: jobPriorities - delegate: CButton { - text: modelData - variant: jobPriorityIndex === index ? "primary" : "ghost" - size: "sm" - onClicked: jobPriorityIndex = index - } - } - - Item { Layout.fillWidth: true } - - CButton { - text: "Submit Job" - variant: "primary" - enabled: jobInputPath.length > 0 && jobOutputPath.length > 0 - onClicked: submitNewJob() - } - } - } - } - - // ── Active Jobs Table ── - CCard { - Layout.fillWidth: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { variant: "h4"; text: "Active Jobs" } - CText { variant: "caption"; text: jobs.length + " total"; color: Theme.textSecondary } - } - - CDivider { Layout.fillWidth: true } - - // Table header - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { variant: "caption"; text: "ID"; Layout.preferredWidth: 100 } - CText { variant: "caption"; text: "Type"; Layout.preferredWidth: 80 } - CText { variant: "caption"; text: "Status"; Layout.preferredWidth: 100 } - CText { variant: "caption"; text: "Progress"; Layout.fillWidth: true } - CText { variant: "caption"; text: "Created"; Layout.preferredWidth: 160 } - CText { variant: "caption"; text: ""; Layout.preferredWidth: 70 } - } - - CDivider { Layout.fillWidth: true } - - // Job rows - Repeater { - model: jobs - - delegate: ColumnLayout { - Layout.fillWidth: true - spacing: 4 - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { - variant: "body2" - text: modelData.id - font.family: "monospace" - Layout.preferredWidth: 100 - } - - CBadge { - text: modelData.type - Layout.preferredWidth: 80 - } - - CStatusBadge { - status: jobStatusColor(modelData.status) - text: modelData.status - Layout.preferredWidth: 100 - } - - // Progress bar area - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 20 - color: "transparent" - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - height: 6 - radius: 3 - color: Theme.border - - Rectangle { - width: parent.width * (modelData.progress / 100) - height: parent.height - radius: 3 - color: modelData.status === "failed" ? Theme.error - : modelData.status === "completed" ? Theme.success - : Theme.primary - } - } - - CText { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - variant: "caption" - text: modelData.progress + "%" - } - } - - CText { - variant: "caption" - text: modelData.created - Layout.preferredWidth: 160 - color: Theme.textSecondary - } - - CButton { - text: "Cancel" - variant: "danger" - size: "sm" - enabled: modelData.status === "queued" || modelData.status === "processing" - visible: modelData.status !== "completed" && modelData.status !== "failed" - Layout.preferredWidth: 70 - onClicked: cancelJobById(modelData.id) - } - - // Placeholder for completed/failed jobs - Item { - visible: modelData.status === "completed" || modelData.status === "failed" - Layout.preferredWidth: 70 - } - } - - CDivider { - Layout.fillWidth: true - visible: index < jobs.length - 1 - } - } - } + MediaJobForm { + id: jobForm + onSubmitRequested: { + submitNewJob(jobForm.jobTypes[jobForm.jobTypeIndex], jobForm.jobInputPath, jobForm.jobOutputPath, jobForm.jobPriorities[jobForm.jobPriorityIndex]) + jobForm.jobInputPath = "" + jobForm.jobOutputPath = "" } } + MediaJobTable { jobs: mediaPanel.jobs; onCancelRequested: function(jobId) { cancelJobById(jobId) } } Item { Layout.preferredHeight: 8 } } } } - // ══════════════════════════════════════════ - // 1 - RADIO - // ══════════════════════════════════════════ - Rectangle { - color: "transparent" + // Radio tab + MediaRadioTab { radioChannels: mediaPanel.radioChannels; onToggleStream: function(index) { toggleRadioStream(index) } } - RowLayout { - anchors.fill: parent - spacing: 16 + // TV tab + MediaTvTab { tvChannels: mediaPanel.tvChannels; onToggleBroadcast: function(index) { toggleTvBroadcast(index) } } - // ── Channel List ── - CCard { - Layout.preferredWidth: 320 - Layout.fillHeight: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { variant: "h4"; text: "Radio Channels" } - CText { variant: "caption"; text: radioChannels.length + " channels"; color: Theme.textSecondary } - } - - CDivider { Layout.fillWidth: true } - - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - model: radioChannels - spacing: 4 - clip: true - - delegate: CListItem { - width: parent ? parent.width : 288 - title: modelData.name - subtitle: modelData.status === "live" - ? modelData.listeners + " listeners" - : "Offline" - selected: index === selectedRadioIndex - onClicked: selectedRadioIndex = index - } - } - } - } - - // ── Channel Detail ── - CCard { - Layout.fillWidth: true - Layout.fillHeight: true - - Flickable { - anchors.fill: parent - anchors.margins: 16 - contentHeight: radioDetailCol.implicitHeight - clip: true - - ColumnLayout { - id: radioDetailCol - width: parent.width - spacing: 16 - - // Channel header - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { variant: "h3"; text: radioChannels[selectedRadioIndex].name } - CStatusBadge { - status: radioChannels[selectedRadioIndex].status === "live" ? "success" : "error" - text: radioChannels[selectedRadioIndex].status === "live" ? "Live" : "Offline" - } - - Item { Layout.fillWidth: true } - - CButton { - text: radioChannels[selectedRadioIndex].status === "live" ? "Stop Stream" : "Start Stream" - variant: radioChannels[selectedRadioIndex].status === "live" ? "danger" : "primary" - onClicked: toggleRadioStream(selectedRadioIndex) - } - } - - CDivider { Layout.fillWidth: true } - - // Stats row - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Listeners" } - CText { variant: "h4"; text: radioChannels[selectedRadioIndex].listeners.toString() } - } - } - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Bitrate" } - CText { variant: "h4"; text: radioChannels[selectedRadioIndex].bitrate } - } - } - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Now Playing" } - CText { - variant: "body2" - text: radioChannels[selectedRadioIndex].currentTrack - elide: Text.ElideRight - Layout.fillWidth: true - } - } - } - } - - CDivider { Layout.fillWidth: true } - - // Playlist - CText { variant: "subtitle1"; text: "Playlist" } - CText { - variant: "caption" - text: radioChannels[selectedRadioIndex].playlist.length + " tracks" - color: Theme.textSecondary - } - - Repeater { - model: radioChannels[selectedRadioIndex].playlist - - delegate: FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { - variant: "caption" - text: (index + 1).toString().padStart(2, " ") + "." - font.family: "monospace" - color: Theme.textSecondary - } - - CText { - variant: "body2" - text: modelData - Layout.fillWidth: true - } - - CStatusBadge { - visible: modelData === radioChannels[selectedRadioIndex].currentTrack - && radioChannels[selectedRadioIndex].status === "live" - status: "success" - text: "Playing" - } - } - } - - Item { Layout.preferredHeight: 8 } - } - } - } - } - } - - // ══════════════════════════════════════════ - // 2 - TV - // ══════════════════════════════════════════ - Rectangle { - color: "transparent" - - RowLayout { - anchors.fill: parent - spacing: 16 - - // ── Channel List ── - CCard { - Layout.preferredWidth: 320 - Layout.fillHeight: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { variant: "h4"; text: "TV Channels" } - CText { variant: "caption"; text: tvChannels.length + " channels"; color: Theme.textSecondary } - } - - CDivider { Layout.fillWidth: true } - - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - model: tvChannels - spacing: 4 - clip: true - - delegate: CListItem { - width: parent ? parent.width : 288 - title: modelData.name - subtitle: modelData.status === "broadcasting" - ? modelData.viewers + " viewers" - : "Offline" - selected: index === selectedTvIndex - onClicked: selectedTvIndex = index - } - } - } - } - - // ── Channel Detail ── - CCard { - Layout.fillWidth: true - Layout.fillHeight: true - - Flickable { - anchors.fill: parent - anchors.margins: 16 - contentHeight: tvDetailCol.implicitHeight - clip: true - - ColumnLayout { - id: tvDetailCol - width: parent.width - spacing: 16 - - // Channel header - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { variant: "h3"; text: tvChannels[selectedTvIndex].name } - CStatusBadge { - status: tvChannels[selectedTvIndex].status === "broadcasting" ? "success" : "error" - text: tvChannels[selectedTvIndex].status === "broadcasting" ? "Broadcasting" : "Offline" - } - - // Resolution badge - Rectangle { - width: resLabel.implicitWidth + 16 - height: 24 - radius: 4 - color: resolutionColor(tvChannels[selectedTvIndex].resolution) - opacity: 0.15 - - CText { - id: resLabel - anchors.centerIn: parent - variant: "caption" - text: tvChannels[selectedTvIndex].resolution - color: resolutionColor(tvChannels[selectedTvIndex].resolution) - font.bold: true - } - } - - Item { Layout.fillWidth: true } - - CButton { - text: tvChannels[selectedTvIndex].status === "broadcasting" ? "Stop Broadcast" : "Start Broadcast" - variant: tvChannels[selectedTvIndex].status === "broadcasting" ? "danger" : "primary" - onClicked: toggleTvBroadcast(selectedTvIndex) - } - } - - CDivider { Layout.fillWidth: true } - - // Stats row - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Viewers" } - CText { variant: "h4"; text: tvChannels[selectedTvIndex].viewers.toString() } - } - } - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Resolution" } - CText { variant: "h4"; text: tvChannels[selectedTvIndex].resolution } - } - } - - CPaper { - Layout.fillWidth: true - implicitHeight: 60 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 2 - CText { variant: "caption"; text: "Uptime" } - CText { variant: "h4"; text: tvChannels[selectedTvIndex].uptime } - } - } - } - - CDivider { Layout.fillWidth: true } - - // Schedule - CText { variant: "subtitle1"; text: "Schedule" } - CText { - variant: "caption" - text: tvChannels[selectedTvIndex].schedule.length + " programs" - color: Theme.textSecondary - } - - // Schedule table header - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { variant: "caption"; text: "Time"; Layout.preferredWidth: 80 } - CText { variant: "caption"; text: "Program"; Layout.fillWidth: true } - CText { variant: "caption"; text: "Duration"; Layout.preferredWidth: 80 } - } - - CDivider { Layout.fillWidth: true } - - Repeater { - model: tvChannels[selectedTvIndex].schedule - - delegate: ColumnLayout { - Layout.fillWidth: true - spacing: 4 - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { - variant: "body2" - text: modelData.time - font.family: "monospace" - font.bold: true - Layout.preferredWidth: 80 - } - - CText { - variant: "body2" - text: modelData.program - Layout.fillWidth: true - } - - CText { - variant: "caption" - text: modelData.duration - color: Theme.textSecondary - Layout.preferredWidth: 80 - } - } - - CDivider { - Layout.fillWidth: true - visible: index < tvChannels[selectedTvIndex].schedule.length - 1 - } - } - } - - Item { Layout.preferredHeight: 8 } - } - } - } - } - } - - // ══════════════════════════════════════════ - // 3 - PLUGINS - // ══════════════════════════════════════════ - Rectangle { - color: "transparent" - - ScrollView { - anchors.fill: parent - clip: true - - ColumnLayout { - width: parent.width - spacing: 16 - - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { variant: "h3"; text: "Installed Plugins" } - CText { variant: "caption"; text: plugins.length + " plugins"; color: Theme.textSecondary } - - Item { Layout.fillWidth: true } - - CButton { - text: "Reload All (Dev)" - variant: "ghost" - size: "sm" - onClicked: { - mediaService.request("POST", "/api/plugins/reload", null, null) - } - } - } - - CDivider { Layout.fillWidth: true } - - // Plugin grid (2 columns) - GridLayout { - Layout.fillWidth: true - columns: 2 - columnSpacing: 16 - rowSpacing: 16 - - Repeater { - model: plugins - - delegate: CCard { - Layout.fillWidth: true - variant: "outlined" - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 10 - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CText { variant: "subtitle1"; text: modelData.name } - - Item { Layout.fillWidth: true } - - CStatusBadge { - status: modelData.status === "active" ? "success" : "warning" - text: modelData.status - } - } - - CText { - variant: "caption" - text: "v" + modelData.version - color: Theme.textSecondary - } - - CDivider { Layout.fillWidth: true } - - CText { variant: "caption"; text: "Capabilities" } - - Flow { - Layout.fillWidth: true - spacing: 6 - - Repeater { - model: modelData.capabilities - - delegate: CChip { - text: modelData - } - } - } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - Item { Layout.fillWidth: true } - - CButton { - text: "Reload" - variant: "ghost" - size: "sm" - onClicked: { - mediaService.request("POST", "/api/plugins/" + plugins[index].name.toLowerCase() + "/reload", null, null) - } - } - } - } - } - } - } - - Item { Layout.preferredHeight: 8 } - } - } + // Plugins tab + MediaPluginsTab { + plugins: mediaPanel.plugins + onReloadAll: mediaService.request("POST", "/api/plugins/reload", null, null) + onReloadPlugin: function(name) { mediaService.request("POST", "/api/plugins/" + name + "/reload", null, null) } } } } diff --git a/frontends/qt6/PageRoutesManager.qml b/frontends/qt6/PageRoutesManager.qml index 6b3da2d4d..5142e747e 100644 --- a/frontends/qt6/PageRoutesManager.qml +++ b/frontends/qt6/PageRoutesManager.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QmlComponents 1.0 import "qmllib/dbal" +import "qmllib/MetaBuilder" Rectangle { id: root @@ -16,26 +17,9 @@ Rectangle { property bool addDialogVisible: false property bool deleteDialogVisible: false - property string newPath: "" - property string newTitle: "" - property int newLevel: 1 - property string newLayout: "default" - property var layoutOptions: ["default", "sidebar", "dashboard", "blank"] property var levelOptions: [1, 2, 3, 4, 5] - property var mockRoutes: [ - { path: "/", title: "Home", level: 1, layout: "default", enabled: true, permissions: "public" }, - { path: "/dashboard", title: "Dashboard", level: 1, layout: "dashboard", enabled: true, permissions: "authenticated" }, - { path: "/admin", title: "Admin Panel", level: 3, layout: "sidebar", enabled: true, permissions: "role:admin" }, - { path: "/forum", title: "Forum", level: 1, layout: "sidebar", enabled: true, permissions: "authenticated" }, - { path: "/gallery", title: "Gallery", level: 1, layout: "default", enabled: true, permissions: "public" }, - { path: "/profile", title: "Profile", level: 1, layout: "sidebar", enabled: true, permissions: "authenticated" }, - { path: "/settings", title: "Settings", level: 2, layout: "sidebar", enabled: true, permissions: "authenticated" }, - { path: "/god-panel", title: "God Panel", level: 4, layout: "dashboard", enabled: true, permissions: "role:god" }, - { path: "/supergod", title: "Super God", level: 5, layout: "blank", enabled: false, permissions: "role:supergod" } - ] - property var routes: [ { path: "/", title: "Home", level: 1, layout: "default", enabled: true, permissions: "public" }, { path: "/dashboard", title: "Dashboard", level: 1, layout: "dashboard", enabled: true, permissions: "authenticated" }, @@ -54,551 +38,141 @@ Rectangle { route[field] = value updated[index] = route routes = updated - if (selectedIndex === index) { - selectedIndex = -1 - selectedIndex = index - } + if (selectedIndex === index) { selectedIndex = -1; selectedIndex = index } if (useLiveData) saveRoute(index) } - function addRoute() { - if (newPath.length === 0 || newTitle.length === 0) return - var newRoute = { - path: newPath, - title: newTitle, - level: newLevel, - layout: newLayout, - enabled: true, - permissions: "authenticated" - } + function addRoute(path, title, level, layout) { + var newRoute = { path: path, title: title, level: level, layout: layout, enabled: true, permissions: "authenticated" } if (useLiveData) { - dbal.create("ui_page", newRoute, function(result, error) { - if (!error) loadRoutes() - }) + dbal.create("ui_page", newRoute, function(result, error) { if (!error) loadRoutes() }) } else { - var updated = routes.slice() - updated.push(newRoute) - routes = updated + var updated = routes.slice(); updated.push(newRoute); routes = updated } - newPath = "" - newTitle = "" - newLevel = 1 - newLayout = "default" - addDialogVisible = false } function deleteRoute() { if (selectedIndex < 0 || selectedIndex >= routes.length) return if (useLiveData && routes[selectedIndex].id) { - dbal.remove("ui_page", routes[selectedIndex].id, function(result, error) { - if (!error) loadRoutes() - }) + dbal.remove("ui_page", routes[selectedIndex].id, function(result, error) { if (!error) loadRoutes() }) } else { - var updated = routes.slice() - updated.splice(selectedIndex, 1) - routes = updated + var updated = routes.slice(); updated.splice(selectedIndex, 1); routes = updated } - selectedIndex = -1 - deleteDialogVisible = false + selectedIndex = -1; deleteDialogVisible = false } function moveRoute(fromIndex, direction) { var toIndex = fromIndex + direction if (toIndex < 0 || toIndex >= routes.length) return var updated = routes.slice() - var temp = updated[fromIndex] - updated[fromIndex] = updated[toIndex] - updated[toIndex] = temp - routes = updated - selectedIndex = toIndex - } - - function levelColor(level) { - if (level <= 1) return "#4caf50" - if (level === 2) return "#8bc34a" - if (level === 3) return "#ff9800" - if (level === 4) return "#f44336" - return "#9c27b0" + var temp = updated[fromIndex]; updated[fromIndex] = updated[toIndex]; updated[toIndex] = temp + routes = updated; selectedIndex = toIndex } // ── DBAL Integration ───────────────────────────────────────────── - function loadRoutes() { dbal.list("ui_page", { take: 100 }, function(result, error) { if (!error && result && result.items && result.items.length > 0) { var parsed = [] for (var i = 0; i < result.items.length; i++) { var r = result.items[i] - parsed.push({ - id: r.id || undefined, - path: r.path || r.route || "", - title: r.title || r.name || "", - level: r.level || 1, - layout: r.layout || "default", - enabled: r.enabled !== undefined ? r.enabled : true, - permissions: r.permissions || "public" - }) + parsed.push({ id: r.id || undefined, path: r.path || r.route || "", title: r.title || r.name || "", + level: r.level || 1, layout: r.layout || "default", enabled: r.enabled !== undefined ? r.enabled : true, permissions: r.permissions || "public" }) } routes = parsed } - // On error or empty result, keep existing mock routes as fallback }) } - onUseLiveDataChanged: { - if (useLiveData) loadRoutes() - } - - Component.onCompleted: { - loadRoutes() - } - - // ── CRUD wiring ────────────────────────────────────────────────── - function saveRoute(index) { if (!useLiveData) return var route = routes[index] var data = { path: route.path, title: route.title, level: route.level, layout: route.layout, enabled: route.enabled, permissions: route.permissions } - if (route.id) { - dbal.update("ui_page", route.id, data, function(result, error) { - if (!error) loadRoutes() - }) - } else { - dbal.create("ui_page", data, function(result, error) { - if (!error) loadRoutes() - }) - } + if (route.id) dbal.update("ui_page", route.id, data, function(r, e) { if (!e) loadRoutes() }) + else dbal.create("ui_page", data, function(r, e) { if (!e) loadRoutes() }) } + onUseLiveDataChanged: { if (useLiveData) loadRoutes() } + Component.onCompleted: { loadRoutes() } + + // ── UI ─────────────────────────────────────────────────────────── ColumnLayout { - anchors.fill: parent - anchors.margins: 20 - spacing: 16 + anchors.fill: parent; anchors.margins: 20; spacing: 16 FlexRow { - Layout.fillWidth: true - spacing: 12 - + Layout.fillWidth: true; spacing: 12 CText { variant: "h3"; text: "Page Routes Manager" } Item { Layout.fillWidth: true } CBadge { text: routes.length + " routes" } - CButton { - text: "Add Route" - variant: "primary" - size: "sm" - onClicked: addDialogVisible = true - } + CButton { text: "Add Route"; variant: "primary"; size: "sm"; onClicked: addDialogVisible = true } } RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 16 + Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16 CCard { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: 580 + Layout.fillWidth: true; Layout.fillHeight: true; Layout.preferredWidth: 580 ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 0 - - Rectangle { - Layout.fillWidth: true - height: 40 - color: Theme.surface - radius: 4 - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 0 - - CText { - variant: "caption" - text: "ORDER" - Layout.preferredWidth: 60 - } - CText { - variant: "caption" - text: "PATH" - Layout.preferredWidth: 120 - } - CText { - variant: "caption" - text: "TITLE" - Layout.preferredWidth: 120 - } - CText { - variant: "caption" - text: "LEVEL" - Layout.preferredWidth: 60 - } - CText { - variant: "caption" - text: "LAYOUT" - Layout.preferredWidth: 100 - } - CText { - variant: "caption" - text: "STATUS" - Layout.fillWidth: true - } - } - } + anchors.fill: parent; anchors.margins: 16; spacing: 0 + CRouteTableHeader { } CDivider { Layout.fillWidth: true } ListView { id: routeList - Layout.fillWidth: true - Layout.fillHeight: true - model: routes - clip: true - spacing: 1 + Layout.fillWidth: true; Layout.fillHeight: true + model: routes; clip: true; spacing: 1 - delegate: Rectangle { - width: routeList.width - height: 48 - color: index === selectedIndex ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : (routeHover.hovered ? Theme.surface : "transparent") - radius: 4 - - HoverHandler { id: routeHover } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: selectedIndex = (selectedIndex === index ? -1 : index) - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 0 - - RowLayout { - Layout.preferredWidth: 60 - spacing: 2 - - CButton { - text: "\u25B2" - variant: "ghost" - size: "sm" - enabled: index > 0 - onClicked: moveRoute(index, -1) - } - CButton { - text: "\u25BC" - variant: "ghost" - size: "sm" - enabled: index < routes.length - 1 - onClicked: moveRoute(index, 1) - } - } - - CText { - variant: "body2" - text: modelData.path - Layout.preferredWidth: 120 - color: Theme.primary - } - - CText { - variant: "body2" - text: modelData.title - Layout.preferredWidth: 120 - } - - Rectangle { - Layout.preferredWidth: 60 - height: 24 - width: 32 - radius: 12 - color: levelColor(modelData.level) - - CText { - anchors.centerIn: parent - variant: "caption" - text: modelData.level.toString() - color: "#ffffff" - } - } - - CChip { - text: modelData.layout - Layout.preferredWidth: 100 - } - - Rectangle { - Layout.fillWidth: true - height: 10 - width: 10 - radius: 5 - color: modelData.enabled ? "#4caf50" : "#9e9e9e" - Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: 10 - Layout.preferredHeight: 10 - } - } + delegate: CRouteTableRow { + routeData: modelData + isSelected: index === selectedIndex + routeIndex: index + routeCount: routes.length + onClicked: selectedIndex = (selectedIndex === index ? -1 : index) + onMoveUp: moveRoute(index, -1) + onMoveDown: moveRoute(index, 1) } } } } - CCard { - Layout.preferredWidth: 340 - Layout.fillHeight: true + CRouteEditPanel { + Layout.preferredWidth: 340; Layout.fillHeight: true visible: selectedIndex >= 0 && selectedIndex < routes.length - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 14 - - CText { variant: "h4"; text: "Edit Route" } - CDivider { Layout.fillWidth: true } - - CText { variant: "caption"; text: "Path" } - CTextField { - Layout.fillWidth: true - text: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].path : "" - onTextChanged: { - if (selectedIndex >= 0 && selectedIndex < routes.length && text !== routes[selectedIndex].path) { - updateRoute(selectedIndex, "path", text) - } - } - } - - CText { variant: "caption"; text: "Page Title" } - CTextField { - Layout.fillWidth: true - text: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].title : "" - onTextChanged: { - if (selectedIndex >= 0 && selectedIndex < routes.length && text !== routes[selectedIndex].title) { - updateRoute(selectedIndex, "title", text) - } - } - } - - CText { variant: "caption"; text: "Required Level (1-5)" } - CSelect { - Layout.fillWidth: true - model: levelOptions - currentIndex: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].level - 1 : 0 - onCurrentIndexChanged: { - if (selectedIndex >= 0 && selectedIndex < routes.length) { - var newLvl = currentIndex + 1 - if (newLvl !== routes[selectedIndex].level) { - updateRoute(selectedIndex, "level", newLvl) - } - } - } - } - - CText { variant: "caption"; text: "Layout Type" } - CSelect { - Layout.fillWidth: true - model: layoutOptions - currentIndex: { - if (selectedIndex >= 0 && selectedIndex < routes.length) { - return layoutOptions.indexOf(routes[selectedIndex].layout) - } - return 0 - } - onCurrentIndexChanged: { - if (selectedIndex >= 0 && selectedIndex < routes.length) { - var newLayoutVal = layoutOptions[currentIndex] - if (newLayoutVal !== routes[selectedIndex].layout) { - updateRoute(selectedIndex, "layout", newLayoutVal) - } - } - } - } - - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CText { variant: "body2"; text: "Enabled" } - Item { Layout.fillWidth: true } - CSwitch { - checked: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].enabled : false - onCheckedChanged: { - if (selectedIndex >= 0 && selectedIndex < routes.length && checked !== routes[selectedIndex].enabled) { - updateRoute(selectedIndex, "enabled", checked) - } - } - } - } - - CDivider { Layout.fillWidth: true } - - CText { variant: "caption"; text: "Permission Rules" } - CTextField { - Layout.fillWidth: true - text: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].permissions : "" - onTextChanged: { - if (selectedIndex >= 0 && selectedIndex < routes.length && text !== routes[selectedIndex].permissions) { - updateRoute(selectedIndex, "permissions", text) - } - } - } - - CAlert { - Layout.fillWidth: true - severity: selectedIndex >= 0 && selectedIndex < routes.length && routes[selectedIndex].level >= 4 ? "warning" : "info" - text: selectedIndex >= 0 && selectedIndex < routes.length && routes[selectedIndex].level >= 4 - ? "High privilege route (level " + routes[selectedIndex].level + ")" - : "Standard access route" - } - - Item { Layout.fillHeight: true } - - CButton { - text: "Delete Route" - variant: "danger" - size: "sm" - Layout.fillWidth: true - onClicked: deleteDialogVisible = true - } - } + route: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex] : null + layoutOptions: root.layoutOptions; levelOptions: root.levelOptions + onFieldChanged: function(field, value) { updateRoute(selectedIndex, field, value) } + onDeleteRequested: deleteDialogVisible = true } CCard { - Layout.preferredWidth: 340 - Layout.fillHeight: true + Layout.preferredWidth: 340; Layout.fillHeight: true visible: selectedIndex < 0 || selectedIndex >= routes.length ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - + anchors.fill: parent; anchors.margins: 16; spacing: 12 Item { Layout.fillHeight: true } - CText { - variant: "body2" - text: "Select a route from the table to edit its configuration." - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - } + CText { variant: "body2"; text: "Select a route from the table to edit its configuration."; Layout.fillWidth: true; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap } Item { Layout.fillHeight: true } } } } } - CDialog { + CAddRouteDialog { id: addDialog visible: addDialogVisible - title: "Add New Route" - - ColumnLayout { - spacing: 12 - width: 320 - - CText { variant: "caption"; text: "Path" } - CTextField { - Layout.fillWidth: true - placeholderText: "/new-page" - text: newPath - onTextChanged: newPath = text - } - - CText { variant: "caption"; text: "Page Title" } - CTextField { - Layout.fillWidth: true - placeholderText: "New Page" - text: newTitle - onTextChanged: newTitle = text - } - - CText { variant: "caption"; text: "Required Level" } - CSelect { - Layout.fillWidth: true - model: levelOptions - currentIndex: newLevel - 1 - onCurrentIndexChanged: newLevel = currentIndex + 1 - } - - CText { variant: "caption"; text: "Layout Type" } - CSelect { - Layout.fillWidth: true - model: layoutOptions - currentIndex: layoutOptions.indexOf(newLayout) - onCurrentIndexChanged: newLayout = layoutOptions[currentIndex] - } - - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CButton { - text: "Cancel" - variant: "ghost" - onClicked: { - addDialogVisible = false - newPath = "" - newTitle = "" - newLevel = 1 - newLayout = "default" - } - } - Item { Layout.fillWidth: true } - CButton { - text: "Add Route" - variant: "primary" - enabled: newPath.length > 0 && newTitle.length > 0 - onClicked: addRoute() - } - } - } + layoutOptions: root.layoutOptions; levelOptions: root.levelOptions + onAddRoute: function(path, title, level, layout) { root.addRoute(path, title, level, layout) } } - CDialog { + CDeleteConfirmDialog { id: deleteDialog visible: deleteDialogVisible title: "Delete Route" - - ColumnLayout { - spacing: 16 - width: 320 - - CAlert { - Layout.fillWidth: true - severity: "warning" - text: selectedIndex >= 0 && selectedIndex < routes.length - ? "Are you sure you want to delete \"" + routes[selectedIndex].path + "\"?" - : "No route selected." - } - - CText { - variant: "body2" - text: "This action cannot be undone. The route will be permanently removed from the configuration." - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - FlexRow { - Layout.fillWidth: true - spacing: 12 - - CButton { - text: "Cancel" - variant: "ghost" - onClicked: deleteDialogVisible = false - } - Item { Layout.fillWidth: true } - CButton { - text: "Delete" - variant: "danger" - onClicked: deleteRoute() - } - } - } + itemName: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].path : "" + description: "This action cannot be undone. The route will be permanently removed from the configuration." + onConfirmed: deleteRoute() } } diff --git a/frontends/qt6/UserManagement.qml b/frontends/qt6/UserManagement.qml index ff27fff6e..ce3296ebe 100644 --- a/frontends/qt6/UserManagement.qml +++ b/frontends/qt6/UserManagement.qml @@ -3,15 +3,14 @@ import QtQuick.Controls import QtQuick.Layouts import QmlComponents 1.0 import "qmllib/dbal" +import "qmllib/MetaBuilder" Rectangle { id: root color: Theme.background // ── DBAL ────────────────────────────────────────────────────────── - DBALProvider { id: dbal } - property bool useLiveData: dbal.connected // ── Local state ────────────────────────────────────────────────── @@ -26,757 +25,178 @@ Rectangle { property string activeRoleFilter: "all" property int nextUid: 5 - // Dialog state property bool createDialogOpen: false property bool editDialogOpen: false property bool deleteDialogOpen: false property int editIndex: -1 property int deleteIndex: -1 - // Form fields - property string formUsername: "" - property string formEmail: "" - property string formPassword: "" - property string formRole: "user" - property bool formActive: true - readonly property var roles: ["user", "admin", "god", "supergod"] - property var mockUsers: JSON.parse(JSON.stringify(users)) - // ── DBAL Integration ───────────────────────────────────────────── - function loadUsers() { dbal.list("user", { take: 50 }, function(result, error) { if (!error && result && result.items && result.items.length > 0) { var parsed = [] for (var i = 0; i < result.items.length; i++) { var u = result.items[i] - parsed.push({ - uid: u.id || u.uid || (i + 1), - username: u.username || "", - email: u.email || "", - role: u.role || "user", - level: levelForRole(u.role || "user"), - status: u.status || "active", - created: u.createdAt ? u.createdAt.slice(0, 10) : (u.created || "") - }) + parsed.push({ uid: u.id || u.uid || (i + 1), username: u.username || "", email: u.email || "", role: u.role || "user", level: levelForRole(u.role || "user"), status: u.status || "active", created: u.createdAt ? u.createdAt.slice(0, 10) : (u.created || "") }) } - users = parsed - nextUid = parsed.length + 1 + users = parsed; nextUid = parsed.length + 1 } - // On error or empty result, keep existing mock users as fallback }) } - - onUseLiveDataChanged: { - if (useLiveData) loadUsers() - } - - Component.onCompleted: { - loadUsers() - } + onUseLiveDataChanged: { if (useLiveData) loadUsers() } + Component.onCompleted: { loadUsers() } // ── Helpers ────────────────────────────────────────────────────── - function initials(name) { - var parts = name.split("") - return (parts[0] || "").toUpperCase() + (parts[1] || "").toUpperCase() + function levelForRole(role) { + if (role === "user") return 2; if (role === "admin") return 3 + if (role === "god") return 4; if (role === "supergod") return 5; return 1 } - function roleColor(role) { - if (role === "supergod") return "#e040fb" - if (role === "god") return "#ff9800" - if (role === "admin") return "#2196f3" - return "#4caf50" - } - - function statusString(s) { return s === "active" ? "success" : "warning" } - - function countByRole(role) { - return users.filter(function(u) { return u.role === role }).length - } + function countByRole(role) { return users.filter(function(u) { return u.role === role }).length } function filteredUsers() { var q = searchText.toLowerCase() return users.filter(function(u) { var matchesRole = activeRoleFilter === "all" || u.role === activeRoleFilter - var matchesSearch = q === "" || - u.username.toLowerCase().indexOf(q) !== -1 || - u.email.toLowerCase().indexOf(q) !== -1 || - u.role.toLowerCase().indexOf(q) !== -1 + var matchesSearch = q === "" || u.username.toLowerCase().indexOf(q) !== -1 || u.email.toLowerCase().indexOf(q) !== -1 || u.role.toLowerCase().indexOf(q) !== -1 return matchesRole && matchesSearch }) } - function clearForm() { - formUsername = "" - formEmail = "" - formPassword = "" - formRole = "user" - formActive = true + function findUserIndex(uid) { + for (var i = 0; i < users.length; i++) { if (users[i].uid === uid) return i } + return -1 } - function openCreateDialog() { - clearForm() - createDialogOpen = true - } - - function openEditDialog(idx) { - var u = users[idx] - formUsername = u.username - formEmail = u.email - formPassword = "" - formRole = u.role - formActive = u.status === "active" - editIndex = idx - editDialogOpen = true - } - - function openDeleteDialog(idx) { - deleteIndex = idx - deleteDialogOpen = true - } - - function levelForRole(role) { - if (role === "user") return 2 - if (role === "admin") return 3 - if (role === "god") return 4 - if (role === "supergod") return 5 - return 1 - } - - function createUser() { - if (formUsername === "" || formEmail === "") return - var userData = { - username: formUsername, - email: formEmail, - role: formRole, - status: formActive ? "active" : "inactive" - } - + function createUser(userData) { if (useLiveData) { dbal.create("user", userData, function(result, error) { - if (!error) { - loadUsers() - } else { - createUserLocally(userData) - } + if (!error) loadUsers(); else createUserLocally(userData) createDialogOpen = false - clearForm() }) - } else { - createUserLocally(userData) - createDialogOpen = false - clearForm() - } + } else { createUserLocally(userData); createDialogOpen = false } } function createUserLocally(userData) { - var newUser = { - uid: nextUid, - username: userData.username, - email: userData.email, - role: userData.role, - level: levelForRole(userData.role), - status: userData.status, - created: new Date().toISOString().slice(0, 10) - } var copy = users.slice() - copy.push(newUser) - users = copy - nextUid++ + copy.push({ uid: nextUid, username: userData.username, email: userData.email, role: userData.role, level: levelForRole(userData.role), status: userData.status, created: new Date().toISOString().slice(0, 10) }) + users = copy; nextUid++ } - function saveEdit() { + function saveEdit(userData) { if (editIndex < 0) return - var userData = { - username: formUsername, - email: formEmail, - role: formRole, - status: formActive ? "active" : "inactive" - } - if (useLiveData) { - var userId = users[editIndex].uid - dbal.update("user", userId, userData, function(result, error) { - if (!error) { - loadUsers() - } else { - saveEditLocally(userData) - } + dbal.update("user", users[editIndex].uid, userData, function(result, error) { + if (!error) loadUsers(); else saveEditLocally(userData) editDialogOpen = false - clearForm() }) - } else { - saveEditLocally(userData) - editDialogOpen = false - clearForm() - } + } else { saveEditLocally(userData); editDialogOpen = false } } function saveEditLocally(userData) { var copy = users.slice() - copy[editIndex] = Object.assign({}, copy[editIndex], { - username: userData.username, - email: userData.email, - role: userData.role, - level: levelForRole(userData.role), - status: userData.status - }) + copy[editIndex] = Object.assign({}, copy[editIndex], { username: userData.username, email: userData.email, role: userData.role, level: levelForRole(userData.role), status: userData.status }) users = copy } function confirmDelete() { if (deleteIndex < 0) return - if (useLiveData) { - var userId = users[deleteIndex].uid - dbal.remove("user", userId, function(result, error) { - if (!error) { - loadUsers() - } else { - confirmDeleteLocally() - } - deleteDialogOpen = false - deleteIndex = -1 + dbal.remove("user", users[deleteIndex].uid, function(result, error) { + if (!error) loadUsers(); else { var c = users.slice(); c.splice(deleteIndex, 1); users = c } + deleteDialogOpen = false; deleteIndex = -1 }) } else { - confirmDeleteLocally() - deleteDialogOpen = false - deleteIndex = -1 + var c = users.slice(); c.splice(deleteIndex, 1); users = c + deleteDialogOpen = false; deleteIndex = -1 } } - function confirmDeleteLocally() { - var copy = users.slice() - copy.splice(deleteIndex, 1) - users = copy - } - // ── Main layout ────────────────────────────────────────────────── ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 16 - // Header FlexRow { - Layout.fillWidth: true - spacing: 12 + Layout.fillWidth: true; spacing: 12 CText { variant: "h3"; text: "User Management" } Item { Layout.fillWidth: true } - CButton { - text: "Create User" - variant: "primary" - onClicked: openCreateDialog() - } + CButton { text: "Create User"; variant: "primary"; onClicked: createDialogOpen = true } } - // ── Stats bar ──────────────────────────────────────────────── - FlexRow { + UserStatsBar { Layout.fillWidth: true - spacing: 12 - - CCard { - Layout.fillWidth: true - implicitHeight: statCol1.implicitHeight + 32 - - ColumnLayout { - id: statCol1 - anchors.fill: parent - anchors.margins: 16 - spacing: 4 - CText { variant: "caption"; text: "Total Users" } - CText { variant: "h4"; text: String(users.length) } - } - } - - CCard { - Layout.fillWidth: true - implicitHeight: statCol2.implicitHeight + 32 - - ColumnLayout { - id: statCol2 - anchors.fill: parent - anchors.margins: 16 - spacing: 4 - CText { variant: "caption"; text: "Admins" } - CText { variant: "h4"; text: String(countByRole("admin")) } - } - } - - CCard { - Layout.fillWidth: true - implicitHeight: statCol3.implicitHeight + 32 - - ColumnLayout { - id: statCol3 - anchors.fill: parent - anchors.margins: 16 - spacing: 4 - CText { variant: "caption"; text: "Gods" } - CText { variant: "h4"; text: String(countByRole("god")) } - } - } - - CCard { - Layout.fillWidth: true - implicitHeight: statCol4.implicitHeight + 32 - - ColumnLayout { - id: statCol4 - anchors.fill: parent - anchors.margins: 16 - spacing: 4 - CText { variant: "caption"; text: "SuperGods" } - CText { variant: "h4"; text: String(countByRole("supergod")) } - } - } + totalUsers: users.length + adminCount: countByRole("admin") + godCount: countByRole("god") + superGodCount: countByRole("supergod") } - // ── Search & filter ────────────────────────────────────────── - CCard { + UserSearchFilter { Layout.fillWidth: true - implicitHeight: filterCol.implicitHeight + 32 - - ColumnLayout { - id: filterCol - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - CTextField { - Layout.fillWidth: true - label: "Search" - placeholderText: "Filter by username, email, or role..." - text: searchText - onTextChanged: searchText = text - } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - CChip { - text: "All" - selected: activeRoleFilter === "all" - onClicked: activeRoleFilter = "all" - } - CChip { - text: "User" - selected: activeRoleFilter === "user" - onClicked: activeRoleFilter = "user" - } - CChip { - text: "Admin" - selected: activeRoleFilter === "admin" - onClicked: activeRoleFilter = "admin" - } - CChip { - text: "God" - selected: activeRoleFilter === "god" - onClicked: activeRoleFilter = "god" - } - CChip { - text: "SuperGod" - selected: activeRoleFilter === "supergod" - onClicked: activeRoleFilter = "supergod" - } - } - } + searchText: root.searchText + activeRoleFilter: root.activeRoleFilter + onSearchChanged: function(text) { root.searchText = text } + onRoleFilterChanged: function(role) { root.activeRoleFilter = role } } - // ── User table ────────────────────────────────────────────── - CCard { + UserTable { Layout.fillWidth: true Layout.fillHeight: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 0 - - // Table header - Rectangle { - Layout.fillWidth: true - height: 40 - color: Theme.surface - radius: 4 - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 - - CText { variant: "caption"; text: "AVATAR"; Layout.preferredWidth: 56 } - CText { variant: "caption"; text: "USERNAME"; Layout.preferredWidth: 120 } - CText { variant: "caption"; text: "EMAIL"; Layout.fillWidth: true } - CText { variant: "caption"; text: "ROLE"; Layout.preferredWidth: 100 } - CText { variant: "caption"; text: "LEVEL"; Layout.preferredWidth: 50 } - CText { variant: "caption"; text: "STATUS"; Layout.preferredWidth: 90 } - CText { variant: "caption"; text: "CREATED"; Layout.preferredWidth: 100 } - CText { variant: "caption"; text: "ACTIONS"; Layout.preferredWidth: 140 } - } - } - - CDivider { Layout.fillWidth: true } - - // Table body - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - model: filteredUsers() - clip: true - spacing: 0 - - delegate: Rectangle { - width: parent ? parent.width : 600 - height: 56 - color: index % 2 === 0 ? "transparent" : Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 0.3) - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 - - // Avatar - Item { - Layout.preferredWidth: 56 - Layout.preferredHeight: 40 - - CAvatar { - anchors.centerIn: parent - initials: modelData.username.substring(0, 2).toUpperCase() - } - } - - // Username - CText { - variant: "body1" - text: modelData.username - Layout.preferredWidth: 120 - elide: Text.ElideRight - } - - // Email - CText { - variant: "body2" - text: modelData.email - Layout.fillWidth: true - elide: Text.ElideRight - } - - // Role badge - Item { - Layout.preferredWidth: 100 - Layout.preferredHeight: 40 - - CBadge { - anchors.verticalCenter: parent.verticalCenter - text: modelData.role - color: roleColor(modelData.role) - } - } - - // Level - CText { - variant: "body2" - text: "L" + modelData.level - Layout.preferredWidth: 50 - } - - // Status - Item { - Layout.preferredWidth: 90 - Layout.preferredHeight: 40 - - CStatusBadge { - anchors.verticalCenter: parent.verticalCenter - status: statusString(modelData.status) - text: modelData.status - } - } - - // Created - CText { - variant: "caption" - text: modelData.created - Layout.preferredWidth: 100 - } - - // Actions - FlexRow { - Layout.preferredWidth: 140 - spacing: 6 - - CButton { - text: "Edit" - variant: "ghost" - size: "sm" - onClicked: { - // Find the real index in the users array - for (var i = 0; i < users.length; i++) { - if (users[i].uid === modelData.uid) { - openEditDialog(i) - break - } - } - } - } - CButton { - text: "Delete" - variant: "danger" - size: "sm" - onClicked: { - for (var i = 0; i < users.length; i++) { - if (users[i].uid === modelData.uid) { - openDeleteDialog(i) - break - } - } - } - } - } - } - } - } - - // Empty state - CText { - visible: filteredUsers().length === 0 - Layout.fillWidth: true - Layout.topMargin: 24 - variant: "body2" - text: "No users match the current filter." - horizontalAlignment: Text.AlignHCenter - } + users: filteredUsers() + allUsers: root.users + onEditClicked: function(uid) { + var idx = findUserIndex(uid); if (idx < 0) return + var u = users[idx] + editFormDialog.formUsername = u.username; editFormDialog.formEmail = u.email + editFormDialog.formPassword = ""; editFormDialog.formRole = u.role + editFormDialog.formActive = u.status === "active" + editIndex = idx; editDialogOpen = true + } + onDeleteClicked: function(uid) { + deleteIndex = findUserIndex(uid); deleteDialogOpen = true } } } // ── Create User Dialog ─────────────────────────────────────────── - CDialog { - id: createDialog + UserFormDialog { visible: createDialogOpen - title: "Create User" - - ColumnLayout { - spacing: 14 - width: 380 - - CTextField { - Layout.fillWidth: true - label: "Username" - placeholderText: "Enter username" - text: formUsername - onTextChanged: formUsername = text - } - - CTextField { - Layout.fillWidth: true - label: "Email" - placeholderText: "Enter email address" - text: formEmail - onTextChanged: formEmail = text - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 4 - - CTextField { - Layout.fillWidth: true - label: "Password" - placeholderText: "Enter password" - text: formPassword - echoMode: TextInput.Password - onTextChanged: formPassword = text - } - - CBadge { text: "SHA-512 hashed"; badgeColor: "#607d8b" } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 6 - CText { variant: "caption"; text: "Role" } - - FlexRow { - Layout.fillWidth: true - spacing: 6 - - Repeater { - model: roles - CChip { - text: modelData - selected: formRole === modelData - onClicked: formRole = modelData - } - } - } - } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - CText { variant: "body2"; text: "Active" } - CSwitch { - checked: formActive - onCheckedChanged: formActive = checked - } - } - - CDivider { Layout.fillWidth: true } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - Item { Layout.fillWidth: true } - CButton { - text: "Cancel" - variant: "ghost" - onClicked: { createDialogOpen = false; clearForm() } - } - CButton { - text: "Create" - variant: "primary" - enabled: formUsername !== "" && formEmail !== "" && formPassword !== "" - onClicked: createUser() - } - } - } + isEdit: false + roles: root.roles + onAccepted: createUser({ username: formUsername, email: formEmail, role: formRole, status: formActive ? "active" : "inactive" }) + onCancelled: createDialogOpen = false } // ── Edit User Dialog ───────────────────────────────────────────── - CDialog { - id: editDialog + UserFormDialog { + id: editFormDialog visible: editDialogOpen - title: "Edit User" - - ColumnLayout { - spacing: 14 - width: 380 - - CTextField { - Layout.fillWidth: true - label: "Username" - placeholderText: "Enter username" - text: formUsername - onTextChanged: formUsername = text - } - - CTextField { - Layout.fillWidth: true - label: "Email" - placeholderText: "Enter email address" - text: formEmail - onTextChanged: formEmail = text - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 4 - - CTextField { - Layout.fillWidth: true - label: "Password" - placeholderText: "Leave blank to keep current" - text: formPassword - echoMode: TextInput.Password - onTextChanged: formPassword = text - } - - CBadge { text: "SHA-512 hashed"; badgeColor: "#607d8b" } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 6 - CText { variant: "caption"; text: "Role" } - - FlexRow { - Layout.fillWidth: true - spacing: 6 - - Repeater { - model: roles - CChip { - text: modelData - selected: formRole === modelData - onClicked: formRole = modelData - } - } - } - } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - CText { variant: "body2"; text: "Active" } - CSwitch { - checked: formActive - onCheckedChanged: formActive = checked - } - } - - CDivider { Layout.fillWidth: true } - - FlexRow { - Layout.fillWidth: true - spacing: 8 - - Item { Layout.fillWidth: true } - CButton { - text: "Cancel" - variant: "ghost" - onClicked: { editDialogOpen = false; clearForm() } - } - CButton { - text: "Save Changes" - variant: "primary" - enabled: formUsername !== "" && formEmail !== "" - onClicked: saveEdit() - } - } - } + isEdit: true + roles: root.roles + onAccepted: saveEdit({ username: formUsername, email: formEmail, role: formRole, status: formActive ? "active" : "inactive" }) + onCancelled: editDialogOpen = false } // ── Delete Confirmation Dialog ─────────────────────────────────── CDialog { - id: deleteDialog - visible: deleteDialogOpen - title: "Delete User" - + visible: deleteDialogOpen; title: "Delete User" ColumnLayout { - spacing: 16 - width: 380 - + spacing: 16; width: 380 CAlert { - Layout.fillWidth: true - severity: "error" + Layout.fillWidth: true; severity: "error" text: deleteIndex >= 0 && deleteIndex < users.length ? "Are you sure you want to delete \"" + users[deleteIndex].username + "\"? This action cannot be undone." : "Confirm deletion?" } - FlexRow { - Layout.fillWidth: true - spacing: 8 - - Item { Layout.fillWidth: true } - CButton { - text: "Cancel" - variant: "ghost" - onClicked: { deleteDialogOpen = false; deleteIndex = -1 } - } - CButton { - text: "Delete" - variant: "danger" - onClicked: confirmDelete() - } + Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true } + CButton { text: "Cancel"; variant: "ghost"; onClicked: { deleteDialogOpen = false; deleteIndex = -1 } } + CButton { text: "Delete"; variant: "danger"; onClicked: confirmDelete() } } } } diff --git a/frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateEditor.qml b/frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateEditor.qml new file mode 100644 index 000000000..2d8a7f8b5 --- /dev/null +++ b/frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateEditor.qml @@ -0,0 +1,119 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + Layout.fillWidth: true + Layout.fillHeight: true + + property bool hasSelection: false + property string templateName: "" + property string templateSubject: "" + property string templateBody: "" + + signal nameChanged(string value) + signal subjectChanged(string value) + signal bodyChanged(string value) + signal saveRequested() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "h4"; text: "Template Editor" } + + Item { Layout.fillWidth: true } + + CButton { + visible: root.hasSelection + text: "Save Template" + variant: "primary" + size: "sm" + onClicked: root.saveRequested() + } + } + + CDivider { Layout.fillWidth: true } + + CText { + visible: !root.hasSelection + variant: "body2" + text: "Select a template from the list to edit." + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 40 + } + + ColumnLayout { + visible: root.hasSelection + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 12 + + CTextField { + Layout.fillWidth: true + label: "Template Name" + placeholderText: "e.g. Welcome Email" + text: root.templateName + onTextChanged: root.nameChanged(text) + } + + CTextField { + Layout.fillWidth: true + label: "Subject Template" + placeholderText: "e.g. Welcome to {{app_name}}" + text: root.templateSubject + onTextChanged: root.subjectChanged(text) + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 4 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "caption"; text: "Body Template" } + Item { Layout.fillWidth: true } + CText { + variant: "caption" + text: "Supports {{variable}} placeholders" + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 180 + color: Theme.surface + border.color: Theme.border + border.width: 1 + radius: 4 + + ScrollView { + anchors.fill: parent + anchors.margins: 8 + + TextArea { + text: root.templateBody + wrapMode: Text.Wrap + color: Theme.text + font.pixelSize: 13 + font.family: "monospace" + background: null + onTextChanged: root.bodyChanged(text) + } + } + } + } + } + } +} diff --git a/frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateList.qml b/frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateList.qml new file mode 100644 index 000000000..9039e7182 --- /dev/null +++ b/frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateList.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + Layout.preferredWidth: 280 + Layout.fillHeight: true + + property var templates: [] + property int selectedIndex: -1 + + signal templateSelected(int index) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 8 + + CText { variant: "h4"; text: "Templates" } + CText { variant: "caption"; text: root.templates.length + " templates" } + CDivider { Layout.fillWidth: true } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 200 + model: root.templates + spacing: 2 + clip: true + + delegate: CListItem { + width: parent ? parent.width : 248 + title: modelData.name + subtitle: modelData.id + selected: root.selectedIndex === index + onClicked: root.templateSelected(index) + } + } + } +} diff --git a/frontends/qt6/qmllib/MetaBuilder/CSmtpTestEmailForm.qml b/frontends/qt6/qmllib/MetaBuilder/CSmtpTestEmailForm.qml new file mode 100644 index 000000000..d268ffc78 --- /dev/null +++ b/frontends/qt6/qmllib/MetaBuilder/CSmtpTestEmailForm.qml @@ -0,0 +1,98 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + Layout.fillWidth: true + + property string recipient: "" + property string subject: "MetaBuilder SMTP Test" + property string body: "This is a test email from MetaBuilder." + property bool sending: false + + signal recipientChanged(string value) + signal subjectChanged(string value) + signal bodyChanged(string value) + signal sendRequested() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + CText { variant: "h4"; text: "Send Test Email" } + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + CTextField { + Layout.fillWidth: true + label: "Recipient" + placeholderText: "test@example.com" + text: root.recipient + onTextChanged: root.recipientChanged(text) + } + + CTextField { + Layout.fillWidth: true + label: "Subject" + placeholderText: "Test subject" + text: root.subject + onTextChanged: root.subjectChanged(text) + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + CText { variant: "caption"; text: "Body" } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 100 + color: Theme.surface + border.color: Theme.border + border.width: 1 + radius: 4 + + ScrollView { + anchors.fill: parent + anchors.margins: 8 + + TextArea { + text: root.body + wrapMode: Text.Wrap + color: Theme.text + font.pixelSize: 13 + background: null + onTextChanged: root.bodyChanged(text) + } + } + } + } + + FlexRow { + Layout.fillWidth: true + spacing: 12 + + CButton { + text: root.sending ? "Sending..." : "Send Test Email" + variant: "primary" + size: "sm" + enabled: !root.sending && root.recipient.length > 0 + onClicked: root.sendRequested() + } + + CText { + visible: root.recipient.length === 0 + variant: "caption" + text: "Enter a recipient to enable sending" + } + } + } +} diff --git a/frontends/qt6/qmllib/MetaBuilder/ThemeLivePreview.qml b/frontends/qt6/qmllib/MetaBuilder/ThemeLivePreview.qml new file mode 100644 index 000000000..bc2ecb5a0 --- /dev/null +++ b/frontends/qt6/qmllib/MetaBuilder/ThemeLivePreview.qml @@ -0,0 +1,256 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: livePreview + Layout.fillWidth: true + + property string customPrimary: "#000000" + property string customBackground: "#000000" + property string customSurface: "#000000" + property string customPaper: "#000000" + property string customText: "#000000" + property string customTextSecondary: "#000000" + property string customBorder: "#000000" + property string customError: "#000000" + property string customWarning: "#000000" + property string customSuccess: "#000000" + property string customInfo: "#000000" + property string fontFamily: "Inter" + property int baseFontSize: 14 + property int radiusSmall: 4 + property int radiusMedium: 8 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + CText { variant: "h4"; text: "Live Preview" } + Item { Layout.fillWidth: true } + CBadge { text: "Interactive" } + } + + CText { variant: "caption"; text: "A sample UI rendered with your current theme configuration" } + + CDivider { Layout.fillWidth: true } + + // Preview container + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 340 + radius: radiusMedium + color: customBackground + border.width: 1 + border.color: customBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 14 + + // Preview header bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + radius: radiusSmall + color: customSurface + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 14 + anchors.rightMargin: 14 + spacing: 12 + + Text { + text: "MetaBuilder" + font.pixelSize: baseFontSize + 2 + font.weight: Font.Bold + font.family: fontFamily + color: customText + } + + Item { Layout.fillWidth: true } + + Repeater { + model: ["Dashboard", "Settings", "Help"] + Text { + text: modelData + font.pixelSize: baseFontSize - 1 + font.family: fontFamily + color: customTextSecondary + } + } + } + } + + // Preview content area + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 12 + + // Preview card 1 - Status + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: radiusMedium + color: customPaper + border.width: 1 + border.color: customBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 8 + + Text { + text: "Status" + font.pixelSize: baseFontSize + font.weight: Font.Bold + font.family: fontFamily + color: customText + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: customBorder + } + + Repeater { + model: [ + { label: "DBAL", col: customSuccess }, + { label: "Auth", col: customSuccess }, + { label: "Storage", col: customWarning } + ] + + RowLayout { + spacing: 8 + Rectangle { + width: 8; height: 8; radius: 4 + color: modelData.col + } + Text { + text: modelData.label + font.pixelSize: baseFontSize - 2 + font.family: fontFamily + color: customTextSecondary + } + } + } + + Item { Layout.fillHeight: true } + + Rectangle { + Layout.fillWidth: true + height: 30 + radius: radiusSmall + color: customPrimary + + Text { + anchors.centerIn: parent + text: "View Details" + font.pixelSize: baseFontSize - 2 + font.family: fontFamily + color: "#ffffff" + } + } + } + } + + // Preview card 2 - Activity + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: radiusMedium + color: customPaper + border.width: 1 + border.color: customBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 8 + + Text { + text: "Activity" + font.pixelSize: baseFontSize + font.weight: Font.Bold + font.family: fontFamily + color: customText + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: customBorder + } + + Repeater { + model: [ + { msg: "User signed in", t: "2m ago" }, + { msg: "Package installed", t: "5m ago" }, + { msg: "Schema updated", t: "1h ago" } + ] + + ColumnLayout { + spacing: 2 + Text { + text: modelData.msg + font.pixelSize: baseFontSize - 2 + font.family: fontFamily + color: customText + } + Text { + text: modelData.t + font.pixelSize: baseFontSize - 4 + font.family: fontFamily + color: customTextSecondary + } + } + } + + Item { Layout.fillHeight: true } + + Rectangle { + Layout.fillWidth: true + height: 24 + radius: radiusSmall + color: Qt.alpha(customError, 0.15) + + Text { + anchors.centerIn: parent + text: "1 alert" + font.pixelSize: baseFontSize - 4 + font.family: fontFamily + color: customError + } + } + + Rectangle { + Layout.fillWidth: true + height: 24 + radius: radiusSmall + color: Qt.alpha(customInfo, 0.15) + + Text { + anchors.centerIn: parent + text: "3 notifications" + font.pixelSize: baseFontSize - 4 + font.family: fontFamily + color: customInfo + } + } + } + } + } + } + } + } +} diff --git a/frontends/qt6/qmllib/MetaBuilder/ThemeSpacingRadius.qml b/frontends/qt6/qmllib/MetaBuilder/ThemeSpacingRadius.qml new file mode 100644 index 000000000..a7daf2c5a --- /dev/null +++ b/frontends/qt6/qmllib/MetaBuilder/ThemeSpacingRadius.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +ColumnLayout { + id: spacingRadiusRoot + Layout.fillWidth: true + spacing: 20 + + property int baseSpacing: 8 + property int radiusSmall: 4 + property int radiusMedium: 8 + property int radiusLarge: 16 + + signal baseSpacingChanged(int value) + signal radiusSmallChanged(int value) + signal radiusMediumChanged(int value) + signal radiusLargeChanged(int value) + + // Spacing section + CCard { + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + CText { variant: "h4"; text: "Spacing" } + CText { variant: "caption"; text: "Base spacing unit used as a multiplier across the layout system" } + + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + CTextField { + Layout.preferredWidth: 120 + label: "Base Spacing (px)" + placeholderText: "8" + text: baseSpacing.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val > 0 && val <= 32) { + baseSpacingChanged(val) + } + } + } + + // Spacing preview + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + CText { variant: "caption"; text: "Preview: spacing scale" } + + RowLayout { + spacing: 8 + + Repeater { + model: [1, 2, 3, 4, 6] + + ColumnLayout { + spacing: 4 + Rectangle { + width: baseSpacing * modelData + height: baseSpacing * modelData + radius: 3 + color: Theme.primary + opacity: 0.3 + (index * 0.15) + } + Text { + text: (baseSpacing * modelData) + "px" + font.pixelSize: 10 + color: Theme.textSecondary + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + } + } + } + + // Border Radius section + CCard { + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + CText { variant: "h4"; text: "Border Radius" } + CText { variant: "caption"; text: "Control corner rounding for small, medium, and large elements" } + + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + CTextField { + Layout.preferredWidth: 100 + label: "Small (px)" + placeholderText: "4" + text: radiusSmall.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val >= 0) { + radiusSmallChanged(val) + } + } + } + + CTextField { + Layout.preferredWidth: 100 + label: "Medium (px)" + placeholderText: "8" + text: radiusMedium.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val >= 0) { + radiusMediumChanged(val) + } + } + } + + CTextField { + Layout.preferredWidth: 100 + label: "Large (px)" + placeholderText: "16" + text: radiusLarge.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val >= 0) { + radiusLargeChanged(val) + } + } + } + + Item { Layout.fillWidth: true } + + // Radius preview + RowLayout { + spacing: 16 + + Repeater { + model: [ + { label: "Sm", r: radiusSmall }, + { label: "Md", r: radiusMedium }, + { label: "Lg", r: radiusLarge } + ] + + ColumnLayout { + spacing: 4 + + Rectangle { + width: 48 + height: 48 + radius: modelData.r + color: "transparent" + border.width: 2 + border.color: Theme.primary + } + + Text { + text: modelData.label + " (" + modelData.r + "px)" + font.pixelSize: 10 + color: Theme.textSecondary + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + } + } +} diff --git a/frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml b/frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml new file mode 100644 index 000000000..a8c1249c3 --- /dev/null +++ b/frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml @@ -0,0 +1,94 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: typographyCard + Layout.fillWidth: true + + property string fontFamily: "Inter" + property int baseFontSize: 14 + + signal fontFamilyChanged(string family) + signal baseFontSizeChanged(int size) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + CText { variant: "h4"; text: "Typography" } + CText { variant: "caption"; text: "Configure font family and base size for the entire interface" } + + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + ColumnLayout { + Layout.fillWidth: true + spacing: 8 + + CTextField { + Layout.fillWidth: true + label: "Font Family" + placeholderText: "e.g., Inter, Roboto, system-ui" + text: fontFamily + onTextChanged: fontFamilyChanged(text) + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "body2"; text: "Base Font Size: " + baseFontSize + "px" } + + Slider { + Layout.fillWidth: true + from: 10 + to: 24 + stepSize: 1 + value: baseFontSize + onValueChanged: baseFontSizeChanged(value) + + background: Rectangle { + x: parent.leftPadding + y: parent.topPadding + parent.availableHeight / 2 - height / 2 + width: parent.availableWidth + height: 4 + radius: 2 + color: Theme.border + + Rectangle { + width: parent.parent.visualPosition * parent.width + height: parent.height + radius: 2 + color: Theme.primary + } + } + + handle: Rectangle { + x: parent.leftPadding + parent.visualPosition * (parent.availableWidth - width) + y: parent.topPadding + parent.availableHeight / 2 - height / 2 + width: 18 + height: 18 + radius: 9 + color: Theme.primary + border.width: 2 + border.color: Theme.background + } + } + + RowLayout { + spacing: 4 + CText { variant: "caption"; text: "10px" } + Item { Layout.fillWidth: true } + CText { variant: "caption"; text: "24px" } + } + } + } + } +}