PatternSelectorPage.qml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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 string playlistName: ""
  12. property var existingPatterns: [] // Raw pattern names already in playlist
  13. // Track patterns added in this session for immediate visual feedback
  14. property var sessionAddedPatterns: []
  15. // Local pattern model for this page
  16. PatternModel {
  17. id: patternModel
  18. }
  19. // Search state
  20. property bool searchExpanded: false
  21. property int patternCount: patternModel ? patternModel.rowCount() : 0
  22. // Update pattern count when model resets
  23. Connections {
  24. target: patternModel
  25. function onModelReset() {
  26. patternCount = patternModel.rowCount()
  27. }
  28. }
  29. // Check if a pattern is already in the playlist
  30. function isPatternInPlaylist(patternName) {
  31. // Check original existing patterns
  32. if (existingPatterns.indexOf(patternName) !== -1) {
  33. return true
  34. }
  35. // Check patterns added during this session
  36. if (sessionAddedPatterns.indexOf(patternName) !== -1) {
  37. return true
  38. }
  39. return false
  40. }
  41. Rectangle {
  42. anchors.fill: parent
  43. color: Components.ThemeManager.backgroundColor
  44. }
  45. ColumnLayout {
  46. anchors.fill: parent
  47. spacing: 0
  48. // Header with back button
  49. Rectangle {
  50. Layout.fillWidth: true
  51. Layout.preferredHeight: 50
  52. color: Components.ThemeManager.surfaceColor
  53. // Bottom border
  54. Rectangle {
  55. anchors.bottom: parent.bottom
  56. width: parent.width
  57. height: 1
  58. color: Components.ThemeManager.borderColor
  59. }
  60. RowLayout {
  61. anchors.fill: parent
  62. anchors.leftMargin: 15
  63. anchors.rightMargin: 10
  64. spacing: 10
  65. // Back button
  66. Button {
  67. text: "← Back"
  68. font.pixelSize: 14
  69. flat: true
  70. visible: !searchExpanded
  71. onClicked: stackView.pop()
  72. contentItem: Text {
  73. text: parent.text
  74. font: parent.font
  75. color: Components.ThemeManager.textPrimary
  76. horizontalAlignment: Text.AlignHCenter
  77. verticalAlignment: Text.AlignVCenter
  78. }
  79. }
  80. // Title
  81. Label {
  82. text: "Add to \"" + playlistName + "\""
  83. font.pixelSize: 16
  84. font.bold: true
  85. color: Components.ThemeManager.textPrimary
  86. Layout.fillWidth: true
  87. elide: Text.ElideRight
  88. visible: !searchExpanded
  89. }
  90. // Pattern count
  91. Label {
  92. text: patternCount + " patterns"
  93. font.pixelSize: 12
  94. color: Components.ThemeManager.textTertiary
  95. visible: !searchExpanded
  96. }
  97. Item {
  98. Layout.fillWidth: true
  99. visible: !searchExpanded
  100. }
  101. // Expandable search (matching ModernPatternListPage)
  102. Rectangle {
  103. Layout.fillWidth: searchExpanded
  104. Layout.preferredWidth: searchExpanded ? parent.width - 60 : 120
  105. Layout.preferredHeight: 32
  106. radius: 16
  107. color: searchExpanded ? Components.ThemeManager.surfaceColor : Components.ThemeManager.cardColor
  108. border.color: searchExpanded ? "#2563eb" : Components.ThemeManager.borderColor
  109. border.width: 1
  110. Behavior on Layout.preferredWidth {
  111. NumberAnimation { duration: 200 }
  112. }
  113. RowLayout {
  114. anchors.fill: parent
  115. anchors.leftMargin: 10
  116. anchors.rightMargin: 10
  117. spacing: 5
  118. Text {
  119. text: "⌕"
  120. font.pixelSize: 16
  121. font.family: "sans-serif"
  122. color: searchExpanded ? "#2563eb" : Components.ThemeManager.textSecondary
  123. }
  124. TextField {
  125. id: searchField
  126. Layout.fillWidth: true
  127. placeholderText: searchExpanded ? "Search patterns... (press Enter)" : "Search"
  128. font.pixelSize: 14
  129. color: Components.ThemeManager.textPrimary
  130. visible: searchExpanded || text.length > 0
  131. property string lastSearchText: ""
  132. property bool hasUnappliedSearch: text !== lastSearchText && text.length > 0
  133. background: Rectangle {
  134. color: "transparent"
  135. border.color: searchField.hasUnappliedSearch ? "#f59e0b" : "transparent"
  136. border.width: searchField.hasUnappliedSearch ? 1 : 0
  137. radius: 4
  138. }
  139. onAccepted: {
  140. patternModel.filter(text)
  141. lastSearchText = text
  142. Qt.inputMethod.hide()
  143. focus = false
  144. }
  145. activeFocusOnPress: true
  146. selectByMouse: true
  147. inputMethodHints: Qt.ImhNoPredictiveText
  148. MouseArea {
  149. anchors.fill: parent
  150. onPressed: {
  151. searchField.forceActiveFocus()
  152. Qt.inputMethod.show()
  153. mouse.accepted = false
  154. }
  155. }
  156. onActiveFocusChanged: {
  157. if (activeFocus) {
  158. searchExpanded = true
  159. Qt.inputMethod.show()
  160. } else {
  161. if (text !== lastSearchText) {
  162. patternModel.filter(text)
  163. lastSearchText = text
  164. }
  165. }
  166. }
  167. Keys.onReturnPressed: {
  168. Qt.inputMethod.hide()
  169. focus = false
  170. }
  171. Keys.onEscapePressed: {
  172. text = ""
  173. lastSearchText = ""
  174. patternModel.filter("")
  175. Qt.inputMethod.hide()
  176. focus = false
  177. }
  178. }
  179. Text {
  180. text: searchExpanded || searchField.text.length > 0 ? "Search" : ""
  181. font.pixelSize: 12
  182. color: Components.ThemeManager.textTertiary
  183. visible: !searchExpanded && searchField.text.length === 0
  184. }
  185. }
  186. MouseArea {
  187. anchors.fill: parent
  188. enabled: !searchExpanded
  189. onClicked: {
  190. searchExpanded = true
  191. searchField.forceActiveFocus()
  192. Qt.inputMethod.show()
  193. }
  194. }
  195. }
  196. // Close button when search expanded
  197. Button {
  198. text: "✕"
  199. font.pixelSize: 18
  200. flat: true
  201. visible: searchExpanded
  202. Layout.preferredWidth: 32
  203. Layout.preferredHeight: 32
  204. onClicked: {
  205. searchExpanded = false
  206. searchField.text = ""
  207. searchField.lastSearchText = ""
  208. searchField.focus = false
  209. patternModel.filter("")
  210. }
  211. }
  212. }
  213. }
  214. // Pattern Grid
  215. GridView {
  216. id: gridView
  217. Layout.fillWidth: true
  218. Layout.fillHeight: true
  219. cellWidth: 200
  220. cellHeight: 220
  221. model: patternModel
  222. clip: true
  223. ScrollBar.vertical: ScrollBar {
  224. active: true
  225. policy: ScrollBar.AsNeeded
  226. }
  227. delegate: Item {
  228. width: gridView.cellWidth - 10
  229. height: gridView.cellHeight - 10
  230. // Check if pattern is already in playlist
  231. property bool isInPlaylist: isPatternInPlaylist(model.name)
  232. ModernPatternCard {
  233. id: patternCard
  234. anchors.fill: parent
  235. name: model.name
  236. preview: model.preview
  237. onClicked: {
  238. // Use the tracking function for immediate visual feedback
  239. page.addPatternToPlaylist(model.name)
  240. }
  241. }
  242. // Selection overlay for patterns already in playlist
  243. Rectangle {
  244. anchors.fill: parent
  245. color: "transparent"
  246. border.color: isInPlaylist ? "#2563eb" : "transparent"
  247. border.width: isInPlaylist ? 3 : 0
  248. radius: 12
  249. // Checkmark badge for selected patterns
  250. Rectangle {
  251. visible: isInPlaylist
  252. anchors.top: parent.top
  253. anchors.right: parent.right
  254. anchors.topMargin: 12
  255. anchors.rightMargin: 12
  256. width: 28
  257. height: 28
  258. radius: 14
  259. color: "#2563eb"
  260. Text {
  261. anchors.centerIn: parent
  262. text: "✓"
  263. font.pixelSize: 16
  264. font.bold: true
  265. color: "white"
  266. }
  267. }
  268. }
  269. }
  270. // Add scroll animations
  271. add: Transition {
  272. NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 300 }
  273. NumberAnimation { property: "scale"; from: 0.8; to: 1; duration: 300 }
  274. }
  275. }
  276. // Empty state when searching
  277. Item {
  278. Layout.fillWidth: true
  279. Layout.fillHeight: true
  280. visible: patternCount === 0 && searchField.text !== ""
  281. Column {
  282. anchors.centerIn: parent
  283. spacing: 20
  284. Text {
  285. text: "⌕"
  286. font.pixelSize: 48
  287. anchors.horizontalCenter: parent.horizontalCenter
  288. color: Components.ThemeManager.placeholderText
  289. }
  290. Label {
  291. text: "No patterns found"
  292. anchors.horizontalCenter: parent.horizontalCenter
  293. color: Components.ThemeManager.textSecondary
  294. font.pixelSize: 18
  295. }
  296. Label {
  297. text: "Try a different search term"
  298. anchors.horizontalCenter: parent.horizontalCenter
  299. color: Components.ThemeManager.textTertiary
  300. font.pixelSize: 14
  301. }
  302. }
  303. }
  304. }
  305. // Handle pattern added signal for live updates
  306. Connections {
  307. target: backend
  308. function onPatternAddedToPlaylist(success, message) {
  309. if (success) {
  310. console.log("Pattern added to playlist, refreshing selection state")
  311. // Extract the pattern name from the message if possible
  312. // The message format is typically "Pattern added to playlist"
  313. // We'll track additions in sessionAddedPatterns instead
  314. // Re-trigger binding evaluation by updating the array reference
  315. var temp = sessionAddedPatterns.slice()
  316. // Try to extract pattern name from recent action
  317. // Since we don't get the pattern name directly, we need another approach
  318. sessionAddedPatterns = temp
  319. }
  320. }
  321. }
  322. // Track which pattern was last clicked for visual feedback
  323. property string lastClickedPattern: ""
  324. // Override the click handler to track additions
  325. Component.onCompleted: {
  326. console.log("PatternSelectorPage loaded for playlist:", playlistName)
  327. console.log("Existing patterns:", existingPatterns)
  328. }
  329. // Function to add pattern and track it
  330. function addPatternToPlaylist(patternName) {
  331. if (!isPatternInPlaylist(patternName) && backend) {
  332. backend.addPatternToPlaylist(playlistName, patternName)
  333. // Immediately add to session tracking for instant visual feedback
  334. var temp = sessionAddedPatterns.slice()
  335. temp.push(patternName)
  336. sessionAddedPatterns = temp
  337. }
  338. }
  339. }