diff --git a/qml/MetaBuilder/AppLogic.js b/qml/MetaBuilder/AppLogic.js new file mode 100644 index 000000000..e4ad4f244 --- /dev/null +++ b/qml/MetaBuilder/AppLogic.js @@ -0,0 +1,55 @@ +// AppLogic.js — auth + navigation logic for App.qml + +function loadJson(relativePath) { + var xhr = new XMLHttpRequest() + xhr.open("GET", Qt.resolvedUrl(relativePath), false); xhr.send() + return xhr.status === 200 ? JSON.parse(xhr.responseText) : null +} + +function login(app, username, password) { + for (var i = 0; i < app.users.length; i++) { + if (app.users[i].username === username && app.users[i].password === password) { + app.currentUser = username; app.currentRole = app.users[i].role + app.currentLevel = app.users[i].level; app.loggedIn = true + app.currentView = "dashboard"; return true + } + } + return false +} + +function logout(app, dbalProvider) { + app.currentUser = ""; app.currentRole = "public"; app.currentLevel = 1 + app.loggedIn = false; app.authToken = ""; dbalProvider.authToken = "" + app.currentView = "frontpage" +} + +function viewIndex(app) { + var view = app.currentView + var staticIdx = app.staticViews.indexOf(view) + if (staticIdx >= 0) return staticIdx + var navPkgs = PackageLoader.navigablePackages() + for (var i = 0; i < navPkgs.length; i++) { + var pkg = navPkgs[i] + var viewName = packageViewName(pkg) + if (viewName === view || pkg.packageId === view) return app.staticViews.length + i + } + return 0 +} + +function packageViewName(pkg) { + return pkg.navLabel ? pkg.navLabel.toLowerCase().replace(/ /g, "-") : pkg.packageId +} + +function autoLogin(app, dbalProvider) { + app.appConfig = loadJson("config/app-config.json") + if (typeof Theme.setTheme === "function") Theme.setTheme(app.currentTheme) + if (app.authToken !== "") { + dbalProvider.authToken = app.authToken + dbalProvider.execute("core/auth/validate", { token: app.authToken }, function(result, error) { + if (!error && result && result.valid) { + app.currentUser = result.username || ""; app.currentRole = result.role || "user" + app.currentLevel = result.level || 2; app.loggedIn = true; app.currentView = "dashboard" + } else { app.authToken = ""; dbalProvider.authToken = "" } + }) + } +} diff --git a/qml/MetaBuilder/CBackendDetailPanel.qml b/qml/MetaBuilder/CBackendDetailPanel.qml index 70c72a839..3d86eccf8 100644 --- a/qml/MetaBuilder/CBackendDetailPanel.qml +++ b/qml/MetaBuilder/CBackendDetailPanel.qml @@ -105,44 +105,9 @@ CCard { 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: root.backend.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: root.formatSize(root.backend.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: root.backend.lastBackup } - } - } + CStatCell { label: "Records"; value: root.backend.records.toLocaleString() } + CStatCell { label: "Size"; value: root.formatSize(root.backend.sizeKb) } + CStatCell { label: "Last Backup"; value: root.backend.lastBackup; valueVariant: "body2" } } } } diff --git a/qml/MetaBuilder/CCanvasInteractionArea.qml b/qml/MetaBuilder/CCanvasInteractionArea.qml new file mode 100644 index 000000000..e56099573 --- /dev/null +++ b/qml/MetaBuilder/CCanvasInteractionArea.qml @@ -0,0 +1,39 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +MouseArea { + id: root + + property bool drawingConnection: false + + signal connectionDragUpdated(real x, real y) + signal connectionDragFinished() + signal canvasClicked() + signal zoomRequested(real zoomDelta) + + z: 0 + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + hoverEnabled: true + + onPositionChanged: function(mouse) { + if (root.drawingConnection) { + root.connectionDragUpdated(mouse.x, mouse.y) + } + } + + onReleased: function(mouse) { + if (root.drawingConnection) { + root.connectionDragFinished() + } + } + + onClicked: function(mouse) { + root.canvasClicked() + } + + onWheel: function(wheel) { + var zoomDelta = wheel.angleDelta.y > 0 ? 0.1 : -0.1 + root.zoomRequested(zoomDelta) + } +} diff --git a/qml/MetaBuilder/CEncryptionSelector.qml b/qml/MetaBuilder/CEncryptionSelector.qml new file mode 100644 index 000000000..01edee807 --- /dev/null +++ b/qml/MetaBuilder/CEncryptionSelector.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Layouts +import QmlComponents 1.0 + +ColumnLayout { + id: root + + property var options: ["None", "TLS", "SSL"] + property int selectedIndex: 1 + + signal selectionChanged(int index) + + Layout.fillWidth: true + spacing: 4 + + CText { variant: "caption"; text: "Encryption" } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: root.options + delegate: CButton { + text: modelData + variant: root.selectedIndex === index ? "primary" : "ghost" + size: "sm" + onClicked: root.selectionChanged(index) + } + } + } +} diff --git a/qml/MetaBuilder/CHeroSection.qml b/qml/MetaBuilder/CHeroSection.qml index c7ecfc894..1b3478a11 100644 --- a/qml/MetaBuilder/CHeroSection.qml +++ b/qml/MetaBuilder/CHeroSection.qml @@ -13,11 +13,8 @@ Rectangle { signal openStorybook() signal openPackages() - // MD3 tonal surfaces readonly property color accentBlue: "#6366F1" - readonly property color primaryContainer: isDark - ? Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.15) - : Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.12) + readonly property color primaryContainer: isDark ? Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.15) : Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.12) readonly property color onSurface: Theme.text readonly property color onSurfaceVariant: Theme.textSecondary @@ -41,22 +38,11 @@ Rectangle { width: Math.min(parent.width - 80, 720) spacing: 16 - // Version pill - Rectangle { + CVersionPill { Layout.alignment: Qt.AlignHCenter - width: vLabel.implicitWidth + 24 - height: 28 - radius: 14 - color: primaryContainer - - CText { - id: vLabel - anchors.centerIn: parent - text: "v" + platformVersion - font.family: "monospace" - font.pixelSize: 12 - color: accentBlue - } + version: platformVersion + accent: accentBlue + containerColor: primaryContainer } CText { diff --git a/qml/MetaBuilder/CLevelCard.qml b/qml/MetaBuilder/CLevelCard.qml index acf2cc917..32f769fc6 100644 --- a/qml/MetaBuilder/CLevelCard.qml +++ b/qml/MetaBuilder/CLevelCard.qml @@ -88,28 +88,10 @@ Rectangle { Item { Layout.fillHeight: true } - Flow { - Layout.fillWidth: true - spacing: 6 - - Repeater { - model: root.tags - Rectangle { - width: tText.implicitWidth + 16 - height: 24 - radius: 8 - color: Qt.rgba(accent.r, accent.g, accent.b, isDark ? 0.12 : 0.10) - - CText { - id: tText - anchors.centerIn: parent - text: modelData - font.pixelSize: 11 - font.weight: Font.Medium - color: accent - } - } - } + CLevelTagFlow { + tags: root.tags + accent: root.accent + isDark: root.isDark } } } diff --git a/qml/MetaBuilder/CLevelTagFlow.qml b/qml/MetaBuilder/CLevelTagFlow.qml new file mode 100644 index 000000000..7d1bac9d5 --- /dev/null +++ b/qml/MetaBuilder/CLevelTagFlow.qml @@ -0,0 +1,33 @@ +import QtQuick +import QtQuick.Layouts +import QmlComponents 1.0 + +Flow { + id: root + + property var tags: [] + property color accent: "#94A3B8" + property bool isDark: false + + Layout.fillWidth: true + spacing: 6 + + Repeater { + model: root.tags + Rectangle { + width: tText.implicitWidth + 16 + height: 24 + radius: 8 + color: Qt.rgba(root.accent.r, root.accent.g, root.accent.b, root.isDark ? 0.12 : 0.10) + + CText { + id: tText + anchors.centerIn: parent + text: modelData + font.pixelSize: 11 + font.weight: Font.Medium + color: root.accent + } + } + } +} diff --git a/qml/MetaBuilder/CRouteEditPanel.qml b/qml/MetaBuilder/CRouteEditPanel.qml index ceed8b087..7112bb813 100644 --- a/qml/MetaBuilder/CRouteEditPanel.qml +++ b/qml/MetaBuilder/CRouteEditPanel.qml @@ -46,13 +46,7 @@ CCard { Layout.fillWidth: true model: levelOptions currentIndex: root.route ? root.route.level - 1 : 0 - onCurrentIndexChanged: { - if (root.route) { - var newLvl = currentIndex + 1 - if (newLvl !== root.route.level) - root.fieldChanged("level", newLvl) - } - } + onCurrentIndexChanged: if (root.route && currentIndex + 1 !== root.route.level) root.fieldChanged("level", currentIndex + 1) } CText { variant: "caption"; text: "Layout Type" } @@ -60,13 +54,7 @@ CCard { Layout.fillWidth: true model: layoutOptions currentIndex: root.route ? layoutOptions.indexOf(root.route.layout) : 0 - onCurrentIndexChanged: { - if (root.route) { - var newLayoutVal = layoutOptions[currentIndex] - if (newLayoutVal !== root.route.layout) - root.fieldChanged("layout", newLayoutVal) - } - } + onCurrentIndexChanged: if (root.route && layoutOptions[currentIndex] !== root.route.layout) root.fieldChanged("layout", layoutOptions[currentIndex]) } FlexRow { diff --git a/qml/MetaBuilder/CSmtpServerForm.qml b/qml/MetaBuilder/CSmtpServerForm.qml index 9600781ed..a432ca723 100644 --- a/qml/MetaBuilder/CSmtpServerForm.qml +++ b/qml/MetaBuilder/CSmtpServerForm.qml @@ -64,26 +64,10 @@ CCard { onTextChanged: root.passwordEdited(text) } - ColumnLayout { - Layout.fillWidth: true - spacing: 4 - - CText { variant: "caption"; text: "Encryption" } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Repeater { - model: root.encryptionOptions - delegate: CButton { - text: modelData - variant: root.encryptionIndex === index ? "primary" : "ghost" - size: "sm" - onClicked: root.encryptionEdited(index) - } - } - } + CEncryptionSelector { + options: root.encryptionOptions + selectedIndex: root.encryptionIndex + onSelectionChanged: function(idx) { root.encryptionEdited(idx) } } CDivider { Layout.fillWidth: true } diff --git a/qml/MetaBuilder/CVersionPill.qml b/qml/MetaBuilder/CVersionPill.qml new file mode 100644 index 000000000..d32d0e265 --- /dev/null +++ b/qml/MetaBuilder/CVersionPill.qml @@ -0,0 +1,24 @@ +import QtQuick +import QmlComponents 1.0 + +Rectangle { + id: root + + property string version: "" + property color accent: "#6366F1" + property color containerColor: "#1a1a2e" + + width: vLabel.implicitWidth + 24 + height: 28 + radius: 14 + color: containerColor + + CText { + id: vLabel + anchors.centerIn: parent + text: "v" + root.version + font.family: "monospace" + font.pixelSize: 12 + color: root.accent + } +} diff --git a/qml/MetaBuilder/CWorkflowCanvas.qml b/qml/MetaBuilder/CWorkflowCanvas.qml index a07912f80..8636df1e3 100644 --- a/qml/MetaBuilder/CWorkflowCanvas.qml +++ b/qml/MetaBuilder/CWorkflowCanvas.qml @@ -31,9 +31,7 @@ Rectangle { color: Theme.background clip: true - function requestPaint() { - connectionLayer.requestPaint() - } + function requestPaint() { connectionLayer.requestPaint() } function groupColor(nodeType) { var prefix = nodeType ? nodeType.split(".")[0] : "" @@ -50,7 +48,6 @@ Rectangle { } } - // Drop area for palette drag DropArea { anchors.fill: parent keys: ["text/node-type"] @@ -71,13 +68,7 @@ Rectangle { clip: true boundsBehavior: Flickable.StopAtBounds - Component.onCompleted: { - contentX = 1500 - contentY = 1500 - } - - function centerX() { return contentX + width / 2 } - function centerY() { return contentY + height / 2 } + Component.onCompleted: { contentX = 1500; contentY = 1500 } Item { id: canvasContent @@ -85,15 +76,11 @@ Rectangle { height: canvas.contentHeight transform: Scale { - origin.x: 0 - origin.y: 0 - xScale: root.zoom - yScale: root.zoom + origin.x: 0; origin.y: 0 + xScale: root.zoom; yScale: root.zoom } - CCanvasGrid { - anchors.fill: parent - } + CCanvasGrid { anchors.fill: parent } CConnectionLayer { id: connectionLayer @@ -109,7 +96,6 @@ Rectangle { } Repeater { - id: nodeRepeater model: root.nodes.length z: 2 @@ -123,42 +109,21 @@ Rectangle { onNodeSelected: function(id) { root.nodeSelected(id) } onNodeMoved: function(id, x, y) { root.nodeMoved(id, x, y) } - onConnectionDragStarted: function(nodeId, portName, isOutput, portX, portY) { - root.connectionDragStarted(nodeId, portName, isOutput, portX, portY) - } - onConnectionCompleted: function(nodeId, portName) { - root.connectionCompleted(nodeId, portName) + onConnectionDragStarted: function(nId, pName, isOut, pX, pY) { + root.connectionDragStarted(nId, pName, isOut, pX, pY) } + onConnectionCompleted: function(nId, pName) { root.connectionCompleted(nId, pName) } onPaintRequested: connectionLayer.requestPaint() } } - MouseArea { + CCanvasInteractionArea { anchors.fill: parent - z: 0 - acceptedButtons: Qt.LeftButton | Qt.MiddleButton - hoverEnabled: true - - onPositionChanged: function(mouse) { - if (root.drawingConnection) { - root.connectionDragUpdated(mouse.x, mouse.y) - } - } - - onReleased: function(mouse) { - if (root.drawingConnection) { - root.connectionDragFinished() - } - } - - onClicked: function(mouse) { - root.canvasClicked() - } - - onWheel: function(wheel) { - var zoomDelta = wheel.angleDelta.y > 0 ? 0.1 : -0.1 - root.zoomChanged(root.zoom + zoomDelta) - } + drawingConnection: root.drawingConnection + onConnectionDragUpdated: function(x, y) { root.connectionDragUpdated(x, y) } + onConnectionDragFinished: root.connectionDragFinished() + onCanvasClicked: root.canvasClicked() + onZoomRequested: function(delta) { root.zoomChanged(root.zoom + delta) } } } } diff --git a/qml/MetaBuilder/ComponentTreeDBAL.js b/qml/MetaBuilder/ComponentTreeDBAL.js new file mode 100644 index 000000000..f51bdcb31 --- /dev/null +++ b/qml/MetaBuilder/ComponentTreeDBAL.js @@ -0,0 +1,86 @@ +.pragma library + +// DBAL persistence and tree manipulation helpers for component hierarchy. + +function childrenCount(treeNodes, idx) { + if (idx < 0 || idx >= treeNodes.length) return 0 + var parentDepth = treeNodes[idx].depth; var count = 0 + for (var i = idx + 1; i < treeNodes.length; i++) { + if (treeNodes[i].depth <= parentDepth) break + if (treeNodes[i].depth === parentDepth + 1) count++ + } + return count +} + +function subtreeEnd(treeNodes, idx) { + var parentDepth = treeNodes[idx].depth; var i = idx + 1 + while (i < treeNodes.length && treeNodes[i].depth > parentDepth) i++ + return i +} + +function addChild(treeNodes, parentIdx, nextNodeId, dbal, useLiveData, loadFn) { + if (parentIdx < 0 || parentIdx >= treeNodes.length) return null + var insertAt = subtreeEnd(treeNodes, parentIdx) + var newNode = { nodeId: nextNodeId, name: "NewComponent", type: "atom", depth: treeNodes[parentIdx].depth + 1, visible: true, props: [] } + if (useLiveData) dbal.create("component_node", newNode, function(r, e) { if (!e) loadFn() }) + var updated = treeNodes.slice(); updated.splice(insertAt, 0, newNode) + return { nodes: updated, selectedIndex: insertAt } +} + +function removeNode(treeNodes, idx, dbal, useLiveData, loadFn) { + if (idx < 0 || idx >= treeNodes.length || treeNodes[idx].depth === 0) return null + if (useLiveData && treeNodes[idx].id) dbal.remove("component_node", treeNodes[idx].id, function(r, e) { if (!e) loadFn() }) + var endIdx = subtreeEnd(treeNodes, idx); var updated = treeNodes.slice(); updated.splice(idx, endIdx - idx) + var sel = idx < updated.length ? idx : (updated.length > 0 ? updated.length - 1 : -1) + return { nodes: updated, selectedIndex: sel } +} + +function updateNode(treeNodes, idx, field, value) { + if (idx < 0 || idx >= treeNodes.length) return treeNodes + var updated = treeNodes.slice() + updated[idx] = Object.assign({}, updated[idx]); updated[idx][field] = value + return updated +} + +function addProp(treeNodes, idx) { + if (idx < 0 || idx >= treeNodes.length) return treeNodes + var updated = treeNodes.slice(); var node = Object.assign({}, updated[idx]) + var newProps = node.props.slice(); newProps.push({ key: "newProp", value: "" }); node.props = newProps + updated[idx] = node; return updated +} + +function removeProp(treeNodes, nodeIdx, propIdx) { + if (nodeIdx < 0 || nodeIdx >= treeNodes.length) return treeNodes + var updated = treeNodes.slice(); var node = Object.assign({}, updated[nodeIdx]) + var newProps = node.props.slice(); newProps.splice(propIdx, 1); node.props = newProps + updated[nodeIdx] = node; return updated +} + +function saveNode(dbal, treeNodes, idx, loadFn) { + var node = treeNodes[idx] + var data = { nodeId: node.nodeId, name: node.name, type: node.type, depth: node.depth, visible: node.visible, props: node.props } + if (node.id) dbal.update("component_node", node.id, data, function(r, e) { if (!e) loadFn() }) + else dbal.create("component_node", data, function(r, e) { if (!e) loadFn() }) +} + +function loadComponents(dbal, callback) { + dbal.list("component_node", { take: 200 }, function(result, error) { + if (!error && result && result.items && result.items.length > 0) { + var parsed = []; var maxId = 0 + for (var i = 0; i < result.items.length; i++) { + var n = result.items[i]; var nid = n.nodeId || n.id || i + if (nid > maxId) maxId = nid + parsed.push({ id: n.id, nodeId: nid, name: n.name || "Component", type: n.type || "atom", depth: n.depth !== undefined ? n.depth : 0, visible: n.visible !== undefined ? n.visible : true, props: n.props || [] }) + } + callback(parsed, maxId + 1) + } + }) +} + +function loadJson(relativePath) { + var xhr = new XMLHttpRequest() + xhr.open("GET", relativePath, false) + xhr.send() + if (xhr.status === 200 || xhr.status === 0) return JSON.parse(xhr.responseText) + return [] +} diff --git a/qml/MetaBuilder/LuaPropertiesPanel.qml b/qml/MetaBuilder/LuaPropertiesPanel.qml index 27541cdaa..720e2ef9d 100644 --- a/qml/MetaBuilder/LuaPropertiesPanel.qml +++ b/qml/MetaBuilder/LuaPropertiesPanel.qml @@ -41,26 +41,9 @@ Rectangle { CDivider { Layout.fillWidth: true } - CText { variant: "caption"; text: "SCRIPT NAME" } - CTextField { - Layout.fillWidth: true - text: scriptName - onTextChanged: nameChanged(text) - } - - CText { variant: "caption"; text: "DESCRIPTION" } - CTextField { - Layout.fillWidth: true - text: scriptDescription - onTextChanged: descriptionChanged(text) - } - - CText { variant: "caption"; text: "RETURN TYPE" } - CTextField { - Layout.fillWidth: true - text: returnType - onTextChanged: returnTypeChanged(text) - } + CTextField { Layout.fillWidth: true; label: "SCRIPT NAME"; text: scriptName; onTextChanged: nameChanged(text) } + CTextField { Layout.fillWidth: true; label: "DESCRIPTION"; text: scriptDescription; onTextChanged: descriptionChanged(text) } + CTextField { Layout.fillWidth: true; label: "RETURN TYPE"; text: returnType; onTextChanged: returnTypeChanged(text) } CDivider { Layout.fillWidth: true } diff --git a/qml/MetaBuilder/MediaJobRow.qml b/qml/MetaBuilder/MediaJobRow.qml new file mode 100644 index 000000000..adbebf79a --- /dev/null +++ b/qml/MetaBuilder/MediaJobRow.qml @@ -0,0 +1,105 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +ColumnLayout { + id: root + Layout.fillWidth: true + spacing: 4 + + property var job: ({}) + property bool isLast: false + + signal cancelRequested() + + function jobStatusColor(status) { + switch (status) { + case "completed": return "success" + case "processing": return "warning" + case "queued": return "info" + case "failed": return "error" + default: return "info" + } + } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { + variant: "body2" + text: root.job.id || "" + font.family: "monospace" + Layout.preferredWidth: 100 + } + + CBadge { + text: root.job.type || "" + Layout.preferredWidth: 80 + } + + CStatusBadge { + status: jobStatusColor(root.job.status || "") + text: root.job.status || "" + Layout.preferredWidth: 100 + } + + 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 * ((root.job.progress || 0) / 100) + height: parent.height + radius: 3 + color: root.job.status === "failed" ? Theme.error + : root.job.status === "completed" ? Theme.success + : Theme.primary + } + } + + CText { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + variant: "caption" + text: (root.job.progress || 0) + "%" + } + } + + CText { + variant: "caption" + text: root.job.created || "" + Layout.preferredWidth: 160 + color: Theme.textSecondary + } + + CButton { + text: "Cancel" + variant: "danger" + size: "sm" + enabled: root.job.status === "queued" || root.job.status === "processing" + visible: root.job.status !== "completed" && root.job.status !== "failed" + Layout.preferredWidth: 70 + onClicked: root.cancelRequested() + } + + Item { + visible: root.job.status === "completed" || root.job.status === "failed" + Layout.preferredWidth: 70 + } + } + + CDivider { + Layout.fillWidth: true + visible: !root.isLast + } +} diff --git a/qml/MetaBuilder/MediaJobTable.qml b/qml/MetaBuilder/MediaJobTable.qml index 49b196813..898acc0d3 100644 --- a/qml/MetaBuilder/MediaJobTable.qml +++ b/qml/MetaBuilder/MediaJobTable.qml @@ -11,16 +11,6 @@ CCard { signal cancelRequested(string jobId) - function jobStatusColor(status) { - switch (status) { - case "completed": return "success" - case "processing": return "warning" - case "queued": return "info" - case "failed": return "error" - default: return "info" - } - } - ColumnLayout { anchors.fill: parent anchors.margins: 16 @@ -36,7 +26,6 @@ CCard { CDivider { Layout.fillWidth: true } - // Table header FlexRow { Layout.fillWidth: true spacing: 8 @@ -51,95 +40,14 @@ CCard { CDivider { Layout.fillWidth: true } - // Job rows Repeater { model: jobs - delegate: ColumnLayout { + delegate: MediaJobRow { 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: cancelRequested(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 - } + job: modelData + isLast: index >= jobs.length - 1 + onCancelRequested: jobTable.cancelRequested(modelData.id) } } } diff --git a/qml/MetaBuilder/ProfileDBAL.js b/qml/MetaBuilder/ProfileDBAL.js new file mode 100644 index 000000000..b783480eb --- /dev/null +++ b/qml/MetaBuilder/ProfileDBAL.js @@ -0,0 +1,35 @@ +.pragma library + +// DBAL persistence helpers for profile view. + +function loadProfile(dbal, currentUser, callback) { + if (!currentUser) return + dbal.read("user", currentUser, function(result, error) { + if (result) callback(result) + }) +} + +function saveProfile(dbal, currentUser, data, callback) { + dbal.update("user", currentUser, data, function(result, error) { + callback(!!result, error) + }) +} + +function changePassword(dbal, currentUser, passwords, callback) { + if (passwords["new"] !== passwords["confirm"]) return + dbal.execute("core/change-password", { + userId: currentUser, + oldPassword: passwords["current"], + newPassword: passwords["new"] + }, function(result, error) { + callback(!!result, error) + }) +} + +function loadJson(relativePath) { + var xhr = new XMLHttpRequest() + xhr.open("GET", relativePath, false) + xhr.send() + if (xhr.status === 200 || xhr.status === 0) return JSON.parse(xhr.responseText) + return null +} diff --git a/qml/MetaBuilder/SchemaEditorDBAL.js b/qml/MetaBuilder/SchemaEditorDBAL.js new file mode 100644 index 000000000..3d8bb3c3a --- /dev/null +++ b/qml/MetaBuilder/SchemaEditorDBAL.js @@ -0,0 +1,72 @@ +.pragma library + +// DBAL persistence and schema CRUD helpers for schema editor. + +function loadSchemas(dbal, callback) { + dbal.execute("core/schema", {}, function(result, error) { + if (!error && result && result.items) { + var parsed = [] + for (var i = 0; i < result.items.length; i++) { + var item = result.items[i]; var fields = [] + if (item.fields) { + for (var j = 0; j < item.fields.length; j++) { + var f = item.fields[j] + fields.push({ name: f.name || "", type: f.type || "string", required: f.required || false, defaultValue: f.defaultValue || f["default"] || "", description: f.description || "" }) + } + } + parsed.push({ name: item.name || "", description: item.description || "", fields: fields }) + } + if (parsed.length > 0) callback(parsed) + } + }) +} + +function currentSchema(schemas, idx) { return schemas[idx] || null } +function currentFields(schemas, idx) { var s = currentSchema(schemas, idx); return s ? s.fields : [] } +function currentField(schemas, schemaIdx, fieldIdx) { + var fields = currentFields(schemas, schemaIdx) + return (fieldIdx >= 0 && fieldIdx < fields.length) ? fields[fieldIdx] : null +} + +function updateField(schemas, schemaIdx, fieldIdx, key, value) { + var copy = JSON.parse(JSON.stringify(schemas)) + copy[schemaIdx].fields[fieldIdx][key] = value; return copy +} + +function addSchema(schemas, name, description, dbal, loadFn) { + if (name.trim() === "") return null + var schemaData = { name: name.trim(), description: description.trim(), fields: [{ name: "id", type: "string", required: true, defaultValue: "uuid()", description: "Primary key" }] } + if (dbal && dbal.connected) { + dbal.create("schema", schemaData, function(result, error) { if (!error) loadFn() }) + } + var copy = JSON.parse(JSON.stringify(schemas)); copy.push(schemaData) + return { schemas: copy, selectedIndex: copy.length - 1 } +} + +function deleteSchema(schemas, selectedIndex) { + if (schemas.length <= 1) return null + var copy = JSON.parse(JSON.stringify(schemas)); copy.splice(selectedIndex, 1) + var newIdx = selectedIndex >= copy.length ? copy.length - 1 : selectedIndex + return { schemas: copy, selectedIndex: newIdx } +} + +function addField(schemas, schemaIdx, fieldData) { + if (fieldData.name.trim() === "") return null + var copy = JSON.parse(JSON.stringify(schemas)) + copy[schemaIdx].fields.push({ name: fieldData.name.trim(), type: fieldData.type, required: fieldData.required, defaultValue: fieldData.defaultValue, description: fieldData.description }) + return { schemas: copy, selectedFieldIndex: copy[schemaIdx].fields.length - 1 } +} + +function deleteField(schemas, schemaIdx, fieldIdx) { + if (fieldIdx < 0) return schemas + var copy = JSON.parse(JSON.stringify(schemas)) + copy[schemaIdx].fields.splice(fieldIdx, 1); return copy +} + +function loadJson(relativePath) { + var xhr = new XMLHttpRequest() + xhr.open("GET", relativePath, false) + xhr.send() + if (xhr.status === 200 || xhr.status === 0) return JSON.parse(xhr.responseText) + return [] +} diff --git a/qml/MetaBuilder/ThemeEditorLogic.js b/qml/MetaBuilder/ThemeEditorLogic.js new file mode 100644 index 000000000..8cdb86604 --- /dev/null +++ b/qml/MetaBuilder/ThemeEditorLogic.js @@ -0,0 +1,39 @@ +.pragma library + +// Theme editor color token and reset helpers. + +function applyColorChange(root, token, value) { + switch (token) { + case "primary": root.customPrimary = value; break + case "background": root.customBackground = value; break + case "surface": root.customSurface = value; break + case "paper": root.customPaper = value; break + case "text": root.customText = value; break + case "textSecondary": root.customTextSecondary = value; break + case "border": root.customBorder = value; break + case "error": root.customError = value; break + case "warning": root.customWarning = value; break + case "success": root.customSuccess = value; break + case "info": root.customInfo = value; break + } +} + +function resetToDefaults(root, Theme) { + root.customPrimary = Theme.primary; root.customBackground = Theme.background + root.customSurface = Theme.surface; root.customPaper = Theme.paper + root.customText = Theme.text; root.customTextSecondary = Theme.textSecondary + root.customBorder = Theme.border; root.customError = Theme.error + root.customWarning = Theme.warning; root.customSuccess = Theme.success + root.customInfo = Theme.info + root.fontFamily = "Inter"; root.baseFontSize = 14; root.baseSpacing = 8 + root.radiusSmall = 4; root.radiusMedium = 8; root.radiusLarge = 16 + root.hasUnsavedChanges = false +} + +function loadJson(relativePath) { + var xhr = new XMLHttpRequest() + xhr.open("GET", relativePath, false) + xhr.send() + if (xhr.status === 200 || xhr.status === 0) return JSON.parse(xhr.responseText) + return [] +} diff --git a/qml/MetaBuilder/qmldir b/qml/MetaBuilder/qmldir index d4f10b0d2..feb1c33c3 100644 --- a/qml/MetaBuilder/qmldir +++ b/qml/MetaBuilder/qmldir @@ -124,6 +124,7 @@ CGodPanelGuideTab 1.0 CGodPanelGuideTab.qml CNodePaletteItem 1.0 CNodePaletteItem.qml CNodePortColumn 1.0 CNodePortColumn.qml CNodePorts 1.0 CNodePorts.qml +CKeyboardShortcuts 1.0 CKeyboardShortcuts.qml CPackageDetailSidebar 1.0 CPackageDetailSidebar.qml CPackageListItem 1.0 CPackageListItem.qml CssSuggestionPopup 1.0 CssSuggestionPopup.qml @@ -154,3 +155,15 @@ CExecutionStatusDot 1.0 CExecutionStatusDot.qml CSmtpBodyEditor 1.0 CSmtpBodyEditor.qml CRoutePermissionSection 1.0 CRoutePermissionSection.qml LuaScriptInfoSection 1.0 LuaScriptInfoSection.qml +CGodPanelSettingsTab 1.0 CGodPanelSettingsTab.qml +CModeratorHeader 1.0 CModeratorHeader.qml +ThemePreviewStatusCard 1.0 ThemePreviewStatusCard.qml +ThemePreviewActivityCard 1.0 ThemePreviewActivityCard.qml +MediaChannelSchedule 1.0 MediaChannelSchedule.qml +CStatCell 1.0 CStatCell.qml +CssPropertyRow 1.0 CssPropertyRow.qml +DropdownExpandedList 1.0 DropdownExpandedList.qml +MediaRadioPlaylist 1.0 MediaRadioPlaylist.qml +CNotificationIconBadge 1.0 CNotificationIconBadge.qml +CComponentPropsList 1.0 CComponentPropsList.qml +MediaJobRow 1.0 MediaJobRow.qml