import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import PhpQml.Bridge import Todo // local module — picks up TodoList.qml + TodoWindow.qml ApplicationWindow { id: window width: 720 height: 560 visible: true title: "php-qml — Todo example" // Single shared model means a second window sees the same data. // (Each ReactiveListModel instance has its own MercureClient // subscription, but the underlying server state is the same.) property alias rest: rest property alias todoModel: todoModel RestClient { id: rest baseUrl: BackendConnection.url token: BackendConnection.token } ReactiveListModel { id: todoModel baseUrl: BackendConnection.url token: BackendConnection.token source: "/api/todos" topic: "app://model/todo" onCommandFailed: function(key, status, problem) { log.append("× failed " + status + ": " + JSON.stringify(problem)) } onCommandTimedOut: function(key) { log.append("× timed out " + key) } } Component { id: secondWindowCmp TodoWindow {} } AppShell { anchors.fill: parent ColumnLayout { anchors.fill: parent anchors.margins: 16 spacing: 12 // ── Header / actions ──────────────────────────────────── RowLayout { Layout.fillWidth: true spacing: 8 Label { text: "Todos" font.pixelSize: 18 font.bold: true } Item { Layout.fillWidth: true } Button { text: "Mark all done" onClicked: { rest.post("/api/mark-all-done").then(function(r) { log.append("→ mark-all-done") }).catch(function(e) { log.append("× mark-all-done " + e.status) }) } } Button { text: "Open second window" onClicked: { const w = secondWindowCmp.createObject() if (w) w.show() } } } // ── Add input ─────────────────────────────────────────── RowLayout { Layout.fillWidth: true spacing: 8 TextField { id: newTodoField Layout.fillWidth: true placeholderText: "What needs doing?" onAccepted: addBtn.clicked() } Button { id: addBtn text: "Add" enabled: newTodoField.text.trim().length > 0 && BackendConnection.connectionState === BackendConnection.Online onClicked: { const title = newTodoField.text.trim() if (!title) return rest.post("/api/todos", {title: title, done: false}) .then(function(r) { log.append("→ added '" + title + "'") }) .catch(function(e) { log.append("× add failed " + e.status) }) newTodoField.clear() } } } // ── Todo list ─────────────────────────────────────────── Frame { Layout.fillWidth: true Layout.fillHeight: true padding: 0 ListView { id: todoView anchors.fill: parent clip: true model: todoModel spacing: 1 delegate: ItemDelegate { required property string id required property string title required property bool done required property bool pending width: ListView.view.width opacity: pending ? 0.5 : 1.0 contentItem: RowLayout { spacing: 8 CheckBox { checked: done enabled: !pending onToggled: { todoModel.invoke( "PATCH", "/api/todos/" + id, { done: checked }, { op: "upsert", id: id, data: { id: id, title: title, done: checked } } ) } } Label { Layout.fillWidth: true text: title font.strikeout: done elide: Text.ElideRight } Button { text: "×" flat: true enabled: !pending onClicked: { todoModel.invoke( "DELETE", "/api/todos/" + id, null, { op: "delete", id: id } ) } } } } Label { anchors.centerIn: parent visible: todoView.count === 0 && todoModel.ready text: "No todos yet — add one above." opacity: 0.6 } } } // ── Status / log ──────────────────────────────────────── Label { text: BackendConnection.url color: "#888" font.pixelSize: 11 elide: Text.ElideRight Layout.fillWidth: true } Frame { Layout.fillWidth: true Layout.preferredHeight: 100 padding: 0 ScrollView { anchors.fill: parent TextArea { id: log readOnly: true wrapMode: TextArea.Wrap font.family: "monospace" font.pixelSize: 11 } } } DevConsole { id: devConsole visible: false Layout.fillWidth: true Layout.preferredHeight: 220 } } } // Ctrl+` toggles the FrankenPHP child output console (Phase 5 §13). Shortcut { sequences: ["Ctrl+`", "Ctrl+~"] onActivated: devConsole.visible = !devConsole.visible } Connections { target: SingleInstance function onLaunchArgsReceived(args) { window.requestActivate() } } }