@livepeer-frameworks/player-svelte 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DevModePanel.svelte +650 -0
- package/dist/DevModePanel.svelte.d.ts +31 -0
- package/dist/DvdLogo.svelte +213 -0
- package/dist/DvdLogo.svelte.d.ts +7 -0
- package/dist/Icons.svelte +27 -0
- package/dist/Icons.svelte.d.ts +25 -0
- package/dist/IdleScreen.svelte +752 -0
- package/dist/IdleScreen.svelte.d.ts +11 -0
- package/dist/LoadingScreen.svelte +689 -0
- package/dist/LoadingScreen.svelte.d.ts +7 -0
- package/dist/Player.svelte +482 -0
- package/dist/Player.svelte.d.ts +26 -0
- package/dist/PlayerControls.svelte +739 -0
- package/dist/PlayerControls.svelte.d.ts +20 -0
- package/dist/SeekBar.svelte +274 -0
- package/dist/SeekBar.svelte.d.ts +25 -0
- package/dist/SkipIndicator.svelte +95 -0
- package/dist/SkipIndicator.svelte.d.ts +14 -0
- package/dist/SpeedIndicator.svelte +38 -0
- package/dist/SpeedIndicator.svelte.d.ts +8 -0
- package/dist/StatsPanel.svelte +155 -0
- package/dist/StatsPanel.svelte.d.ts +27 -0
- package/dist/StreamStateOverlay.svelte +266 -0
- package/dist/StreamStateOverlay.svelte.d.ts +18 -0
- package/dist/SubtitleRenderer.svelte +234 -0
- package/dist/SubtitleRenderer.svelte.d.ts +41 -0
- package/dist/ThumbnailOverlay.svelte +96 -0
- package/dist/ThumbnailOverlay.svelte.d.ts +11 -0
- package/dist/TitleOverlay.svelte +47 -0
- package/dist/TitleOverlay.svelte.d.ts +9 -0
- package/dist/assets/logomark.svg +56 -0
- package/dist/components/VolumeIcons.svelte +53 -0
- package/dist/components/VolumeIcons.svelte.d.ts +10 -0
- package/dist/global.d.ts +15 -0
- package/dist/icons/FullscreenExitIcon.svelte +33 -0
- package/dist/icons/FullscreenExitIcon.svelte.d.ts +8 -0
- package/dist/icons/FullscreenIcon.svelte +33 -0
- package/dist/icons/FullscreenIcon.svelte.d.ts +8 -0
- package/dist/icons/PauseIcon.svelte +28 -0
- package/dist/icons/PauseIcon.svelte.d.ts +8 -0
- package/dist/icons/PictureInPictureIcon.svelte +28 -0
- package/dist/icons/PictureInPictureIcon.svelte.d.ts +8 -0
- package/dist/icons/PlayIcon.svelte +27 -0
- package/dist/icons/PlayIcon.svelte.d.ts +8 -0
- package/dist/icons/SeekToLiveIcon.svelte +30 -0
- package/dist/icons/SeekToLiveIcon.svelte.d.ts +8 -0
- package/dist/icons/SettingsIcon.svelte +40 -0
- package/dist/icons/SettingsIcon.svelte.d.ts +8 -0
- package/dist/icons/SkipBackIcon.svelte +32 -0
- package/dist/icons/SkipBackIcon.svelte.d.ts +8 -0
- package/dist/icons/SkipForwardIcon.svelte +32 -0
- package/dist/icons/SkipForwardIcon.svelte.d.ts +8 -0
- package/dist/icons/StatsIcon.svelte +29 -0
- package/dist/icons/StatsIcon.svelte.d.ts +8 -0
- package/dist/icons/VolumeOffIcon.svelte +29 -0
- package/dist/icons/VolumeOffIcon.svelte.d.ts +8 -0
- package/dist/icons/VolumeUpIcon.svelte +34 -0
- package/dist/icons/VolumeUpIcon.svelte.d.ts +8 -0
- package/dist/icons/index.d.ts +17 -0
- package/dist/icons/index.js +17 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +54 -0
- package/dist/player.css +2 -0
- package/dist/stores/index.d.ts +15 -0
- package/dist/stores/index.js +21 -0
- package/dist/stores/playbackQuality.d.ts +43 -0
- package/dist/stores/playbackQuality.js +107 -0
- package/dist/stores/playerContext.d.ts +73 -0
- package/dist/stores/playerContext.js +166 -0
- package/dist/stores/playerController.d.ts +178 -0
- package/dist/stores/playerController.js +358 -0
- package/dist/stores/playerSelection.d.ts +84 -0
- package/dist/stores/playerSelection.js +159 -0
- package/dist/stores/streamState.d.ts +44 -0
- package/dist/stores/streamState.js +314 -0
- package/dist/stores/viewerEndpoints.d.ts +48 -0
- package/dist/stores/viewerEndpoints.js +178 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +4 -0
- package/dist/ui/Badge.svelte +21 -0
- package/dist/ui/Badge.svelte.d.ts +32 -0
- package/dist/ui/Button.svelte +42 -0
- package/dist/ui/Button.svelte.d.ts +35 -0
- package/dist/ui/Slider.svelte +100 -0
- package/dist/ui/Slider.svelte.d.ts +17 -0
- package/dist/ui/badge.d.ts +6 -0
- package/dist/ui/badge.js +10 -0
- package/dist/ui/button.d.ts +8 -0
- package/dist/ui/button.js +21 -0
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +31 -0
- package/dist/ui/context-menu/ContextMenuContent.svelte +17 -0
- package/dist/ui/context-menu/ContextMenuContent.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuItem.svelte +22 -0
- package/dist/ui/context-menu/ContextMenuItem.svelte.d.ts +8 -0
- package/dist/ui/context-menu/ContextMenuLabel.svelte +22 -0
- package/dist/ui/context-menu/ContextMenuLabel.svelte.d.ts +8 -0
- package/dist/ui/context-menu/ContextMenuPortal.svelte +11 -0
- package/dist/ui/context-menu/ContextMenuPortal.svelte.d.ts +6 -0
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +31 -0
- package/dist/ui/context-menu/ContextMenuSeparator.svelte +14 -0
- package/dist/ui/context-menu/ContextMenuSeparator.svelte.d.ts +6 -0
- package/dist/ui/context-menu/ContextMenuShortcut.svelte +19 -0
- package/dist/ui/context-menu/ContextMenuShortcut.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuSubContent.svelte +20 -0
- package/dist/ui/context-menu/ContextMenuSubContent.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
- package/dist/ui/context-menu/ContextMenuSubTrigger.svelte.d.ts +8 -0
- package/dist/ui/context-menu/index.d.ts +17 -0
- package/dist/ui/context-menu/index.js +17 -0
- package/package.json +51 -0
- package/src/DevModePanel.svelte +650 -0
- package/src/DvdLogo.svelte +213 -0
- package/src/Icons.svelte +27 -0
- package/src/IdleScreen.svelte +739 -0
- package/src/LoadingScreen.svelte +674 -0
- package/src/Player.svelte +483 -0
- package/src/PlayerControls.svelte +752 -0
- package/src/SeekBar.svelte +274 -0
- package/src/SkipIndicator.svelte +95 -0
- package/src/SpeedIndicator.svelte +37 -0
- package/src/StatsPanel.svelte +155 -0
- package/src/StreamStateOverlay.svelte +266 -0
- package/src/SubtitleRenderer.svelte +234 -0
- package/src/ThumbnailOverlay.svelte +96 -0
- package/src/TitleOverlay.svelte +47 -0
- package/src/assets/logomark.svg +56 -0
- package/src/components/VolumeIcons.svelte +53 -0
- package/src/global.d.ts +15 -0
- package/src/icons/FullscreenExitIcon.svelte +33 -0
- package/src/icons/FullscreenIcon.svelte +33 -0
- package/src/icons/PauseIcon.svelte +28 -0
- package/src/icons/PictureInPictureIcon.svelte +28 -0
- package/src/icons/PlayIcon.svelte +27 -0
- package/src/icons/SeekToLiveIcon.svelte +30 -0
- package/src/icons/SettingsIcon.svelte +40 -0
- package/src/icons/SkipBackIcon.svelte +32 -0
- package/src/icons/SkipForwardIcon.svelte +32 -0
- package/src/icons/StatsIcon.svelte +29 -0
- package/src/icons/VolumeOffIcon.svelte +29 -0
- package/src/icons/VolumeUpIcon.svelte +34 -0
- package/src/icons/index.ts +18 -0
- package/src/index.ts +84 -0
- package/src/player.css +2 -0
- package/src/stores/index.ts +88 -0
- package/src/stores/playbackQuality.ts +137 -0
- package/src/stores/playerContext.ts +221 -0
- package/src/stores/playerController.ts +568 -0
- package/src/stores/playerSelection.ts +216 -0
- package/src/stores/streamState.ts +367 -0
- package/src/stores/viewerEndpoints.ts +224 -0
- package/src/types.ts +6 -0
- package/src/ui/Badge.svelte +21 -0
- package/src/ui/Button.svelte +42 -0
- package/src/ui/Slider.svelte +100 -0
- package/src/ui/badge.ts +20 -0
- package/src/ui/button.ts +35 -0
- package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
- package/src/ui/context-menu/ContextMenuContent.svelte +17 -0
- package/src/ui/context-menu/ContextMenuItem.svelte +22 -0
- package/src/ui/context-menu/ContextMenuLabel.svelte +22 -0
- package/src/ui/context-menu/ContextMenuPortal.svelte +11 -0
- package/src/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
- package/src/ui/context-menu/ContextMenuSeparator.svelte +14 -0
- package/src/ui/context-menu/ContextMenuShortcut.svelte +19 -0
- package/src/ui/context-menu/ContextMenuSubContent.svelte +20 -0
- package/src/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
- package/src/ui/context-menu/index.ts +36 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
IdleScreen.svelte - Idle/offline state screen with Tokyo Night theme
|
|
3
|
+
Port of src/components/IdleScreen.tsx
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- Animated colored bubbles with Tokyo Night palette
|
|
7
|
+
- Center logo with mouse-tracking "push away" effect
|
|
8
|
+
- Bouncing DVD logo component
|
|
9
|
+
- Hitmarker sound effects (Web Audio API synthesis)
|
|
10
|
+
- Floating particles with gradients
|
|
11
|
+
- Pulsing circle around logo
|
|
12
|
+
- Animated background gradient shifts
|
|
13
|
+
- Status overlay at bottom
|
|
14
|
+
-->
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { onMount, onDestroy } from 'svelte';
|
|
17
|
+
import type { StreamStatus } from '@livepeer-frameworks/player-core';
|
|
18
|
+
import DvdLogo from './DvdLogo.svelte';
|
|
19
|
+
import logomarkAsset from './assets/logomark.svg';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
status?: StreamStatus;
|
|
23
|
+
message?: string;
|
|
24
|
+
percentage?: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
onRetry?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
status = 'OFFLINE',
|
|
31
|
+
message = 'Waiting for stream...',
|
|
32
|
+
percentage = undefined,
|
|
33
|
+
error = undefined,
|
|
34
|
+
onRetry = undefined,
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
// Container ref for mouse tracking
|
|
38
|
+
let containerRef: HTMLDivElement | undefined = $state();
|
|
39
|
+
|
|
40
|
+
// Hitmarker state
|
|
41
|
+
interface Hitmarker {
|
|
42
|
+
id: number;
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
}
|
|
46
|
+
let hitmarkers = $state<Hitmarker[]>([]);
|
|
47
|
+
|
|
48
|
+
// Center logo state
|
|
49
|
+
let logoSize = $state(100);
|
|
50
|
+
let offset = $state({ x: 0, y: 0 });
|
|
51
|
+
let isHovered = $state(false);
|
|
52
|
+
|
|
53
|
+
// Tokyo Night inspired pastel colors for bubbles
|
|
54
|
+
const bubbleColors = [
|
|
55
|
+
'rgba(122, 162, 247, 0.2)', // Terminal Blue
|
|
56
|
+
'rgba(187, 154, 247, 0.2)', // Terminal Magenta
|
|
57
|
+
'rgba(158, 206, 106, 0.2)', // Strings/CSS classes
|
|
58
|
+
'rgba(115, 218, 202, 0.2)', // Terminal Green
|
|
59
|
+
'rgba(125, 207, 255, 0.2)', // Terminal Cyan
|
|
60
|
+
'rgba(247, 118, 142, 0.2)', // Keywords/Terminal Red
|
|
61
|
+
'rgba(224, 175, 104, 0.2)', // Terminal Yellow
|
|
62
|
+
'rgba(42, 195, 222, 0.2)', // Language functions
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Particle colors
|
|
66
|
+
const particleColors = [
|
|
67
|
+
'#7aa2f7', // Terminal Blue
|
|
68
|
+
'#bb9af7', // Terminal Magenta
|
|
69
|
+
'#9ece6a', // Strings/CSS classes
|
|
70
|
+
'#73daca', // Terminal Green
|
|
71
|
+
'#7dcfff', // Terminal Cyan
|
|
72
|
+
'#f7768e', // Keywords/Terminal Red
|
|
73
|
+
'#e0af68', // Terminal Yellow
|
|
74
|
+
'#2ac3de', // Language functions
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Generate random particles (matching React's 12 particles)
|
|
78
|
+
const particles = Array.from({ length: 12 }, (_, i) => ({
|
|
79
|
+
left: Math.random() * 100,
|
|
80
|
+
size: Math.random() * 4 + 2,
|
|
81
|
+
color: particleColors[i % 8],
|
|
82
|
+
duration: 8 + Math.random() * 4,
|
|
83
|
+
delay: Math.random() * 8,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
// Animated bubble state
|
|
87
|
+
interface BubbleState {
|
|
88
|
+
position: { top: number; left: number };
|
|
89
|
+
size: number;
|
|
90
|
+
opacity: number;
|
|
91
|
+
color: string;
|
|
92
|
+
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let bubbles = $state<BubbleState[]>(
|
|
96
|
+
Array.from({ length: 8 }, (_, i) => ({
|
|
97
|
+
position: { top: Math.random() * 80 + 10, left: Math.random() * 80 + 10 },
|
|
98
|
+
size: Math.random() * 60 + 30,
|
|
99
|
+
opacity: 0,
|
|
100
|
+
color: bubbleColors[i % bubbleColors.length],
|
|
101
|
+
timeoutId: null,
|
|
102
|
+
}))
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Bubble animation cycle
|
|
106
|
+
function animateBubble(index: number) {
|
|
107
|
+
bubbles[index].opacity = 0.15;
|
|
108
|
+
|
|
109
|
+
const visibleDuration = 4000 + Math.random() * 3000;
|
|
110
|
+
|
|
111
|
+
const timeout1 = setTimeout(() => {
|
|
112
|
+
bubbles[index].opacity = 0;
|
|
113
|
+
|
|
114
|
+
const timeout2 = setTimeout(() => {
|
|
115
|
+
bubbles[index].position = {
|
|
116
|
+
top: Math.random() * 80 + 10,
|
|
117
|
+
left: Math.random() * 80 + 10,
|
|
118
|
+
};
|
|
119
|
+
bubbles[index].size = Math.random() * 60 + 30;
|
|
120
|
+
|
|
121
|
+
const timeout3 = setTimeout(() => {
|
|
122
|
+
animateBubble(index);
|
|
123
|
+
}, 200);
|
|
124
|
+
bubbles[index].timeoutId = timeout3;
|
|
125
|
+
}, 1500);
|
|
126
|
+
bubbles[index].timeoutId = timeout2;
|
|
127
|
+
}, visibleDuration);
|
|
128
|
+
bubbles[index].timeoutId = timeout1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Hitmarker sound synthesis
|
|
132
|
+
function createSyntheticHitmarkerSound() {
|
|
133
|
+
try {
|
|
134
|
+
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
135
|
+
|
|
136
|
+
const oscillator1 = audioContext.createOscillator();
|
|
137
|
+
const oscillator2 = audioContext.createOscillator();
|
|
138
|
+
const noiseBuffer = audioContext.createBuffer(1, audioContext.sampleRate * 0.1, audioContext.sampleRate);
|
|
139
|
+
const noiseSource = audioContext.createBufferSource();
|
|
140
|
+
|
|
141
|
+
// Generate white noise for the initial "crack"
|
|
142
|
+
const noiseData = noiseBuffer.getChannelData(0);
|
|
143
|
+
for (let i = 0; i < noiseData.length; i++) {
|
|
144
|
+
noiseData[i] = Math.random() * 2 - 1;
|
|
145
|
+
}
|
|
146
|
+
noiseSource.buffer = noiseBuffer;
|
|
147
|
+
|
|
148
|
+
const gainNode1 = audioContext.createGain();
|
|
149
|
+
const gainNode2 = audioContext.createGain();
|
|
150
|
+
const noiseGain = audioContext.createGain();
|
|
151
|
+
const masterGain = audioContext.createGain();
|
|
152
|
+
|
|
153
|
+
oscillator1.connect(gainNode1);
|
|
154
|
+
oscillator2.connect(gainNode2);
|
|
155
|
+
noiseSource.connect(noiseGain);
|
|
156
|
+
|
|
157
|
+
gainNode1.connect(masterGain);
|
|
158
|
+
gainNode2.connect(masterGain);
|
|
159
|
+
noiseGain.connect(masterGain);
|
|
160
|
+
masterGain.connect(audioContext.destination);
|
|
161
|
+
|
|
162
|
+
// Sharp metallic frequencies
|
|
163
|
+
oscillator1.frequency.setValueAtTime(1800, audioContext.currentTime);
|
|
164
|
+
oscillator1.frequency.exponentialRampToValueAtTime(900, audioContext.currentTime + 0.08);
|
|
165
|
+
|
|
166
|
+
oscillator2.frequency.setValueAtTime(3600, audioContext.currentTime);
|
|
167
|
+
oscillator2.frequency.exponentialRampToValueAtTime(1800, audioContext.currentTime + 0.04);
|
|
168
|
+
|
|
169
|
+
oscillator1.type = 'triangle';
|
|
170
|
+
oscillator2.type = 'sine';
|
|
171
|
+
|
|
172
|
+
// Sharp attack, quick decay
|
|
173
|
+
gainNode1.gain.setValueAtTime(0, audioContext.currentTime);
|
|
174
|
+
gainNode1.gain.linearRampToValueAtTime(0.4, audioContext.currentTime + 0.002);
|
|
175
|
+
gainNode1.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.12);
|
|
176
|
+
|
|
177
|
+
gainNode2.gain.setValueAtTime(0, audioContext.currentTime);
|
|
178
|
+
gainNode2.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.001);
|
|
179
|
+
gainNode2.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.06);
|
|
180
|
+
|
|
181
|
+
// Noise burst
|
|
182
|
+
noiseGain.gain.setValueAtTime(0, audioContext.currentTime);
|
|
183
|
+
noiseGain.gain.linearRampToValueAtTime(0.2, audioContext.currentTime + 0.001);
|
|
184
|
+
noiseGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.01);
|
|
185
|
+
|
|
186
|
+
masterGain.gain.setValueAtTime(0.5, audioContext.currentTime);
|
|
187
|
+
|
|
188
|
+
const startTime = audioContext.currentTime;
|
|
189
|
+
const stopTime = startTime + 0.15;
|
|
190
|
+
|
|
191
|
+
oscillator1.start(startTime);
|
|
192
|
+
oscillator2.start(startTime);
|
|
193
|
+
noiseSource.start(startTime);
|
|
194
|
+
|
|
195
|
+
oscillator1.stop(stopTime);
|
|
196
|
+
oscillator2.stop(stopTime);
|
|
197
|
+
noiseSource.stop(startTime + 0.02);
|
|
198
|
+
} catch {
|
|
199
|
+
// Audio context not available
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function playHitmarkerSound() {
|
|
204
|
+
try {
|
|
205
|
+
const hitmarkerDataUrl = 'data:audio/mpeg;base64,SUQzBAAAAAAANFRDT04AAAAHAAADT3RoZXIAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAA' +
|
|
206
|
+
'AAD/+1QAAAAAAAAAAAAAAAAAAAAA' +
|
|
207
|
+
'AAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAYAAAnAADs7Ozs7Ozs7Ozs7Ozs7OztiYmJiYmJiYmJi' +
|
|
208
|
+
'YmJiYmJiYomJiYmJiYmJiYmJiYmJiYmxsbGxsbGxsbGxsbGxsbGxsdjY2NjY2NjY2NjY2NjY2NjY////' +
|
|
209
|
+
'/////////////////wAAAABMYXZjNTcuMTAAAAAAAAAAAAAAAAAkAkAAAAAAAAAJwOuMZun/+5RkAA8S' +
|
|
210
|
+
'/F23AGAaAi0AF0AAAAAInXsEAIRXyQ8D4OQgjEhE3cO7ujuHF0XCOu4G7xKbi3Funu7u7p9dw7unu7u7' +
|
|
211
|
+
'p7u7u6fXcW7om7u7uiU3dxdT67u7p7uHdxelN3cW6fXcW7oXXd3eJTd3d0+u4t3iXdw4up70W4uiPruL' +
|
|
212
|
+
'DzMw8Pz79Y99JfkyfPv5/h9uTJoy79Y99Y97q3vyZPJk0ZfrL6x73Vn+J35dKKS/STQyQ8CAiCPNuRAO' +
|
|
213
|
+
'OqquAx+fzJeBKDAsgAMBuWcBsHKhjJTcCwIALyAvABbI0ZIcCmP8jHJe8gZAdVRp2TpnU/kUXV4iQuBA';
|
|
214
|
+
|
|
215
|
+
const audio = new Audio(hitmarkerDataUrl);
|
|
216
|
+
audio.volume = 0.3;
|
|
217
|
+
audio.play().catch(() => {
|
|
218
|
+
createSyntheticHitmarkerSound();
|
|
219
|
+
});
|
|
220
|
+
} catch {
|
|
221
|
+
createSyntheticHitmarkerSound();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function createHitmarker(e: { clientX: number; clientY: number }) {
|
|
226
|
+
if (!containerRef) return;
|
|
227
|
+
|
|
228
|
+
const rect = containerRef.getBoundingClientRect();
|
|
229
|
+
const x = e.clientX - rect.left;
|
|
230
|
+
const y = e.clientY - rect.top;
|
|
231
|
+
|
|
232
|
+
const newHitmarker: Hitmarker = {
|
|
233
|
+
id: Date.now() + Math.random(),
|
|
234
|
+
x,
|
|
235
|
+
y,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
hitmarkers = [...hitmarkers, newHitmarker];
|
|
239
|
+
playHitmarkerSound();
|
|
240
|
+
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
hitmarkers = hitmarkers.filter(h => h.id !== newHitmarker.id);
|
|
243
|
+
}, 600);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Mouse tracking for logo push-away effect
|
|
247
|
+
function handleMouseMove(e: MouseEvent) {
|
|
248
|
+
if (!containerRef) return;
|
|
249
|
+
|
|
250
|
+
const rect = containerRef.getBoundingClientRect();
|
|
251
|
+
const centerX = rect.left + rect.width / 2;
|
|
252
|
+
const centerY = rect.top + rect.height / 2;
|
|
253
|
+
|
|
254
|
+
const mouseX = e.clientX;
|
|
255
|
+
const mouseY = e.clientY;
|
|
256
|
+
|
|
257
|
+
const deltaX = mouseX - centerX;
|
|
258
|
+
const deltaY = mouseY - centerY;
|
|
259
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
260
|
+
|
|
261
|
+
const maxDistance = logoSize * 1.5;
|
|
262
|
+
if (distance < maxDistance && distance > 0) {
|
|
263
|
+
const pushStrength = (maxDistance - distance) / maxDistance;
|
|
264
|
+
const pushDistance = 50 * pushStrength;
|
|
265
|
+
|
|
266
|
+
offset = {
|
|
267
|
+
x: -(deltaX / distance) * pushDistance,
|
|
268
|
+
y: -(deltaY / distance) * pushDistance,
|
|
269
|
+
};
|
|
270
|
+
isHovered = true;
|
|
271
|
+
} else {
|
|
272
|
+
offset = { x: 0, y: 0 };
|
|
273
|
+
isHovered = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function handleMouseLeave() {
|
|
278
|
+
offset = { x: 0, y: 0 };
|
|
279
|
+
isHovered = false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function handleLogoClick(e: MouseEvent) {
|
|
283
|
+
e.stopPropagation();
|
|
284
|
+
createHitmarker({ clientX: e.clientX, clientY: e.clientY });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Status helpers
|
|
288
|
+
function getStatusLabel(s?: StreamStatus): string {
|
|
289
|
+
switch (s) {
|
|
290
|
+
case 'ONLINE': return 'ONLINE';
|
|
291
|
+
case 'OFFLINE': return 'OFFLINE';
|
|
292
|
+
case 'INITIALIZING': return 'STARTING';
|
|
293
|
+
case 'BOOTING': return 'STARTING';
|
|
294
|
+
case 'WAITING_FOR_DATA': return 'WAITING';
|
|
295
|
+
case 'SHUTTING_DOWN': return 'ENDING';
|
|
296
|
+
case 'ERROR': return 'ERROR';
|
|
297
|
+
case 'INVALID': return 'ERROR';
|
|
298
|
+
default: return 'CONNECTING';
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let statusLabel = $derived(getStatusLabel(status));
|
|
303
|
+
let showRetry = $derived((status === 'ERROR' || status === 'INVALID') && onRetry);
|
|
304
|
+
let showProgress = $derived(status === 'INITIALIZING' && percentage !== undefined);
|
|
305
|
+
let displayMessage = $derived(error || message);
|
|
306
|
+
let isLoading = $derived(status === 'INITIALIZING' || status === 'BOOTING' || status === 'WAITING_FOR_DATA' || !status);
|
|
307
|
+
let isError = $derived(status === 'ERROR' || status === 'INVALID');
|
|
308
|
+
let isOffline = $derived(status === 'OFFLINE');
|
|
309
|
+
|
|
310
|
+
// Update logo size on container resize
|
|
311
|
+
$effect(() => {
|
|
312
|
+
if (!containerRef) return;
|
|
313
|
+
|
|
314
|
+
const updateLogoSize = () => {
|
|
315
|
+
if (containerRef) {
|
|
316
|
+
const minDimension = Math.min(containerRef.clientWidth, containerRef.clientHeight);
|
|
317
|
+
logoSize = minDimension * 0.2;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
updateLogoSize();
|
|
322
|
+
|
|
323
|
+
const resizeObserver = new ResizeObserver(updateLogoSize);
|
|
324
|
+
resizeObserver.observe(containerRef);
|
|
325
|
+
|
|
326
|
+
return () => resizeObserver.disconnect();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Start bubble animations on mount
|
|
330
|
+
onMount(() => {
|
|
331
|
+
bubbles.forEach((_, index) => {
|
|
332
|
+
setTimeout(() => animateBubble(index), index * 500);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Cleanup on destroy
|
|
337
|
+
onDestroy(() => {
|
|
338
|
+
bubbles.forEach(bubble => {
|
|
339
|
+
if (bubble.timeoutId) clearTimeout(bubble.timeoutId);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
</script>
|
|
343
|
+
|
|
344
|
+
<style>
|
|
345
|
+
@keyframes fadeInOut {
|
|
346
|
+
0%, 100% { opacity: 0.6; }
|
|
347
|
+
50% { opacity: 0.9; }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@keyframes logoPulse {
|
|
351
|
+
0%, 100% {
|
|
352
|
+
opacity: 0.15;
|
|
353
|
+
transform: scale(1);
|
|
354
|
+
}
|
|
355
|
+
50% {
|
|
356
|
+
opacity: 0.25;
|
|
357
|
+
transform: scale(1.05);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@keyframes floatUp {
|
|
362
|
+
0% {
|
|
363
|
+
transform: translateY(100vh) rotate(0deg);
|
|
364
|
+
opacity: 0;
|
|
365
|
+
}
|
|
366
|
+
10% { opacity: 0.6; }
|
|
367
|
+
90% { opacity: 0.6; }
|
|
368
|
+
100% {
|
|
369
|
+
transform: translateY(-100px) rotate(360deg);
|
|
370
|
+
opacity: 0;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@keyframes gradientShift {
|
|
375
|
+
0%, 100% { background-position: 0% 50%; }
|
|
376
|
+
50% { background-position: 100% 50%; }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@keyframes hitmarkerFade45 {
|
|
380
|
+
0% {
|
|
381
|
+
opacity: 1;
|
|
382
|
+
transform: translate(-50%, -50%) rotate(45deg) scale(0.5);
|
|
383
|
+
}
|
|
384
|
+
20% {
|
|
385
|
+
opacity: 1;
|
|
386
|
+
transform: translate(-50%, -50%) rotate(45deg) scale(1.2);
|
|
387
|
+
}
|
|
388
|
+
100% {
|
|
389
|
+
opacity: 0;
|
|
390
|
+
transform: translate(-50%, -50%) rotate(45deg) scale(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@keyframes hitmarkerFadeNeg45 {
|
|
395
|
+
0% {
|
|
396
|
+
opacity: 1;
|
|
397
|
+
transform: translate(-50%, -50%) rotate(-45deg) scale(0.5);
|
|
398
|
+
}
|
|
399
|
+
20% {
|
|
400
|
+
opacity: 1;
|
|
401
|
+
transform: translate(-50%, -50%) rotate(-45deg) scale(1.2);
|
|
402
|
+
}
|
|
403
|
+
100% {
|
|
404
|
+
opacity: 0;
|
|
405
|
+
transform: translate(-50%, -50%) rotate(-45deg) scale(1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
@keyframes spin {
|
|
410
|
+
from { transform: rotate(0deg); }
|
|
411
|
+
to { transform: rotate(360deg); }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.idle-container {
|
|
415
|
+
position: absolute;
|
|
416
|
+
inset: 0;
|
|
417
|
+
z-index: 5;
|
|
418
|
+
background: linear-gradient(135deg,
|
|
419
|
+
hsl(var(--tn-bg-dark, 235 21% 11%)) 0%,
|
|
420
|
+
hsl(var(--tn-bg, 233 23% 17%)) 25%,
|
|
421
|
+
hsl(var(--tn-bg-dark, 235 21% 11%)) 50%,
|
|
422
|
+
hsl(var(--tn-bg, 233 23% 17%)) 75%,
|
|
423
|
+
hsl(var(--tn-bg-dark, 235 21% 11%)) 100%
|
|
424
|
+
);
|
|
425
|
+
background-size: 400% 400%;
|
|
426
|
+
animation: gradientShift 16s ease-in-out infinite;
|
|
427
|
+
display: flex;
|
|
428
|
+
flex-direction: column;
|
|
429
|
+
align-items: center;
|
|
430
|
+
justify-content: center;
|
|
431
|
+
overflow: hidden;
|
|
432
|
+
border-radius: 0;
|
|
433
|
+
user-select: none;
|
|
434
|
+
-webkit-user-select: none;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.bubble {
|
|
438
|
+
position: absolute;
|
|
439
|
+
border-radius: 50%;
|
|
440
|
+
transition: opacity 1s ease-in-out;
|
|
441
|
+
pointer-events: none;
|
|
442
|
+
user-select: none;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.particle {
|
|
446
|
+
position: absolute;
|
|
447
|
+
border-radius: 50%;
|
|
448
|
+
opacity: 0;
|
|
449
|
+
animation: floatUp linear infinite;
|
|
450
|
+
pointer-events: none;
|
|
451
|
+
user-select: none;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.center-logo {
|
|
455
|
+
position: absolute;
|
|
456
|
+
top: 50%;
|
|
457
|
+
left: 50%;
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
justify-content: center;
|
|
461
|
+
z-index: 10;
|
|
462
|
+
transition: transform 0.3s ease-out;
|
|
463
|
+
user-select: none;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.logo-pulse {
|
|
467
|
+
position: absolute;
|
|
468
|
+
border-radius: 50%;
|
|
469
|
+
background: rgba(122, 162, 247, 0.15);
|
|
470
|
+
animation: logoPulse 3s ease-in-out infinite;
|
|
471
|
+
user-select: none;
|
|
472
|
+
pointer-events: none;
|
|
473
|
+
transition: transform 0.3s ease-out;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.logo-pulse.hovered {
|
|
477
|
+
animation: logoPulse 1s ease-in-out infinite;
|
|
478
|
+
transform: scale(1.2);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.logo-image {
|
|
482
|
+
position: relative;
|
|
483
|
+
z-index: 1;
|
|
484
|
+
filter: drop-shadow(0 4px 8px rgba(36, 40, 59, 0.3));
|
|
485
|
+
transition: all 0.3s ease-out;
|
|
486
|
+
user-select: none;
|
|
487
|
+
-webkit-user-drag: none;
|
|
488
|
+
-webkit-touch-callout: none;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.logo-image.hovered {
|
|
492
|
+
filter: drop-shadow(0 6px 12px rgba(36, 40, 59, 0.4)) brightness(1.1);
|
|
493
|
+
transform: scale(1.1);
|
|
494
|
+
cursor: pointer;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.overlay-texture {
|
|
498
|
+
position: absolute;
|
|
499
|
+
top: 0;
|
|
500
|
+
left: 0;
|
|
501
|
+
right: 0;
|
|
502
|
+
bottom: 0;
|
|
503
|
+
background:
|
|
504
|
+
radial-gradient(circle at 20% 80%, rgba(122, 162, 247, 0.03) 0%, transparent 50%),
|
|
505
|
+
radial-gradient(circle at 80% 20%, rgba(187, 154, 247, 0.03) 0%, transparent 50%),
|
|
506
|
+
radial-gradient(circle at 40% 40%, rgba(158, 206, 106, 0.02) 0%, transparent 50%);
|
|
507
|
+
pointer-events: none;
|
|
508
|
+
user-select: none;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.hitmarker {
|
|
512
|
+
position: absolute;
|
|
513
|
+
transform: translate(-50%, -50%);
|
|
514
|
+
pointer-events: none;
|
|
515
|
+
z-index: 100;
|
|
516
|
+
width: 40px;
|
|
517
|
+
height: 40px;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.hitmarker-line {
|
|
521
|
+
position: absolute;
|
|
522
|
+
width: 12px;
|
|
523
|
+
height: 3px;
|
|
524
|
+
background-color: #ffffff;
|
|
525
|
+
box-shadow: 0 0 8px rgba(255, 255, 255, 0.8);
|
|
526
|
+
border-radius: 1px;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.hitmarker-line.tl {
|
|
530
|
+
top: 25%;
|
|
531
|
+
left: 25%;
|
|
532
|
+
animation: hitmarkerFade45 0.6s ease-out forwards;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.hitmarker-line.tr {
|
|
536
|
+
top: 25%;
|
|
537
|
+
left: 75%;
|
|
538
|
+
animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.hitmarker-line.bl {
|
|
542
|
+
top: 75%;
|
|
543
|
+
left: 25%;
|
|
544
|
+
animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.hitmarker-line.br {
|
|
548
|
+
top: 75%;
|
|
549
|
+
left: 75%;
|
|
550
|
+
animation: hitmarkerFade45 0.6s ease-out forwards;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.status-overlay {
|
|
554
|
+
position: absolute;
|
|
555
|
+
bottom: 16px;
|
|
556
|
+
left: 50%;
|
|
557
|
+
transform: translateX(-50%);
|
|
558
|
+
z-index: 20;
|
|
559
|
+
display: flex;
|
|
560
|
+
flex-direction: column;
|
|
561
|
+
align-items: center;
|
|
562
|
+
gap: 8px;
|
|
563
|
+
max-width: 280px;
|
|
564
|
+
text-align: center;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.status-indicator {
|
|
568
|
+
display: flex;
|
|
569
|
+
align-items: center;
|
|
570
|
+
gap: 8px;
|
|
571
|
+
color: #787c99;
|
|
572
|
+
font-size: 13px;
|
|
573
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.status-icon {
|
|
577
|
+
width: 20px;
|
|
578
|
+
height: 20px;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.status-icon.spinning {
|
|
582
|
+
animation: spin 1s linear infinite;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.progress-bar {
|
|
586
|
+
width: 160px;
|
|
587
|
+
height: 4px;
|
|
588
|
+
background: rgba(65, 72, 104, 0.4);
|
|
589
|
+
border-radius: 2px;
|
|
590
|
+
overflow: hidden;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.progress-fill {
|
|
594
|
+
height: 100%;
|
|
595
|
+
background: hsl(var(--tn-cyan, 193 100% 75%));
|
|
596
|
+
transition: width 0.3s ease-out;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.retry-button {
|
|
600
|
+
padding: 6px 16px;
|
|
601
|
+
background: transparent;
|
|
602
|
+
border: 1px solid rgba(122, 162, 247, 0.4);
|
|
603
|
+
border-radius: 4px;
|
|
604
|
+
color: #7aa2f7;
|
|
605
|
+
font-size: 11px;
|
|
606
|
+
font-weight: 500;
|
|
607
|
+
cursor: pointer;
|
|
608
|
+
transition: all 0.2s ease;
|
|
609
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.retry-button:hover {
|
|
613
|
+
background: rgba(122, 162, 247, 0.1);
|
|
614
|
+
}
|
|
615
|
+
</style>
|
|
616
|
+
|
|
617
|
+
<div
|
|
618
|
+
bind:this={containerRef}
|
|
619
|
+
class="idle-container fw-player-root"
|
|
620
|
+
role="status"
|
|
621
|
+
aria-label="Stream status"
|
|
622
|
+
onmousemove={handleMouseMove}
|
|
623
|
+
onmouseleave={handleMouseLeave}
|
|
624
|
+
>
|
|
625
|
+
<!-- Hitmarkers -->
|
|
626
|
+
{#each hitmarkers as hitmarker (hitmarker.id)}
|
|
627
|
+
<div class="hitmarker" style="left: {hitmarker.x}px; top: {hitmarker.y}px;">
|
|
628
|
+
<div class="hitmarker-line tl"></div>
|
|
629
|
+
<div class="hitmarker-line tr"></div>
|
|
630
|
+
<div class="hitmarker-line bl"></div>
|
|
631
|
+
<div class="hitmarker-line br"></div>
|
|
632
|
+
</div>
|
|
633
|
+
{/each}
|
|
634
|
+
|
|
635
|
+
<!-- Floating particles -->
|
|
636
|
+
{#each particles as particle, i}
|
|
637
|
+
<div
|
|
638
|
+
class="particle"
|
|
639
|
+
style="
|
|
640
|
+
left: {particle.left}%;
|
|
641
|
+
width: {particle.size}px;
|
|
642
|
+
height: {particle.size}px;
|
|
643
|
+
background: {particle.color};
|
|
644
|
+
animation-duration: {particle.duration}s;
|
|
645
|
+
animation-delay: {particle.delay}s;
|
|
646
|
+
"
|
|
647
|
+
/>
|
|
648
|
+
{/each}
|
|
649
|
+
|
|
650
|
+
<!-- Animated bubbles -->
|
|
651
|
+
{#each bubbles as bubble, i}
|
|
652
|
+
<div
|
|
653
|
+
class="bubble"
|
|
654
|
+
style="
|
|
655
|
+
top: {bubble.position.top}%;
|
|
656
|
+
left: {bubble.position.left}%;
|
|
657
|
+
width: {bubble.size}px;
|
|
658
|
+
height: {bubble.size}px;
|
|
659
|
+
background: {bubble.color};
|
|
660
|
+
opacity: {bubble.opacity};
|
|
661
|
+
"
|
|
662
|
+
/>
|
|
663
|
+
{/each}
|
|
664
|
+
|
|
665
|
+
<!-- Center logo with push-away effect -->
|
|
666
|
+
<div
|
|
667
|
+
class="center-logo"
|
|
668
|
+
style="transform: translate(-50%, -50%) translate({offset.x}px, {offset.y}px);"
|
|
669
|
+
>
|
|
670
|
+
<!-- Pulsing circle background -->
|
|
671
|
+
<div
|
|
672
|
+
class="logo-pulse"
|
|
673
|
+
class:hovered={isHovered}
|
|
674
|
+
style="width: {logoSize * 1.4}px; height: {logoSize * 1.4}px;"
|
|
675
|
+
/>
|
|
676
|
+
|
|
677
|
+
<!-- Logo image -->
|
|
678
|
+
<img
|
|
679
|
+
src={logomarkAsset}
|
|
680
|
+
alt="Logo"
|
|
681
|
+
class="logo-image"
|
|
682
|
+
class:hovered={isHovered}
|
|
683
|
+
style="width: {logoSize}px; height: {logoSize}px;"
|
|
684
|
+
onclick={handleLogoClick}
|
|
685
|
+
draggable="false"
|
|
686
|
+
/>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<!-- Bouncing DVD Logo -->
|
|
690
|
+
<DvdLogo parentRef={containerRef} scale={0.08} />
|
|
691
|
+
|
|
692
|
+
<!-- Status overlay at bottom -->
|
|
693
|
+
<div class="status-overlay">
|
|
694
|
+
<div class="status-indicator">
|
|
695
|
+
<!-- Status icon -->
|
|
696
|
+
{#if isLoading}
|
|
697
|
+
<svg class="status-icon spinning" fill="none" viewBox="0 0 24 24" style="color: hsl(var(--tn-yellow, 40 95% 64%));">
|
|
698
|
+
<circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
699
|
+
<path style="opacity: 0.75;" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
700
|
+
</svg>
|
|
701
|
+
{:else if isOffline}
|
|
702
|
+
<svg class="status-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="color: hsl(var(--tn-red, 348 100% 72%));">
|
|
703
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
|
|
704
|
+
</svg>
|
|
705
|
+
{:else if isError}
|
|
706
|
+
<svg class="status-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="color: hsl(var(--tn-red, 348 100% 72%));">
|
|
707
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
708
|
+
</svg>
|
|
709
|
+
{:else}
|
|
710
|
+
<svg class="status-icon spinning" fill="none" viewBox="0 0 24 24" style="color: hsl(var(--tn-cyan, 193 100% 75%));">
|
|
711
|
+
<circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
712
|
+
<path style="opacity: 0.75;" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
713
|
+
</svg>
|
|
714
|
+
{/if}
|
|
715
|
+
<span>{displayMessage}</span>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
<!-- Progress bar -->
|
|
719
|
+
{#if showProgress}
|
|
720
|
+
<div class="progress-bar">
|
|
721
|
+
<div class="progress-fill" style="width: {Math.min(100, percentage ?? 0)}%;" />
|
|
722
|
+
</div>
|
|
723
|
+
{/if}
|
|
724
|
+
|
|
725
|
+
<!-- Retry button -->
|
|
726
|
+
{#if showRetry}
|
|
727
|
+
<button
|
|
728
|
+
type="button"
|
|
729
|
+
class="retry-button"
|
|
730
|
+
onclick={onRetry}
|
|
731
|
+
>
|
|
732
|
+
Retry
|
|
733
|
+
</button>
|
|
734
|
+
{/if}
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<!-- Subtle overlay texture -->
|
|
738
|
+
<div class="overlay-texture" />
|
|
739
|
+
</div>
|