1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { desktop } from '$lib/states/desktop.svelte';
4 import { chatState } from '$lib/states/chat.svelte';
5 import { soundsEnabled, setSoundsEnabled, unlockAudio } from '$lib/aim/sounds.svelte';
6
7 let menuOpen = $state(false);
8 const soundOn = $derived(soundsEnabled());
9 let clock = $state('');
10 let menuElement = $state<HTMLElement>();
11 let startElement = $state<HTMLButtonElement>();
12 const user = $derived(chatState.getCurrentUser());
13
14 function updateClock() {
15 clock = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
16 }
17
18 onMount(() => {
19 updateClock();
20 const timer = setInterval(updateClock, 30_000);
21 function outside(event: PointerEvent) {
22 if (
23 menuOpen &&
24 !menuElement?.contains(event.target as Node) &&
25 !startElement?.contains(event.target as Node)
26 )
27 menuOpen = false;
28 }
29 function escape(event: KeyboardEvent) {
30 if (event.key === 'Escape') menuOpen = false;
31 }
32 document.addEventListener('pointerdown', outside);
33 document.addEventListener('keydown', escape);
34 return () => {
35 clearInterval(timer);
36 document.removeEventListener('pointerdown', outside);
37 document.removeEventListener('keydown', escape);
38 };
39 });
40
41 function choose(action: () => void) {
42 menuOpen = false;
43 action();
44 }
45
46 async function logout() {
47 const response = await fetch('/api/session/logout', { method: 'POST' });
48 if (response.ok) window.location.reload();
49 }
50</script>
51
52<div class="taskbar">
53 <button
54 class="start"
55 bind:this={startElement}
56 aria-expanded={menuOpen}
57 aria-controls="start-menu"
58 onclick={() => (menuOpen = !menuOpen)}><span class="start-label">démarrer</span></button
59 >
60 {#if menuOpen}
61 <div class="start-menu" id="start-menu" bind:this={menuElement}>
62 <div class="menu-header">{user?.nickname ?? 'Invité'}</div>
63 <button onclick={() => choose(() => desktop.openBuddyList())}
64 >PDR AIM — Liste de contacts</button
65 >
66 <button onclick={() => choose(() => desktop.openChatRoom(null, 'General'))}
67 >Salon General</button
68 >
69 <button onclick={() => choose(() => desktop.openNewRoom())}>Nouveau salon…</button>
70 <hr />
71 <button onclick={() => choose(() => desktop.openPreferences())}>Préférences</button>
72 <button onclick={() => choose(() => desktop.openAbout())}>À propos</button>
73 <hr />
74 {#if user}
75 <button
76 onclick={() => {
77 menuOpen = false;
78 void logout();
79 }}>Se déconnecter</button
80 >
81 {:else}
82 <button onclick={() => choose(() => desktop.openLogin('signin'))}>Se connecter</button>
83 <button onclick={() => choose(() => desktop.openLogin('signup'))}>S'inscrire</button>
84 {/if}
85 </div>
86 {/if}
87 <div class="tasks">
88 {#each desktop.taskbarWindows as w (w.id)}
89 <button
90 class="task"
91 class:pressed={desktop.focusedId === w.id}
92 class:minimized={w.minimized}
93 class:attention={w.attention}
94 title={w.title}
95 onclick={() => desktop.toggleFromTaskbar(w.id)}
96 ><img src={w.icon} alt="" width="16" height="16" /><span>{w.title}</span></button
97 >
98 {/each}
99 </div>
100 <div class="tray">
101 <button
102 class="tray-button"
103 title={soundOn ? 'Désactiver les sons' : 'Activer les sons'}
104 aria-label={soundOn ? 'Désactiver les sons' : 'Activer les sons'}
105 onclick={() => {
106 unlockAudio();
107 setSoundsEnabled(!soundOn);
108 }}
109 ><img
110 src={soundOn ? '/aim/xp-volume-16.png' : '/aim/xp-volume-muted-16.png'}
111 alt=""
112 width="16"
113 height="16"
114 class:muted={!soundOn}
115 /></button
116 >
117 <img
118 src="/aim/running-man-16.png"
119 alt=""
120 title={user ? `Statut : ${user.status}` : 'Hors ligne'}
121 width="16"
122 height="16"
123 />
124 <time>{clock}</time>
125 </div>
126</div>
127
128<style>
129 .taskbar {
130 position: fixed;
131 bottom: 0;
132 left: 0;
133 right: 0;
134 height: 30px;
135 z-index: 5000;
136 display: flex;
137 align-items: stretch;
138 gap: 4px;
139 background: linear-gradient(
140 to bottom,
141 #1f2f86 0,
142 #3165c4 3%,
143 #3682e5 6%,
144 #4490e6 10%,
145 #3883e5 12%,
146 #2b71e0 15%,
147 #2663da 18%,
148 #235bd6 20%,
149 #2258d5 23%,
150 #2157d6 38%,
151 #245ddb 54%,
152 #2562df 86%,
153 #245fdc 89%,
154 #2158d4 92%,
155 #1d4ec0 95%,
156 #1941a5 98%
157 );
158 border-top: 0;
159 box-sizing: border-box;
160 color: white;
161 font:
162 11px Tahoma,
163 'Pixelated MS Sans Serif',
164 sans-serif;
165 }
166 /* Real XP "start" button: 3-slice of the original bitmap (logo | stretch | rounded cap) */
167 .start {
168 display: flex;
169 align-items: center;
170 border: 0;
171 border-radius: 0;
172 height: 30px;
173 padding: 0 17px 0 34px;
174 margin: 0;
175 background:
176 url('/desktop/start-left.png') left / auto 100% no-repeat,
177 url('/desktop/start-right.png') right / auto 100% no-repeat,
178 url('/desktop/start-mid.png') 34px 0 / calc(100% - 51px) 100% no-repeat;
179 color: white;
180 font:
181 bold italic 15px 'Franklin Gothic Medium',
182 'Trebuchet MS',
183 Tahoma,
184 sans-serif;
185 cursor: pointer;
186 text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6);
187 }
188 .start:hover {
189 filter: brightness(1.08);
190 }
191 .start:active,
192 .start[aria-expanded='true'] {
193 filter: brightness(0.85);
194 }
195 .start-label {
196 line-height: 1;
197 padding-bottom: 2px;
198 }
199 .start-menu {
200 position: absolute;
201 bottom: 30px;
202 left: 0;
203 width: 250px;
204 background: white;
205 border: 2px solid #245edb;
206 box-shadow: 2px 2px 8px #3339;
207 color: #222;
208 padding-bottom: 4px;
209 }
210 .menu-header {
211 background: linear-gradient(#245edb, #4f91ef);
212 color: white;
213 font-weight: bold;
214 padding: 10px;
215 }
216 .start-menu button {
217 display: block;
218 width: 100%;
219 text-align: left;
220 background: transparent;
221 border: 0;
222 box-shadow: none;
223 border-radius: 0;
224 padding: 6px 12px;
225 font: inherit;
226 color: #222;
227 cursor: pointer;
228 }
229 .start-menu button:hover,
230 .start-menu button:focus-visible {
231 background: #316ac5;
232 color: white;
233 }
234 .start-menu hr {
235 border: 0;
236 border-top: 1px solid #ccc;
237 margin: 4px 8px;
238 }
239 .tasks {
240 display: flex;
241 gap: 3px;
242 align-items: center;
243 flex: 1;
244 overflow: hidden;
245 min-width: 0;
246 }
247 .task {
248 display: flex;
249 align-items: center;
250 gap: 4px;
251 width: 160px;
252 max-width: 160px;
253 min-width: 36px;
254 height: 24px;
255 flex-shrink: 1;
256 color: white;
257 background: #ffffff30;
258 border: 1px solid #ffffff44;
259 border-radius: 3px;
260 padding: 2px 6px;
261 font: inherit;
262 text-align: left;
263 cursor: pointer;
264 }
265 .task span {
266 overflow: hidden;
267 text-overflow: ellipsis;
268 white-space: nowrap;
269 }
270 .task img {
271 flex: 0 0 16px;
272 }
273 .task.pressed {
274 background: #103b96;
275 box-shadow: inset 1px 1px 3px #082a76;
276 }
277 .task.minimized {
278 background: #ffffff18;
279 }
280 .task.attention {
281 animation: flash 1s infinite alternate;
282 }
283 @keyframes flash {
284 from {
285 background: #e69a12;
286 }
287 to {
288 background: #ffdc55;
289 color: #222;
290 }
291 }
292 .tray {
293 display: flex;
294 align-items: center;
295 gap: 8px;
296 padding: 0 8px;
297 background: linear-gradient(#0c91e8, #1684d1);
298 border-left: 1px solid #78baff;
299 white-space: nowrap;
300 }
301 .tray-button img.muted {
302 opacity: 0.45;
303 filter: grayscale(1);
304 }
305
306 .tray-button {
307 min-width: 0;
308 padding: 0;
309 border: 0;
310 box-shadow: none;
311 background: none;
312 cursor: pointer;
313 font-size: 14px;
314 }
315 .tray time {
316 font: inherit;
317 }
318</style>