Interaktiver HTML/CSS/JS Prototyp im Discord-Stil mit: - Sound-Button Grid mit 15 Beispiel-Sounds - Kategorie-Sidebar (Memes, Musik, Effekte, Eigene, Favoriten) - Suchfunktion, Favoriten-Toggle, Rechtsklick-Kontextmenü - Lautstärkeregler, Playing-Animation, "Alle stoppen" - Responsive Layout, Discord Dark Theme Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1077 lines
32 KiB
HTML
1077 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Soundboard — Discord</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap');
|
|
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
|
|
:root {
|
|
--bg-deep: #1a1b1e;
|
|
--bg-primary: #1e1f22;
|
|
--bg-secondary: #2b2d31;
|
|
--bg-tertiary: #313338;
|
|
--bg-modifier-hover: rgba(79,84,92,.16);
|
|
--bg-modifier-active: rgba(79,84,92,.24);
|
|
--bg-modifier-selected: rgba(79,84,92,.32);
|
|
--text-normal: #dbdee1;
|
|
--text-muted: #949ba4;
|
|
--text-faint: #6d6f78;
|
|
--blurple: #5865f2;
|
|
--blurple-hover: #4752c4;
|
|
--blurple-glow: rgba(88,101,242,.45);
|
|
--green: #23a55a;
|
|
--red: #f23f42;
|
|
--yellow: #f0b232;
|
|
--white: #ffffff;
|
|
--font: 'DM Sans', 'gg sans', 'Noto Sans', Whitney, 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
--radius: 8px;
|
|
--radius-lg: 12px;
|
|
--shadow-low: 0 1px 3px rgba(0,0,0,.24);
|
|
--shadow-med: 0 4px 12px rgba(0,0,0,.32);
|
|
--shadow-high: 0 8px 24px rgba(0,0,0,.4);
|
|
--transition: 150ms cubic-bezier(.4,0,.2,1);
|
|
}
|
|
|
|
html, body {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: var(--bg-deep);
|
|
color: var(--text-normal);
|
|
font-family: var(--font);
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* Scrollbar */
|
|
::-webkit-scrollbar { width: 8px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--bg-tertiary);
|
|
border-radius: 4px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover { background: #3f4147; }
|
|
|
|
/* ── LAYOUT ── */
|
|
.app {
|
|
display: grid;
|
|
grid-template-rows: 48px 1fr 52px;
|
|
grid-template-columns: 220px 1fr;
|
|
grid-template-areas:
|
|
"topbar topbar"
|
|
"sidebar main"
|
|
"bottombar bottombar";
|
|
height: 100vh;
|
|
position: relative;
|
|
}
|
|
|
|
/* Subtle noise overlay */
|
|
.app::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* ── TOP BAR ── */
|
|
.topbar {
|
|
grid-area: topbar;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 16px;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid rgba(0,0,0,.24);
|
|
z-index: 10;
|
|
gap: 12px;
|
|
}
|
|
|
|
.topbar-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.topbar-icon {
|
|
width: 24px; height: 24px;
|
|
color: var(--text-muted);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.topbar-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
letter-spacing: -.01em;
|
|
white-space: nowrap;
|
|
color: var(--white);
|
|
}
|
|
|
|
.topbar-divider {
|
|
width: 1px;
|
|
height: 24px;
|
|
background: rgba(255,255,255,.08);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.connection-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 8px; height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--green);
|
|
box-shadow: 0 0 6px rgba(35,165,90,.5);
|
|
animation: pulse-dot 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse-dot {
|
|
0%,100% { box-shadow: 0 0 6px rgba(35,165,90,.5); }
|
|
50% { box-shadow: 0 0 12px rgba(35,165,90,.8); }
|
|
}
|
|
|
|
.topbar-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.topbar-btn {
|
|
width: 32px; height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
}
|
|
.topbar-btn:hover {
|
|
background: var(--bg-modifier-hover);
|
|
color: var(--text-normal);
|
|
}
|
|
.topbar-btn svg { width: 20px; height: 20px; }
|
|
|
|
/* ── SEARCH (in topbar) ── */
|
|
.search-wrap {
|
|
position: relative;
|
|
width: 200px;
|
|
flex-shrink: 0;
|
|
}
|
|
.search-wrap svg {
|
|
position: absolute;
|
|
left: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 16px; height: 16px;
|
|
color: var(--text-faint);
|
|
pointer-events: none;
|
|
transition: color var(--transition);
|
|
}
|
|
.search-input {
|
|
width: 100%;
|
|
height: 28px;
|
|
padding: 0 8px 0 30px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: var(--bg-deep);
|
|
color: var(--text-normal);
|
|
font-family: var(--font);
|
|
font-size: 13px;
|
|
outline: none;
|
|
transition: all var(--transition);
|
|
}
|
|
.search-input::placeholder { color: var(--text-faint); }
|
|
.search-input:focus {
|
|
background: var(--bg-primary);
|
|
box-shadow: 0 0 0 2px var(--blurple);
|
|
}
|
|
.search-input:focus + svg { color: var(--blurple); }
|
|
|
|
/* ── SIDEBAR ── */
|
|
.sidebar {
|
|
grid-area: sidebar;
|
|
background: var(--bg-secondary);
|
|
border-right: 1px solid rgba(0,0,0,.16);
|
|
padding: 12px 8px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 6px 8px 10px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
color: var(--text-faint);
|
|
}
|
|
|
|
.sidebar-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 10px;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
position: relative;
|
|
user-select: none;
|
|
}
|
|
|
|
.sidebar-item:hover {
|
|
background: var(--bg-modifier-hover);
|
|
color: var(--text-normal);
|
|
}
|
|
|
|
.sidebar-item.active {
|
|
background: var(--bg-modifier-selected);
|
|
color: var(--white);
|
|
}
|
|
|
|
.sidebar-item.active::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 3px;
|
|
height: 60%;
|
|
border-radius: 0 3px 3px 0;
|
|
background: var(--blurple);
|
|
}
|
|
|
|
.sidebar-item .emoji {
|
|
font-size: 18px;
|
|
width: 24px;
|
|
text-align: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-item .count {
|
|
margin-left: auto;
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
font-weight: 400;
|
|
background: var(--bg-primary);
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
min-width: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── MAIN CONTENT ── */
|
|
.main {
|
|
grid-area: main;
|
|
background: var(--bg-primary);
|
|
padding: 20px 24px;
|
|
overflow-y: auto;
|
|
position: relative;
|
|
}
|
|
|
|
.main-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.main-title {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
color: var(--text-faint);
|
|
}
|
|
|
|
.sort-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 8px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
font-family: var(--font);
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
}
|
|
.sort-btn:hover { background: var(--bg-modifier-hover); color: var(--text-normal); }
|
|
.sort-btn svg { width: 14px; height: 14px; }
|
|
|
|
/* ── SOUND GRID ── */
|
|
.sound-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(105px, 1fr));
|
|
max-width: 100%;
|
|
gap: 10px;
|
|
}
|
|
|
|
.sound-card {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
padding: 14px 8px 10px;
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius-lg);
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
border: 2px solid transparent;
|
|
user-select: none;
|
|
overflow: hidden;
|
|
min-height: 100px;
|
|
}
|
|
|
|
.sound-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: inherit;
|
|
opacity: 0;
|
|
transition: opacity var(--transition);
|
|
background: radial-gradient(ellipse at center, var(--blurple-glow) 0%, transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.sound-card:hover {
|
|
background: var(--bg-tertiary);
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-med), 0 0 20px var(--blurple-glow);
|
|
border-color: rgba(88,101,242,.25);
|
|
}
|
|
.sound-card:hover::before { opacity: 1; }
|
|
|
|
.sound-card:active {
|
|
transform: translateY(0);
|
|
transition-duration: 50ms;
|
|
}
|
|
|
|
.sound-card.playing {
|
|
border-color: var(--blurple);
|
|
animation: playing-pulse 800ms ease-in-out;
|
|
}
|
|
|
|
@keyframes playing-pulse {
|
|
0% { border-color: var(--blurple); box-shadow: 0 0 0 0 var(--blurple-glow); }
|
|
50% { box-shadow: 0 0 16px 4px var(--blurple-glow); }
|
|
100% { border-color: transparent; box-shadow: 0 0 0 0 transparent; }
|
|
}
|
|
|
|
/* Ripple */
|
|
.ripple {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
background: rgba(88,101,242,.35);
|
|
transform: scale(0);
|
|
animation: ripple-expand 500ms ease-out forwards;
|
|
pointer-events: none;
|
|
}
|
|
@keyframes ripple-expand {
|
|
to { transform: scale(3); opacity: 0; }
|
|
}
|
|
|
|
.sound-emoji {
|
|
font-size: 28px;
|
|
line-height: 1;
|
|
z-index: 1;
|
|
transition: transform var(--transition);
|
|
}
|
|
.sound-card:hover .sound-emoji { transform: scale(1.15); }
|
|
.sound-card.playing .sound-emoji { animation: emoji-bounce 400ms ease; }
|
|
|
|
@keyframes emoji-bounce {
|
|
0%,100% { transform: scale(1); }
|
|
40% { transform: scale(1.3); }
|
|
70% { transform: scale(0.95); }
|
|
}
|
|
|
|
.sound-name {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
color: var(--text-normal);
|
|
z-index: 1;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sound-duration {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
z-index: 1;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Favorite star */
|
|
.fav-star {
|
|
position: absolute;
|
|
top: 6px;
|
|
right: 6px;
|
|
width: 18px;
|
|
height: 18px;
|
|
opacity: 0;
|
|
transition: all var(--transition);
|
|
cursor: pointer;
|
|
z-index: 2;
|
|
color: var(--text-faint);
|
|
filter: drop-shadow(0 1px 2px rgba(0,0,0,.3));
|
|
}
|
|
.sound-card:hover .fav-star { opacity: .7; }
|
|
.fav-star:hover { opacity: 1 !important; color: var(--yellow); transform: scale(1.2); }
|
|
.fav-star.active { opacity: 1 !important; color: var(--yellow); }
|
|
|
|
/* Playing wave indicator */
|
|
.playing-indicator {
|
|
position: absolute;
|
|
bottom: 4px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: none;
|
|
gap: 2px;
|
|
align-items: flex-end;
|
|
height: 12px;
|
|
}
|
|
.sound-card.playing .playing-indicator { display: flex; }
|
|
.wave-bar {
|
|
width: 2px;
|
|
background: var(--blurple);
|
|
border-radius: 1px;
|
|
animation: wave 600ms ease-in-out infinite alternate;
|
|
}
|
|
.wave-bar:nth-child(1) { height: 4px; animation-delay: 0ms; }
|
|
.wave-bar:nth-child(2) { height: 8px; animation-delay: 150ms; }
|
|
.wave-bar:nth-child(3) { height: 6px; animation-delay: 300ms; }
|
|
.wave-bar:nth-child(4) { height: 10px; animation-delay: 100ms; }
|
|
.wave-bar:nth-child(5) { height: 5px; animation-delay: 250ms; }
|
|
|
|
@keyframes wave {
|
|
from { height: 3px; }
|
|
to { height: 12px; }
|
|
}
|
|
|
|
/* ── ADD SOUND CARD ── */
|
|
.sound-card.add-card {
|
|
border: 2px dashed rgba(255,255,255,.1);
|
|
background: transparent;
|
|
color: var(--text-faint);
|
|
gap: 6px;
|
|
}
|
|
.sound-card.add-card:hover {
|
|
border-color: var(--blurple);
|
|
color: var(--blurple);
|
|
background: rgba(88,101,242,.06);
|
|
box-shadow: none;
|
|
transform: translateY(-1px);
|
|
}
|
|
.sound-card.add-card::before { display: none; }
|
|
.add-icon {
|
|
font-size: 24px;
|
|
font-weight: 300;
|
|
line-height: 1;
|
|
}
|
|
.add-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── EMPTY STATE ── */
|
|
.empty-state {
|
|
display: none;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
padding: 60px 20px;
|
|
text-align: center;
|
|
}
|
|
.empty-state.visible { display: flex; }
|
|
.empty-emoji { font-size: 48px; }
|
|
.empty-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: var(--text-normal);
|
|
}
|
|
.empty-desc {
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
max-width: 280px;
|
|
}
|
|
|
|
/* ── BOTTOM BAR ── */
|
|
.bottombar {
|
|
grid-area: bottombar;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 0 20px;
|
|
background: var(--bg-secondary);
|
|
border-top: 1px solid rgba(0,0,0,.24);
|
|
z-index: 10;
|
|
}
|
|
|
|
.now-playing {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.now-playing .np-label { color: var(--text-faint); font-size: 12px; white-space: nowrap; }
|
|
.now-playing .np-name {
|
|
color: var(--blurple);
|
|
font-weight: 600;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.np-waves {
|
|
display: none;
|
|
gap: 1.5px;
|
|
align-items: flex-end;
|
|
height: 14px;
|
|
}
|
|
.np-waves.active { display: flex; }
|
|
.np-wave-bar {
|
|
width: 2.5px;
|
|
background: var(--blurple);
|
|
border-radius: 1px;
|
|
animation: wave 500ms ease-in-out infinite alternate;
|
|
}
|
|
.np-wave-bar:nth-child(1) { height: 5px; animation-delay: 0ms; }
|
|
.np-wave-bar:nth-child(2) { height: 10px; animation-delay: 120ms; }
|
|
.np-wave-bar:nth-child(3) { height: 7px; animation-delay: 240ms; }
|
|
.np-wave-bar:nth-child(4) { height: 12px; animation-delay: 80ms; }
|
|
|
|
.volume-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-left: auto;
|
|
}
|
|
.volume-icon {
|
|
width: 20px; height: 20px;
|
|
color: var(--text-muted);
|
|
flex-shrink: 0;
|
|
cursor: pointer;
|
|
transition: color var(--transition);
|
|
}
|
|
.volume-icon:hover { color: var(--text-normal); }
|
|
|
|
.volume-slider {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 100px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: linear-gradient(to right, var(--blurple) 0%, var(--blurple) var(--vol, 80%), var(--bg-tertiary) var(--vol, 80%));
|
|
outline: none;
|
|
cursor: pointer;
|
|
transition: height var(--transition);
|
|
}
|
|
.volume-slider:hover { height: 6px; }
|
|
.volume-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 14px; height: 14px;
|
|
border-radius: 50%;
|
|
background: var(--white);
|
|
box-shadow: 0 1px 4px rgba(0,0,0,.3);
|
|
cursor: pointer;
|
|
transition: transform var(--transition);
|
|
}
|
|
.volume-slider::-webkit-slider-thumb:hover { transform: scale(1.2); }
|
|
|
|
.volume-pct {
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
min-width: 32px;
|
|
text-align: right;
|
|
font-weight: 500;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.stop-all-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 14px;
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
background: var(--red);
|
|
color: var(--white);
|
|
font-family: var(--font);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
white-space: nowrap;
|
|
opacity: .85;
|
|
}
|
|
.stop-all-btn:hover { opacity: 1; transform: scale(1.03); box-shadow: 0 0 12px rgba(242,63,66,.3); }
|
|
.stop-all-btn svg { width: 14px; height: 14px; }
|
|
|
|
/* ── CONTEXT MENU ── */
|
|
.ctx-menu {
|
|
position: fixed;
|
|
min-width: 170px;
|
|
background: var(--bg-deep);
|
|
border: 1px solid rgba(255,255,255,.06);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-high);
|
|
padding: 4px;
|
|
z-index: 1000;
|
|
display: none;
|
|
animation: ctx-in 120ms ease-out;
|
|
}
|
|
.ctx-menu.visible { display: block; }
|
|
|
|
@keyframes ctx-in {
|
|
from { opacity: 0; transform: scale(.96) translateY(-4px); }
|
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
}
|
|
|
|
.ctx-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 7px 10px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
color: var(--text-normal);
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
}
|
|
.ctx-item:hover { background: var(--blurple); color: var(--white); }
|
|
.ctx-item.danger { color: var(--red); }
|
|
.ctx-item.danger:hover { background: var(--red); color: var(--white); }
|
|
.ctx-sep { height: 1px; background: rgba(255,255,255,.06); margin: 4px 8px; }
|
|
.ctx-item svg { width: 16px; height: 16px; flex-shrink: 0; }
|
|
|
|
/* ── TOOLTIP ── */
|
|
.tooltip {
|
|
position: fixed;
|
|
padding: 6px 10px;
|
|
background: var(--bg-deep);
|
|
color: var(--text-normal);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
border-radius: 6px;
|
|
box-shadow: var(--shadow-high);
|
|
pointer-events: none;
|
|
z-index: 999;
|
|
display: none;
|
|
white-space: nowrap;
|
|
animation: tt-in 100ms ease-out;
|
|
}
|
|
.tooltip.visible { display: block; }
|
|
@keyframes tt-in { from { opacity: 0; transform: translateY(4px); } }
|
|
|
|
/* ── RESPONSIVE ── */
|
|
@media (max-width: 768px) {
|
|
.app {
|
|
grid-template-columns: 1fr;
|
|
grid-template-areas:
|
|
"topbar"
|
|
"main"
|
|
"bottombar";
|
|
}
|
|
.sidebar { display: none; }
|
|
.search-wrap { width: 140px; }
|
|
}
|
|
|
|
/* ── ENTRANCE ANIMATION ── */
|
|
.sound-card {
|
|
opacity: 0;
|
|
animation: card-enter 400ms ease-out forwards;
|
|
}
|
|
@keyframes card-enter {
|
|
from { opacity: 0; transform: translateY(12px) scale(.96); }
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app">
|
|
|
|
<!-- TOP BAR -->
|
|
<header class="topbar">
|
|
<div class="topbar-left">
|
|
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
|
<span class="topbar-title">Soundboard</span>
|
|
<div class="topbar-divider"></div>
|
|
<div class="connection-status">
|
|
<span class="status-dot"></span>
|
|
<span>Verbunden mit Sprachkanal</span>
|
|
</div>
|
|
</div>
|
|
<div class="topbar-right">
|
|
<div class="search-wrap">
|
|
<input class="search-input" type="text" placeholder="Sounds durchsuchen…" id="searchInput">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
</div>
|
|
<button class="topbar-btn" title="Einstellungen">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.32 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- SIDEBAR -->
|
|
<nav class="sidebar">
|
|
<div class="sidebar-header">Kategorien</div>
|
|
<div class="sidebar-item active" data-cat="all">
|
|
<span class="emoji">🎵</span>
|
|
<span>Alle Sounds</span>
|
|
<span class="count" id="countAll">15</span>
|
|
</div>
|
|
<div class="sidebar-item" data-cat="favorites">
|
|
<span class="emoji">⭐</span>
|
|
<span>Favoriten</span>
|
|
<span class="count" id="countFav">0</span>
|
|
</div>
|
|
<div class="sidebar-item" data-cat="memes">
|
|
<span class="emoji">🐸</span>
|
|
<span>Memes</span>
|
|
<span class="count">5</span>
|
|
</div>
|
|
<div class="sidebar-item" data-cat="music">
|
|
<span class="emoji">🎸</span>
|
|
<span>Musik</span>
|
|
<span class="count">3</span>
|
|
</div>
|
|
<div class="sidebar-item" data-cat="effects">
|
|
<span class="emoji">💥</span>
|
|
<span>Effekte</span>
|
|
<span class="count">5</span>
|
|
</div>
|
|
<div class="sidebar-item" data-cat="custom">
|
|
<span class="emoji">🎨</span>
|
|
<span>Eigene</span>
|
|
<span class="count">2</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- MAIN -->
|
|
<main class="main" id="mainArea">
|
|
<div class="main-header">
|
|
<span class="main-title" id="mainTitle">Alle Sounds — 15</span>
|
|
<button class="sort-btn">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="m21 8-4-4-4 4"/><path d="M17 4v16"/></svg>
|
|
Sortieren
|
|
</button>
|
|
</div>
|
|
|
|
<div class="sound-grid" id="soundGrid"></div>
|
|
|
|
<div class="empty-state" id="emptyState">
|
|
<div class="empty-emoji">🔇</div>
|
|
<div class="empty-title">Keine Sounds gefunden</div>
|
|
<div class="empty-desc">In dieser Kategorie sind noch keine Sounds vorhanden. Füge welche hinzu!</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- BOTTOM BAR -->
|
|
<div class="bottombar">
|
|
<div class="now-playing">
|
|
<div class="np-waves" id="npWaves">
|
|
<div class="np-wave-bar"></div>
|
|
<div class="np-wave-bar"></div>
|
|
<div class="np-wave-bar"></div>
|
|
<div class="np-wave-bar"></div>
|
|
</div>
|
|
<span class="np-label">Spielt:</span>
|
|
<span class="np-name" id="npName">—</span>
|
|
</div>
|
|
<div class="volume-section">
|
|
<svg class="volume-icon" id="volIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
|
|
<input type="range" class="volume-slider" id="volSlider" min="0" max="100" value="80">
|
|
<span class="volume-pct" id="volPct">80%</span>
|
|
</div>
|
|
<button class="stop-all-btn" id="stopAllBtn">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
|
Alle stoppen
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Context Menu -->
|
|
<div class="ctx-menu" id="ctxMenu">
|
|
<div class="ctx-item" data-action="play">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
Abspielen
|
|
</div>
|
|
<div class="ctx-item" data-action="fav">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
|
Favorit umschalten
|
|
</div>
|
|
<div class="ctx-sep"></div>
|
|
<div class="ctx-item" data-action="edit">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
Bearbeiten
|
|
</div>
|
|
<div class="ctx-item danger" data-action="delete">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
Löschen
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tooltip -->
|
|
<div class="tooltip" id="tooltip"></div>
|
|
|
|
<script>
|
|
const SOUNDS = [
|
|
{ id: 1, name: 'Airhorn', emoji: '📯', duration: '0:02', cat: 'memes', fav: false },
|
|
{ id: 2, name: 'Bruh', emoji: '😐', duration: '0:01', cat: 'memes', fav: false },
|
|
{ id: 3, name: 'Sad Trombone', emoji: '🎺', duration: '0:03', cat: 'memes', fav: false },
|
|
{ id: 4, name: 'Applause', emoji: '👏', duration: '0:04', cat: 'effects', fav: false },
|
|
{ id: 5, name: 'Drum Roll', emoji: '🥁', duration: '0:05', cat: 'effects', fav: false },
|
|
{ id: 6, name: 'Vine Boom', emoji: '💥', duration: '0:01', cat: 'memes', fav: false },
|
|
{ id: 7, name: 'Oof', emoji: '😵', duration: '0:01', cat: 'memes', fav: false },
|
|
{ id: 8, name: 'Rickroll', emoji: '🕺', duration: '0:08', cat: 'music', fav: false },
|
|
{ id: 9, name: 'Suspense', emoji: '😰', duration: '0:06', cat: 'effects', fav: false },
|
|
{ id: 10, name: 'Victory Fanfare',emoji: '🏆', duration: '0:05', cat: 'music', fav: false },
|
|
{ id: 11, name: 'Crickets', emoji: '🦗', duration: '0:03', cat: 'effects', fav: false },
|
|
{ id: 12, name: 'Laugh Track', emoji: '😂', duration: '0:04', cat: 'effects', fav: false },
|
|
{ id: 13, name: 'Epic Sax', emoji: '🎷', duration: '0:07', cat: 'music', fav: false },
|
|
{ id: 14, name: 'Mein Sound 1', emoji: '🔊', duration: '0:03', cat: 'custom', fav: false },
|
|
{ id: 15, name: 'Custom Alert', emoji: '🔔', duration: '0:02', cat: 'custom', fav: false },
|
|
];
|
|
|
|
let currentCat = 'all';
|
|
let currentlyPlaying = null;
|
|
let ctxTarget = null;
|
|
|
|
const grid = document.getElementById('soundGrid');
|
|
const emptyState = document.getElementById('emptyState');
|
|
const mainTitle = document.getElementById('mainTitle');
|
|
const searchInput = document.getElementById('searchInput');
|
|
const ctxMenu = document.getElementById('ctxMenu');
|
|
const tooltip = document.getElementById('tooltip');
|
|
const npName = document.getElementById('npName');
|
|
const npWaves = document.getElementById('npWaves');
|
|
const volSlider = document.getElementById('volSlider');
|
|
const volPct = document.getElementById('volPct');
|
|
const stopAllBtn = document.getElementById('stopAllBtn');
|
|
const countFav = document.getElementById('countFav');
|
|
|
|
// ── Render ──
|
|
function render() {
|
|
const q = searchInput.value.toLowerCase().trim();
|
|
let filtered = SOUNDS.filter(s => {
|
|
if (currentCat === 'favorites') return s.fav;
|
|
if (currentCat !== 'all') return s.cat === currentCat;
|
|
return true;
|
|
});
|
|
if (q) filtered = filtered.filter(s => s.name.toLowerCase().includes(q));
|
|
|
|
grid.innerHTML = '';
|
|
emptyState.classList.toggle('visible', filtered.length === 0);
|
|
|
|
const catLabels = { all:'Alle Sounds', favorites:'Favoriten', memes:'Memes', music:'Musik', effects:'Effekte', custom:'Eigene' };
|
|
mainTitle.textContent = `${catLabels[currentCat] || 'Sounds'} — ${filtered.length}`;
|
|
|
|
filtered.forEach((s, i) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'sound-card' + (currentlyPlaying === s.id ? ' playing' : '');
|
|
card.dataset.id = s.id;
|
|
card.style.animationDelay = `${i * 30}ms`;
|
|
card.innerHTML = `
|
|
<svg class="fav-star ${s.fav ? 'active' : ''}" viewBox="0 0 24 24" fill="${s.fav ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
|
<span class="sound-emoji">${s.emoji}</span>
|
|
<span class="sound-name">${s.name}</span>
|
|
<span class="sound-duration">${s.duration}</span>
|
|
<div class="playing-indicator">
|
|
<div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div>
|
|
</div>
|
|
`;
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
// Add sound card
|
|
const addCard = document.createElement('div');
|
|
addCard.className = 'sound-card add-card';
|
|
addCard.innerHTML = `<span class="add-icon">+</span><span class="add-label">Sound hinzufügen</span>`;
|
|
grid.appendChild(addCard);
|
|
|
|
updateFavCount();
|
|
}
|
|
|
|
function updateFavCount() {
|
|
countFav.textContent = SOUNDS.filter(s => s.fav).length;
|
|
}
|
|
|
|
// ── Events ──
|
|
|
|
// Sound card click
|
|
grid.addEventListener('click', e => {
|
|
const star = e.target.closest('.fav-star');
|
|
const card = e.target.closest('.sound-card');
|
|
if (!card || card.classList.contains('add-card')) return;
|
|
|
|
const id = parseInt(card.dataset.id);
|
|
const sound = SOUNDS.find(s => s.id === id);
|
|
if (!sound) return;
|
|
|
|
if (star) {
|
|
sound.fav = !sound.fav;
|
|
render();
|
|
return;
|
|
}
|
|
|
|
// Ripple
|
|
const rect = card.getBoundingClientRect();
|
|
const ripple = document.createElement('div');
|
|
ripple.className = 'ripple';
|
|
const size = Math.max(rect.width, rect.height);
|
|
ripple.style.width = ripple.style.height = size + 'px';
|
|
ripple.style.left = (e.clientX - rect.left - size / 2) + 'px';
|
|
ripple.style.top = (e.clientY - rect.top - size / 2) + 'px';
|
|
card.appendChild(ripple);
|
|
setTimeout(() => ripple.remove(), 500);
|
|
|
|
playSound(id, sound.name);
|
|
});
|
|
|
|
function playSound(id, name) {
|
|
// Remove previous playing
|
|
document.querySelectorAll('.sound-card.playing').forEach(c => c.classList.remove('playing'));
|
|
currentlyPlaying = id;
|
|
|
|
const card = grid.querySelector(`[data-id="${id}"]`);
|
|
if (card) card.classList.add('playing');
|
|
|
|
npName.textContent = name;
|
|
npWaves.classList.add('active');
|
|
|
|
// Auto stop after a bit
|
|
const s = SOUNDS.find(s => s.id === id);
|
|
const dur = s ? parseDuration(s.duration) : 3;
|
|
setTimeout(() => {
|
|
if (currentlyPlaying === id) stopPlaying();
|
|
}, dur * 1000);
|
|
}
|
|
|
|
function parseDuration(str) {
|
|
const parts = str.split(':');
|
|
return parseInt(parts[0]) * 60 + parseInt(parts[1]);
|
|
}
|
|
|
|
function stopPlaying() {
|
|
currentlyPlaying = null;
|
|
document.querySelectorAll('.sound-card.playing').forEach(c => c.classList.remove('playing'));
|
|
npName.textContent = '—';
|
|
npWaves.classList.remove('active');
|
|
}
|
|
|
|
stopAllBtn.addEventListener('click', stopPlaying);
|
|
|
|
// Sidebar
|
|
document.querySelectorAll('.sidebar-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
|
|
item.classList.add('active');
|
|
currentCat = item.dataset.cat;
|
|
render();
|
|
});
|
|
});
|
|
|
|
// Search
|
|
searchInput.addEventListener('input', render);
|
|
|
|
// Volume
|
|
volSlider.addEventListener('input', () => {
|
|
const v = volSlider.value;
|
|
volPct.textContent = v + '%';
|
|
volSlider.style.setProperty('--vol', v + '%');
|
|
});
|
|
volSlider.style.setProperty('--vol', '80%');
|
|
|
|
// Context menu
|
|
grid.addEventListener('contextmenu', e => {
|
|
e.preventDefault();
|
|
const card = e.target.closest('.sound-card');
|
|
if (!card || card.classList.contains('add-card')) return;
|
|
|
|
ctxTarget = parseInt(card.dataset.id);
|
|
ctxMenu.style.left = Math.min(e.clientX, window.innerWidth - 180) + 'px';
|
|
ctxMenu.style.top = Math.min(e.clientY, window.innerHeight - 160) + 'px';
|
|
ctxMenu.classList.add('visible');
|
|
});
|
|
|
|
document.addEventListener('click', () => ctxMenu.classList.remove('visible'));
|
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') ctxMenu.classList.remove('visible'); });
|
|
|
|
ctxMenu.addEventListener('click', e => {
|
|
const item = e.target.closest('.ctx-item');
|
|
if (!item || ctxTarget == null) return;
|
|
const action = item.dataset.action;
|
|
const sound = SOUNDS.find(s => s.id === ctxTarget);
|
|
if (!sound) return;
|
|
|
|
if (action === 'play') playSound(sound.id, sound.name);
|
|
if (action === 'fav') { sound.fav = !sound.fav; render(); }
|
|
if (action === 'delete') {
|
|
const idx = SOUNDS.findIndex(s => s.id === ctxTarget);
|
|
if (idx > -1) { SOUNDS.splice(idx, 1); render(); }
|
|
}
|
|
ctxMenu.classList.remove('visible');
|
|
});
|
|
|
|
// Tooltip
|
|
let ttTimeout;
|
|
grid.addEventListener('mouseover', e => {
|
|
const card = e.target.closest('.sound-card');
|
|
if (!card || card.classList.contains('add-card')) return;
|
|
const nameEl = card.querySelector('.sound-name');
|
|
if (!nameEl || nameEl.scrollWidth <= nameEl.clientWidth) return;
|
|
|
|
clearTimeout(ttTimeout);
|
|
ttTimeout = setTimeout(() => {
|
|
const rect = card.getBoundingClientRect();
|
|
tooltip.textContent = nameEl.textContent;
|
|
tooltip.style.left = (rect.left + rect.width / 2) + 'px';
|
|
tooltip.style.top = (rect.top - 8) + 'px';
|
|
tooltip.style.transform = 'translateX(-50%) translateY(-100%)';
|
|
tooltip.classList.add('visible');
|
|
}, 600);
|
|
});
|
|
|
|
grid.addEventListener('mouseout', e => {
|
|
if (e.target.closest('.sound-card')) {
|
|
clearTimeout(ttTimeout);
|
|
tooltip.classList.remove('visible');
|
|
}
|
|
});
|
|
|
|
// Init
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|