playlists.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. {% extends "base.html" %}
  2. {% block title %}Playlists - {{ app_name or 'Dune Weaver' }}{% endblock %}
  3. {% block additional_styles %}
  4. /* Minimal custom styles - rely on Tailwind utilities */
  5. .pattern-preview {
  6. border: 1px solid #e2e8f0;
  7. overflow: hidden;
  8. display: flex;
  9. align-items: center;
  10. justify-content: center;
  11. }
  12. .dark .pattern-preview img {
  13. filter: invert(1);
  14. }
  15. /* Mobile responsive utilities */
  16. @media (max-width: 768px) {
  17. .mobile-hidden {
  18. display: none !important;
  19. }
  20. .mobile-show {
  21. display: block !important;
  22. }
  23. .mobile-flex {
  24. display: flex !important;
  25. }
  26. .mobile-playlist-container {
  27. padding-top: 0.5rem;
  28. padding-bottom: 75px;
  29. padding-left: 0;
  30. padding-right: 0;
  31. }
  32. .mobile-full-width {
  33. width: 100% !important;
  34. max-width: 100% !important;
  35. border-radius: 0 !important;
  36. border-left: none !important;
  37. border-right: none !important;
  38. }
  39. .mobile-patterns-grid {
  40. grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) !important;
  41. gap: 0.75rem !important;
  42. padding: 1rem !important;
  43. }
  44. }
  45. .mobile-playlists-sidebar {
  46. padding: 0 !important;
  47. border-radius: 0 !important;
  48. border-left: none !important;
  49. border-right: none !important;
  50. border-bottom: none !important;
  51. }
  52. /* Override base template's dark mode rules for playlists page in light mode */
  53. html:not(.dark) #playlistsSidebar,
  54. html:not(.dark) #playlistDetails,
  55. html:not(.dark) #playlistsNav,
  56. html:not(.dark) .bg-white {
  57. background-color: #ffffff !important;
  58. }
  59. html:not(.dark) .bg-gray-50 {
  60. background-color: #f9fafb !important;
  61. }
  62. html:not(.dark) .bg-gray-100 {
  63. background-color: #f3f4f6 !important;
  64. }
  65. html:not(.dark) .text-gray-900 {
  66. color: #111827 !important;
  67. }
  68. html:not(.dark) .text-gray-700 {
  69. color: #374151 !important;
  70. }
  71. html:not(.dark) .text-gray-500 {
  72. color: #6b7280 !important;
  73. }
  74. html:not(.dark) .border-gray-200 {
  75. border-color: #e5e7eb !important;
  76. }
  77. html:not(.dark) .border-gray-300 {
  78. border-color: #d1d5db !important;
  79. }
  80. /* Fix hover states in light mode */
  81. html:not(.dark) .hover\:bg-gray-100:hover,
  82. html:not(.dark) #playlistsNav a:hover {
  83. background-color: #f3f4f6 !important;
  84. }
  85. html:not(.dark) .hover\:bg-gray-200:hover {
  86. background-color: #e5e7eb !important;
  87. }
  88. html:not(.dark) .hover\:text-gray-900:hover {
  89. color: #111827 !important;
  90. }
  91. html:not(.dark) .hover\:text-gray-700:hover {
  92. color: #374151 !important;
  93. }
  94. /* Ensure active playlist item styling works in light mode */
  95. html:not(.dark) #playlistsNav a.active,
  96. html:not(.dark) .bg-gray-100 {
  97. background-color: #f3f4f6 !important;
  98. color: #111827 !important;
  99. }
  100. /* Fix pattern text colors in light mode */
  101. html:not(.dark) .text-gray-800 {
  102. color: #1f2937 !important;
  103. }
  104. html:not(.dark) .group:hover .group-hover\:text-gray-900,
  105. html:not(.dark) .group-hover\:text-gray-900 {
  106. color: #111827 !important;
  107. }
  108. /* Pattern cards text in light mode */
  109. html:not(.dark) #patternsGrid p,
  110. html:not(.dark) #patternsGrid .text-sm {
  111. color: #1f2937 !important;
  112. }
  113. html:not(.dark) #patternsGrid .group:hover p,
  114. html:not(.dark) #patternsGrid .group:hover .text-sm {
  115. color: #111827 !important;
  116. }
  117. /* Available patterns modal text in light mode */
  118. html:not(.dark) #availablePatternsGrid p,
  119. html:not(.dark) #availablePatternsGrid .text-xs {
  120. color: #1f2937 !important;
  121. }
  122. {% endblock %}
  123. {% block content %}
  124. <div class="flex flex-1 justify-center p-4 sm:p-6 gap-6 bg-gray-50 dark:bg-gray-900 max-w-5xl mx-auto w-full mobile-playlist-container" style="min-height: calc(100vh - 72px - 64px);">
  125. <!-- Sidebar for Playlists -->
  126. <aside id="playlistsSidebar" class="w-80 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 flex flex-col mobile-full-width mobile-playlists-sidebar" style="height: calc(100vh - 72px - 64px - 48px);">
  127. <div class="flex items-center justify-between gap-3 p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 bg-white dark:bg-gray-800">
  128. <h3 class="text-gray-900 dark:text-gray-100 text-xl font-semibold leading-tight">Playlists</h3>
  129. <button id="addPlaylistBtn" class="flex items-center justify-center size-8 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-150">
  130. <span class="material-icons text-xl">add</span>
  131. </button>
  132. </div>
  133. <nav id="playlistsNav" class="flex-1 overflow-y-auto p-3 space-y-1 bg-white dark:bg-gray-800">
  134. <!-- Playlists will be populated here -->
  135. <div class="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
  136. <span class="text-sm">Loading playlists...</span>
  137. </div>
  138. </nav>
  139. </aside>
  140. <!-- Main Content -->
  141. <main id="playlistDetails" class="flex-1 bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col mobile-full-width" style="height: calc(100vh - 72px - 64px - 48px);">
  142. <header class="flex items-center justify-between gap-4 p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 bg-white dark:bg-gray-800">
  143. <!-- Mobile back button in header -->
  144. <div class="flex items-center gap-3 flex-1 min-w-0">
  145. <button id="mobileBackBtn" class="hidden mobile-flex md:hidden items-center justify-center size-8 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-150 flex-shrink-0">
  146. <span class="material-icons text-lg">arrow_back</span>
  147. </button>
  148. <div id="currentPlaylistTitle" class="flex items-center gap-3 min-w-0 flex-1">
  149. <h1 class="text-gray-900 dark:text-gray-100 text-2xl font-semibold leading-tight truncate">Select a Playlist</h1>
  150. </div>
  151. </div>
  152. <button id="addPatternsBtn" class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0" disabled>
  153. <span class="material-icons text-lg">add_photo_alternate</span>
  154. <span class="hidden sm:inline">Add Patterns</span>
  155. <span class="sm:hidden">Add</span>
  156. </button>
  157. </header>
  158. <div class="flex-1 flex flex-col overflow-hidden bg-white dark:bg-gray-800">
  159. <!-- Patterns Grid - Scrollable -->
  160. <div class="flex-1 overflow-y-auto border-b border-gray-200 dark:border-gray-700">
  161. <div id="patternsGrid" class="grid grid-cols-[repeat(auto-fill,minmax(128px,1fr))] gap-4 p-4 mobile-patterns-grid">
  162. <div class="flex items-center justify-center col-span-full py-12 text-gray-500 dark:text-gray-400">
  163. <span class="text-sm text-center">Select a playlist to view its patterns</span>
  164. </div>
  165. </div>
  166. </div>
  167. <!-- Playback Settings - Fixed at bottom -->
  168. <div id="playbackSettings" class="bg-white dark:bg-gray-800 flex-shrink-0 hidden">
  169. <div class="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
  170. <h2 class="text-gray-900 dark:text-gray-100 text-base font-semibold mb-3">Playback Settings</h2>
  171. <div class="space-y-4">
  172. <!-- Run Mode & Shuffle Section -->
  173. <div class="pb-3 border-b border-gray-100 dark:border-gray-600">
  174. <div class="flex flex-wrap items-center gap-3 mb-3">
  175. <div class="flex gap-2">
  176. <label class="text-xs font-medium flex items-center justify-center rounded-md border-2 border-gray-300 dark:border-gray-600 px-3 h-8 text-gray-700 dark:text-gray-300 has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-900/20 has-[:checked]:text-blue-700 dark:has-[:checked]:text-blue-300 has-[:checked]:font-semibold relative cursor-pointer transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500">
  177. <span class="material-icons text-sm mr-1">play_circle</span>
  178. Once
  179. <input class="invisible absolute" name="run_playlist" type="radio" value="single" checked/>
  180. </label>
  181. <label class="text-xs font-medium flex items-center justify-center rounded-md border-2 border-gray-300 dark:border-gray-600 px-3 h-8 text-gray-700 dark:text-gray-300 has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-900/20 has-[:checked]:text-blue-700 dark:has-[:checked]:text-blue-300 has-[:checked]:font-semibold relative cursor-pointer transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500">
  182. <span class="material-icons text-sm mr-1">repeat</span>
  183. Loop
  184. <input class="invisible absolute" name="run_playlist" type="radio" value="indefinite"/>
  185. </label>
  186. </div>
  187. <div class="flex items-center gap-2">
  188. <input id="shuffleCheckbox" type="checkbox" class="h-4 w-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600">
  189. <label for="shuffleCheckbox" class="text-xs font-medium text-gray-700 dark:text-gray-300 select-none cursor-pointer flex items-center gap-1">
  190. <span class="material-icons text-sm">shuffle</span>
  191. Shuffle
  192. </label>
  193. </div>
  194. </div>
  195. </div>
  196. <!-- Timing & Clear Pattern Section -->
  197. <div class="pb-3 border-b border-gray-100 dark:border-gray-600">
  198. <div class="grid grid-cols-2 gap-3">
  199. <div class="flex items-center gap-2">
  200. <span class="material-icons text-sm text-gray-500 dark:text-gray-400">schedule</span>
  201. <span class="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">Pause:</span>
  202. <div class="relative flex-1">
  203. <input id="pauseTimeInput" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 h-8 px-2 pr-8 text-xs" type="number" value="5" min="0" step="1"/>
  204. <span class="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-gray-500 dark:text-gray-400">s</span>
  205. </div>
  206. </div>
  207. <div class="flex items-center gap-2">
  208. <span class="material-icons text-sm text-gray-500 dark:text-gray-400">clear_all</span>
  209. <span class="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">Clear:</span>
  210. <select id="clearPatternSelect" class="flex-1 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 h-8 px-2 text-xs">
  211. <option value="adaptive">Adaptive</option>
  212. <option value="clear_from_in">Center</option>
  213. <option value="clear_from_out">Perimeter</option>
  214. <option value="clear_sideway">Sideway</option>
  215. <option value="none">None</option>
  216. </select>
  217. </div>
  218. </div>
  219. </div>
  220. <!-- Run Button Section -->
  221. <div class="pt-1">
  222. <button id="runPlaylistBtn" class="flex items-center justify-center gap-2 w-full h-9 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:hover:bg-gray-400 text-white text-sm font-medium rounded-md transition-colors duration-200 disabled:opacity-60 disabled:cursor-not-allowed" disabled>
  223. <span class="material-icons text-base">play_arrow</span>
  224. <span>Run Playlist</span>
  225. </button>
  226. </div>
  227. </div>
  228. </div>
  229. </div>
  230. </div>
  231. </main>
  232. </div>
  233. <!-- Add Playlist Modal -->
  234. <div id="addPlaylistModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
  235. <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
  236. <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Create New Playlist</h3>
  237. <div class="space-y-4">
  238. <div>
  239. <label for="newPlaylistName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Playlist Name</label>
  240. <input id="newPlaylistName" type="text" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 px-3 py-2" placeholder="Enter playlist name" autofocus>
  241. </div>
  242. <div class="flex gap-3 justify-end">
  243. <button id="cancelPlaylistBtn" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150">Cancel</button>
  244. <button id="createPlaylistBtn" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-150">Create</button>
  245. </div>
  246. </div>
  247. </div>
  248. </div>
  249. <!-- Add Patterns Modal -->
  250. <div id="addPatternsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
  251. <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-4xl mx-4 max-h-[80vh] overflow-hidden flex flex-col">
  252. <div class="flex items-center justify-between mb-4 flex-shrink-0">
  253. <h3 id="modalTitle" class="text-lg font-semibold text-gray-900 dark:text-gray-100">Add Patterns to Playlist</h3>
  254. </div>
  255. <!-- Search Bar and Controls -->
  256. <div class="mb-4 flex-shrink-0 space-y-3">
  257. <div class="relative">
  258. <span class="material-icons absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500 text-lg">search</span>
  259. <input
  260. id="patternSearchInput"
  261. type="text"
  262. placeholder="Search patterns..."
  263. class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
  264. />
  265. <button id="clearSearchBtn" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hidden">
  266. <span class="material-icons text-lg">clear</span>
  267. </button>
  268. </div>
  269. <!-- Sort and Filter Controls -->
  270. <div class="flex flex-wrap gap-3 items-center">
  271. <div class="flex items-center gap-2">
  272. <span class="text-xs font-medium text-gray-700 dark:text-gray-300">Sort by:</span>
  273. <select id="sortFieldSelect" class="text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1">
  274. <option value="name">Name</option>
  275. <option value="date">Date Modified</option>
  276. <option value="coordinates">Coordinates</option>
  277. <option value="favorite">Favorite</option>
  278. </select>
  279. <button id="sortDirectionBtn" class="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400" title="Toggle sort direction">
  280. <span class="material-icons text-sm" id="sortDirectionIcon">arrow_upward</span>
  281. </button>
  282. </div>
  283. <div class="flex items-center gap-2">
  284. <span class="text-xs font-medium text-gray-700 dark:text-gray-300">Folder:</span>
  285. <select id="categoryFilterSelect" class="text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1">
  286. <option value="all">All</option>
  287. </select>
  288. </div>
  289. </div>
  290. <!-- Smart Toggle Select All button -->
  291. <div class="flex items-center justify-between">
  292. <button
  293. id="toggleSelectAllBtn"
  294. class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150"
  295. >
  296. <span class="material-icons text-base" id="toggleSelectAllIcon">check_box_outline_blank</span>
  297. <span id="toggleSelectAllText">Select All</span>
  298. </button>
  299. <span id="selectionCount" class="text-xs text-gray-500 dark:text-gray-400">
  300. 0 selected
  301. </span>
  302. </div>
  303. </div>
  304. <div class="flex-1 overflow-y-auto">
  305. <div id="availablePatternsGrid" class="grid grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-4 p-2">
  306. <!-- Available patterns will be populated here -->
  307. </div>
  308. <!-- Loading indicator -->
  309. <div id="patternsLoadingIndicator" class="flex items-center justify-center py-12 text-gray-500 dark:text-gray-400 hidden">
  310. <div class="flex items-center gap-3">
  311. <div class="bg-gray-200 dark:bg-gray-700 rounded-full h-6 w-6 flex items-center justify-center">
  312. <div class="bg-gray-500 dark:bg-gray-400 rounded-full h-3 w-3"></div>
  313. </div>
  314. <span class="text-sm">Loading patterns...</span>
  315. </div>
  316. </div>
  317. <!-- No results message -->
  318. <div id="noResultsMessage" class="flex items-center justify-center py-12 text-gray-500 dark:text-gray-400 hidden">
  319. <span class="text-sm">No patterns found matching your search</span>
  320. </div>
  321. </div>
  322. <div class="flex gap-3 justify-end mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
  323. <button id="cancelAddPatternsBtn" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150">Cancel</button>
  324. <button id="confirmAddPatternsBtn" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-150">Save</button>
  325. </div>
  326. </div>
  327. </div>
  328. {% endblock %}
  329. {% block scripts %}
  330. <script>
  331. // Force light mode styling if not in dark mode
  332. document.addEventListener('DOMContentLoaded', function() {
  333. const isDark = document.documentElement.classList.contains('dark');
  334. if (!isDark) {
  335. // Force light backgrounds
  336. const sidebar = document.getElementById('playlistsSidebar');
  337. const mainContent = document.getElementById('playlistDetails');
  338. const nav = document.getElementById('playlistsNav');
  339. if (sidebar) {
  340. sidebar.style.backgroundColor = '#ffffff';
  341. sidebar.style.borderColor = '#e5e7eb';
  342. }
  343. if (mainContent) {
  344. mainContent.style.backgroundColor = '#ffffff';
  345. mainContent.style.borderColor = '#e5e7eb';
  346. }
  347. if (nav) {
  348. nav.style.backgroundColor = '#ffffff';
  349. }
  350. // Force light text colors
  351. document.querySelectorAll('h1, h2, h3').forEach(el => {
  352. if (!el.closest('.dark')) {
  353. el.style.color = '#111827';
  354. }
  355. });
  356. }
  357. });
  358. </script>
  359. <script src="/static/js/playlists.js"></script>
  360. {% endblock %}