PatternSelectorPage.qml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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. placeholderTextColor: Components.ThemeManager.textTertiary
  129. font.pixelSize: 14
  130. color: Components.ThemeManager.textPrimary
  131. visible: searchExpanded || text.length > 0
  132. property string lastSearchText: ""
  133. property bool hasUnappliedSearch: text !== lastSearchText && text.length > 0
  134. background: Rectangle {
  135. color: "transparent"
  136. border.color: searchField.hasUnappliedSearch ? "#f59e0b" : "transparent"
  137. border.width: searchField.hasUnappliedSearch ? 1 : 0
  138. radius: 4
  139. }
  140. onAccepted: {
  141. patternModel.filter(text)
  142. lastSearchText = text
  143. Qt.inputMethod.hide()
  144. focus = false
  145. }
  146. activeFocusOnPress: true
  147. selectByMouse: true
  148. inputMethodHints: Qt.ImhNoPredictiveText
  149. MouseArea {
  150. anchors.fill: parent
  151. onPressed: {
  152. searchField.forceActiveFocus()
  153. Qt.inputMethod.show()
  154. mouse.accepted = false
  155. }
  156. }
  157. onActiveFocusChanged: {
  158. if (activeFocus) {
  159. searchExpanded = true
  160. Qt.inputMethod.show()
  161. } else {
  162. if (text !== lastSearchText) {
  163. patternModel.filter(text)
  164. lastSearchText = text
  165. }
  166. }
  167. }
  168. Keys.onReturnPressed: {
  169. Qt.inputMethod.hide()
  170. focus = false
  171. }
  172. Keys.onEscapePressed: {
  173. text = ""
  174. lastSearchText = ""
  175. patternModel.filter("")
  176. Qt.inputMethod.hide()
  177. focus = false
  178. }
  179. }
  180. Text {
  181. text: searchExpanded || searchField.text.length > 0 ? "Search" : ""
  182. font.pixelSize: 12
  183. color: Components.ThemeManager.textTertiary
  184. visible: !searchExpanded && searchField.text.length === 0
  185. }
  186. }
  187. MouseArea {
  188. anchors.fill: parent
  189. enabled: !searchExpanded
  190. onClicked: {
  191. searchExpanded = true
  192. searchField.forceActiveFocus()
  193. Qt.inputMethod.show()
  194. }
  195. }
  196. }
  197. // Close button when search expanded
  198. Button {
  199. text: "✕"
  200. font.pixelSize: 18
  201. flat: true
  202. visible: searchExpanded
  203. Layout.preferredWidth: 32
  204. Layout.preferredHeight: 32
  205. onClicked: {
  206. searchExpanded = false
  207. searchField.text = ""
  208. searchField.lastSearchText = ""
  209. searchField.focus = false
  210. patternModel.filter("")
  211. }
  212. }
  213. }
  214. }
  215. // Pattern Grid
  216. GridView {
  217. id: gridView
  218. Layout.fillWidth: true
  219. Layout.fillHeight: true
  220. cellWidth: 200
  221. cellHeight: 220
  222. model: patternModel
  223. clip: true
  224. ScrollBar.vertical: ScrollBar {
  225. active: true
  226. policy: ScrollBar.AsNeeded
  227. }
  228. delegate: Item {
  229. width: gridView.cellWidth - 10
  230. height: gridView.cellHeight - 10
  231. // Check if pattern is already in playlist
  232. property bool isInPlaylist: isPatternInPlaylist(model.name)
  233. ModernPatternCard {
  234. id: patternCard
  235. anchors.fill: parent
  236. name: model.name
  237. preview: model.preview
  238. onClicked: {
  239. // Use the tracking function for immediate visual feedback
  240. page.addPatternToPlaylist(model.name)
  241. }
  242. }
  243. // Selection overlay for patterns already in playlist
  244. Rectangle {
  245. anchors.fill: parent
  246. color: "transparent"
  247. border.color: isInPlaylist ? "#2563eb" : "transparent"
  248. border.width: isInPlaylist ? 3 : 0
  249. radius: 12
  250. // Checkmark badge for selected patterns
  251. Rectangle {
  252. visible: isInPlaylist
  253. anchors.top: parent.top
  254. anchors.right: parent.right
  255. anchors.topMargin: 12
  256. anchors.rightMargin: 12
  257. width: 28
  258. height: 28
  259. radius: 14
  260. color: "#2563eb"
  261. Text {
  262. anchors.centerIn: parent
  263. text: "✓"
  264. font.pixelSize: 16
  265. font.bold: true
  266. color: "white"
  267. }
  268. }
  269. }
  270. }
  271. // Add scroll animations
  272. add: Transition {
  273. NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 300 }
  274. NumberAnimation { property: "scale"; from: 0.8; to: 1; duration: 300 }
  275. }
  276. }
  277. // Empty state when searching
  278. Item {
  279. Layout.fillWidth: true
  280. Layout.fillHeight: true
  281. visible: patternCount === 0 && searchField.text !== ""
  282. Column {
  283. anchors.centerIn: parent
  284. spacing: 20
  285. Text {
  286. text: "⌕"
  287. font.pixelSize: 48
  288. anchors.horizontalCenter: parent.horizontalCenter
  289. color: Components.ThemeManager.placeholderText
  290. }
  291. Label {
  292. text: "No patterns found"
  293. anchors.horizontalCenter: parent.horizontalCenter
  294. color: Components.ThemeManager.textSecondary
  295. font.pixelSize: 18
  296. }
  297. Label {
  298. text: "Try a different search term"
  299. anchors.horizontalCenter: parent.horizontalCenter
  300. color: Components.ThemeManager.textTertiary
  301. font.pixelSize: 14
  302. }
  303. }
  304. }
  305. }
  306. // Handle pattern added signal for live updates
  307. Connections {
  308. target: backend
  309. function onPatternAddedToPlaylist(success, message) {
  310. if (success) {
  311. console.log("Pattern added to playlist, refreshing selection state")
  312. // Extract the pattern name from the message if possible
  313. // The message format is typically "Pattern added to playlist"
  314. // We'll track additions in sessionAddedPatterns instead
  315. // Re-trigger binding evaluation by updating the array reference
  316. var temp = sessionAddedPatterns.slice()
  317. // Try to extract pattern name from recent action
  318. // Since we don't get the pattern name directly, we need another approach
  319. sessionAddedPatterns = temp
  320. }
  321. }
  322. }
  323. // Track which pattern was last clicked for visual feedback
  324. property string lastClickedPattern: ""
  325. // Override the click handler to track additions
  326. Component.onCompleted: {
  327. console.log("PatternSelectorPage loaded for playlist:", playlistName)
  328. console.log("Existing patterns:", existingPatterns)
  329. }
  330. // Function to add pattern and track it
  331. function addPatternToPlaylist(patternName) {
  332. if (!isPatternInPlaylist(patternName) && backend) {
  333. backend.addPatternToPlaylist(playlistName, patternName)
  334. // Immediately add to session tracking for instant visual feedback
  335. var temp = sessionAddedPatterns.slice()
  336. temp.push(patternName)
  337. sessionAddedPatterns = temp
  338. }
  339. }
  340. }