LedControlPage.qml 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. import QtQuick 2.15
  2. import QtQuick.Controls 2.15
  3. import QtQuick.Layouts 1.15
  4. import QtQuick.Dialogs
  5. import "../components"
  6. import "../components" as Components
  7. Page {
  8. id: page
  9. property var backend: null
  10. // Local state
  11. property bool ledPowerOn: false
  12. property int ledBrightness: 100
  13. property string ledProvider: "none"
  14. property bool ledConnected: false
  15. property int currentEffectIndex: 0
  16. property int currentPaletteIndex: 0
  17. property string ledColor: "#ffffff"
  18. property var effectsList: []
  19. property var palettesList: []
  20. readonly property bool hasRing: ledProvider === "dw_leds"
  21. // 'ball' tracker state (firmware-native effect that follows the sand ball)
  22. property string ballColor2: "#000000"
  23. property int ballFgBright: 255
  24. property int ballBgBright: 255
  25. property int ballSize: 3
  26. property string ballBg: "static"
  27. property string ballDirection: "cw"
  28. property int ballAlign: 0
  29. // Resolve the 'ball' effect id from the catalogue (id 38 today).
  30. property int ballEffectId: {
  31. for (var i = 0; i < effectsList.length; i++)
  32. if (effectsList[i].name === "ball")
  33. return effectsList[i].id
  34. return 38
  35. }
  36. property bool ballActive: currentEffectIndex === ballEffectId
  37. // The ball tracker has its own card (matching the web UI), so it is not
  38. // offered as a plain effect chip.
  39. property var selectableEffects: effectsList.filter(function(e) {
  40. return e.name !== "ball"
  41. })
  42. // Background options for the ball: solid colour, off, or any other effect.
  43. property var ballBgOptions: {
  44. var opts = [
  45. {"label": "Solid", "value": "static"},
  46. {"label": "Off", "value": "off"}
  47. ]
  48. for (var i = 0; i < effectsList.length; i++) {
  49. var n = effectsList[i].name
  50. if (n !== "ball" && n !== "off" && n !== "static")
  51. opts.push({"label": n.charAt(0).toUpperCase() + n.slice(1), "value": n})
  52. }
  53. return opts
  54. }
  55. // Predefined colors for quick selection (muted tones to fit the dark UI)
  56. property var presetColors: [
  57. {"name": "White", "color": "#e8e8e8", "sendColor": "#ffffff"},
  58. {"name": "Warm", "color": "#d4a574", "sendColor": "#ffaa55"},
  59. {"name": "Red", "color": "#c45c5c", "sendColor": "#ff0000"},
  60. {"name": "Orange", "color": "#d4875c", "sendColor": "#ff8800"},
  61. {"name": "Yellow", "color": "#c9b95c", "sendColor": "#ffff00"},
  62. {"name": "Green", "color": "#5cb85c", "sendColor": "#00ff00"},
  63. {"name": "Cyan", "color": "#5cb8b8", "sendColor": "#00ffff"},
  64. {"name": "Blue", "color": "#5c7cc4", "sendColor": "#0000ff"},
  65. {"name": "Purple", "color": "#8b5cc4", "sendColor": "#8800ff"},
  66. {"name": "Pink", "color": "#c45c99", "sendColor": "#ff00ff"}
  67. ]
  68. // Backend signal connections
  69. Connections {
  70. target: backend
  71. function onLedStatusChanged() {
  72. if (backend) {
  73. ledPowerOn = backend.ledPowerOn
  74. ledBrightness = backend.ledBrightness
  75. ledProvider = backend.ledProvider
  76. ledConnected = backend.ledConnected
  77. currentEffectIndex = backend.ledCurrentEffect
  78. currentPaletteIndex = backend.ledCurrentPalette
  79. ledColor = backend.ledColor
  80. ballColor2 = backend.ledColor2
  81. ballFgBright = backend.ledBallFgBright
  82. ballBgBright = backend.ledBallBgBright
  83. ballSize = backend.ledBallSize
  84. ballBg = backend.ledBallBg
  85. ballDirection = backend.ledBallDirection
  86. ballAlign = backend.ledBallAlign
  87. }
  88. }
  89. function onLedEffectsLoaded(effects) {
  90. effectsList = effects
  91. }
  92. function onLedPalettesLoaded(palettes) {
  93. palettesList = palettes
  94. }
  95. }
  96. // Load LED config on page load
  97. Component.onCompleted: {
  98. if (backend) {
  99. backend.loadLedConfig()
  100. }
  101. }
  102. Rectangle {
  103. anchors.fill: parent
  104. color: Components.ThemeManager.backgroundColor
  105. }
  106. ColumnLayout {
  107. anchors.fill: parent
  108. spacing: 0
  109. // Header
  110. Rectangle {
  111. Layout.fillWidth: true
  112. Layout.preferredHeight: Components.ThemeManager.headerHeight
  113. color: Components.ThemeManager.surfaceColor
  114. Rectangle {
  115. anchors.bottom: parent.bottom
  116. width: parent.width
  117. height: 1
  118. color: Components.ThemeManager.borderColor
  119. }
  120. RowLayout {
  121. anchors.fill: parent
  122. anchors.leftMargin: Components.ThemeManager.spaceLg
  123. anchors.rightMargin: Components.ThemeManager.spaceLg
  124. ConnectionStatus {
  125. backend: page.backend
  126. Layout.rightMargin: Components.ThemeManager.spaceSm
  127. }
  128. Label {
  129. text: "Light"
  130. font.family: Components.ThemeManager.fontDisplay
  131. font.pixelSize: Components.ThemeManager.fontSizeTitle
  132. color: Components.ThemeManager.textPrimary
  133. }
  134. Item {
  135. Layout.fillWidth: true
  136. }
  137. }
  138. }
  139. // Content — the light's state stays pinned on the left (power and
  140. // brightness never scroll away); the right column scrolls through
  141. // every effect, the ball tracker, every palette, and quick colors.
  142. RowLayout {
  143. Layout.fillWidth: true
  144. Layout.fillHeight: true
  145. spacing: 0
  146. // ---- Left: state panel (fits without scrolling in most
  147. // setups; scrolls independently when the backlight card is
  148. // present on the Pi) ----
  149. ScrollView {
  150. Layout.fillWidth: true
  151. Layout.fillHeight: true
  152. Layout.maximumWidth: hasRing ? Math.round(page.width * 0.42) : page.width
  153. contentWidth: availableWidth
  154. ColumnLayout {
  155. width: parent.width
  156. spacing: 0
  157. // Table light: power + brightness (or provider notices)
  158. SettingsCard {
  159. Layout.rightMargin: hasRing ? Components.ThemeManager.spaceSm
  160. : Components.ThemeManager.spaceLg
  161. Layout.preferredHeight: providerColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  162. ColumnLayout {
  163. id: providerColumn
  164. anchors.fill: parent
  165. anchors.margins: Components.ThemeManager.spaceLg
  166. spacing: Components.ThemeManager.spaceMd
  167. RowLayout {
  168. Layout.fillWidth: true
  169. spacing: Components.ThemeManager.spaceSm
  170. SectionLabel {
  171. text: "Table light"
  172. Layout.fillWidth: true
  173. }
  174. Rectangle {
  175. visible: hasRing
  176. width: 8
  177. height: 8
  178. radius: 4
  179. color: ledConnected ? Components.ThemeManager.ok
  180. : Components.ThemeManager.danger
  181. }
  182. Label {
  183. visible: hasRing
  184. text: ledConnected ? "Connected" : "Disconnected"
  185. font.family: Components.ThemeManager.fontBody
  186. font.pixelSize: Components.ThemeManager.fontSizeCaption
  187. color: Components.ThemeManager.textTertiary
  188. }
  189. }
  190. // Not configured message
  191. Label {
  192. visible: ledProvider === "none"
  193. text: "No light ring is set up for this table. Configure one in the Dune Weaver web interface."
  194. font.family: Components.ThemeManager.fontBody
  195. font.pixelSize: Components.ThemeManager.fontSizeBody
  196. color: Components.ThemeManager.textSecondary
  197. wrapMode: Text.WordWrap
  198. Layout.fillWidth: true
  199. }
  200. // WLED notice
  201. Label {
  202. visible: ledProvider === "wled"
  203. text: "This table's light is driven by WLED — use the Dune Weaver web interface to control it."
  204. font.family: Components.ThemeManager.fontBody
  205. font.pixelSize: Components.ThemeManager.fontSizeBody
  206. color: Components.ThemeManager.textSecondary
  207. wrapMode: Text.WordWrap
  208. Layout.fillWidth: true
  209. }
  210. // Power row
  211. RowLayout {
  212. visible: hasRing
  213. Layout.fillWidth: true
  214. spacing: Components.ThemeManager.spaceMd
  215. Label {
  216. text: ledPowerOn ? "On" : "Off"
  217. font.family: Components.ThemeManager.fontDisplay
  218. font.pixelSize: Components.ThemeManager.fontSizeBody
  219. color: Components.ThemeManager.textPrimary
  220. Layout.fillWidth: true
  221. }
  222. DwSwitch {
  223. id: powerSwitch
  224. checked: ledPowerOn
  225. onToggled: {
  226. if (backend) {
  227. backend.toggleLedPower()
  228. }
  229. }
  230. // A user toggle breaks the declarative binding;
  231. // this keeps the switch following backend state.
  232. Binding {
  233. target: powerSwitch
  234. property: "checked"
  235. value: ledPowerOn
  236. }
  237. }
  238. }
  239. // Brightness row
  240. RowLayout {
  241. visible: hasRing
  242. Layout.fillWidth: true
  243. spacing: Components.ThemeManager.spaceMd
  244. Label {
  245. text: "Brightness"
  246. font.family: Components.ThemeManager.fontBody
  247. font.pixelSize: Components.ThemeManager.fontSizeCaption
  248. color: Components.ThemeManager.textSecondary
  249. Layout.preferredWidth: 76
  250. }
  251. DwSlider {
  252. id: brightnessSlider
  253. Layout.fillWidth: true
  254. from: 0
  255. to: 100
  256. stepSize: 5
  257. value: ledBrightness
  258. onMoved: {
  259. if (backend) {
  260. backend.setLedBrightness(Math.round(value))
  261. }
  262. }
  263. }
  264. Label {
  265. text: Math.round(brightnessSlider.value) + "%"
  266. font.family: Components.ThemeManager.fontMedium
  267. font.pixelSize: Components.ThemeManager.fontSizeCaption
  268. color: Components.ThemeManager.textPrimary
  269. Layout.preferredWidth: 36
  270. horizontalAlignment: Text.AlignRight
  271. }
  272. }
  273. }
  274. }
  275. // Screen brightness (always visible, controls Pi LCD backlight)
  276. SettingsCard {
  277. Layout.rightMargin: hasRing ? Components.ThemeManager.spaceSm
  278. : Components.ThemeManager.spaceLg
  279. Layout.preferredHeight: lcdColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  280. visible: backend && backend.lcdMaxBrightness > 0
  281. ColumnLayout {
  282. id: lcdColumn
  283. anchors.fill: parent
  284. anchors.margins: Components.ThemeManager.spaceLg
  285. spacing: Components.ThemeManager.spaceSm
  286. SectionLabel {
  287. text: "Screen brightness"
  288. }
  289. RowLayout {
  290. Layout.fillWidth: true
  291. spacing: Components.ThemeManager.spaceMd
  292. Components.Icon {
  293. name: "brightness"
  294. size: 20
  295. color: Components.ThemeManager.textSecondary
  296. }
  297. DwSlider {
  298. id: lcdBrightnessSlider
  299. Layout.fillWidth: true
  300. from: 0
  301. to: backend ? backend.lcdMaxBrightness : 255
  302. stepSize: 1
  303. value: backend ? backend.lcdBrightness : 255
  304. onMoved: {
  305. if (backend) {
  306. backend.setLcdBrightness(Math.round(value))
  307. }
  308. }
  309. }
  310. Label {
  311. text: {
  312. var max = backend ? backend.lcdMaxBrightness : 255
  313. if (max <= 0) return "0%"
  314. return Math.round(lcdBrightnessSlider.value / max * 100) + "%"
  315. }
  316. font.family: Components.ThemeManager.fontMedium
  317. font.pixelSize: Components.ThemeManager.fontSizeCaption
  318. color: Components.ThemeManager.textPrimary
  319. Layout.preferredWidth: 36
  320. horizontalAlignment: Text.AlignRight
  321. }
  322. }
  323. }
  324. }
  325. // Quick colors — set the whole ring to one plain colour
  326. SettingsCard {
  327. Layout.rightMargin: Components.ThemeManager.spaceSm
  328. Layout.preferredHeight: quickColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  329. visible: hasRing
  330. ColumnLayout {
  331. id: quickColumn
  332. anchors.fill: parent
  333. anchors.margins: Components.ThemeManager.spaceLg
  334. spacing: Components.ThemeManager.spaceMd
  335. SectionLabel {
  336. text: "Quick colors"
  337. }
  338. GridLayout {
  339. Layout.fillWidth: true
  340. columns: 5
  341. rowSpacing: Components.ThemeManager.spaceSm
  342. columnSpacing: Components.ThemeManager.spaceSm
  343. Repeater {
  344. model: presetColors
  345. Rectangle {
  346. property bool isSel: ledColor.toLowerCase() === modelData.sendColor.toLowerCase()
  347. Layout.fillWidth: true
  348. Layout.preferredHeight: 40
  349. radius: 20
  350. color: modelData.color
  351. border.color: isSel ? Components.ThemeManager.textPrimary
  352. : Qt.darker(modelData.color, 1.2)
  353. border.width: isSel ? 2 : 1
  354. MouseArea {
  355. anchors.fill: parent
  356. onClicked: {
  357. if (backend) {
  358. backend.setLedColorHex(modelData.sendColor)
  359. }
  360. }
  361. }
  362. }
  363. }
  364. }
  365. }
  366. }
  367. }
  368. }
  369. // ---- Right: everything the ring can do (scrolls) ----
  370. ScrollView {
  371. Layout.fillWidth: true
  372. Layout.fillHeight: true
  373. contentWidth: availableWidth
  374. visible: hasRing
  375. ColumnLayout {
  376. width: parent.width
  377. spacing: 0
  378. // Effects — the full firmware catalogue
  379. SettingsCard {
  380. Layout.leftMargin: Components.ThemeManager.spaceSm
  381. Layout.preferredHeight: effectsColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  382. ColumnLayout {
  383. id: effectsColumn
  384. anchors.fill: parent
  385. anchors.margins: Components.ThemeManager.spaceLg
  386. spacing: Components.ThemeManager.spaceMd
  387. SectionLabel {
  388. text: "Effect"
  389. }
  390. Label {
  391. visible: selectableEffects.length === 0
  392. text: "No effects available"
  393. font.family: Components.ThemeManager.fontBody
  394. font.pixelSize: Components.ThemeManager.fontSizeCaption
  395. color: Components.ThemeManager.textSecondary
  396. }
  397. GridLayout {
  398. Layout.fillWidth: true
  399. columns: 3
  400. rowSpacing: Components.ThemeManager.spaceSm
  401. columnSpacing: Components.ThemeManager.spaceSm
  402. visible: selectableEffects.length > 0
  403. Repeater {
  404. model: selectableEffects
  405. ChoiceChip {
  406. property int effectId: modelData.id !== undefined ? modelData.id : index
  407. property string effectName: modelData.name || ("Effect " + effectId)
  408. Layout.fillWidth: true
  409. label: effectName.charAt(0).toUpperCase() + effectName.slice(1)
  410. selected: effectId === currentEffectIndex
  411. onClicked: {
  412. if (backend) {
  413. backend.setLedEffect(effectId)
  414. currentEffectIndex = effectId
  415. }
  416. }
  417. }
  418. }
  419. }
  420. }
  421. }
  422. // Ball Tracker — firmware-native effect (id 38) that
  423. // follows the sand ball, so it lives with the effects.
  424. SettingsCard {
  425. Layout.leftMargin: Components.ThemeManager.spaceSm
  426. Layout.preferredHeight: ballCol.implicitHeight + 2 * Components.ThemeManager.spaceLg
  427. ColumnLayout {
  428. id: ballCol
  429. anchors.fill: parent
  430. anchors.margins: Components.ThemeManager.spaceLg
  431. spacing: Components.ThemeManager.spaceMd
  432. // Header row: title + enable toggle
  433. RowLayout {
  434. Layout.fillWidth: true
  435. spacing: Components.ThemeManager.spaceSm
  436. ColumnLayout {
  437. Layout.fillWidth: true
  438. spacing: 2
  439. SectionLabel {
  440. text: "Ball tracker"
  441. }
  442. Label {
  443. text: ballActive
  444. ? "Following the sand ball — replaces the effect above."
  445. : "A glowing dot that follows the sand ball."
  446. font.family: Components.ThemeManager.fontBody
  447. font.pixelSize: Components.ThemeManager.fontSizeCaption
  448. color: Components.ThemeManager.textSecondary
  449. wrapMode: Text.WordWrap
  450. Layout.fillWidth: true
  451. }
  452. }
  453. DwSwitch {
  454. id: ballSwitch
  455. checked: ballActive
  456. onToggled: {
  457. if (backend)
  458. backend.setBallTracker(!ballActive)
  459. }
  460. Binding {
  461. target: ballSwitch
  462. property: "checked"
  463. value: ballActive
  464. }
  465. }
  466. }
  467. // Controls (only meaningful while the ball effect is on)
  468. ColumnLayout {
  469. Layout.fillWidth: true
  470. visible: ballActive
  471. spacing: Components.ThemeManager.spaceMd
  472. // ---- Blob ----
  473. Label {
  474. text: "The dot"
  475. font.family: Components.ThemeManager.fontMedium
  476. font.pixelSize: Components.ThemeManager.fontSizeCaption
  477. color: Components.ThemeManager.textSecondary
  478. }
  479. // Blob colour swatches
  480. GridLayout {
  481. Layout.fillWidth: true
  482. columns: 10
  483. rowSpacing: Components.ThemeManager.spaceSm
  484. columnSpacing: Components.ThemeManager.spaceSm
  485. Repeater {
  486. model: presetColors
  487. Rectangle {
  488. property bool isSel: ledColor.toLowerCase() === modelData.sendColor.toLowerCase()
  489. Layout.fillWidth: true
  490. Layout.preferredHeight: 32
  491. radius: 16
  492. color: modelData.color
  493. border.color: isSel ? Components.ThemeManager.textPrimary
  494. : Qt.darker(modelData.color, 1.2)
  495. border.width: isSel ? 2 : 1
  496. MouseArea {
  497. anchors.fill: parent
  498. onClicked: { if (backend) backend.setLedColorHex(modelData.sendColor) }
  499. }
  500. }
  501. }
  502. }
  503. // Blob brightness
  504. RowLayout {
  505. Layout.fillWidth: true
  506. spacing: Components.ThemeManager.spaceMd
  507. Label {
  508. text: "Brightness"
  509. font.family: Components.ThemeManager.fontBody
  510. font.pixelSize: Components.ThemeManager.fontSizeCaption
  511. color: Components.ThemeManager.textSecondary
  512. Layout.preferredWidth: 84
  513. }
  514. DwSlider {
  515. id: ballFgSlider
  516. Layout.fillWidth: true
  517. from: 0; to: 255; stepSize: 1
  518. value: ballFgBright
  519. onMoved: { if (backend) backend.setLedBallFgBright(Math.round(value)) }
  520. }
  521. Label {
  522. text: Math.round(ballFgSlider.value)
  523. font.family: Components.ThemeManager.fontMedium
  524. font.pixelSize: Components.ThemeManager.fontSizeCaption
  525. color: Components.ThemeManager.textPrimary
  526. Layout.preferredWidth: 36
  527. horizontalAlignment: Text.AlignRight
  528. }
  529. }
  530. // Direction
  531. RowLayout {
  532. Layout.fillWidth: true
  533. spacing: Components.ThemeManager.spaceMd
  534. Label {
  535. text: "Direction"
  536. font.family: Components.ThemeManager.fontBody
  537. font.pixelSize: Components.ThemeManager.fontSizeCaption
  538. color: Components.ThemeManager.textSecondary
  539. Layout.preferredWidth: 84
  540. }
  541. Repeater {
  542. model: [
  543. {"label": "Clockwise", "value": "cw"},
  544. {"label": "Counter-CW", "value": "ccw"}
  545. ]
  546. ChoiceChip {
  547. Layout.fillWidth: true
  548. label: modelData.label
  549. selected: ballDirection === modelData.value
  550. onClicked: { if (backend) backend.setLedBallDirection(modelData.value) }
  551. }
  552. }
  553. }
  554. // Glow size
  555. RowLayout {
  556. Layout.fillWidth: true
  557. spacing: Components.ThemeManager.spaceMd
  558. Label {
  559. text: "Glow size"
  560. font.family: Components.ThemeManager.fontBody
  561. font.pixelSize: Components.ThemeManager.fontSizeCaption
  562. color: Components.ThemeManager.textSecondary
  563. Layout.preferredWidth: 84
  564. }
  565. DwSlider {
  566. id: ballSizeSlider
  567. Layout.fillWidth: true
  568. from: 1; to: 30; stepSize: 1
  569. value: ballSize
  570. onMoved: { if (backend) backend.setLedBallSize(Math.round(value)) }
  571. }
  572. Label {
  573. text: Math.round(ballSizeSlider.value)
  574. font.family: Components.ThemeManager.fontMedium
  575. font.pixelSize: Components.ThemeManager.fontSizeCaption
  576. color: Components.ThemeManager.textPrimary
  577. Layout.preferredWidth: 36
  578. horizontalAlignment: Text.AlignRight
  579. }
  580. }
  581. // Alignment
  582. RowLayout {
  583. Layout.fillWidth: true
  584. spacing: Components.ThemeManager.spaceMd
  585. Label {
  586. text: "Alignment"
  587. font.family: Components.ThemeManager.fontBody
  588. font.pixelSize: Components.ThemeManager.fontSizeCaption
  589. color: Components.ThemeManager.textSecondary
  590. Layout.preferredWidth: 84
  591. }
  592. DwSlider {
  593. id: ballAlignSlider
  594. Layout.fillWidth: true
  595. from: 0; to: 359; stepSize: 1
  596. value: ballAlign
  597. onMoved: { if (backend) backend.setLedBallAlign(Math.round(value)) }
  598. }
  599. Label {
  600. text: Math.round(ballAlignSlider.value) + "°"
  601. font.family: Components.ThemeManager.fontMedium
  602. font.pixelSize: Components.ThemeManager.fontSizeCaption
  603. color: Components.ThemeManager.textPrimary
  604. Layout.preferredWidth: 36
  605. horizontalAlignment: Text.AlignRight
  606. }
  607. }
  608. // ---- Background ----
  609. Rectangle {
  610. Layout.fillWidth: true
  611. Layout.preferredHeight: 1
  612. Layout.topMargin: Components.ThemeManager.spaceXs
  613. color: Components.ThemeManager.borderColor
  614. }
  615. Label {
  616. text: "Behind the dot"
  617. font.family: Components.ThemeManager.fontMedium
  618. font.pixelSize: Components.ThemeManager.fontSizeCaption
  619. color: Components.ThemeManager.textSecondary
  620. }
  621. // Background selector (Solid / Off / any effect)
  622. GridLayout {
  623. Layout.fillWidth: true
  624. columns: 3
  625. rowSpacing: Components.ThemeManager.spaceSm
  626. columnSpacing: Components.ThemeManager.spaceSm
  627. Repeater {
  628. model: ballBgOptions
  629. ChoiceChip {
  630. Layout.fillWidth: true
  631. label: modelData.label
  632. selected: ballBg === modelData.value
  633. onClicked: { if (backend) backend.setLedBallBg(modelData.value) }
  634. }
  635. }
  636. }
  637. // Background colour (only for the solid background)
  638. ColumnLayout {
  639. Layout.fillWidth: true
  640. visible: ballBg === "static"
  641. spacing: Components.ThemeManager.spaceSm
  642. Label {
  643. text: "Background colour"
  644. font.family: Components.ThemeManager.fontBody
  645. font.pixelSize: Components.ThemeManager.fontSizeCaption
  646. color: Components.ThemeManager.textSecondary
  647. }
  648. GridLayout {
  649. Layout.fillWidth: true
  650. columns: 10
  651. rowSpacing: Components.ThemeManager.spaceSm
  652. columnSpacing: Components.ThemeManager.spaceSm
  653. Repeater {
  654. model: presetColors
  655. Rectangle {
  656. property bool isSel: ballColor2.toLowerCase() === modelData.sendColor.toLowerCase()
  657. Layout.fillWidth: true
  658. Layout.preferredHeight: 32
  659. radius: 16
  660. color: modelData.color
  661. border.color: isSel ? Components.ThemeManager.textPrimary
  662. : Qt.darker(modelData.color, 1.2)
  663. border.width: isSel ? 2 : 1
  664. MouseArea {
  665. anchors.fill: parent
  666. onClicked: { if (backend) backend.setLedColor2Hex(modelData.sendColor) }
  667. }
  668. }
  669. }
  670. }
  671. }
  672. // Background brightness (hidden when background is off)
  673. RowLayout {
  674. Layout.fillWidth: true
  675. visible: ballBg !== "off"
  676. spacing: Components.ThemeManager.spaceMd
  677. Label {
  678. text: "Bg brightness"
  679. font.family: Components.ThemeManager.fontBody
  680. font.pixelSize: Components.ThemeManager.fontSizeCaption
  681. color: Components.ThemeManager.textSecondary
  682. Layout.preferredWidth: 84
  683. }
  684. DwSlider {
  685. id: ballBgSlider
  686. Layout.fillWidth: true
  687. from: 0; to: 255; stepSize: 1
  688. value: ballBgBright
  689. onMoved: { if (backend) backend.setLedBallBgBright(Math.round(value)) }
  690. }
  691. Label {
  692. text: Math.round(ballBgSlider.value)
  693. font.family: Components.ThemeManager.fontMedium
  694. font.pixelSize: Components.ThemeManager.fontSizeCaption
  695. color: Components.ThemeManager.textPrimary
  696. Layout.preferredWidth: 36
  697. horizontalAlignment: Text.AlignRight
  698. }
  699. }
  700. }
  701. }
  702. }
  703. // Palettes — the full firmware catalogue
  704. SettingsCard {
  705. Layout.leftMargin: Components.ThemeManager.spaceSm
  706. Layout.bottomMargin: Components.ThemeManager.spaceLg
  707. Layout.preferredHeight: palettesColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  708. ColumnLayout {
  709. id: palettesColumn
  710. anchors.fill: parent
  711. anchors.margins: Components.ThemeManager.spaceLg
  712. spacing: Components.ThemeManager.spaceMd
  713. SectionLabel {
  714. text: "Palette"
  715. }
  716. Label {
  717. visible: palettesList.length === 0
  718. text: "No palettes available"
  719. font.family: Components.ThemeManager.fontBody
  720. font.pixelSize: Components.ThemeManager.fontSizeCaption
  721. color: Components.ThemeManager.textSecondary
  722. }
  723. GridLayout {
  724. Layout.fillWidth: true
  725. columns: 3
  726. rowSpacing: Components.ThemeManager.spaceSm
  727. columnSpacing: Components.ThemeManager.spaceSm
  728. visible: palettesList.length > 0
  729. Repeater {
  730. model: palettesList
  731. ChoiceChip {
  732. property int paletteId: modelData.id !== undefined ? modelData.id : index
  733. property string paletteName: modelData.name || ("Palette " + paletteId)
  734. Layout.fillWidth: true
  735. label: paletteName.charAt(0).toUpperCase() + paletteName.slice(1)
  736. selected: paletteId === currentPaletteIndex
  737. onClicked: {
  738. if (backend) {
  739. backend.setLedPalette(paletteId)
  740. currentPaletteIndex = paletteId
  741. }
  742. }
  743. }
  744. }
  745. }
  746. }
  747. }
  748. }
  749. }
  750. }
  751. }
  752. }