1
0

ModernPlaylistPage.qml 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. import QtQuick 2.15
  2. import QtQuick.Controls 2.15
  3. import QtQuick.Layouts 1.15
  4. import DuneWeaver 1.0
  5. import "../components"
  6. import "../components" as Components
  7. Page {
  8. id: page
  9. property var backend: null
  10. property var stackView: null
  11. property var mainWindow: null
  12. // State management for navigation
  13. property bool showingPlaylistDetail: false
  14. property string selectedPlaylist: ""
  15. property var selectedPlaylistData: null
  16. property var currentPlaylistPatterns: []
  17. property var currentPlaylistRawPatterns: [] // Raw patterns with full paths for API calls
  18. // Playlist execution settings (loaded from backend/persisted settings)
  19. property real pauseTime: backend ? backend.pauseBetweenPatterns : 10800
  20. property string clearPattern: backend ? backend.playlistClearPattern : "adaptive"
  21. property string runMode: backend ? backend.playlistRunMode : "loop"
  22. property bool shuffle: backend ? backend.playlistShuffle : true
  23. PlaylistModel {
  24. id: playlistModel
  25. }
  26. // Update patterns when playlist selection changes
  27. onSelectedPlaylistChanged: {
  28. if (selectedPlaylist) {
  29. currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
  30. currentPlaylistRawPatterns = playlistModel.getRawPatternsForPlaylist(selectedPlaylist)
  31. } else {
  32. currentPlaylistPatterns = []
  33. currentPlaylistRawPatterns = []
  34. }
  35. }
  36. // Function to remove a pattern from the current playlist
  37. function removePatternAtIndex(index) {
  38. if (index >= 0 && index < currentPlaylistRawPatterns.length && backend) {
  39. var updatedPatterns = currentPlaylistRawPatterns.slice() // Create a copy
  40. updatedPatterns.splice(index, 1) // Remove the pattern at index
  41. backend.updatePlaylistPatterns(selectedPlaylist, updatedPatterns)
  42. }
  43. }
  44. // Function to navigate to playlist detail
  45. function showPlaylistDetail(playlistName, playlistData) {
  46. selectedPlaylist = playlistName
  47. selectedPlaylistData = playlistData
  48. showingPlaylistDetail = true
  49. }
  50. // Function to go back to playlist list
  51. function showPlaylistList() {
  52. showingPlaylistDetail = false
  53. selectedPlaylist = ""
  54. selectedPlaylistData = null
  55. }
  56. Rectangle {
  57. anchors.fill: parent
  58. color: Components.ThemeManager.backgroundColor
  59. }
  60. // Playlist List View (shown by default)
  61. Item {
  62. id: playlistListView
  63. anchors.fill: parent
  64. visible: !showingPlaylistDetail
  65. ColumnLayout {
  66. anchors.fill: parent
  67. spacing: 0
  68. // Header
  69. Rectangle {
  70. Layout.fillWidth: true
  71. Layout.preferredHeight: Components.ThemeManager.headerHeight
  72. color: Components.ThemeManager.surfaceColor
  73. Rectangle {
  74. anchors.bottom: parent.bottom
  75. width: parent.width
  76. height: 1
  77. color: Components.ThemeManager.borderColor
  78. }
  79. RowLayout {
  80. anchors.fill: parent
  81. anchors.leftMargin: Components.ThemeManager.spaceLg
  82. anchors.rightMargin: Components.ThemeManager.spaceMd
  83. spacing: Components.ThemeManager.spaceSm
  84. ConnectionStatus {
  85. backend: page.backend
  86. }
  87. Label {
  88. text: "Playlists"
  89. font.family: Components.ThemeManager.fontDisplay
  90. font.pixelSize: Components.ThemeManager.fontSizeTitle
  91. color: Components.ThemeManager.textPrimary
  92. }
  93. Label {
  94. // ListView.count is reactive; rowCount() is a plain
  95. // function call and would only evaluate once.
  96. text: playlistsList.count + " playlists"
  97. font.family: Components.ThemeManager.fontBody
  98. font.pixelSize: Components.ThemeManager.fontSizeCaption
  99. color: Components.ThemeManager.textTertiary
  100. }
  101. Item {
  102. Layout.fillWidth: true
  103. }
  104. // Create new playlist
  105. Rectangle {
  106. Layout.preferredWidth: 44
  107. Layout.preferredHeight: 44
  108. radius: 22
  109. color: createPlaylistMouseArea.pressed ? Components.ThemeManager.accentPressed
  110. : Components.ThemeManager.accent
  111. Components.Icon {
  112. anchors.centerIn: parent
  113. name: "add"
  114. size: 22
  115. color: Components.ThemeManager.onAccent
  116. }
  117. MouseArea {
  118. id: createPlaylistMouseArea
  119. anchors.fill: parent
  120. onClicked: createPlaylistDialog.open()
  121. }
  122. }
  123. }
  124. }
  125. // Playlist List
  126. ListView {
  127. id: playlistsList
  128. Layout.fillWidth: true
  129. Layout.fillHeight: true
  130. Layout.margins: Components.ThemeManager.spaceLg
  131. model: playlistModel
  132. spacing: Components.ThemeManager.spaceMd
  133. clip: true
  134. ScrollBar.vertical: ScrollBar {
  135. active: true
  136. policy: ScrollBar.AsNeeded
  137. }
  138. delegate: Rectangle {
  139. width: ListView.view.width
  140. height: 76
  141. color: mouseArea.pressed ? Components.ThemeManager.pressedColor
  142. : Components.ThemeManager.surfaceColor
  143. radius: Components.ThemeManager.radiusMd
  144. border.color: Components.ThemeManager.borderColor
  145. border.width: 1
  146. scale: mouseArea.pressed ? 0.98 : 1.0
  147. Behavior on scale {
  148. NumberAnimation { duration: 100; easing.type: Easing.OutQuad }
  149. }
  150. RowLayout {
  151. anchors.fill: parent
  152. anchors.leftMargin: Components.ThemeManager.spaceLg
  153. anchors.rightMargin: Components.ThemeManager.spaceLg
  154. spacing: Components.ThemeManager.spaceLg
  155. Rectangle {
  156. Layout.preferredWidth: 44
  157. Layout.preferredHeight: 44
  158. radius: 22
  159. color: Components.ThemeManager.accentSoft
  160. Components.Icon {
  161. anchors.centerIn: parent
  162. name: "queue_music"
  163. size: 20
  164. color: Components.ThemeManager.accent
  165. }
  166. }
  167. Column {
  168. Layout.fillWidth: true
  169. spacing: 2
  170. Label {
  171. text: model.name
  172. font.family: Components.ThemeManager.fontDisplay
  173. font.pixelSize: 16
  174. color: Components.ThemeManager.textPrimary
  175. elide: Text.ElideRight
  176. width: parent.width
  177. }
  178. Label {
  179. text: model.itemCount + " patterns"
  180. color: Components.ThemeManager.textSecondary
  181. font.family: Components.ThemeManager.fontBody
  182. font.pixelSize: Components.ThemeManager.fontSizeCaption
  183. }
  184. }
  185. }
  186. MouseArea {
  187. id: mouseArea
  188. anchors.fill: parent
  189. onClicked: {
  190. showPlaylistDetail(model.name, model)
  191. }
  192. }
  193. }
  194. // Empty state
  195. Item {
  196. anchors.fill: parent
  197. visible: playlistsList.count === 0
  198. Column {
  199. anchors.centerIn: parent
  200. spacing: Components.ThemeManager.spaceLg
  201. Components.Icon {
  202. name: "queue_music"
  203. size: 44
  204. color: Components.ThemeManager.textTertiary
  205. anchors.horizontalCenter: parent.horizontalCenter
  206. }
  207. Label {
  208. text: "No playlists yet"
  209. anchors.horizontalCenter: parent.horizontalCenter
  210. color: Components.ThemeManager.textSecondary
  211. font.family: Components.ThemeManager.fontDisplay
  212. font.pixelSize: Components.ThemeManager.fontSizeTitle
  213. }
  214. Label {
  215. text: "Tap + to gather patterns into a set\nthe table can weave through"
  216. anchors.horizontalCenter: parent.horizontalCenter
  217. color: Components.ThemeManager.textTertiary
  218. font.family: Components.ThemeManager.fontBody
  219. font.pixelSize: Components.ThemeManager.fontSizeBody
  220. horizontalAlignment: Text.AlignHCenter
  221. }
  222. }
  223. }
  224. }
  225. }
  226. }
  227. // Playlist Detail View (shown when a playlist is selected)
  228. Item {
  229. id: playlistDetailView
  230. anchors.fill: parent
  231. visible: showingPlaylistDetail
  232. ColumnLayout {
  233. anchors.fill: parent
  234. spacing: 0
  235. // Header with back button
  236. Rectangle {
  237. Layout.fillWidth: true
  238. Layout.preferredHeight: Components.ThemeManager.headerHeight
  239. color: Components.ThemeManager.surfaceColor
  240. Rectangle {
  241. anchors.bottom: parent.bottom
  242. width: parent.width
  243. height: 1
  244. color: Components.ThemeManager.borderColor
  245. }
  246. RowLayout {
  247. anchors.fill: parent
  248. anchors.leftMargin: Components.ThemeManager.spaceSm
  249. anchors.rightMargin: Components.ThemeManager.spaceMd
  250. spacing: Components.ThemeManager.spaceSm
  251. Rectangle {
  252. Layout.preferredWidth: 44
  253. Layout.preferredHeight: 44
  254. radius: 22
  255. color: backArea.pressed ? Components.ThemeManager.pressedColor : "transparent"
  256. Components.Icon {
  257. anchors.centerIn: parent
  258. name: "arrow_back"
  259. size: 20
  260. color: Components.ThemeManager.textPrimary
  261. }
  262. MouseArea {
  263. id: backArea
  264. anchors.fill: parent
  265. onClicked: showPlaylistList()
  266. }
  267. }
  268. Label {
  269. text: selectedPlaylist
  270. font.family: Components.ThemeManager.fontDisplay
  271. font.pixelSize: Components.ThemeManager.fontSizeTitle
  272. color: Components.ThemeManager.textPrimary
  273. Layout.fillWidth: true
  274. elide: Text.ElideRight
  275. }
  276. Label {
  277. text: currentPlaylistPatterns.length + " patterns"
  278. font.family: Components.ThemeManager.fontBody
  279. font.pixelSize: Components.ThemeManager.fontSizeCaption
  280. color: Components.ThemeManager.textTertiary
  281. }
  282. // Delete playlist
  283. Rectangle {
  284. Layout.preferredWidth: 44
  285. Layout.preferredHeight: 44
  286. radius: 22
  287. color: deletePlaylistMouseArea.pressed ? Components.ThemeManager.pressedColor : "transparent"
  288. Components.Icon {
  289. anchors.centerIn: parent
  290. name: "delete"
  291. size: 20
  292. color: Components.ThemeManager.danger
  293. }
  294. MouseArea {
  295. id: deletePlaylistMouseArea
  296. anchors.fill: parent
  297. onClicked: deletePlaylistDialog.open()
  298. }
  299. }
  300. }
  301. }
  302. // Content - Pattern list on left, controls on right
  303. RowLayout {
  304. Layout.fillWidth: true
  305. Layout.fillHeight: true
  306. spacing: 0
  307. // Left side - Pattern List
  308. Rectangle {
  309. Layout.fillHeight: true
  310. Layout.preferredWidth: page.width * 0.4
  311. color: Components.ThemeManager.surfaceColor
  312. ColumnLayout {
  313. anchors.fill: parent
  314. anchors.margins: Components.ThemeManager.spaceLg
  315. spacing: Components.ThemeManager.spaceMd
  316. RowLayout {
  317. Layout.fillWidth: true
  318. spacing: Components.ThemeManager.spaceSm
  319. SectionLabel {
  320. text: "Patterns"
  321. Layout.fillWidth: true
  322. }
  323. // Add pattern button
  324. Rectangle {
  325. Layout.preferredWidth: 40
  326. Layout.preferredHeight: 40
  327. radius: 20
  328. color: addPatternMouseArea.pressed ? Components.ThemeManager.pressedColor : "transparent"
  329. border.width: 1
  330. border.color: Components.ThemeManager.accent
  331. Components.Icon {
  332. anchors.centerIn: parent
  333. name: "add"
  334. size: 20
  335. color: Components.ThemeManager.accent
  336. }
  337. MouseArea {
  338. id: addPatternMouseArea
  339. anchors.fill: parent
  340. onClicked: {
  341. // Navigate to full-page pattern selector
  342. stackView.push("PatternSelectorPage.qml", {
  343. backend: backend,
  344. stackView: stackView,
  345. playlistName: selectedPlaylist,
  346. existingPatterns: currentPlaylistRawPatterns
  347. })
  348. }
  349. }
  350. }
  351. }
  352. ListView {
  353. id: patternListView
  354. Layout.fillWidth: true
  355. Layout.fillHeight: true
  356. clip: true
  357. model: currentPlaylistPatterns
  358. spacing: Components.ThemeManager.spaceSm
  359. ScrollBar.vertical: ScrollBar {
  360. active: true
  361. policy: ScrollBar.AsNeeded
  362. }
  363. delegate: Rectangle {
  364. width: ListView.view.width
  365. height: 48
  366. color: Components.ThemeManager.cardColor
  367. radius: Components.ThemeManager.radiusSm
  368. border.color: Components.ThemeManager.borderLight
  369. border.width: 1
  370. RowLayout {
  371. anchors.fill: parent
  372. anchors.leftMargin: Components.ThemeManager.spaceMd
  373. anchors.rightMargin: Components.ThemeManager.spaceXs
  374. spacing: Components.ThemeManager.spaceSm
  375. Label {
  376. text: index + 1
  377. font.family: Components.ThemeManager.fontMedium
  378. font.pixelSize: Components.ThemeManager.fontSizeCaption
  379. color: Components.ThemeManager.textTertiary
  380. Layout.preferredWidth: 20
  381. horizontalAlignment: Text.AlignRight
  382. }
  383. Label {
  384. text: modelData
  385. font.family: Components.ThemeManager.fontBody
  386. font.pixelSize: 13
  387. color: Components.ThemeManager.textPrimary
  388. Layout.fillWidth: true
  389. elide: Text.ElideRight
  390. }
  391. // Remove pattern
  392. Rectangle {
  393. Layout.preferredWidth: 40
  394. Layout.preferredHeight: 40
  395. radius: 20
  396. color: removePatternArea.pressed ? Components.ThemeManager.pressedColor : "transparent"
  397. Components.Icon {
  398. anchors.centerIn: parent
  399. name: "close"
  400. size: 16
  401. color: Components.ThemeManager.textTertiary
  402. }
  403. MouseArea {
  404. id: removePatternArea
  405. anchors.fill: parent
  406. onClicked: {
  407. removePatternAtIndex(index)
  408. }
  409. }
  410. }
  411. }
  412. }
  413. }
  414. // Empty playlist message
  415. Item {
  416. Layout.fillWidth: true
  417. Layout.fillHeight: true
  418. visible: currentPlaylistPatterns.length === 0
  419. Column {
  420. anchors.centerIn: parent
  421. spacing: Components.ThemeManager.spaceSm
  422. Components.Icon {
  423. name: "queue_music"
  424. size: 30
  425. color: Components.ThemeManager.textTertiary
  426. anchors.horizontalCenter: parent.horizontalCenter
  427. }
  428. Label {
  429. text: "No patterns yet — tap +"
  430. anchors.horizontalCenter: parent.horizontalCenter
  431. color: Components.ThemeManager.textSecondary
  432. font.family: Components.ThemeManager.fontBody
  433. font.pixelSize: Components.ThemeManager.fontSizeBody
  434. }
  435. }
  436. }
  437. }
  438. }
  439. Rectangle {
  440. Layout.fillHeight: true
  441. Layout.preferredWidth: 1
  442. color: Components.ThemeManager.borderColor
  443. }
  444. // Right side - controls
  445. Rectangle {
  446. Layout.fillHeight: true
  447. Layout.fillWidth: true
  448. color: Components.ThemeManager.backgroundColor
  449. ColumnLayout {
  450. anchors.fill: parent
  451. anchors.margins: Components.ThemeManager.spaceLg
  452. spacing: Components.ThemeManager.spaceMd
  453. // Play + shuffle
  454. RowLayout {
  455. Layout.fillWidth: true
  456. spacing: Components.ThemeManager.spaceSm
  457. ModernControlButton {
  458. Layout.fillWidth: true
  459. Layout.preferredHeight: Components.ThemeManager.controlHeight
  460. icon: "play_arrow"
  461. text: "Weave this playlist"
  462. buttonColor: Components.ThemeManager.accent
  463. onClicked: {
  464. if (backend) {
  465. backend.executePlaylist(selectedPlaylist, pauseTime, clearPattern, runMode, shuffle)
  466. // Navigate to execution page
  467. if (mainWindow) {
  468. mainWindow.shouldNavigateToExecution = true
  469. }
  470. }
  471. }
  472. }
  473. ChoiceChip {
  474. Layout.preferredWidth: 110
  475. Layout.preferredHeight: Components.ThemeManager.controlHeight
  476. label: "Shuffle"
  477. selected: shuffle
  478. onClicked: {
  479. // Don't assign directly to shuffle - that breaks the binding.
  480. // Update the backend and let the binding propagate the change.
  481. if (backend) backend.setPlaylistShuffle(!backend.playlistShuffle)
  482. }
  483. }
  484. }
  485. // Settings
  486. ScrollView {
  487. Layout.fillWidth: true
  488. Layout.fillHeight: true
  489. contentWidth: availableWidth
  490. clip: true
  491. ScrollBar.vertical.policy: ScrollBar.AsNeeded
  492. ColumnLayout {
  493. width: parent.width
  494. spacing: Components.ThemeManager.spaceMd
  495. SectionLabel {
  496. text: "Play order"
  497. }
  498. RowLayout {
  499. Layout.fillWidth: true
  500. spacing: Components.ThemeManager.spaceSm
  501. ChoiceChip {
  502. Layout.fillWidth: true
  503. label: "Loop forever"
  504. selected: runMode === "loop"
  505. onClicked: {
  506. runMode = "loop"
  507. if (backend) backend.setPlaylistRunMode("loop")
  508. }
  509. }
  510. ChoiceChip {
  511. Layout.fillWidth: true
  512. label: "Play once"
  513. selected: runMode === "single"
  514. onClicked: {
  515. runMode = "single"
  516. if (backend) backend.setPlaylistRunMode("single")
  517. }
  518. }
  519. }
  520. SectionLabel {
  521. Layout.topMargin: Components.ThemeManager.spaceSm
  522. text: "Rest between patterns"
  523. }
  524. GridLayout {
  525. id: pauseGrid
  526. Layout.fillWidth: true
  527. columns: 6
  528. rowSpacing: Components.ThemeManager.spaceSm
  529. columnSpacing: Components.ThemeManager.spaceSm
  530. property string currentSelection: backend ? backend.getCurrentPauseOption() : "0s"
  531. Connections {
  532. target: backend
  533. function onPauseBetweenPatternsChanged(pause) {
  534. if (backend) {
  535. pauseGrid.currentSelection = backend.getCurrentPauseOption()
  536. pauseTime = backend.pauseBetweenPatterns
  537. }
  538. }
  539. }
  540. Repeater {
  541. model: [
  542. { label: "0 s", option: "0s", seconds: 0 },
  543. { label: "1 m", option: "1 min", seconds: 60 },
  544. { label: "5 m", option: "5 min", seconds: 300 },
  545. { label: "15 m", option: "15 min", seconds: 900 },
  546. { label: "30 m", option: "30 min", seconds: 1800 },
  547. { label: "1 h", option: "1 hour", seconds: 3600 },
  548. { label: "2 h", option: "2 hour", seconds: 7200 },
  549. { label: "3 h", option: "3 hour", seconds: 10800 },
  550. { label: "4 h", option: "4 hour", seconds: 14400 },
  551. { label: "5 h", option: "5 hour", seconds: 18000 },
  552. { label: "6 h", option: "6 hour", seconds: 21600 },
  553. { label: "12 h", option: "12 hour", seconds: 43200 }
  554. ]
  555. ChoiceChip {
  556. required property var modelData
  557. Layout.fillWidth: true
  558. label: modelData.label
  559. selected: pauseGrid.currentSelection === modelData.option
  560. onClicked: {
  561. if (backend) {
  562. backend.setPauseByOption(modelData.option)
  563. pauseGrid.currentSelection = modelData.option
  564. pauseTime = modelData.seconds
  565. }
  566. }
  567. }
  568. }
  569. }
  570. SectionLabel {
  571. Layout.topMargin: Components.ThemeManager.spaceSm
  572. text: "Clear before each pattern"
  573. }
  574. GridLayout {
  575. Layout.fillWidth: true
  576. columns: 2
  577. rowSpacing: Components.ThemeManager.spaceSm
  578. columnSpacing: Components.ThemeManager.spaceSm
  579. Repeater {
  580. model: [
  581. { label: "Adaptive clear", value: "adaptive" },
  582. { label: "Clear from center", value: "clear_center" },
  583. { label: "Clear from edge", value: "clear_perimeter" },
  584. { label: "Keep the sand", value: "none" }
  585. ]
  586. ChoiceChip {
  587. required property var modelData
  588. Layout.fillWidth: true
  589. label: modelData.label
  590. selected: clearPattern === modelData.value
  591. onClicked: {
  592. clearPattern = modelData.value
  593. if (backend) backend.setPlaylistClearPattern(modelData.value)
  594. }
  595. }
  596. }
  597. }
  598. }
  599. }
  600. }
  601. }
  602. }
  603. }
  604. }
  605. // ==================== Dialogs ====================
  606. // Create Playlist Dialog
  607. Popup {
  608. id: createPlaylistDialog
  609. modal: true
  610. x: (parent.width - width) / 2
  611. y: (parent.height - height) / 2
  612. width: 340
  613. height: 210
  614. closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
  615. background: Rectangle {
  616. color: Components.ThemeManager.surfaceColor
  617. radius: Components.ThemeManager.radiusMd
  618. border.color: Components.ThemeManager.borderColor
  619. border.width: 1
  620. }
  621. contentItem: ColumnLayout {
  622. anchors.fill: parent
  623. anchors.margins: Components.ThemeManager.spaceLg
  624. spacing: Components.ThemeManager.spaceMd
  625. Label {
  626. text: "New playlist"
  627. font.family: Components.ThemeManager.fontDisplay
  628. font.pixelSize: Components.ThemeManager.fontSizeTitle
  629. color: Components.ThemeManager.textPrimary
  630. Layout.alignment: Qt.AlignHCenter
  631. }
  632. TextField {
  633. id: newPlaylistNameField
  634. Layout.fillWidth: true
  635. Layout.preferredHeight: 48
  636. placeholderText: "Playlist name"
  637. placeholderTextColor: Components.ThemeManager.textTertiary
  638. font.family: Components.ThemeManager.fontBody
  639. font.pixelSize: Components.ThemeManager.fontSizeBody
  640. color: Components.ThemeManager.textPrimary
  641. verticalAlignment: TextInput.AlignVCenter
  642. leftPadding: Components.ThemeManager.spaceLg
  643. rightPadding: Components.ThemeManager.spaceLg
  644. background: Rectangle {
  645. color: Components.ThemeManager.backgroundColor
  646. radius: 24
  647. border.color: newPlaylistNameField.activeFocus ? Components.ThemeManager.accent
  648. : Components.ThemeManager.borderColor
  649. border.width: 1
  650. }
  651. onAccepted: {
  652. if (text.trim().length > 0 && backend) {
  653. backend.createPlaylist(text.trim())
  654. }
  655. }
  656. }
  657. RowLayout {
  658. Layout.fillWidth: true
  659. spacing: Components.ThemeManager.spaceSm
  660. ModernControlButton {
  661. Layout.fillWidth: true
  662. Layout.preferredHeight: Components.ThemeManager.touchTarget
  663. text: "Cancel"
  664. outlined: true
  665. buttonColor: Components.ThemeManager.textSecondary
  666. onClicked: {
  667. newPlaylistNameField.text = ""
  668. createPlaylistDialog.close()
  669. }
  670. }
  671. ModernControlButton {
  672. Layout.fillWidth: true
  673. Layout.preferredHeight: Components.ThemeManager.touchTarget
  674. text: "Create"
  675. buttonColor: Components.ThemeManager.accent
  676. enabled: newPlaylistNameField.text.trim().length > 0
  677. onClicked: {
  678. if (backend) {
  679. backend.createPlaylist(newPlaylistNameField.text.trim())
  680. }
  681. }
  682. }
  683. }
  684. }
  685. onOpened: {
  686. newPlaylistNameField.text = ""
  687. newPlaylistNameField.forceActiveFocus()
  688. }
  689. }
  690. // Delete Playlist Confirmation Dialog
  691. Popup {
  692. id: deletePlaylistDialog
  693. modal: true
  694. x: (parent.width - width) / 2
  695. y: (parent.height - height) / 2
  696. width: 340
  697. height: 200
  698. closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
  699. background: Rectangle {
  700. color: Components.ThemeManager.surfaceColor
  701. radius: Components.ThemeManager.radiusMd
  702. border.color: Components.ThemeManager.borderColor
  703. border.width: 1
  704. }
  705. contentItem: ColumnLayout {
  706. anchors.fill: parent
  707. anchors.margins: Components.ThemeManager.spaceLg
  708. spacing: Components.ThemeManager.spaceMd
  709. Label {
  710. text: "Delete playlist?"
  711. font.family: Components.ThemeManager.fontDisplay
  712. font.pixelSize: Components.ThemeManager.fontSizeTitle
  713. color: Components.ThemeManager.textPrimary
  714. Layout.alignment: Qt.AlignHCenter
  715. }
  716. Label {
  717. text: "\"" + selectedPlaylist + "\" will be removed from the table. Its patterns stay in your library."
  718. font.family: Components.ThemeManager.fontBody
  719. font.pixelSize: Components.ThemeManager.fontSizeBody
  720. color: Components.ThemeManager.textSecondary
  721. horizontalAlignment: Text.AlignHCenter
  722. wrapMode: Text.WordWrap
  723. Layout.fillWidth: true
  724. }
  725. RowLayout {
  726. Layout.fillWidth: true
  727. spacing: Components.ThemeManager.spaceSm
  728. ModernControlButton {
  729. Layout.fillWidth: true
  730. Layout.preferredHeight: Components.ThemeManager.touchTarget
  731. text: "Cancel"
  732. outlined: true
  733. buttonColor: Components.ThemeManager.textSecondary
  734. onClicked: deletePlaylistDialog.close()
  735. }
  736. ModernControlButton {
  737. Layout.fillWidth: true
  738. Layout.preferredHeight: Components.ThemeManager.touchTarget
  739. text: "Delete"
  740. buttonColor: Components.ThemeManager.danger
  741. onClicked: {
  742. if (backend && selectedPlaylist) {
  743. backend.deletePlaylist(selectedPlaylist)
  744. }
  745. deletePlaylistDialog.close()
  746. }
  747. }
  748. }
  749. }
  750. }
  751. // ==================== Backend Signal Handlers ====================
  752. Connections {
  753. target: backend
  754. function onPlaylistCreated(success, message) {
  755. if (success) {
  756. playlistModel.refresh()
  757. }
  758. newPlaylistNameField.text = ""
  759. createPlaylistDialog.close()
  760. }
  761. function onPlaylistDeleted(success, message) {
  762. if (success) {
  763. playlistModel.refresh()
  764. showPlaylistList() // Navigate back to list
  765. }
  766. }
  767. function onPatternAddedToPlaylist(success, message) {
  768. if (success) {
  769. playlistModel.refresh()
  770. // Refresh current playlist patterns if we're viewing one
  771. if (selectedPlaylist) {
  772. currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
  773. currentPlaylistRawPatterns = playlistModel.getRawPatternsForPlaylist(selectedPlaylist)
  774. }
  775. }
  776. }
  777. function onPlaylistModified(success, message) {
  778. if (success) {
  779. playlistModel.refresh()
  780. // Refresh current playlist patterns
  781. if (selectedPlaylist) {
  782. currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
  783. currentPlaylistRawPatterns = playlistModel.getRawPatternsForPlaylist(selectedPlaylist)
  784. }
  785. }
  786. }
  787. }
  788. }