1
0

PatternDetailPage.qml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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 string patternName: ""
  10. property string patternPath: ""
  11. property string patternPreview: ""
  12. property var backend: null
  13. property bool showAddedFeedback: false
  14. // Selected clear mode ("adaptive" | "clear_center" | "clear_perimeter" | "none")
  15. property string clearMode: "adaptive"
  16. property string cleanName: {
  17. var parts = patternName.split('/')
  18. return parts[parts.length - 1].replace('.thr', '')
  19. }
  20. // Playlist model for selecting which playlist to add to
  21. PlaylistModel {
  22. id: playlistModel
  23. }
  24. Rectangle {
  25. anchors.fill: parent
  26. color: Components.ThemeManager.backgroundColor
  27. }
  28. ColumnLayout {
  29. anchors.fill: parent
  30. spacing: 0
  31. // Header
  32. Rectangle {
  33. Layout.fillWidth: true
  34. Layout.preferredHeight: Components.ThemeManager.headerHeight
  35. color: Components.ThemeManager.surfaceColor
  36. Rectangle {
  37. anchors.bottom: parent.bottom
  38. width: parent.width
  39. height: 1
  40. color: Components.ThemeManager.borderColor
  41. }
  42. RowLayout {
  43. anchors.fill: parent
  44. anchors.leftMargin: Components.ThemeManager.spaceSm
  45. anchors.rightMargin: Components.ThemeManager.spaceLg
  46. spacing: Components.ThemeManager.spaceSm
  47. // Back button
  48. Rectangle {
  49. Layout.preferredWidth: 44
  50. Layout.preferredHeight: 44
  51. radius: 22
  52. color: backArea.pressed ? Components.ThemeManager.pressedColor : "transparent"
  53. Components.Icon {
  54. anchors.centerIn: parent
  55. name: "arrow_back"
  56. size: 20
  57. color: Components.ThemeManager.textPrimary
  58. }
  59. MouseArea {
  60. id: backArea
  61. anchors.fill: parent
  62. onClicked: stackView.pop()
  63. }
  64. }
  65. Label {
  66. text: cleanName
  67. Layout.fillWidth: true
  68. elide: Label.ElideRight
  69. font.family: Components.ThemeManager.fontDisplay
  70. font.pixelSize: Components.ThemeManager.fontSizeTitle
  71. color: Components.ThemeManager.textPrimary
  72. }
  73. ConnectionStatus {
  74. backend: page.backend
  75. }
  76. }
  77. }
  78. // Content
  79. RowLayout {
  80. Layout.fillWidth: true
  81. Layout.fillHeight: true
  82. spacing: 0
  83. // ---- Left: circular preview ----
  84. Item {
  85. Layout.fillHeight: true
  86. Layout.preferredWidth: page.width * 0.55
  87. Image {
  88. id: previewImage
  89. anchors.centerIn: parent
  90. width: Math.min(parent.width, parent.height) - 2 * Components.ThemeManager.spaceXl
  91. height: width
  92. source: patternPreview ? "file:///" + patternPreview : ""
  93. fillMode: Image.PreserveAspectFit
  94. asynchronous: true
  95. }
  96. // Empty dish placeholder
  97. Rectangle {
  98. anchors.centerIn: parent
  99. width: previewImage.width
  100. height: width
  101. radius: width / 2
  102. color: Components.ThemeManager.surfaceColor
  103. border.width: 1
  104. border.color: Components.ThemeManager.borderColor
  105. visible: previewImage.status === Image.Error || previewImage.source == ""
  106. Column {
  107. anchors.centerIn: parent
  108. spacing: Components.ThemeManager.spaceSm
  109. Components.Icon {
  110. name: "radio_unchecked"
  111. size: 34
  112. color: Components.ThemeManager.textTertiary
  113. anchors.horizontalCenter: parent.horizontalCenter
  114. }
  115. Label {
  116. text: "No preview yet"
  117. color: Components.ThemeManager.textSecondary
  118. font.family: Components.ThemeManager.fontMedium
  119. font.pixelSize: Components.ThemeManager.fontSizeBody
  120. anchors.horizontalCenter: parent.horizontalCenter
  121. }
  122. }
  123. }
  124. }
  125. // ---- Right: actions ----
  126. Rectangle {
  127. Layout.fillHeight: true
  128. Layout.fillWidth: true
  129. color: Components.ThemeManager.surfaceColor
  130. Rectangle {
  131. anchors.left: parent.left
  132. width: 1
  133. height: parent.height
  134. color: Components.ThemeManager.borderColor
  135. }
  136. ColumnLayout {
  137. anchors.fill: parent
  138. anchors.margins: Components.ThemeManager.spaceXl
  139. spacing: 0
  140. Label {
  141. text: "Before weaving"
  142. font.family: Components.ThemeManager.fontDisplay
  143. font.pixelSize: 11
  144. font.letterSpacing: 1.4
  145. font.capitalization: Font.AllUppercase
  146. color: Components.ThemeManager.textTertiary
  147. }
  148. // Clear-mode chips
  149. GridLayout {
  150. Layout.fillWidth: true
  151. Layout.topMargin: Components.ThemeManager.spaceSm
  152. columns: 2
  153. rowSpacing: Components.ThemeManager.spaceSm
  154. columnSpacing: Components.ThemeManager.spaceSm
  155. Repeater {
  156. model: [
  157. { label: "Adaptive clear", value: "adaptive" },
  158. { label: "Clear from center", value: "clear_center" },
  159. { label: "Clear from edge", value: "clear_perimeter" },
  160. { label: "Keep the sand", value: "none" }
  161. ]
  162. ChoiceChip {
  163. required property var modelData
  164. Layout.fillWidth: true
  165. Layout.preferredHeight: Components.ThemeManager.touchTarget
  166. label: modelData.label
  167. selected: page.clearMode === modelData.value
  168. onClicked: page.clearMode = modelData.value
  169. }
  170. }
  171. }
  172. Item { Layout.fillHeight: true }
  173. // Play — the one thing this page is for
  174. ModernControlButton {
  175. Layout.fillWidth: true
  176. Layout.preferredHeight: Components.ThemeManager.controlHeight
  177. icon: "play_arrow"
  178. text: "Weave this pattern"
  179. buttonColor: Components.ThemeManager.accent
  180. enabled: backend !== null
  181. onClicked: {
  182. if (backend) backend.executePattern(patternName, page.clearMode)
  183. }
  184. }
  185. ModernControlButton {
  186. Layout.fillWidth: true
  187. Layout.topMargin: Components.ThemeManager.spaceSm
  188. Layout.preferredHeight: Components.ThemeManager.touchTarget
  189. icon: showAddedFeedback ? "check" : "queue_music"
  190. text: showAddedFeedback ? "Added" : "Add to playlist"
  191. outlined: true
  192. buttonColor: showAddedFeedback ? Components.ThemeManager.ok
  193. : Components.ThemeManager.accent
  194. enabled: backend !== null && !showAddedFeedback
  195. onClicked: {
  196. playlistModel.refresh()
  197. playlistSelectorPopup.open()
  198. }
  199. }
  200. }
  201. }
  202. }
  203. }
  204. // ==================== Playlist Selector Popup ====================
  205. Popup {
  206. id: playlistSelectorPopup
  207. modal: true
  208. x: (parent.width - width) / 2
  209. y: (parent.height - height) / 2
  210. width: 340
  211. // ListView.count is reactive; rowCount() would only evaluate once
  212. // (at creation, before playlists have loaded).
  213. height: Math.min(400, 130 + playlistSelectorList.count * 62)
  214. closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
  215. background: Rectangle {
  216. color: Components.ThemeManager.surfaceColor
  217. radius: Components.ThemeManager.radiusMd
  218. border.color: Components.ThemeManager.borderColor
  219. border.width: 1
  220. }
  221. contentItem: ColumnLayout {
  222. anchors.fill: parent
  223. anchors.margins: Components.ThemeManager.spaceLg
  224. spacing: Components.ThemeManager.spaceMd
  225. Label {
  226. text: "Add to playlist"
  227. font.family: Components.ThemeManager.fontDisplay
  228. font.pixelSize: Components.ThemeManager.fontSizeTitle
  229. color: Components.ThemeManager.textPrimary
  230. Layout.alignment: Qt.AlignHCenter
  231. }
  232. // Playlist list
  233. ListView {
  234. id: playlistSelectorList
  235. Layout.fillWidth: true
  236. Layout.fillHeight: true
  237. clip: true
  238. model: playlistModel
  239. spacing: Components.ThemeManager.spaceSm
  240. delegate: Rectangle {
  241. width: ListView.view.width
  242. height: 56
  243. radius: Components.ThemeManager.radiusSm
  244. color: playlistItemArea.pressed ? Components.ThemeManager.pressedColor
  245. : Components.ThemeManager.cardColor
  246. border.color: Components.ThemeManager.borderColor
  247. border.width: 1
  248. RowLayout {
  249. anchors.fill: parent
  250. anchors.margins: Components.ThemeManager.spaceMd
  251. spacing: Components.ThemeManager.spaceMd
  252. Components.Icon {
  253. name: "queue_music"
  254. size: 18
  255. color: Components.ThemeManager.accent
  256. }
  257. Label {
  258. text: model.name
  259. font.family: Components.ThemeManager.fontMedium
  260. font.pixelSize: Components.ThemeManager.fontSizeBody
  261. color: Components.ThemeManager.textPrimary
  262. Layout.fillWidth: true
  263. elide: Text.ElideRight
  264. }
  265. Label {
  266. text: model.itemCount + " patterns"
  267. font.family: Components.ThemeManager.fontBody
  268. font.pixelSize: Components.ThemeManager.fontSizeCaption
  269. color: Components.ThemeManager.textTertiary
  270. }
  271. }
  272. MouseArea {
  273. id: playlistItemArea
  274. anchors.fill: parent
  275. onClicked: {
  276. if (backend) {
  277. backend.addPatternToPlaylist(model.name, patternName)
  278. }
  279. playlistSelectorPopup.close()
  280. }
  281. }
  282. }
  283. }
  284. // Empty state
  285. Column {
  286. Layout.fillWidth: true
  287. Layout.fillHeight: true
  288. spacing: Components.ThemeManager.spaceSm
  289. visible: playlistSelectorList.count === 0
  290. Item { height: Components.ThemeManager.spaceSm; width: 1 }
  291. Components.Icon {
  292. name: "queue_music"
  293. size: 30
  294. color: Components.ThemeManager.textTertiary
  295. anchors.horizontalCenter: parent.horizontalCenter
  296. }
  297. Label {
  298. text: "No playlists yet"
  299. anchors.horizontalCenter: parent.horizontalCenter
  300. color: Components.ThemeManager.textSecondary
  301. font.family: Components.ThemeManager.fontMedium
  302. font.pixelSize: Components.ThemeManager.fontSizeBody
  303. }
  304. Label {
  305. text: "Create one on the Playlists page first"
  306. anchors.horizontalCenter: parent.horizontalCenter
  307. color: Components.ThemeManager.textTertiary
  308. font.family: Components.ThemeManager.fontBody
  309. font.pixelSize: Components.ThemeManager.fontSizeCaption
  310. }
  311. }
  312. ModernControlButton {
  313. Layout.fillWidth: true
  314. Layout.preferredHeight: Components.ThemeManager.touchTarget
  315. text: "Cancel"
  316. outlined: true
  317. buttonColor: Components.ThemeManager.textSecondary
  318. onClicked: playlistSelectorPopup.close()
  319. }
  320. }
  321. }
  322. // ==================== Backend Signal Handlers ====================
  323. Connections {
  324. target: backend
  325. function onPatternAddedToPlaylist(success, message) {
  326. if (success) {
  327. showAddedFeedback = true
  328. feedbackTimer.start()
  329. }
  330. }
  331. }
  332. Timer {
  333. id: feedbackTimer
  334. interval: 2000 // Show "Added" for 2 seconds
  335. onTriggered: showAddedFeedback = false
  336. }
  337. }