Files
php-qml/examples/todo/qml/Main.qml
magdev 4c15ac281c Phase 5 sub-commit 1: DevConsole + child-output capture + Ctrl+` toggle
BackendConnection now captures the bundled FrankenPHP child's merged
stdout+stderr into a 500-line ring buffer, mirrors each line through
qCInfo(lcBundled) so terminal users still see logs, and exposes
childLogTail() / childLogLine for QML.

DevConsole.qml is an opt-in monospaced viewer with auto-scroll + clear
that the skeleton and the todo example bind to Ctrl+`. Dev mode (when
BRIDGE_URL is set, no bundled child) renders an explanatory hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 20:58:53 +02:00

226 lines
7.7 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
}
}