| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- {% extends "base.html" %}
- {% block title %}Playlists - {{ app_name or 'Dune Weaver' }}{% endblock %}
- {% block additional_styles %}
- /* Minimal custom styles - rely on Tailwind utilities */
- .pattern-preview {
- border: 1px solid #e2e8f0;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .dark .pattern-preview img {
- filter: invert(1);
- }
- /* Fix checkbox visibility in light mode */
- html:not(.dark) input[type="checkbox"]:checked {
- background-color: #2563eb !important; /* Darker blue for better contrast */
- border-color: #2563eb !important;
- }
- html:not(.dark) input[type="checkbox"] {
- background-color: #ffffff !important;
- border-color: #d1d5db !important;
- }
- /* Mobile responsive utilities */
- @media (max-width: 768px) {
- .mobile-hidden {
- display: none !important;
- }
-
- .mobile-show {
- display: block !important;
- }
-
- .mobile-flex {
- display: flex !important;
- }
-
- .mobile-playlist-container {
- padding-top: 0.5rem;
- padding-bottom: 75px;
- padding-left: 0;
- padding-right: 0;
- }
-
- .mobile-full-width {
- width: 100% !important;
- max-width: 100% !important;
- border-radius: 0 !important;
- border-left: none !important;
- border-right: none !important;
- }
-
- .mobile-patterns-grid {
- grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) !important;
- gap: 0.75rem !important;
- padding: 1rem !important;
- }
- }
- .mobile-playlists-sidebar {
- padding: 0 !important;
- border-radius: 0 !important;
- border-left: none !important;
- border-right: none !important;
- border-bottom: none !important;
- }
- /* Override base template's dark mode rules for playlists page in light mode */
- html:not(.dark) #playlistsSidebar,
- html:not(.dark) #playlistDetails,
- html:not(.dark) #playlistsNav,
- html:not(.dark) .bg-white {
- background-color: #ffffff !important;
- }
- html:not(.dark) .bg-gray-50 {
- background-color: #f9fafb !important;
- }
- html:not(.dark) .bg-gray-100 {
- background-color: #f3f4f6 !important;
- }
- html:not(.dark) .text-gray-900 {
- color: #111827 !important;
- }
- html:not(.dark) .text-gray-700 {
- color: #374151 !important;
- }
- html:not(.dark) .text-gray-500 {
- color: #6b7280 !important;
- }
- html:not(.dark) .border-gray-200 {
- border-color: #e5e7eb !important;
- }
- html:not(.dark) .border-gray-300 {
- border-color: #d1d5db !important;
- }
- /* Fix hover states in light mode */
- html:not(.dark) .hover\:bg-gray-100:hover,
- html:not(.dark) #playlistsNav a:hover {
- background-color: #f3f4f6 !important;
- }
- html:not(.dark) .hover\:bg-gray-200:hover {
- background-color: #e5e7eb !important;
- }
- html:not(.dark) .hover\:text-gray-900:hover {
- color: #111827 !important;
- }
- html:not(.dark) .hover\:text-gray-700:hover {
- color: #374151 !important;
- }
- /* Ensure active playlist item styling works in light mode */
- html:not(.dark) #playlistsNav a.active,
- html:not(.dark) .bg-gray-100 {
- background-color: #f3f4f6 !important;
- color: #111827 !important;
- }
- /* Fix pattern text colors in light mode */
- html:not(.dark) .text-gray-800 {
- color: #1f2937 !important;
- }
- html:not(.dark) .group:hover .group-hover\:text-gray-900,
- html:not(.dark) .group-hover\:text-gray-900 {
- color: #111827 !important;
- }
- /* Pattern cards text in light mode */
- html:not(.dark) #patternsGrid p,
- html:not(.dark) #patternsGrid .text-sm {
- color: #1f2937 !important;
- }
- html:not(.dark) #patternsGrid .group:hover p,
- html:not(.dark) #patternsGrid .group:hover .text-sm {
- color: #111827 !important;
- }
- /* Available patterns modal text in light mode */
- html:not(.dark) #availablePatternsGrid p,
- html:not(.dark) #availablePatternsGrid .text-xs {
- color: #1f2937 !important;
- }
- {% endblock %}
- {% block content %}
- <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);">
-
- <!-- Sidebar for Playlists -->
- <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);">
- <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">
- <h3 class="text-gray-900 dark:text-gray-100 text-xl font-semibold leading-tight">Playlists</h3>
- <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">
- <span class="material-icons text-xl">add</span>
- </button>
- </div>
- <nav id="playlistsNav" class="flex-1 overflow-y-auto p-3 space-y-1 bg-white dark:bg-gray-800">
- <!-- Playlists will be populated here -->
- <div class="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
- <span class="text-sm">Loading playlists...</span>
- </div>
- </nav>
- </aside>
- <!-- Main Content -->
- <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);">
- <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">
- <!-- Mobile back button in header -->
- <div class="flex items-center gap-3 flex-1 min-w-0">
- <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">
- <span class="material-icons text-lg">arrow_back</span>
- </button>
- <div id="currentPlaylistTitle" class="flex items-center gap-3 min-w-0 flex-1">
- <h1 class="text-gray-900 dark:text-gray-100 text-2xl font-semibold leading-tight truncate">Select a Playlist</h1>
- </div>
- </div>
- <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>
- <span class="material-icons text-lg">add_photo_alternate</span>
- <span class="hidden sm:inline">Add Patterns</span>
- <span class="sm:hidden">Add</span>
- </button>
- </header>
-
- <div class="flex-1 flex flex-col overflow-hidden bg-white dark:bg-gray-800">
- <!-- Patterns Grid - Scrollable -->
- <div class="flex-1 overflow-y-auto border-b border-gray-200 dark:border-gray-700">
- <div id="patternsGrid" class="grid grid-cols-[repeat(auto-fill,minmax(128px,1fr))] gap-4 p-4 mobile-patterns-grid">
- <div class="flex items-center justify-center col-span-full py-12 text-gray-500 dark:text-gray-400">
- <span class="text-sm text-center">Select a playlist to view its patterns</span>
- </div>
- </div>
- </div>
- <!-- Playback Settings - Fixed at bottom -->
- <div id="playbackSettings" class="bg-white dark:bg-gray-800 flex-shrink-0 hidden">
- <div class="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
- <h2 class="text-gray-900 dark:text-gray-100 text-base font-semibold mb-3">Playback Settings</h2>
-
- <div class="space-y-4">
- <!-- Run Mode & Shuffle Section -->
- <div class="pb-3 border-b border-gray-100 dark:border-gray-600">
- <div class="flex flex-wrap items-center gap-3 mb-3">
- <div class="flex gap-2">
- <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">
- <span class="material-icons text-sm mr-1">play_circle</span>
- Once
- <input class="invisible absolute" name="run_playlist" type="radio" value="single" checked/>
- </label>
- <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">
- <span class="material-icons text-sm mr-1">repeat</span>
- Loop
- <input class="invisible absolute" name="run_playlist" type="radio" value="indefinite"/>
- </label>
- </div>
-
- <div class="flex items-center gap-2">
- <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">
- <label for="shuffleCheckbox" class="text-xs font-medium text-gray-700 dark:text-gray-300 select-none cursor-pointer flex items-center gap-1">
- <span class="material-icons text-sm">shuffle</span>
- Shuffle
- </label>
- </div>
- </div>
- </div>
- <!-- Timing & Clear Pattern Section -->
- <div class="pb-3 border-b border-gray-100 dark:border-gray-600">
- <div class="grid grid-cols-2 gap-3">
- <div class="flex items-center gap-2">
- <span class="material-icons text-sm text-gray-500 dark:text-gray-400">schedule</span>
- <span class="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">Pause:</span>
- <div class="relative flex-1">
- <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"/>
- <span class="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-gray-500 dark:text-gray-400">s</span>
- </div>
- </div>
-
- <div class="flex items-center gap-2">
- <span class="material-icons text-sm text-gray-500 dark:text-gray-400">clear_all</span>
- <span class="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">Clear:</span>
- <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">
- <option value="adaptive">Adaptive</option>
- <option value="clear_from_in">Center</option>
- <option value="clear_from_out">Perimeter</option>
- <option value="clear_sideway">Sideway</option>
- <option value="none">None</option>
- </select>
- </div>
- </div>
- </div>
- <!-- Run Button Section -->
- <div class="pt-1">
- <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>
- <span class="material-icons text-base">play_arrow</span>
- <span>Run Playlist</span>
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </main>
- </div>
- <!-- Add Playlist Modal -->
- <div id="addPlaylistModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
- <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Create New Playlist</h3>
- <div class="space-y-4">
- <div>
- <label for="newPlaylistName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Playlist Name</label>
- <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>
- </div>
- <div class="flex gap-3 justify-end">
- <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>
- <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>
- </div>
- </div>
- </div>
- </div>
- <!-- Add Patterns Modal -->
- <div id="addPatternsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
- <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">
- <div class="flex items-center justify-between mb-4 flex-shrink-0">
- <h3 id="modalTitle" class="text-lg font-semibold text-gray-900 dark:text-gray-100">Add Patterns to Playlist</h3>
- </div>
-
- <!-- Search Bar and Controls -->
- <div class="mb-4 flex-shrink-0 space-y-3">
- <div class="relative">
- <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>
- <input
- id="patternSearchInput"
- type="text"
- placeholder="Search patterns..."
- 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"
- />
- <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">
- <span class="material-icons text-lg">clear</span>
- </button>
- </div>
-
- <!-- Sort and Filter Controls -->
- <div class="flex flex-wrap gap-3 items-center">
- <div class="flex items-center gap-2">
- <span class="text-xs font-medium text-gray-700 dark:text-gray-300">Sort by:</span>
- <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">
- <option value="name">Name</option>
- <option value="date">Date Modified</option>
- <option value="coordinates">Coordinates</option>
- <option value="favorite">Favorite</option>
- </select>
- <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">
- <span class="material-icons text-sm" id="sortDirectionIcon">arrow_upward</span>
- </button>
- </div>
-
- <div class="flex items-center gap-2">
- <span class="text-xs font-medium text-gray-700 dark:text-gray-300">Folder:</span>
- <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">
- <option value="all">All</option>
- </select>
- </div>
- </div>
-
- <!-- Smart Toggle Select All button -->
- <div class="flex items-center justify-between">
- <button
- id="toggleSelectAllBtn"
- 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"
- >
- <span class="material-icons text-base" id="toggleSelectAllIcon">check_box_outline_blank</span>
- <span id="toggleSelectAllText">Select All</span>
- </button>
- <span id="selectionCount" class="text-xs text-gray-500 dark:text-gray-400">
- 0 selected
- </span>
- </div>
- </div>
-
- <div class="flex-1 overflow-y-auto">
- <div id="availablePatternsGrid" class="grid grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-4 p-2">
- <!-- Available patterns will be populated here -->
- </div>
-
- <!-- Loading indicator -->
- <div id="patternsLoadingIndicator" class="flex items-center justify-center py-12 text-gray-500 dark:text-gray-400 hidden">
- <div class="flex items-center gap-3">
- <div class="bg-gray-200 dark:bg-gray-700 rounded-full h-6 w-6 flex items-center justify-center">
- <div class="bg-gray-500 dark:bg-gray-400 rounded-full h-3 w-3"></div>
- </div>
- <span class="text-sm">Loading patterns...</span>
- </div>
- </div>
-
- <!-- No results message -->
- <div id="noResultsMessage" class="flex items-center justify-center py-12 text-gray-500 dark:text-gray-400 hidden">
- <span class="text-sm">No patterns found matching your search</span>
- </div>
- </div>
-
- <div class="flex gap-3 justify-end mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
- <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>
- <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>
- </div>
- </div>
- </div>
- {% endblock %}
- {% block scripts %}
- <script>
- // Force light mode styling if not in dark mode
- document.addEventListener('DOMContentLoaded', function() {
- const isDark = document.documentElement.classList.contains('dark');
-
- if (!isDark) {
- // Force light backgrounds
- const sidebar = document.getElementById('playlistsSidebar');
- const mainContent = document.getElementById('playlistDetails');
- const nav = document.getElementById('playlistsNav');
-
- if (sidebar) {
- sidebar.style.backgroundColor = '#ffffff';
- sidebar.style.borderColor = '#e5e7eb';
- }
-
- if (mainContent) {
- mainContent.style.backgroundColor = '#ffffff';
- mainContent.style.borderColor = '#e5e7eb';
- }
-
- if (nav) {
- nav.style.backgroundColor = '#ffffff';
- }
-
- // Force light text colors
- document.querySelectorAll('h1, h2, h3').forEach(el => {
- if (!el.closest('.dark')) {
- el.style.color = '#111827';
- }
- });
- }
- });
- </script>
- <script src="/static/js/playlists.js"></script>
- {% endblock %}
|