| <!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> |