| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Dungeon Master Soundscapes</title> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <script src="https://cdn.jsdelivr.net/npm/howler@2.2.3/dist/howler.min.js"></script> |
| | <script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script> |
| | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| | <link rel="preconnect" href="https://fonts.googleapis.com"> |
| | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| | <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=MedievalSharp&display=swap" rel="stylesheet"> |
| | |
| | <script> |
| | tailwind.config = { |
| | theme: { |
| | extend: { |
| | colors: { |
| | primary: '#D4AF37', |
| | secondary: '#5C2D91', |
| | dark: '#1A0B23', |
| | light: '#F2EBD3' |
| | }, |
| | fontFamily: { |
| | medieval: ['Cinzel', 'MedievalSharp', 'serif'], |
| | body: ['Roboto', 'sans-serif'] |
| | }, |
| | backgroundImage: { |
| | 'paper-texture': "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MDAiIGhlaWdodD0iNTAwIj4KICA8cmVjdCB3aWR0aD0iNTAwIiBoZWlnaHQ9IjUwMCIgZmlsbD0iI0YyRUJEMyIvPgogIDxwYXRoIGQ9Ik0gMCwwIEwgMCw1MDAgTCA1MDAsNTAwIEwgNTAwLDAgeiIgc3Ryb2tlPSIjRkZGIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiLz4KICA8cGF0aCBkPSJNIDI1LDI1IGMgMTAsMTAgMTUsMjAgMTUsMzAgIiBzdHJva2U9IiNGOEYyREIiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgb3BhY2l0eT0iMC4yIi8+Cjwvc3ZnPg==')" |
| | } |
| | } |
| | } |
| | } |
| | </script> |
| | |
| | <style type="text/css"> |
| | body { |
| | font-family: 'Roboto', sans-serif; |
| | background: #1A0B23; |
| | color: #F2EBD3; |
| | overflow-x: hidden; |
| | touch-action: manipulation; |
| | } |
| | |
| | .medieval-font { |
| | font-family: 'Cinzel', 'MedievalSharp', serif; |
| | letter-spacing: 0.5px; |
| | } |
| | |
| | .scrollbar-hide::-webkit-scrollbar { |
| | display: none; |
| | } |
| | |
| | .scrollbar-hide { |
| | -ms-overflow-style: none; |
| | scrollbar-width: none; |
| | } |
| | |
| | .gradient-border { |
| | border: 2px solid transparent; |
| | background-clip: padding-box; |
| | position: relative; |
| | background: #1A0B23; |
| | } |
| | |
| | .gradient-border::before { |
| | content: ''; |
| | position: absolute; |
| | top: -2px; |
| | left: -2px; |
| | right: -2px; |
| | bottom: -2px; |
| | background: linear-gradient(135deg, #D4AF37, #5C2D91); |
| | z-index: -1; |
| | border-radius: inherit; |
| | } |
| | |
| | .sound-item:hover, .pad:hover { |
| | transform: scale(1.02); |
| | box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3); |
| | transition: all 0.2s ease; |
| | } |
| | |
| | .playing { |
| | box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.6); |
| | animation: pulse 1.5s infinite; |
| | } |
| | |
| | @keyframes pulse { |
| | 0% { box-shadow: 0 0 0 0 rgba(212, 175, 55, 0.6); } |
| | 70% { box-shadow: 0 0 0 10px rgba(212, 175, 55, 0); } |
| | 100% { box-shadow: 0 0 0 0 rgba(212, 175, 55, 0); } |
| | } |
| | |
| | .volume-slider { |
| | -webkit-appearance: none; |
| | appearance: none; |
| | height: 6px; |
| | border-radius: 3px; |
| | background: #5C2D91; |
| | outline: none; |
| | } |
| | |
| | .volume-slider::-webkit-slider-thumb { |
| | -webkit-appearance: none; |
| | appearance: none; |
| | width: 18px; |
| | height: 18px; |
| | border-radius: 50%; |
| | background: #D4AF37; |
| | cursor: pointer; |
| | } |
| | |
| | .sound-wave { |
| | display: flex; |
| | align-items: flex-end; |
| | justify-content: space-between; |
| | height: 40px; |
| | width: 100%; |
| | } |
| | |
| | .sound-bar { |
| | width: 3px; |
| | background-color: #D4AF37; |
| | border-radius: 2px; |
| | margin: 0 1px; |
| | } |
| | |
| | .page-enter { |
| | animation: fadeIn 0.3s forwards; |
| | } |
| | |
| | @keyframes fadeIn { |
| | from { opacity: 0; transform: translateY(10px); } |
| | to { opacity: 1; transform: translateY(0); } |
| | } |
| | |
| | @media (max-width: 640px) { |
| | .bottom-nav { |
| | box-shadow: 0 -3px 10px rgba(0,0,0,0.2); |
| | } |
| | } |
| | </style> |
| | </head> |
| | <body class="min-h-screen bg-gradient-to-b from-dark to-secondary"> |
| | <div id="app" class="flex flex-col min-h-screen max-w-full overflow-hidden"> |
| | |
| | <header class="gradient-border px-4 py-3 flex items-center justify-between"> |
| | <button id="menuBtn" class="text-primary text-2xl"> |
| | <i class="fas fa-bars"></i> |
| | </button> |
| | <h1 class="medieval-font text-2xl text-primary text-center font-bold"> |
| | Dungeon Master Soundscapes |
| | </h1> |
| | <button class="text-primary text-2xl"> |
| | <i class="fas fa-user"></i> |
| | </button> |
| | </header> |
| | |
| | |
| | <main class="flex-grow overflow-hidden relative" style="background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cGF0aCBkPSJNMCAwTDEwMCAwTDEwMCAxMDBMMCAxMDBaTTAgMEw1MCA1MFo='); background-size: 20px 20px; background-color: #1A0B23; background-blend-mode: overlay; opacity: 0.15;"> |
| | <div id="page-content" class="absolute inset-0 overflow-auto scrollbar-hide p-4"> |
| | |
| | </div> |
| | </main> |
| | |
| | |
| | <nav id="bottomNav" class="bottom-nav bg-secondary flex justify-around py-2 border-t border-primary"> |
| | <button data-page="dashboard" class="nav-item flex flex-col items-center text-primary hover:text-white"> |
| | <i class="fas fa-home text-xl"></i> |
| | <span class="text-xs mt-1">Dashboard</span> |
| | </button> |
| | <button data-page="sounds" class="nav-item flex flex-col items-center text-primary hover:text-white"> |
| | <i class="fas fa-music text-xl"></i> |
| | <span class="text-xs mt-1">Sounds</span> |
| | </button> |
| | <button data-page="pad" class="nav-item flex flex-col items-center text-primary hover:text-white"> |
| | <i class="fas fa-th-large text-xl"></i> |
| | <span class="text-xs mt-1">Sound Pad</span> |
| | </button> |
| | <button data-page="playlists" class="nav-item flex flex-col items-center text-primary hover:text-white"> |
| | <i class="fas fa-list text-xl"></i> |
| | <span class="text-xs mt-1">Playlists</span> |
| | </button> |
| | <button data-page="projects" class="nav-item flex flex-col items-center text-primary hover:text-white"> |
| | <i class="fas fa-folder text-xl"></i> |
| | <span class="text-xs mt-1">Projects</span> |
| | </button> |
| | </nav> |
| | </div> |
| | |
| | <script> |
| | // Main app state |
| | const state = { |
| | currentPage: 'dashboard', |
| | sounds: [], |
| | playlists: [], |
| | projects: [], |
| | currentProject: null, |
| | playingSounds: {}, |
| | volume: 0.7 |
| | }; |
| | |
| | // DOM Elements |
| | const elements = { |
| | pageContent: document.getElementById('page-content'), |
| | bottomNav: document.getElementById('bottomNav'), |
| | menuBtn: document.getElementById('menuBtn') |
| | }; |
| | |
| | // Initialize the app |
| | function initApp() { |
| | // Load state from localStorage |
| | loadState(); |
| | |
| | // Setup navigation |
| | setupNavigation(); |
| | |
| | // Render initial page |
| | renderPage(state.currentPage); |
| | |
| | // Load sample data if empty |
| | setupSampleData(); |
| | } |
| | |
| | // Setup bottom navigation |
| | function setupNavigation() { |
| | // Handle bottom nav clicks |
| | elements.bottomNav.querySelectorAll('.nav-item').forEach(item => { |
| | item.addEventListener('click', () => { |
| | const page = item.dataset.page; |
| | navigateTo(page); |
| | }); |
| | }); |
| | |
| | // Handle menu button |
| | elements.menuBtn.addEventListener('click', showMainMenu); |
| | } |
| | |
| | // Navigate to a page |
| | function navigateTo(pageName) { |
| | state.currentPage = pageName; |
| | renderPage(pageName); |
| | |
| | // Update active nav item |
| | elements.bottomNav.querySelectorAll('.nav-item').forEach(item => { |
| | const isActive = item.dataset.page === pageName; |
| | item.classList.toggle('text-white', isActive); |
| | item.classList.toggle('text-primary', !isActive); |
| | }); |
| | } |
| | |
| | // Render the current page |
| | function renderPage(pageName) { |
| | elements.pageContent.innerHTML = ''; |
| | elements.pageContent.classList.add('page-enter'); |
| | |
| | switch(pageName) { |
| | case 'dashboard': |
| | renderDashboard(); |
| | break; |
| | case 'sounds': |
| | renderSoundLibrary(); |
| | break; |
| | case 'pad': |
| | renderSoundPad(); |
| | break; |
| | case 'playlists': |
| | renderPlaylists(); |
| | break; |
| | case 'projects': |
| | renderProjects(); |
| | break; |
| | default: |
| | renderDashboard(); |
| | } |
| | } |
| | |
| | // Sample Pages Rendering Functions |
| | |
| | function renderDashboard() { |
| | const content = ` |
| | <div class="p-4"> |
| | <h2 class="medieval-font text-xl text-primary mb-4 font-bold">Current Project</h2> |
| | ${renderProjectCard()} |
| | |
| | <div class="mt-8"> |
| | <h2 class="medieval-font text-xl text-primary mb-4 font-bold">Quick Actions</h2> |
| | <div class="grid grid-cols-2 gap-4"> |
| | <button class="bg-secondary py-4 rounded-lg flex flex-col items-center justify-center gradient-border"> |
| | <i class="fas fa-plus-circle text-primary text-3xl mb-2"></i> |
| | <span class="text-primary">New Sound</span> |
| | </button> |
| | <button class="bg-secondary py-4 rounded-lg flex flex-col items-center justify-center gradient-border"> |
| | <i class="fas fa-headphones text-primary text-3xl mb-2"></i> |
| | <span class="text-primary">Ambience</span> |
| | </button> |
| | <button class="bg-secondary py-4 rounded-lg flex flex-col items-center justify-center gradient-border"> |
| | <i class="fas fa-cloud-download-alt text-primary text-3xl mb-2"></i> |
| | <span class="text-primary">Import</span> |
| | </button> |
| | <button class="bg-secondary py-4 rounded-lg flex flex-col items-center justify-center gradient-border"> |
| | <i class="fas fa-dragon text-primary text-3xl mb-2"></i> |
| | <span class="text-primary">Monsters</span> |
| | </button> |
| | </div> |
| | </div> |
| | |
| | <div class="mt-8"> |
| | <h2 class="medieval-font text-xl text-primary mb-4 font-bold">Recent Sounds</h2> |
| | <div class="grid grid-cols-3 gap-3"> |
| | ${state.sounds.slice(0, 6).map(sound => ` |
| | <div class="sound-item bg-secondary rounded-lg p-3 cursor-pointer gradient-border"> |
| | <div class="text-primary text-lg mb-2"><i class="fas ${sound.icon}"></i></div> |
| | <p class="text-white truncate text-xs">${sound.name}</p> |
| | </div> |
| | `).join('')} |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | |
| | elements.pageContent.innerHTML = content; |
| | } |
| | |
| | function renderProjectCard() { |
| | if (!state.currentProject) { |
| | return ` |
| | <div class="bg-secondary rounded-lg p-4 mb-4 gradient-border"> |
| | <p class="text-light mb-4">No project currently selected</p> |
| | <button class="bg-primary hover:bg-opacity-90 text-dark py-2 px-4 rounded-md w-full medieval-font"> |
| | Create New Project |
| | </button> |
| | </div> |
| | `; |
| | } |
| | |
| | const project = state.currentProject; |
| | return ` |
| | <div class="bg-secondary rounded-lg p-4 mb-4 gradient-border"> |
| | <div class="flex justify-between items-center mb-3"> |
| | <h3 class="text-lg text-primary medieval-font">${project.name}</h3> |
| | <span class="text-xs text-light">${project.lastEdited}</span> |
| | </div> |
| | <p class="text-light text-sm mb-4">${project.description}</p> |
| | <div class="grid grid-cols-3 gap-2 text-center text-xs text-light"> |
| | <div class="bg-dark py-1 rounded"> |
| | <div class="text-primary font-bold">${project.playlists.length}</div> |
| | <div>Playlists</div> |
| | </div> |
| | <div class="bg-dark py-1 rounded"> |
| | <div class="text-primary font-bold">${countProjectSounds(project)}</div> |
| | <div>Sounds</div> |
| | </div> |
| | <div class="bg-dark py-1 rounded"> |
| | <div class="text-primary font-bold">5</div> |
| | <div>Pads</div> |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | } |
| | |
| | function renderSoundLibrary() { |
| | const content = ` |
| | <div class="p-4"> |
| | <div class="flex mb-4"> |
| | <input type="text" placeholder="Search sounds..." class="flex-grow bg-dark text-light p-2 rounded-l-lg border border-secondary focus:outline-none"> |
| | <button class="bg-primary text-dark p-2 rounded-r-lg"> |
| | <i class="fas fa-search"></i> |
| | </button> |
| | </div> |
| | |
| | <div class="mb-6"> |
| | <div class="flex justify-between items-center mb-3"> |
| | <h2 class="medieval-font text-lg text-primary font-bold">All Sounds</h2> |
| | <button class="text-primary text-sm"> |
| | <i class="fas fa-sort"></i> Sort |
| | </button> |
| | </div> |
| | |
| | <div class="grid grid-cols-2 gap-3"> |
| | ${state.sounds.slice(0, 12).map(sound => renderSoundItem(sound)).join('')} |
| | </div> |
| | </div> |
| | |
| | <div class="mt-6"> |
| | <div class="flex justify-between items-center mb-3"> |
| | <h2 class="medieval-font text-lg text-primary font-bold">Atmosphere</h2> |
| | </div> |
| | |
| | <div class="grid grid-cols-3 gap-3"> |
| | ${state.sounds.slice(12, 18).map(sound => renderSoundItem(sound)).join('')} |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | |
| | elements.pageContent.innerHTML = content; |
| | } |
| | |
| | function renderSoundItem(sound) { |
| | const isPlaying = state.playingSounds[sound.id]; |
| | const classes = isPlaying ? 'playing' : ''; |
| | return ` |
| | <div class="sound-item ${classes} bg-secondary rounded-lg p-3 cursor-pointer gradient-border" data-id="${sound.id}"> |
| | <div class="flex justify-between items-start"> |
| | <div class="text-primary text-lg"><i class="fas ${sound.icon}"></i></div> |
| | <button class="text-light text-xs"> |
| | <i class="fas ${isPlaying ? 'fa-stop' : 'fa-play'}"></i> |
| | </button> |
| | </div> |
| | <div class="mt-2"> |
| | <p class="text-white truncate text-sm">${sound.name}</p> |
| | <div class="text-light text-xs flex mt-1"> |
| | <span class="bg-dark px-1 rounded mr-1">${sound.category}</span> |
| | <span class="bg-dark px-1 rounded">${sound.duration}</span> |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | } |
| | |
| | function renderSoundPad() { |
| | const content = ` |
| | <div class="p-4"> |
| | <div class="bg-secondary rounded-lg p-3 mb-4 gradient-border"> |
| | <h2 class="medieval-font text-xl text-primary font-bold text-center">Sound Pad</h2> |
| | <p class="text-center text-light text-sm">Tap any pad to play sounds simultaneously</p> |
| | </div> |
| | |
| | <div class="grid grid-cols-4 gap-3"> |
| | ${Array.from({length: 16}, (_, i) => renderPad(i + 1)).join('')} |
| | </div> |
| | |
| | <div class="mt-8"> |
| | <div class="flex items-center"> |
| | <span class="text-light mr-2"><i class="fas fa-volume-up text-primary"></i></span> |
| | <input type="range" min="0" max="1" step="0.01" value="${state.volume}" class="volume-slider flex-grow"> |
| | <span class="text-light ml-2"><i class="fas fa-wave-square text-primary"></i></span> |
| | </div> |
| | </div> |
| | |
| | <div class="flex justify-around mt-8"> |
| | <button class="bg-primary hover:bg-opacity-90 text-dark py-2 px-6 rounded-lg medieval-font font-bold"> |
| | Stop All |
| | </button> |
| | <button class="bg-secondary border border-primary text-primary py-2 px-6 rounded-lg medieval-font font-bold"> |
| | Save |
| | </button> |
| | </div> |
| | </div> |
| | `; |
| | |
| | elements.pageContent.innerHTML = content; |
| | } |
| | |
| | function renderPad(index) { |
| | const sound = state.sounds.length > index ? state.sounds[index] : null; |
| | const padContent = sound ? ` |
| | <div class="text-center"> |
| | <div class="text-primary text-lg mb-1"><i class="fas ${sound.icon}"></i></div> |
| | <div class="text-white text-xs truncate">${sound.name}</div> |
| | </div> |
| | ` : ` |
| | <div class="text-center"> |
| | <div class="text-primary text-lg mb-1"><i class="fas fa-plus"></i></div> |
| | <div class="text-light text-xs">Add Sound</div> |
| | </div> |
| | `; |
| | |
| | return ` |
| | <div class="pad aspect-square bg-secondary rounded-lg flex items-center justify-center gradient-border cursor-pointer"> |
| | ${padContent} |
| | </div> |
| | `; |
| | } |
| | |
| | function renderPlaylists() { |
| | const content = ` |
| | <div class="p-4"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h2 class="medieval-font text-lg text-primary font-bold">Playlists</h2> |
| | <button class="text-primary"> |
| | <i class="fas fa-plus-circle"></i> New |
| | </button> |
| | </div> |
| | |
| | <div class="grid grid-cols-2 gap-4"> |
| | ${state.playlists.map(playlist => renderPlaylistCard(playlist)).join('')} |
| | </div> |
| | </div> |
| | `; |
| | |
| | elements.pageContent.innerHTML = content; |
| | } |
| | |
| | function renderPlaylistCard(playlist) { |
| | return ` |
| | <div class="bg-secondary rounded-lg p-3 gradient-border"> |
| | <div class="flex justify-between items-start mb-2"> |
| | <h3 class="text-primary font-bold truncate">${playlist.name}</h3> |
| | <button class="text-light text-xs"> |
| | <i class="fas fa-play"></i> |
| | </button> |
| | </div> |
| | <div class="text-light text-xs mb-3">${playlist.sounds.length} sounds</div> |
| | <div class="text-light text-xs"> |
| | <div class="flex overflow-hidden w-full h-6 items-center mb-1"> |
| | ${playlist.sounds.slice(0, 3).map(sound => ` |
| | <div class="rounded-full border border-primary h-4 w-4 flex items-center justify-center mr-1 flex-shrink-0"> |
| | <i class="fas ${sound.icon} text-xs text-primary"></i> |
| | </div> |
| | `).join('')} |
| | </div> |
| | <div class="text-xs text-light opacity-80">${playlist.tags.join(', ')}</div> |
| | </div> |
| | </div> |
| | `; |
| | } |
| | |
| | function renderProjects() { |
| | const content = ` |
| | <div class="p-4"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h2 class="medieval-font text-lg text-primary font-bold">Projects</h2> |
| | <button class="text-primary"> |
| | <i class="fas fa-plus-circle"></i> New |
| | </button> |
| | </div> |
| | |
| | ${state.projects.map(project => renderProjectItem(project)).join('')} |
| | </div> |
| | `; |
| | |
| | elements.pageContent.innerHTML = content; |
| | } |
| | |
| | function renderProjectItem(project) { |
| | const isActive = project.id === (state.currentProject?.id || null); |
| | const classes = isActive ? 'ring-2 ring-primary' : ''; |
| | return ` |
| | <div class="project-item bg-secondary rounded-lg p-3 mb-3 gradient-border ${classes}"> |
| | <div class="flex justify-between items-center mb-2"> |
| | <h3 class="text-primary font-bold">${project.name}</h3> |
| | <div> |
| | <button class="text-light p-1"> |
| | <i class="fas fa-pen"></i> |
| | </button> |
| | <button class="text-light p-1"> |
| | <i class="fas fa-ellipsis-v"></i> |
| | </button> |
| | </div> |
| | </div> |
| | <div class="text-light text-sm mb-3">${project.description}</div> |
| | <div class="flex justify-between text-xs text-light"> |
| | <div> |
| | <i class="fas fa-play-circle mr-1 text-primary"></i> |
| | ${project.playlists.length} playlists |
| | </div> |
| | <div>Last edited: ${project.lastEdited}</div> |
| | </div> |
| | </div> |
| | `; |
| | } |
| | |
| | function showMainMenu() { |
| | const menuContent = ` |
| | <div class="fixed inset-0 bg-black bg-opacity-70 flex z-50"> |
| | <div class="bg-secondary w-4/5 max-w-sm h-full p-4 overflow-auto gradient-border"> |
| | <div class="flex justify-between items-center mb-8"> |
| | <h2 class="medieval-font text-xl text-primary">Menu</h2> |
| | <button id="closeMenuBtn" class="text-primary text-2xl"> |
| | <i class="fas fa-times"></i> |
| | </button> |
| | </div> |
| | |
| | <div class="mb-6"> |
| | <h3 class="medieval-font text-primary text-lg mb-2 font-bold">App Settings</h3> |
| | <div class="pl-4 text-light"> |
| | <div class="flex items-center py-2"> |
| | <i class="fas fa-volume-up mr-3 text-primary"></i> |
| | <div class="flex-grow">Volume</div> |
| | <div class="w-1/3"> |
| | <input type="range" min="0" max="1" step="0.01" value="0.7" class="volume-slider w-full"> |
| | </div> |
| | </div> |
| | <div class="flex items-center py-2"> |
| | <i class="fas fa-music mr-3 text-primary"></i> |
| | <div class="flex-grow">Auto-play</div> |
| | <label class="relative inline-block w-10 h-6"> |
| | <input type="checkbox" class="opacity-0 w-0 h-0 peer"> |
| | <span class="absolute cursor-pointer top-0 left-0 right-0 bottom-0 bg-dark rounded-full transition peer-checked:bg-primary"></span> |
| | <span class="absolute h-4 w-4 bg-white rounded-full left-1 top-1 transition peer-checked:translate-x-4"></span> |
| | </label> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="mb-6"> |
| | <h3 class="medieval-font text-primary text-lg mb-2 font-bold">Import/Export</h3> |
| | <div class="grid grid-cols-2 gap-2"> |
| | <button class="py-2 text-primary border border-primary rounded-md text-center"> |
| | <i class="fas fa-download mr-1"></i> Import |
| | </button> |
| | <button class="py-2 bg-primary text-dark rounded-md text-center"> |
| | <i class="fas fa-upload mr-1"></i> Export |
| | </button> |
| | </div> |
| | </div> |
| | |
| | <div> |
| | <h3 class="medieval-font text-primary text-lg mb-2 font-bold">Pre-made Packs</h3> |
| | <div class="flex overflow-x-auto pb-2 -mx-2 px-2"> |
| | <div class="flex-shrink-0 w-32 mr-3"> |
| | <div class="bg-dark aspect-video rounded flex items-center justify-center"> |
| | <i class="fas fa-dragon text-2xl text-primary"></i> |
| | </div> |
| | <div class="text-light text-xs text-center mt-1">Monsters</div> |
| | </div> |
| | <div class="flex-shrink-0 w-32 mr-3"> |
| | <div class="bg-dark aspect-video rounded flex items-center justify-center"> |
| | <i class="fas fa-city text-2xl text-primary"></i> |
| | </div> |
| | <div class="text-light text-xs text-center mt-1">Cities</div> |
| | </div> |
| | <div class="flex-shrink-0 w-32 mr-3"> |
| | <div class="bg-dark aspect-video rounded flex items-center justify-center"> |
| | <i class="fas fa-water text-2xl text-primary"></i> |
| | </div> |
| | <div class="text-light text-xs text-center mt-1">Nature</div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | |
| | const menuContainer = document.createElement('div'); |
| | menuContainer.innerHTML = menuContent; |
| | document.body.appendChild(menuContainer); |
| | |
| | menuContainer.querySelector('#closeMenuBtn').addEventListener('click', () => { |
| | menuContainer.remove(); |
| | }); |
| | } |
| | |
| | // Helper Functions |
| | |
| | function loadState() { |
| | // Load from localStorage or localForage |
| | const savedState = localStorage.getItem('dmsoundscapes-state'); |
| | if (savedState) { |
| | Object.assign(state, JSON.parse(savedState)); |
| | } |
| | } |
| | |
| | function saveState() { |
| | localStorage.setItem('dmsoundscapes-state', JSON.stringify(state)); |
| | } |
| | |
| | function setupSampleData() { |
| | if (state.sounds.length === 0) { |
| | state.sounds = [ |
| | { id: 1, name: "Medieval Tavern", icon: "fa-beer", category: "Ambience", duration: "4:20" }, |
| | { id: 2, name: "Castle Hall", icon: "fa-landmark", category: "Ambience", duration: "3:45" }, |
| | { id: 3, name: "Forest Night", icon: "fa-tree", category: "Ambience", duration: "5:15" }, |
| | { id: 4, name: "Sword Clash", icon: "fa-swords", category: "Combat", duration: "0:07" }, |
| | { id: 5, name: "Fireball", icon: "fa-fire", category: "Spells", duration: "0:12" }, |
| | { id: 6, name: "Orc Roar", icon: "fa-dragon", category: "Monsters", duration: "0:08" }, |
| | { id: 7, name: "Rainstorm", icon: "fa-cloud-rain", category: "Weather", duration: "6:00" }, |
| | { id: 8, name: "Horse Galloping", icon: "fa-horse", category: "Travel", duration: "0:45" }, |
| | { id: 9, name: "Crowd Cheer", icon: "fa-users", category: "Ambience", duration: "0:15" }, |
| | { id: 10, name: "Creaky Door", icon: "fa-door-open", category: "Effects", duration: "0:05" }, |
| | { id: 11, name: "Dragon Roar", icon: "fa-dragon", category: "Monsters", duration: "0:18" }, |
| | { id: 12, name: "Magic Portal", icon: "fa-portal-enter", category: "Spells", duration: "0:20" } |
| | ]; |
| | } |
| | |
| | if (state.playlists.length === 0) { |
| | state.playlists = [ |
| | { |
| | id: 1, |
| | name: "Castle Siege", |
| | sounds: [state.sounds[2], state.sounds[3], state.sounds[5], state.sounds[10]], |
| | tags: ["combat", "dramatic"], |
| | lastEdited: "Yesterday" |
| | }, |
| | { |
| | id: 2, |
| | name: "Peaceful Village", |
| | sounds: [state.sounds[0], state.sounds[1], state.sounds[8], state.sounds[11]], |
| | tags: ["ambience", "calm"], |
| | lastEdited: "2 days ago" |
| | } |
| | ]; |
| | } |
| | |
| | if (state.projects.length === 0) { |
| | state.projects = [ |
| | { |
| | id: 1, |
| | name: "Lost Mines Campaign", |
| | description: "Main campaign sound setup", |
| | playlists: [state.playlists[0], state.playlists[1]], |
| | lastEdited: "Today" |
| | }, |
| | { |
| | id: 2, |
| | name: "Icewind Dale", |
| | description: "Winter campaign sounds", |
| | playlists: [state.playlists[1]], |
| | lastEdited: "1 week ago" |
| | } |
| | ]; |
| | state.currentProject = state.projects[0]; |
| | } |
| | |
| | saveState(); |
| | } |
| | |
| | function countProjectSounds(project) { |
| | return project.playlists.reduce((acc, playlist) => acc + playlist.sounds.length, 0); |
| | } |
| | |
| | // Initialize app when DOM is loaded |
| | document.addEventListener('DOMContentLoaded', initApp); |
| | </script> |
| | <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=ZoroaStrella/rawdio" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| | </html> |