From 7cfe8ef05ed49cc8651149780ee0059adbb3b803 Mon Sep 17 00:00:00 2001 From: pythoninthegrass <4097471+pythoninthegrass@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:39:02 -0500 Subject: [PATCH] feat: implement HTMX base templates and partials - Add main application template with three-panel layout (base.html) - Include all required dependencies (HTMX, Alpine.js, Basecoat UI) - Create reusable partial templates for navbar, sidebar, player controls, queue - Implement modal and toast notification components - Add track-item partial for consistent track display - Include comprehensive JavaScript utilities and keyboard shortcuts - Add template filters for duration and file size formatting - Create detailed documentation for the template system Closes task-026 --- static/css/main.css | 279 +++++++++++++++++++ static/js/app.js | 314 ++++++++++++++++++++++ templates/README.md | 190 +++++++++++++ templates/base/base.html | 136 ++++++++++ templates/components/navbar.html | 120 +++++++++ templates/components/player-controls.html | 192 +++++++++++++ templates/components/queue.html | 110 ++++++++ templates/components/sidebar.html | 104 +++++++ templates/filters.py | 58 ++++ templates/pages/library.html | 145 ++++++++++ templates/partials/modal.html | 62 +++++ templates/partials/toast.html | 50 ++++ templates/partials/track-item.html | 103 +++++++ 13 files changed, 1863 insertions(+) create mode 100644 static/css/main.css create mode 100644 static/js/app.js create mode 100644 templates/README.md create mode 100644 templates/base/base.html create mode 100644 templates/components/navbar.html create mode 100644 templates/components/player-controls.html create mode 100644 templates/components/queue.html create mode 100644 templates/components/sidebar.html create mode 100644 templates/filters.py create mode 100644 templates/pages/library.html create mode 100644 templates/partials/modal.html create mode 100644 templates/partials/toast.html create mode 100644 templates/partials/track-item.html diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 00000000..b2c3455f --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,279 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +/* Tailwind CSS Base */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Custom CSS Variables for Dark Theme */ +:root { + --bg-primary: #030712; /* gray-950 */ + --bg-secondary: #111827; /* gray-900 */ + --bg-tertiary: #1f2937; /* gray-800 */ + --text-primary: #f3f4f6; /* gray-100 */ + --text-secondary: #9ca3af; /* gray-400 */ + --text-muted: #6b7280; /* gray-500 */ + --border-color: #1f2937; /* gray-800 */ + --accent-blue: #3b82f6; /* blue-500 */ + --accent-purple: #8b5cf6; /* purple-500 */ + --accent-green: #10b981; /* green-500 */ + --accent-red: #ef4444; /* red-500 */ +} + +/* Base Styles */ +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--bg-tertiary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #374151; +} + +/* HTMX Loading Indicators */ +.htmx-indicator { + display: none; +} + +.htmx-request .htmx-indicator { + display: inline-block; +} + +.htmx-request.htmx-indicator { + display: inline-block; +} + +/* Transition Classes */ +.transition-all-300 { + transition: all 300ms ease; +} + +/* Custom Component Styles */ + +/* Player Progress Bar */ +input[type="range"] { + -webkit-appearance: none; + appearance: none; + background: transparent; +} + +input[type="range"]::-webkit-slider-track { + background: #374151; + height: 4px; + border-radius: 2px; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent-blue); + cursor: pointer; + margin-top: -4px; + opacity: 0; + transition: opacity 200ms; +} + +input[type="range"]:hover::-webkit-slider-thumb { + opacity: 1; +} + +/* Track List Hover Effects */ +.track-item { + transition: background-color 150ms ease; +} + +.track-item:hover { + background-color: var(--bg-tertiary); +} + +.track-item:hover .track-actions { + opacity: 1; +} + +/* Album Grid */ +.album-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 1.5rem; +} + +.album-card { + background: var(--bg-secondary); + border-radius: 8px; + padding: 1rem; + transition: transform 150ms ease, box-shadow 150ms ease; + cursor: pointer; +} + +.album-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); +} + +.album-card img { + width: 100%; + height: auto; + aspect-ratio: 1; + object-fit: cover; + border-radius: 4px; + margin-bottom: 0.75rem; +} + +/* Now Playing Animation */ +@keyframes music-bars { + 0%, 100% { + height: 3px; + } + 50% { + height: 12px; + } +} + +.music-bars { + display: flex; + align-items: flex-end; + gap: 2px; + height: 12px; +} + +.music-bars span { + width: 3px; + background: var(--accent-blue); + animation: music-bars 1s ease-in-out infinite; + transform-origin: bottom; +} + +.music-bars span:nth-child(2) { + animation-delay: 0.2s; +} + +.music-bars span:nth-child(3) { + animation-delay: 0.4s; +} + +/* Modal Backdrop */ +.modal-backdrop { + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); +} + +/* Context Menu */ +.context-menu { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 180px; +} + +.context-menu-item { + padding: 0.5rem 1rem; + transition: background-color 150ms; + cursor: pointer; +} + +.context-menu-item:hover { + background: var(--bg-tertiary); +} + +/* Toast Animations */ +@keyframes slide-in { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slide-out { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +.toast-enter { + animation: slide-in 300ms ease-out; +} + +.toast-leave { + animation: slide-out 200ms ease-in; +} + +/* Skeleton Loading */ +.skeleton { + background: linear-gradient( + 90deg, + var(--bg-tertiary) 0%, + #374151 50%, + var(--bg-tertiary) 100% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.5s ease-in-out infinite; + border-radius: 4px; +} + +@keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +/* Utility Classes */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.text-gradient { + background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* Responsive Utilities */ +@media (max-width: 768px) { + .album-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } +} + +@media (max-width: 640px) { + .album-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 1rem; + } +} \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 00000000..9eec47a5 --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,314 @@ +// mt Music Player - HTMX Frontend JavaScript +// Handles client-side interactions, Alpine.js integrations, and HTMX enhancements + +// Toast notification system +function showToast(message, type = 'info', duration = 3000) { + const toastContainer = document.getElementById('toast-container'); + if (!toastContainer) return; + + const toastId = 'toast-' + Date.now(); + const toastHTML = ` +
+
+ +
+
+ ${getToastIcon(type)} +
+
+

${message}

+
+ +
+
+
+ `; + + toastContainer.insertAdjacentHTML('beforeend', toastHTML); + + // Auto-remove after animation + setTimeout(() => { + const toast = document.getElementById(toastId); + if (toast) toast.remove(); + }, duration + 500); +} + +function getToastIcon(type) { + switch (type) { + case 'success': + return ''; + case 'error': + return ''; + case 'warning': + return ''; + default: + return ''; + } +} + +// Modal management +function showModal(content, title = '') { + const modalContainer = document.getElementById('modal-container'); + if (!modalContainer) return; + + const modalHTML = ` +
+ +
+ +
+
+ + ${title ? ` +
+

${title}

+ +
+ ` : ''} + +
+ ${content} +
+
+
+
+ `; + + modalContainer.innerHTML = modalHTML; +} + +function closeModal() { + const modalContainer = document.getElementById('modal-container'); + if (modalContainer) { + modalContainer.innerHTML = ''; + } +} + +// Keyboard shortcuts +document.addEventListener('keydown', function(event) { + // Don't trigger shortcuts when typing in inputs + if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { + return; + } + + switch (event.key) { + case ' ': + // Spacebar - toggle play/pause + event.preventDefault(); + htmx.ajax('POST', '/api/player/toggle', { swap: 'none' }); + break; + case 'ArrowLeft': + // Left arrow - previous track + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + htmx.ajax('POST', '/api/player/previous', { swap: 'none' }); + } + break; + case 'ArrowRight': + // Right arrow - next track + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + htmx.ajax('POST', '/api/player/next', { swap: 'none' }); + } + break; + case 'ArrowUp': + // Up arrow - increase volume + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + // Volume up logic would go here + } + break; + case 'ArrowDown': + // Down arrow - decrease volume + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + // Volume down logic would go here + } + break; + case 'm': + case 'M': + // M - toggle mute + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + htmx.ajax('POST', '/api/player/mute', { swap: 'none' }); + } + break; + case 's': + case 'S': + // S - toggle shuffle + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + htmx.ajax('POST', '/api/player/shuffle', { swap: 'none' }); + } + break; + case 'r': + case 'R': + // R - toggle repeat + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + htmx.ajax('POST', '/api/player/repeat', { swap: 'none' }); + } + break; + } +}); + +// HTMX event handlers +document.addEventListener('DOMContentLoaded', function() { + // HTMX configuration + htmx.config.globalViewTransitions = true; + htmx.config.useTemplateFragments = true; + + // Custom event listeners + document.body.addEventListener('show-toast', function(event) { + const { message, type, duration } = event.detail; + showToast(message, type, duration); + }); + + document.body.addEventListener('show-modal', function(event) { + const { content, title } = event.detail; + showModal(content, title); + }); + + document.body.addEventListener('close-modal', function() { + closeModal(); + }); + + // Track selection and drag/drop + let draggedElement = null; + + document.addEventListener('dragstart', function(event) { + if (event.target.closest('[draggable="true"]')) { + draggedElement = event.target.closest('[draggable="true"]'); + draggedElement.classList.add('opacity-50'); + } + }); + + document.addEventListener('dragend', function(event) { + if (draggedElement) { + draggedElement.classList.remove('opacity-50'); + draggedElement = null; + } + }); + + // Progress bar interaction + document.addEventListener('input', function(event) { + if (event.target.id === 'progress-bar') { + const value = event.target.value; + // Update progress visually + const progressFill = event.target.nextElementSibling; + if (progressFill) { + progressFill.style.width = value + '%'; + } + } + }); + + // Volume slider interaction + document.addEventListener('input', function(event) { + if (event.target.id === 'volume-slider') { + const value = event.target.value; + // Update volume visually + const volumeFill = event.target.nextElementSibling; + if (volumeFill) { + volumeFill.style.width = value + '%'; + } + } + }); +}); + +// Utility functions +function formatDuration(seconds) { + if (!seconds || seconds === 0) return '0:00'; + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; +} + +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// Search functionality +const searchInput = document.querySelector('input[name="q"]'); +if (searchInput) { + searchInput.addEventListener('input', debounce(function(event) { + const query = event.target.value.trim(); + if (query.length > 2) { + // Trigger search + htmx.ajax('GET', `/api/search?q=${encodeURIComponent(query)}`, { + target: '#search-results', + swap: 'innerHTML' + }); + } else { + // Clear results + const results = document.getElementById('search-results'); + if (results) results.innerHTML = ''; + } + }, 300)); +} + +// Theme management +function setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('theme', theme); + + // Update Alpine.js data + if (window.Alpine && Alpine.store) { + Alpine.store('theme', theme); + } +} + +// Initialize theme on load +const savedTheme = localStorage.getItem('theme') || 'dark'; +setTheme(savedTheme); + +// Export functions for global use +window.showToast = showToast; +window.showModal = showModal; +window.closeModal = closeModal; +window.formatDuration = formatDuration; +window.setTheme = setTheme; \ No newline at end of file diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 00000000..07161cab --- /dev/null +++ b/templates/README.md @@ -0,0 +1,190 @@ +# HTMX Base Templates + +This directory contains the foundational HTMX templates and partials for the mt music player web interface, designed to match the MusicBee-inspired UI. + +## Structure + +``` +templates/ +├── base/ +│ └── base.html # Main application layout with three-panel design +├── components/ +│ ├── navbar.html # Top navigation bar with search and user menu +│ ├── sidebar.html # Left sidebar with library navigation and playlists +│ ├── player-controls.html # Bottom player controls with progress and volume +│ └── queue.html # Right panel queue management +├── pages/ +│ └── library.html # Library view page extending base template +└── partials/ + ├── modal.html # Reusable modal dialog template + ├── toast.html # Toast notification template + └── track-item.html # Individual track item component +``` + +## Features + +### Three-Panel Layout +- **Left Sidebar**: Library navigation, playlists, and library statistics +- **Center Panel**: Main content area with HTMX-powered dynamic loading +- **Right Panel**: Queue management with drag-and-drop reordering + +### UI Components +- **Navigation**: Responsive navbar with search, theme toggle, and user menu +- **Player Controls**: Full-featured audio controls with progress bar and volume +- **Queue Management**: Interactive queue with drag-and-drop, history, and suggestions +- **Modals**: Flexible modal system for dialogs and forms +- **Toast Notifications**: Non-intrusive notification system + +### Design System +- **Basecoat UI**: Custom design system with dark theme +- **Tailwind CSS**: Utility-first CSS framework +- **Inter Font**: Modern, readable typography +- **Bootstrap Icons**: Consistent iconography + +### HTMX Integration +- **Dynamic Content Loading**: Seamless page updates without full reloads +- **WebSocket Support**: Real-time updates via Server-Sent Events +- **Form Handling**: Progressive enhancement with HTMX +- **Loading States**: Built-in loading indicators and transitions + +### Alpine.js Enhancements +- **Reactive UI**: State management for interactive components +- **Transitions**: Smooth animations and micro-interactions +- **Keyboard Shortcuts**: Global keyboard navigation +- **Drag & Drop**: Native drag-and-drop for queue management + +## Dependencies + +### Frontend +- **HTMX 1.9.10**: For dynamic content and interactions +- **Alpine.js 3.13.5**: For reactive UI components +- **Tailwind CSS**: For styling and layout +- **Inter Font**: For typography +- **Bootstrap Icons**: For icons + +### Backend Requirements +The templates expect the following backend endpoints: + +#### Player API +- `POST /api/player/toggle` - Toggle play/pause +- `POST /api/player/next` - Next track +- `POST /api/player/previous` - Previous track +- `POST /api/player/seek` - Seek to position +- `POST /api/player/volume` - Set volume +- `POST /api/player/mute` - Toggle mute +- `POST /api/player/shuffle` - Toggle shuffle +- `POST /api/player/repeat` - Cycle repeat mode + +#### Queue API +- `GET /api/queue/tracks` - Get queue tracks +- `POST /api/queue/add/{track_id}` - Add track to queue +- `POST /api/queue/reorder` - Reorder queue items +- `POST /api/queue/clear` - Clear queue +- `POST /api/queue/shuffle` - Shuffle queue + +#### Library API +- `GET /api/library/tracks` - Get library tracks +- `GET /api/library/stats` - Get library statistics +- `POST /api/library/scan` - Scan for new music + +#### Search API +- `GET /api/search?q={query}` - Search tracks + +## Template Filters + +Custom Jinja2 filters are provided in `filters.py`: + +- `format_duration(seconds)` - Format seconds to MM:SS +- `format_file_size(bytes)` - Format bytes to human readable +- `pluralize(count, singular, plural)` - Pluralize words + +## Usage + +### Extending Base Template + +```html +{% extends "base/base.html" %} + +{% block title %}Page Title - mt music player{% endblock %} + +{% block content %} + +{% endblock %} +``` + +### Including Partials + +```html +{% include "partials/modal.html" %} +{% include "components/navbar.html" %} +``` + +### HTMX Patterns + +```html + +
+ Loading... +
+ + +
+ + +
+ + + +``` + +## Customization + +### Theme Variables +CSS custom properties are defined in `static/css/main.css`: + +```css +:root { + --bg-primary: #030712; + --bg-secondary: #111827; + --text-primary: #f3f4f6; + --accent-blue: #3b82f6; + /* ... */ +} +``` + +### Responsive Design +Templates include responsive breakpoints: +- Mobile: < 640px +- Tablet: 640px - 768px +- Desktop: > 768px + +### Accessibility +- Semantic HTML structure +- Keyboard navigation support +- Screen reader friendly +- Focus management in modals + +## Development + +### File Organization +- Keep templates modular and reusable +- Use consistent naming conventions +- Document template variables and blocks +- Include loading states for dynamic content + +### Performance +- Minimize CSS and JavaScript bundle size +- Use HTMX for efficient content updates +- Implement lazy loading for large lists +- Cache static assets appropriately + +## Browser Support + +- Chrome 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ + +Requires modern browser features like CSS Grid, Flexbox, and ES6 modules. \ No newline at end of file diff --git a/templates/base/base.html b/templates/base/base.html new file mode 100644 index 00000000..a017a1f7 --- /dev/null +++ b/templates/base/base.html @@ -0,0 +1,136 @@ + + + + + + + {% block title %}mt music player{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + {% block styles %}{% endblock %} + + + + + + + +
+ + +
+ {% include 'components/navbar.html' %} +
+ + +
+ + + + + +
+
+ {% block content %}{% endblock %} +
+
+ + + +
+ + + +
+ + +
+
+ + + + + + + {% block scripts %}{% endblock %} + + + + + \ No newline at end of file diff --git a/templates/components/navbar.html b/templates/components/navbar.html new file mode 100644 index 00000000..c18ce0a7 --- /dev/null +++ b/templates/components/navbar.html @@ -0,0 +1,120 @@ + \ No newline at end of file diff --git a/templates/components/player-controls.html b/templates/components/player-controls.html new file mode 100644 index 00000000..775af1b7 --- /dev/null +++ b/templates/components/player-controls.html @@ -0,0 +1,192 @@ +
+ + +
+
+ +
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ +
+ + + + + + + + + + + + + + +
+ + +
+ + + 0:00 + + + +
+ + +
+
+ + + + 0:00 + +
+
+ + +
+ + + + + + + +
+ + +
+ + +
+
+
+ + + +
+
\ No newline at end of file diff --git a/templates/components/queue.html b/templates/components/queue.html new file mode 100644 index 00000000..89f433e3 --- /dev/null +++ b/templates/components/queue.html @@ -0,0 +1,110 @@ +
+ +
+
+

Queue

+
+ + + + +
+
+ + +
+ + 0 tracks + + + 0:00 + +
+
+ + +
+ +
+
+
+
+
+
+
+
+ + +
+
+ +
+

Queue is empty

+

Add tracks to start playing

+
+
+
+ + +
+
+ + +
+
+
\ No newline at end of file diff --git a/templates/components/sidebar.html b/templates/components/sidebar.html new file mode 100644 index 00000000..36ffc622 --- /dev/null +++ b/templates/components/sidebar.html @@ -0,0 +1,104 @@ +
+ +
+

Library

+ +
+ + +
+
+

Playlists

+ +
+ + +
+ +
+
+

Loading playlists...

+
+
+
+ + +
+
+
+ Library Size + + -- + +
+
+ Total Tracks + + -- + +
+
+
+ + +
+ +
+
\ No newline at end of file diff --git a/templates/filters.py b/templates/filters.py new file mode 100644 index 00000000..67239bc5 --- /dev/null +++ b/templates/filters.py @@ -0,0 +1,58 @@ +""" +Template filters for HTMX base templates. +These would typically be registered with your template engine (Jinja2, etc.) +""" + + +def format_duration(seconds): + """ + Format duration in seconds to MM:SS format + """ + if not seconds or seconds == 0: + return "0:00" + + try: + seconds = int(seconds) + minutes = seconds // 60 + remaining_seconds = seconds % 60 + return f"{minutes}:{remaining_seconds:02d}" + except (ValueError, TypeError): + return "0:00" + + +def format_file_size(bytes_size): + """ + Format file size in bytes to human readable format + """ + if not bytes_size: + return "0 B" + + try: + bytes_size = int(bytes_size) + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes_size < 1024.0: + return ".1f" + bytes_size /= 1024.0 + return ".1f" + except (ValueError, TypeError): + return "0 B" + + +def pluralize(count, singular, plural=None): + """ + Return singular or plural form based on count + """ + if plural is None: + plural = singular + 's' + + try: + count = int(count) + return singular if count == 1 else plural + except (ValueError, TypeError): + return plural + + +# Example usage in templates: +# {{ track.duration|format_duration }} +# {{ file.size|format_file_size }} +# {{ count|pluralize('track', 'tracks') }} diff --git a/templates/pages/library.html b/templates/pages/library.html new file mode 100644 index 00000000..c1b3a328 --- /dev/null +++ b/templates/pages/library.html @@ -0,0 +1,145 @@ +{% extends "base/base.html" %} + +{% block title %}Library - mt music player{% endblock %} + +{% block content %} +
+ +
+
+

Music Library

+ + +
+
+ + +
+ + +
+ +
+ + + + + +
+
+ + + +
+
+ + +
+ {{ total_tracks }} tracks + {{ total_albums }} albums + {{ total_artists }} artists + {{ total_duration|format_duration }} total +
+
+ + +
+ + +
+
+
+

Loading library...

+
+
+
+ + +
+
+ Showing {{ start }}-{{ end }} of {{ total_tracks }} tracks +
+ +
+ + + +
+ {% for page_num in page_numbers %} + + {% endfor %} +
+ + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/partials/modal.html b/templates/partials/modal.html new file mode 100644 index 00000000..8377d66e --- /dev/null +++ b/templates/partials/modal.html @@ -0,0 +1,62 @@ + +
+ + +
+ + +
+
+ + +
+

{{ modal_title|default('Modal') }}

+ +
+ + +
+ {% block modal_content %} + + {% endblock %} +
+ + + {% block modal_footer %} + {% if show_footer|default(true) %} +
+ + {% block modal_actions %} + + {% endblock %} +
+ {% endif %} + {% endblock %} +
+
+
\ No newline at end of file diff --git a/templates/partials/toast.html b/templates/partials/toast.html new file mode 100644 index 00000000..cff6edff --- /dev/null +++ b/templates/partials/toast.html @@ -0,0 +1,50 @@ + +
+ +
+ +
+ {% if type == 'success' %} + + {% elif type == 'error' %} + + {% elif type == 'warning' %} + + {% else %} + + {% endif %} +
+ + +
+ {% if title %} +

{{ title }}

+ {% endif %} +

{{ message }}

+
+ + + +
+ + + {% if show_progress|default(false) %} +
+
+
+ {% endif %} +
\ No newline at end of file diff --git a/templates/partials/track-item.html b/templates/partials/track-item.html new file mode 100644 index 00000000..5c2e1a7a --- /dev/null +++ b/templates/partials/track-item.html @@ -0,0 +1,103 @@ + +
+ + +
+ {{ track.position|default(loop.index) }} +
+ + + +
+ {% if track.album_art %} + {{ track.album }} + {% else %} +
+ +
+ {% endif %} +
+ + +
+
+ {{ track.title }} + {% if track.is_playing %} +
+
+
+
+
+ {% endif %} +
+
+ {{ track.artist }} + {% if track.album %} + • {{ track.album }} + {% endif %} +
+
+ + +
+ + +
+ +
+ + + +
+ +
+
+
+ + +
+ {{ track.duration|format_duration }} +
+
\ No newline at end of file