forked from Keelrleek/SoulStream
130 lines
3.7 KiB
Svelte
130 lines
3.7 KiB
Svelte
<script>
|
|
// Someone proposed a change to a playlist of ours. The board is only read
|
|
// on open and Refresh, so without this a proposal arrives in silence.
|
|
// Same slot, pill and motion as the other toasts; like UploadToast, a
|
|
// second proposal for the same playlist inside the window extends the
|
|
// hold and counts up rather than replaying the entrance.
|
|
import Icon from "./Icon.svelte";
|
|
import { proposalToast } from "../lib/state.svelte.js";
|
|
|
|
// Without a linked Soulseek name the backend can only give us the raw
|
|
// SoulStream ID, and a 44-character key runs the pill off the screen.
|
|
const who = $derived.by(() => {
|
|
const f = proposalToast.from ?? "";
|
|
return f.length > 24 ? `${f.slice(0, 4)}…${f.slice(-4)}` : f;
|
|
});
|
|
|
|
const HOLD_MS = 4200;
|
|
const LEAVE_MS = 280;
|
|
|
|
let shown = $state(false);
|
|
let leaving = $state(false);
|
|
let holdTimer = 0;
|
|
let leaveTimer = 0;
|
|
|
|
$effect(() => {
|
|
const seq = proposalToast.seq;
|
|
if (!seq) return;
|
|
clearTimeout(holdTimer);
|
|
clearTimeout(leaveTimer);
|
|
leaving = false;
|
|
shown = true;
|
|
holdTimer = setTimeout(() => {
|
|
leaving = true;
|
|
leaveTimer = setTimeout(() => {
|
|
shown = false;
|
|
leaving = false;
|
|
}, LEAVE_MS);
|
|
}, HOLD_MS);
|
|
return () => {
|
|
clearTimeout(holdTimer);
|
|
clearTimeout(leaveTimer);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
{#if shown}
|
|
<div class="layer" aria-live="polite">
|
|
<div class="toast" class:leaving>
|
|
<span class="mark"><Icon name="playlist" size={14} /></span>
|
|
{#if proposalToast.count > 1}
|
|
<span class="text"
|
|
><b>{proposalToast.count} proposals</b> for {proposalToast.playlist}</span
|
|
>
|
|
{:else}
|
|
<span class="text"
|
|
><b>{who}</b> proposed a change to {proposalToast.playlist}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
/* The shared toast slot — UploadToast's geometry verbatim. */
|
|
.layer {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: calc(var(--player-height, 62px) + 8px + 26px);
|
|
display: flex;
|
|
justify-content: center;
|
|
z-index: 80;
|
|
pointer-events: none;
|
|
}
|
|
.toast {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
max-width: min(70vw, 560px);
|
|
padding: 9px 16px;
|
|
border-radius: 99px;
|
|
background: rgba(22, 23, 28, 0.94);
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
backdrop-filter: blur(14px);
|
|
-webkit-backdrop-filter: blur(14px);
|
|
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
|
|
font-size: 12.5px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
animation: toast-in 340ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
.toast.leaving {
|
|
animation: toast-out 260ms ease both;
|
|
}
|
|
@keyframes toast-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(16px) scale(0.95);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
@keyframes toast-out {
|
|
to {
|
|
opacity: 0;
|
|
transform: translateY(12px) scale(0.97);
|
|
}
|
|
}
|
|
.mark {
|
|
display: inline-flex;
|
|
color: var(--accent);
|
|
}
|
|
.text {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
b {
|
|
font-weight: 800;
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.toast,
|
|
.toast.leaving {
|
|
animation-duration: 1ms;
|
|
}
|
|
}
|
|
</style>
|