atagen

malice

public
Star0
Watch0
4ac384dcce6c4c0b2d9806e2fa8512728546430d
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/NotificationSettings.svelteSvelte107 lines4.2 KB
1
<script lang="ts">
2
  import { disableNotifications, enableNotifications, notificationPermission, notificationsEnabled } from './notifications';
3
4
  import type { MessageSoundMode } from './message-sound-model';
5
  import { messageSound, playMessageSound, saveMessageSound, uploadMessageSound } from './message-sound';
6
7
  let busy = false;
8
  let soundBusy = false;
9
  let soundError = '';
10
  let fileInput: HTMLInputElement;
11
12
  const SOUND_MODES: { id: MessageSoundMode; label: string }[] = [
13
    { id: 'all', label: 'All messages' },
14
    { id: 'mentions_dms', label: 'Mentions and DMs' },
15
    { id: 'off', label: 'Off' }
16
  ];
17
18
  async function guarded(task: () => Promise<void>): Promise<void> {
19
    if (soundBusy) return;
20
    soundBusy = true;
21
    soundError = '';
22
    try {
23
      await task();
24
    } catch (cause) {
25
      soundError = cause instanceof Error ? cause.message : 'Could not save the sound settings.';
26
    } finally {
27
      soundBusy = false;
28
    }
29
  }
30
31
  async function upload(event: Event): Promise<void> {
32
    const input = event.currentTarget as HTMLInputElement;
33
    const file = input.files?.[0];
34
    input.value = '';
35
    if (file) await guarded(() => uploadMessageSound(file));
36
  }
37
38
  $: denied = $notificationPermission === 'denied';
39
  $: unsupported = $notificationPermission === 'unsupported';
40
  $: on = $notificationsEnabled && $notificationPermission === 'granted';
41
42
  async function toggle(): Promise<void> {
43
    if (busy) return;
44
    busy = true;
45
    try {
46
      if (on) disableNotifications();
47
      else await enableNotifications();
48
    } finally {
49
      busy = false;
50
    }
51
  }
52
</script>
53
54
<section aria-label="Notifications">
55
  <div class="sec-head">
56
    <h3 class="sec-h">Notifications</h3>
57
    <span class="state" class:ok={on}>{unsupported ? 'Unavailable' : denied ? 'Blocked' : on ? 'On' : 'Off'}</span>
58
  </div>
59
  <button type="button" class="ghost-btn" disabled={busy || denied || unsupported} aria-pressed={on} on:click={() => void toggle()}>
60
    {on ? 'Turn off' : 'Turn on'}
61
  </button>
62
  <div class="sound-modes" role="radiogroup" aria-label="Message sound">
63
    {#each SOUND_MODES as option (option.id)}
64
      <button
65
        type="button"
66
        role="radio"
67
        aria-checked={$messageSound.mode === option.id}
68
        class:active={$messageSound.mode === option.id}
69
        disabled={soundBusy}
70
        on:click={() => void guarded(() => saveMessageSound({ ...$messageSound, mode: option.id }))}
71
      >
72
        {option.label}
73
      </button>
74
    {/each}
75
  </div>
76
  <div class="sound-actions">
77
    <button type="button" class="ghost-btn" on:click={() => void playMessageSound()}>Play</button>
78
    <button type="button" class="ghost-btn" disabled={soundBusy} on:click={() => fileInput.click()}>Upload sound</button>
79
    <button
80
      type="button"
81
      class="ghost-btn"
82
      disabled={soundBusy || $messageSound.sound === null}
83
      on:click={() => void guarded(() => saveMessageSound({ ...$messageSound, sound: null }))}
84
    >
85
      Reset
86
    </button>
87
    <input bind:this={fileInput} type="file" accept="audio/*" hidden on:change={(e) => void upload(e)} />
88
  </div>
89
  {#if soundError}
90
    <p class="sound-error" role="alert">{soundError}</p>
91
  {/if}
92
</section>
93
94
<style>
95
  section { margin-top: 18px; }
96
  .sec-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
97
  .sec-h { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin: 0; }
98
  .state { font-size: 12px; color: var(--muted); }
99
  .state.ok { color: var(--accent); }
100
  .sound-modes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
101
  .sound-modes button { border: 1px solid var(--border); background: transparent; color: var(--muted); border-radius: var(--r-sm); padding: 3px 10px; font: inherit; font-size: 12.5px; cursor: pointer; transition: color var(--pop), border-color var(--pop), background var(--pop); }
102
  .sound-modes button:hover:not(:disabled) { color: var(--text); }
103
  .sound-modes button.active { color: var(--text); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
104
  .sound-modes button:disabled { cursor: wait; }
105
  .sound-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
106
  .sound-error { margin: 8px 0 0; color: var(--danger); font-size: 13px; }
107
</style>