|
1 | 1 | <script lang="ts"> |
| 2 | + import { DragDropProvider } from "@dnd-kit-svelte/svelte"; |
| 3 | + import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers"; |
| 4 | + import { move } from "@dnd-kit/helpers"; |
2 | 5 | import { onDestroy } from "svelte"; |
3 | 6 | import { flip } from "svelte/animate"; |
4 | | - import Users from "~icons/ph/users-bold"; |
5 | 7 | import { app } from "$lib/app.svelte"; |
6 | 8 | import type { Channel } from "$lib/models/channel.svelte"; |
| 9 | + import { settings } from "$lib/settings"; |
| 10 | + import Draggable from "./Draggable.svelte"; |
| 11 | + import Droppable from "./Droppable.svelte"; |
| 12 | + import { getSidebarContext } from "./Sidebar.svelte"; |
7 | 13 | import StreamTooltip from "./StreamTooltip.svelte"; |
8 | 14 | import { Separator } from "./ui/separator"; |
9 | 15 |
|
10 | | - const { collapsed = false } = $props(); |
| 16 | + const sidebar = getSidebarContext(); |
11 | 17 |
|
12 | 18 | const sorted = $derived( |
13 | 19 | app.channels |
|
26 | 32 | ); |
27 | 33 |
|
28 | 34 | const groups = $derived.by(() => { |
29 | | - const ephemeral = { type: "Ephemeral", channels: sorted.filter((c) => c.ephemeral) }; |
| 35 | + const pinnedChannels = sorted |
| 36 | + .filter((c) => c.pinned) |
| 37 | + .sort((a, b) => { |
| 38 | + const indexA = settings.state.pinned.indexOf(a.id); |
| 39 | + const indexB = settings.state.pinned.indexOf(b.id); |
| 40 | +
|
| 41 | + return indexA - indexB; |
| 42 | + }); |
30 | 43 |
|
| 44 | + const pinned = { type: "Pinned", channels: pinnedChannels }; |
| 45 | + const ephemeral = { type: "Ephemeral", channels: [] as Channel[] }; |
31 | 46 | const online = { type: "Online", channels: [] as Channel[] }; |
32 | 47 | const offline = { type: "Offline", channels: [] as Channel[] }; |
33 | 48 |
|
34 | 49 | for (const channel of sorted) { |
35 | | - if (channel.id === app.user?.id || channel.ephemeral) continue; |
| 50 | + if (channel.id === app.user?.id || channel.pinned) { |
| 51 | + continue; |
| 52 | + } |
36 | 53 |
|
37 | | - if (channel.stream) { |
| 54 | + if (channel.ephemeral) { |
| 55 | + ephemeral.channels.push(channel); |
| 56 | + } else if (channel.stream) { |
38 | 57 | online.channels.push(channel); |
39 | 58 | } else { |
40 | 59 | offline.channels.push(channel); |
41 | 60 | } |
42 | 61 | } |
43 | 62 |
|
44 | | - return [ephemeral, online, offline].filter((g) => g.channels.length); |
| 63 | + return [pinned, ephemeral, online, offline].filter((g) => g.channels.length); |
45 | 64 | }); |
46 | 65 |
|
47 | 66 | const interval = setInterval( |
|
58 | 77 | ); |
59 | 78 |
|
60 | 79 | onDestroy(() => clearInterval(interval)); |
61 | | -
|
62 | | - function formatViewers(viewers: number) { |
63 | | - if (viewers >= 1000) { |
64 | | - return `${(viewers / 1000).toFixed(1)}K`; |
65 | | - } |
66 | | -
|
67 | | - return viewers.toString(); |
68 | | - } |
69 | 80 | </script> |
70 | 81 |
|
71 | | -{#each groups as group} |
72 | | - {#if collapsed} |
73 | | - <Separator /> |
74 | | - {:else} |
75 | | - <span class="text-muted-foreground mt-2 inline-block px-2 text-xs font-semibold uppercase"> |
76 | | - {group.type} |
77 | | - </span> |
78 | | - {/if} |
79 | | - |
80 | | - {#each group.channels as channel (channel.user.id)} |
81 | | - <div class="px-1.5" animate:flip={{ duration: 500 }}> |
82 | | - <StreamTooltip {channel} {collapsed}> |
83 | | - {#if !collapsed} |
84 | | - {#if channel.stream} |
85 | | - <div class="min-w-0 flex-1"> |
86 | | - <div class="flex items-center justify-between"> |
87 | | - <div |
88 | | - class="text-sidebar-foreground flex items-center gap-x-1 truncate text-sm font-medium" |
89 | | - > |
90 | | - {channel.user.displayName} |
91 | | - |
92 | | - {#if channel.stream.guests.size} |
93 | | - <span class="text-xs">+{channel.stream.guests.size}</span> |
94 | | - {/if} |
95 | | - </div> |
96 | | - |
97 | | - <div |
98 | | - class="flex items-center gap-1 text-xs font-medium text-red-400" |
99 | | - > |
100 | | - <Users /> |
101 | | - {formatViewers(channel.stream.viewers)} |
102 | | - </div> |
103 | | - </div> |
104 | | - |
105 | | - <p class="text-muted-foreground truncate text-xs"> |
106 | | - {channel.stream.game} |
107 | | - </p> |
108 | | - </div> |
109 | | - {:else} |
110 | | - <span class="text-muted-foreground truncate text-sm font-medium"> |
111 | | - {channel.user.displayName} |
112 | | - </span> |
113 | | - {/if} |
114 | | - {/if} |
115 | | - </StreamTooltip> |
116 | | - </div> |
| 82 | +<DragDropProvider |
| 83 | + modifiers={[ |
| 84 | + // @ts-expect-error - type mismatch |
| 85 | + RestrictToVerticalAxis, |
| 86 | + ]} |
| 87 | + onDragOver={(event) => { |
| 88 | + settings.state.pinned = move(settings.state.pinned, event); |
| 89 | + }} |
| 90 | +> |
| 91 | + {#each groups as group} |
| 92 | + {#if sidebar.collapsed} |
| 93 | + <Separator /> |
| 94 | + {:else} |
| 95 | + <span |
| 96 | + class="text-muted-foreground mt-2 inline-block px-2 text-xs font-semibold uppercase" |
| 97 | + > |
| 98 | + {group.type} |
| 99 | + </span> |
| 100 | + {/if} |
| 101 | + |
| 102 | + {#if group.type === "Pinned"} |
| 103 | + <Droppable id="pinned-channels" class="space-y-1.5"> |
| 104 | + {#each group.channels as channel, i (channel.user.id)} |
| 105 | + <Draggable id={channel.id} index={i} {channel} /> |
| 106 | + {/each} |
| 107 | + </Droppable> |
| 108 | + {:else} |
| 109 | + {#each group.channels as channel (channel.user.id)} |
| 110 | + <div class="px-1.5" animate:flip={{ duration: 500 }}> |
| 111 | + <StreamTooltip {channel} /> |
| 112 | + </div> |
| 113 | + {/each} |
| 114 | + {/if} |
117 | 115 | {/each} |
118 | | -{/each} |
| 116 | +</DragDropProvider> |
0 commit comments