| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- import QtQuick 2.15
- import QtQuick.Controls 2.15
- import QtQuick.Layouts 1.15
- import "../components"
- import "../components" as Components
- // Now Playing — the signature screen. Progress is not a percent bar: it is
- // an ember arc traced around the live pattern disc, with the ball as the
- // moving endpoint — the interface shows progress the way the machine makes
- // it. Plain Canvas drawing only (no effects layers; linuxfb-safe).
- Page {
- id: page
- property var backend: null
- property var stackView: null
- property string patternName: ""
- property string patternPreview: "" // Backend provides this via executionStarted signal
- readonly property bool hasPattern: (backend && backend.currentFile !== "") || patternName !== ""
- readonly property real progressRatio: backend ? backend.progress / 100 : 0
- readonly property bool inPause: backend && backend.pauseRemaining >= 0
- property string displayName: {
- var name = ""
- if (backend && backend.currentFile) name = backend.currentFile
- else if (patternName) name = patternName
- if (!name) return ""
- var parts = name.split('/')
- return parts[parts.length - 1].replace('.thr', '')
- }
- function formatDuration(s) {
- if (s < 0) return ""
- var h = Math.floor(s / 3600)
- var m = Math.floor((s % 3600) / 60)
- var sec = s % 60
- function pad(n) { return (n < 10 ? "0" : "") + n }
- return h > 0 ? h + ":" + pad(m) + ":" + pad(sec) : m + ":" + pad(sec)
- }
- // Direct connection to backend signals
- Connections {
- target: backend
- function onExecutionStarted(fileName, preview) {
- patternName = fileName
- patternPreview = preview
- }
- }
- Rectangle {
- anchors.fill: parent
- color: Components.ThemeManager.backgroundColor
- }
- ColumnLayout {
- anchors.fill: parent
- spacing: 0
- // Header
- Rectangle {
- Layout.fillWidth: true
- Layout.preferredHeight: Components.ThemeManager.headerHeight
- color: Components.ThemeManager.surfaceColor
- Rectangle {
- anchors.bottom: parent.bottom
- width: parent.width
- height: 1
- color: Components.ThemeManager.borderColor
- }
- RowLayout {
- anchors.fill: parent
- anchors.leftMargin: Components.ThemeManager.spaceLg
- anchors.rightMargin: Components.ThemeManager.spaceLg
- ConnectionStatus {
- backend: page.backend
- Layout.rightMargin: Components.ThemeManager.spaceSm
- }
- Label {
- text: "Now Playing"
- font.family: Components.ThemeManager.fontDisplay
- font.pixelSize: Components.ThemeManager.fontSizeTitle
- color: Components.ThemeManager.textPrimary
- }
- Item {
- Layout.fillWidth: true
- }
- }
- }
- // Content
- RowLayout {
- Layout.fillWidth: true
- Layout.fillHeight: true
- spacing: 0
- // ---- Left: the disc and its progress ring ----
- Item {
- id: stage
- Layout.fillHeight: true
- Layout.preferredWidth: page.width * 0.55
- Item {
- id: ringWrap
- anchors.centerIn: parent
- width: Math.min(stage.width, stage.height) - 2 * Components.ThemeManager.spaceXl
- height: width
- // Track + progress arc, redrawn only when progress moves
- Canvas {
- id: arcCanvas
- anchors.fill: parent
- // While weaving: fraction of the pattern drawn.
- // Between patterns: the countdown to the next one.
- property real ratio: {
- if (inPause)
- return backend && backend.pauseTotal > 0
- ? 1 - backend.pauseRemaining / backend.pauseTotal : 0
- return hasPattern ? progressRatio : 0
- }
- onRatioChanged: requestPaint()
- Connections {
- target: Components.ThemeManager
- function onDarkModeChanged() { arcCanvas.requestPaint() }
- }
- onPaint: {
- var ctx = getContext("2d")
- var w = width, h = height
- ctx.clearRect(0, 0, w, h)
- var cx = w / 2, cy = h / 2
- var lineW = 5
- var r = Math.min(w, h) / 2 - lineW / 2
- if (r <= 0)
- return // layout not settled yet
- ctx.lineWidth = lineW
- ctx.lineCap = "round"
- // Track
- ctx.beginPath()
- ctx.strokeStyle = String(Components.ThemeManager.cardColor)
- ctx.arc(cx, cy, r, 0, 2 * Math.PI)
- ctx.stroke()
- // Progress, from 12 o'clock
- if (ratio > 0) {
- ctx.beginPath()
- ctx.strokeStyle = String(Components.ThemeManager.accent)
- ctx.arc(cx, cy, r, -Math.PI / 2,
- -Math.PI / 2 + ratio * 2 * Math.PI)
- ctx.stroke()
- }
- }
- }
- // The ball: endpoint of the arc (two stacked dots stand in
- // for a glow — no shadow effects on linuxfb)
- Item {
- anchors.fill: parent
- rotation: arcCanvas.ratio * 360
- visible: (hasPattern || inPause) && arcCanvas.ratio > 0
- Rectangle {
- anchors.horizontalCenter: parent.horizontalCenter
- y: -7
- width: 19
- height: 19
- radius: 9.5
- color: Components.ThemeManager.accent
- opacity: 0.3
- }
- Rectangle {
- anchors.horizontalCenter: parent.horizontalCenter
- y: -3
- width: 11
- height: 11
- radius: 5.5
- color: Components.ThemeManager.accent
- }
- }
- // Live pattern disc (the preview PNG is itself a round dish)
- Image {
- anchors.fill: parent
- anchors.margins: 14
- source: patternPreview ? "file://" + patternPreview : ""
- fillMode: Image.PreserveAspectFit
- asynchronous: true
- visible: hasPattern && status === Image.Ready
- }
- // Resting dish when idle (or while the preview renders)
- Rectangle {
- anchors.fill: parent
- anchors.margins: 14
- radius: width / 2
- color: Components.ThemeManager.surfaceColor
- border.width: 1
- border.color: Components.ThemeManager.borderColor
- visible: !hasPattern || patternPreview === ""
- Column {
- anchors.centerIn: parent
- spacing: Components.ThemeManager.spaceSm
- Components.Icon {
- name: "radio_unchecked"
- size: 34
- color: Components.ThemeManager.textTertiary
- anchors.horizontalCenter: parent.horizontalCenter
- }
- Label {
- text: {
- if (inPause) return "Resting between patterns"
- return hasPattern ? "Rendering preview" : "The table is resting"
- }
- font.family: Components.ThemeManager.fontMedium
- font.pixelSize: Components.ThemeManager.fontSizeBody
- color: Components.ThemeManager.textSecondary
- anchors.horizontalCenter: parent.horizontalCenter
- }
- }
- }
- }
- }
- // ---- Right: name, state, transport, speed ----
- Rectangle {
- Layout.fillHeight: true
- Layout.fillWidth: true
- color: Components.ThemeManager.surfaceColor
- Rectangle {
- anchors.left: parent.left
- width: 1
- height: parent.height
- color: Components.ThemeManager.borderColor
- }
- ColumnLayout {
- anchors.fill: parent
- anchors.margins: Components.ThemeManager.spaceXl
- spacing: 0
- Label {
- text: inPause ? "Up next" : "Now weaving"
- font.family: Components.ThemeManager.fontDisplay
- font.pixelSize: 11
- font.letterSpacing: 1.6
- font.capitalization: Font.AllUppercase
- color: Components.ThemeManager.accent
- }
- Label {
- Layout.fillWidth: true
- Layout.topMargin: Components.ThemeManager.spaceSm
- text: {
- if (inPause && backend && backend.nextPattern)
- return backend.nextPattern
- return displayName || "Nothing playing"
- }
- font.family: Components.ThemeManager.fontDisplay
- font.pixelSize: Components.ThemeManager.fontSizeDisplay
- color: hasPattern || inPause ? Components.ThemeManager.textPrimary
- : Components.ThemeManager.textTertiary
- elide: Text.ElideRight
- maximumLineCount: 2
- wrapMode: Text.Wrap
- }
- // Playlist position
- Label {
- Layout.fillWidth: true
- Layout.topMargin: Components.ThemeManager.spaceXs
- visible: backend && backend.playlistActive && backend.playlistTotal > 0
- text: {
- if (!backend) return ""
- var s = (backend.playlistName ? backend.playlistName + " · " : "")
- + (backend.playlistIndex + 1) + " of " + backend.playlistTotal
- if (backend.playlistClearing) s += " · clearing"
- return s
- }
- font.family: Components.ThemeManager.fontBody
- font.pixelSize: Components.ThemeManager.fontSizeCaption
- color: Components.ThemeManager.textSecondary
- elide: Text.ElideMiddle
- }
- // Progress / pause countdown line
- Label {
- Layout.fillWidth: true
- Layout.topMargin: Components.ThemeManager.spaceLg
- visible: hasPattern || inPause
- textFormat: Text.StyledText
- text: {
- if (!backend) return ""
- if (inPause)
- return "<b>" + formatDuration(backend.pauseRemaining)
- + "</b> until the next pattern"
- var pct = Math.round(backend.progress)
- var s = "<b>" + pct + "%</b> woven"
- if (backend.isPaused) s += " · paused"
- return s
- }
- font.family: Components.ThemeManager.fontBody
- font.pixelSize: Components.ThemeManager.fontSizeBody
- color: Components.ThemeManager.textSecondary
- }
- Item { Layout.fillHeight: true }
- // Transport
- RowLayout {
- Layout.fillWidth: true
- spacing: Components.ThemeManager.spaceSm
- ModernControlButton {
- Layout.fillWidth: true
- Layout.preferredWidth: 3
- Layout.preferredHeight: Components.ThemeManager.controlHeight
- icon: (backend && backend.isRunning && !backend.isPaused) ? "pause" : "play_arrow"
- text: (backend && backend.isRunning && !backend.isPaused) ? "Pause" : "Resume"
- buttonColor: Components.ThemeManager.accent
- enabled: backend && backend.currentFile !== ""
- onClicked: {
- if (!backend) return
- if (backend.isPaused) backend.resumeExecution()
- else backend.pauseExecution()
- }
- }
- ModernControlButton {
- Layout.fillWidth: true
- Layout.preferredWidth: 2
- Layout.preferredHeight: Components.ThemeManager.controlHeight
- icon: "stop"
- text: "Stop"
- outlined: true
- buttonColor: Components.ThemeManager.danger
- enabled: backend !== null
- onClicked: if (backend) backend.stopExecution()
- }
- ModernControlButton {
- Layout.fillWidth: true
- Layout.preferredWidth: 2
- Layout.preferredHeight: Components.ThemeManager.controlHeight
- icon: "skip_next"
- text: "Skip"
- buttonColor: Components.ThemeManager.cardColor
- enabled: backend !== null
- onClicked: if (backend) backend.skipPattern()
- }
- }
- // Speed
- Label {
- Layout.topMargin: Components.ThemeManager.spaceLg
- text: "Speed · mm/s"
- font.family: Components.ThemeManager.fontDisplay
- font.pixelSize: 11
- font.letterSpacing: 1.4
- font.capitalization: Font.AllUppercase
- color: Components.ThemeManager.textTertiary
- }
- // Segmented control
- Rectangle {
- id: speedSeg
- Layout.fillWidth: true
- Layout.topMargin: Components.ThemeManager.spaceSm
- Layout.preferredHeight: 48
- radius: height / 2
- color: Components.ThemeManager.cardColor
- property string currentSelection: backend ? backend.getCurrentSpeedOption() : "200"
- Connections {
- target: backend
- function onSpeedChanged(speed) {
- if (backend)
- speedSeg.currentSelection = backend.getCurrentSpeedOption()
- }
- }
- Row {
- anchors.fill: parent
- anchors.margins: 4
- spacing: 2
- Repeater {
- model: ["50", "100", "150", "200", "300", "500"]
- Rectangle {
- property bool selected: speedSeg.currentSelection === modelData
- width: (parent.width - 10) / 6
- height: parent.height
- radius: height / 2
- color: selected ? Components.ThemeManager.backgroundColor : "transparent"
- border.width: selected ? 1 : 0
- border.color: Components.ThemeManager.borderColor
- Label {
- anchors.centerIn: parent
- text: modelData
- font.family: Components.ThemeManager.fontMedium
- font.pixelSize: 13
- color: parent.selected ? Components.ThemeManager.accent
- : Components.ThemeManager.textSecondary
- }
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (backend) {
- backend.setSpeedByOption(modelData)
- speedSeg.currentSelection = modelData
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
|