rawdio / index.html
ZoroaStrella's picture
Create a mobile-ready, responsive app designed primarily for tablets and phones, with a UI to help dungeon masters select and play sounds together to create immersive atmospheres during gameplay. Use local forage, consider that we will run this in webview or progressive webapp, consider options for those persistence Users can: - Create projects and playlists, pads - a project may have many playlist, a pad is a collection of songs and sounds, noises that can play together, simultaneously or played as clicked with a UI - Search for projects, songs, videos, sounds, or playlists. - Add, delete, and organize sounds, videos, or songs in projects and playlists. - Handle YouTube, SoundCloud, Vimeo, and Dailymotion URLs for external audio and video sources. - Support multiple sound formats like m4a, wav, mp3, mp4, and playlists. - Arrange sounds and scroll through project or playlist items. - Play multiple sounds, songs, or videos simultaneously. - Use a pad-like interface to load sounds, videos, or songs and edit settings, such as selecting sound segments like a live sampler like a daw workstation such as ableton wit hminimalism and ux in mind. - Create sections within playlists for easy navigation. - Include basic atmospheric sounds and noise generators. - Automatically or manually tag and categorize sounds, videos, and songs for easy searching. - Handle transition properties in advance or live mode. This app enables dungeon masters to craft sonic soundscapes, access fast sounds, or use pre-prepared playlists while optimizing background processes for smooth performance on phones. It allows simultaneous playback and searching of playlists and sounds without restrictions. Optimize data loading and buffering, and provide basic sound editing with signal processing features. Use the best available TypeScript audio library and React for development. The design should feature gold and purple colors with medieval fonts. Include specific tags for various game sounds and background music, such as OSTs and original soundtracks for films and games. Add pre-built TTRPG, D&D playlists, draw inspiration from existing products with similar features, and prioritize user experience with bottom navigation and action buttons. - Initial Deployment
c69d8f2 verified
<!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/[email protected]/dist/howler.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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', // Gold
secondary: '#5C2D91', // Purple
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">
<!-- Top Navigation Bar -->
<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 Content Area -->
<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">
<!-- Dashboard Page will be inserted here -->
</div>
</main>
<!-- Bottom Navigation -->
<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>