+107
src/lib/components/Player1.svelte
+107
src/lib/components/Player1.svelte
···
1
+
<script lang="ts">
2
+
import {
3
+
List,
4
+
Pause,
5
+
Play,
6
+
Repeat,
7
+
Shuffle,
8
+
SkipBack,
9
+
SkipForward,
10
+
Volume2,
11
+
type Icon as LucideIcon,
12
+
} from "@lucide/svelte";
13
+
import { Slider } from "bits-ui";
14
+
import cn from "clsx";
15
+
16
+
let playing = $state(true);
17
+
let shuffle = $state(false);
18
+
let repeat = $state(false);
19
+
20
+
const MainIcon = $derived(playing ? Pause : Play);
21
+
22
+
const songLength = 256;
23
+
let playback = $state(0);
24
+
</script>
25
+
26
+
{#snippet plainButton(Icon: typeof LucideIcon, label: string)}
27
+
<button class="flex cursor-pointer" aria-label={label}>
28
+
<Icon />
29
+
</button>
30
+
{/snippet}
31
+
32
+
{#snippet clickable(content: string)}
33
+
<span class="cursor-pointer hover:underline">{content}</span>
34
+
{/snippet}
35
+
36
+
<!-- TODO: labelled by the artist & title -->
37
+
<aside
38
+
class="fixed right-2 bottom-2 left-2 flex items-center gap-4 rounded-lg border border-slate-300 bg-white p-2 px-4 text-slate-500"
39
+
>
40
+
<div class="flex items-center gap-2 text-slate-900">
41
+
{@render plainButton(SkipBack, "Previous song")}
42
+
<button
43
+
class="flex cursor-pointer items-center justify-center rounded-full bg-orange-500 p-2 text-white"
44
+
aria-label="Play"
45
+
onclick={() => (playing = !playing)}
46
+
>
47
+
<MainIcon />
48
+
</button>
49
+
{@render plainButton(SkipForward, "Next song")}
50
+
</div>
51
+
52
+
<div class="flex items-center gap-2">
53
+
<img
54
+
src="https://lh3.googleusercontent.com/0z6Kg2GFi8hFgZYxWm3c3UNul0gyaCQjuqmY-p1oeFC1n5EMOf1dxrownTzhzk-_cdtO_FLLktQcMecwGQ=w544-h544-l90-rj"
55
+
class="h-12 w-12 rounded object-cover object-center"
56
+
alt=""
57
+
/>
58
+
<div class="flex flex-col">
59
+
<span class="text-sm font-semibold text-slate-900 opacity-70">
60
+
{@render clickable("Protostar")}, {@render clickable("Laminar")} & {@render clickable(
61
+
"imallryt",
62
+
)}
63
+
</span>
64
+
<span class="font-bolder text-sm font-semibold text-slate-900">
65
+
{@render clickable("Blood in the Water")}
66
+
<!-- <span class="opacity-50">| {@render clickable("Epic Album")}</span> -->
67
+
</span>
68
+
</div>
69
+
</div>
70
+
71
+
<div class="flex flex-1 px-30">
72
+
<Slider.Root
73
+
type="single"
74
+
bind:value={playback}
75
+
max={songLength}
76
+
class="relative flex flex-1 touch-none items-center select-none"
77
+
>
78
+
{#snippet children()}
79
+
<span
80
+
class="relative h-1 w-full cursor-pointer overflow-hidden rounded-full bg-slate-200"
81
+
>
82
+
<Slider.Range class="absolute h-full rounded-full bg-orange-500" />
83
+
</span>
84
+
<Slider.Thumb
85
+
index={0}
86
+
class="block size-4 cursor-pointer rounded-full border border-slate-900 bg-slate-50 focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-offset-2 "
87
+
/>
88
+
{/snippet}
89
+
</Slider.Root>
90
+
</div>
91
+
92
+
<button
93
+
class={cn("flex", "cursor-pointer", { "text-orange-500": shuffle })}
94
+
onclick={() => (shuffle = !shuffle)}
95
+
>
96
+
<Shuffle />
97
+
</button>
98
+
<button
99
+
class={cn("flex", "cursor-pointer", { "text-orange-500": repeat })}
100
+
onclick={() => (repeat = !repeat)}
101
+
>
102
+
<Repeat />
103
+
</button>
104
+
105
+
<List class="cursor-pointer" />
106
+
<Volume2 class="cursor-pointer" />
107
+
</aside>
+201
src/lib/components/Player2.svelte
+201
src/lib/components/Player2.svelte
···
1
+
<script lang="ts">
2
+
import {
3
+
ChevronDown,
4
+
List,
5
+
Pause,
6
+
Play,
7
+
Repeat,
8
+
Repeat1,
9
+
Shuffle,
10
+
SkipBack,
11
+
SkipForward,
12
+
Volume2,
13
+
type Icon as LucideIcon,
14
+
} from "@lucide/svelte";
15
+
import { Slider } from "bits-ui";
16
+
import cn from "clsx";
17
+
18
+
let expanded = $state(true);
19
+
let playing = $state(false);
20
+
let shuffle = $state(false);
21
+
let repeat: "none" | "all" | "one" = $state("none");
22
+
let interval: ReturnType<typeof setInterval>;
23
+
24
+
const MainIcon = $derived(playing ? Pause : Play);
25
+
const RepeatIcon = $derived.by(() => (repeat === "one" ? Repeat1 : Repeat));
26
+
27
+
// TODO: separate progress state so that the thumb does not jump around to it's real value while the user is dragging it.
28
+
// Probably done through checking click state of the thumb and temporarily disconnecting the progress?
29
+
const songLength = 256;
30
+
let playback = $state(0);
31
+
32
+
$effect(() => {
33
+
// console.log({ playback });
34
+
35
+
if (playing)
36
+
interval = setInterval(() => {
37
+
playback++;
38
+
if (playback > songLength) playback = 0;
39
+
}, 1000);
40
+
else clearInterval(interval);
41
+
});
42
+
43
+
// TODO: see if I need to i18n time format.
44
+
const formatTime = (inputSeconds: number) => {
45
+
const minutes = Math.floor(inputSeconds / 60);
46
+
const seconds = `${inputSeconds % 60}`.padStart(2, "0");
47
+
48
+
return `${minutes}:${seconds}`;
49
+
};
50
+
51
+
const cycleRepeat = () => {
52
+
if (repeat === "none") repeat = "all";
53
+
else if (repeat === "all") repeat = "one";
54
+
else repeat = "none";
55
+
};
56
+
</script>
57
+
58
+
{#snippet plainButton(Icon: typeof LucideIcon, label: string)}
59
+
<button class="flex cursor-pointer" aria-label={label}>
60
+
<Icon />
61
+
</button>
62
+
{/snippet}
63
+
64
+
{#snippet clickable(content: string)}
65
+
<span class="cursor-pointer hover:underline">{content}</span>
66
+
{/snippet}
67
+
68
+
<!-- TODO: labelled by the artist & title -->
69
+
<!-- TODO: keep width when collapsed? -->
70
+
<aside
71
+
class="fixed bottom-2 left-2 flex flex-col gap-2 overflow-hidden rounded-lg border border-slate-300 bg-white"
72
+
>
73
+
<header class="flex gap-2 bg-black p-2 text-slate-50">
74
+
<button
75
+
class={cn("flex cursor-pointer transition-transform", {
76
+
"rotate-180": !expanded,
77
+
})}
78
+
onclick={() => (expanded = !expanded)}
79
+
>
80
+
<ChevronDown />
81
+
</button>
82
+
{#if expanded}
83
+
<span class="font-bold">Now Playing</span>
84
+
<div class="flex-1"></div>
85
+
86
+
<button
87
+
class={cn("flex", "cursor-pointer", { "text-orange-500": shuffle })}
88
+
onclick={() => (shuffle = !shuffle)}
89
+
>
90
+
<Shuffle />
91
+
</button>
92
+
<button
93
+
class={cn("flex", "cursor-pointer", {
94
+
"text-orange-500": repeat !== "none",
95
+
})}
96
+
onclick={cycleRepeat}
97
+
>
98
+
<RepeatIcon />
99
+
</button>
100
+
101
+
<List class="cursor-pointer" />
102
+
<Volume2 class="cursor-pointer" />
103
+
{:else}
104
+
<div class="flex gap-1">
105
+
<button
106
+
class="flex cursor-pointer"
107
+
aria-label={playing ? "Pause current song" : "Play current song"}
108
+
onclick={() => (playing = !playing)}
109
+
>
110
+
<MainIcon />
111
+
</button>
112
+
<!-- TODO: scrolling text -->
113
+
<div class="line-clamp-1 max-w-[300px] text-ellipsis">
114
+
<span>Protostar, Laminar, imallryt</span>
115
+
-
116
+
<span>Blood in the Water</span>
117
+
</div>
118
+
119
+
<Volume2 class="cursor-pointer" />
120
+
</div>
121
+
{/if}
122
+
</header>
123
+
124
+
{#if expanded}
125
+
<div class="flex flex-col gap-2 px-4 py-2 text-slate-500">
126
+
<div class="flex items-center gap-4">
127
+
<div class="flex items-center gap-2 text-slate-900">
128
+
{@render plainButton(SkipBack, "Previous song")}
129
+
<button
130
+
class="flex cursor-pointer items-center justify-center rounded-full bg-orange-500 p-2 text-white"
131
+
aria-label={playing ? "Pause current song" : "Play current song"}
132
+
onclick={() => (playing = !playing)}
133
+
>
134
+
<MainIcon />
135
+
</button>
136
+
{@render plainButton(SkipForward, "Next song")}
137
+
</div>
138
+
139
+
<div class="flex items-center gap-2">
140
+
<img
141
+
src="https://lh3.googleusercontent.com/0z6Kg2GFi8hFgZYxWm3c3UNul0gyaCQjuqmY-p1oeFC1n5EMOf1dxrownTzhzk-_cdtO_FLLktQcMecwGQ=w544-h544-l90-rj"
142
+
class="h-12 w-12 rounded object-cover object-center"
143
+
alt=""
144
+
/>
145
+
<!-- TODO: max width with scrolling texts -->
146
+
<div class="flex flex-col">
147
+
<span class="text-sm font-semibold text-slate-900 opacity-70">
148
+
{@render clickable("Protostar")}, {@render clickable("Laminar")} &
149
+
{@render clickable("imallryt")}
150
+
</span>
151
+
<span class="font-bolder text-sm font-semibold text-slate-900">
152
+
{@render clickable("Blood in the Water")}
153
+
<!-- <span class="opacity-50">| {@render clickable("Epic Album")}</span> -->
154
+
</span>
155
+
</div>
156
+
</div>
157
+
</div>
158
+
159
+
<div class="flex w-full gap-2 py-2">
160
+
<Slider.Root
161
+
type="single"
162
+
max={songLength}
163
+
class="relative flex flex-1 touch-none items-center select-none"
164
+
value={playback}
165
+
onValueCommit={(value) => (playback = value)}
166
+
>
167
+
{#snippet children()}
168
+
<span
169
+
class="relative h-1 w-full cursor-pointer overflow-hidden rounded-full bg-slate-200"
170
+
>
171
+
<Slider.Range
172
+
class="absolute h-full rounded-full bg-orange-500"
173
+
/>
174
+
</span>
175
+
<Slider.Thumb
176
+
index={0}
177
+
class="block size-4 cursor-pointer rounded-full border border-slate-900 bg-slate-50 focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-offset-2 "
178
+
/>
179
+
{/snippet}
180
+
</Slider.Root>
181
+
182
+
<span class="text-sm">
183
+
{formatTime(playback)}/{formatTime(songLength)}
184
+
</span>
185
+
186
+
<!-- <button
187
+
class={cn("flex", "cursor-pointer", { "text-orange-500": shuffle })}
188
+
onclick={() => (shuffle = !shuffle)}
189
+
>
190
+
<Shuffle />
191
+
</button>
192
+
<button
193
+
class={cn("flex", "cursor-pointer", { "text-orange-500": repeat })}
194
+
onclick={() => (repeat = !repeat)}
195
+
>
196
+
<Repeat />
197
+
</button> -->
198
+
</div>
199
+
</div>
200
+
{/if}
201
+
</aside>
+9
-1
src/routes/+layout.svelte
+9
-1
src/routes/+layout.svelte
···
1
1
<script lang="ts">
2
+
import Navbar from "$lib/components/Navbar.svelte";
3
+
import Player1 from "$lib/components/Player1.svelte";
4
+
import Player2 from "$lib/components/Player2.svelte";
2
5
import "../app.css";
3
6
4
7
let { children } = $props();
5
8
</script>
6
9
7
-
{@render children()}
10
+
<Navbar />
11
+
<main class="m-2 flex flex-col items-center">
12
+
{@render children()}
13
+
</main>
14
+
<!-- <Player1 /> -->
15
+
<Player2 />
+32
-5
src/routes/+page.svelte
+32
-5
src/routes/+page.svelte
···
1
-
<h1>Welcome to SvelteKit</h1>
2
-
<p>
3
-
Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the
4
-
documentation
5
-
</p>
1
+
<section class="flex flex-col items-center gap-2 pt-10">
2
+
<header class="flex flex-col items-center">
3
+
<h1 class="text-5xl font-bold tracking-tighter text-orange-600">Comet</h1>
4
+
<p class="flex flex-col items-center text-center text-lg">
5
+
Your music, on ATProto.
6
+
</p>
7
+
</header>
8
+
9
+
<div>
10
+
<h2 class="text-2xl font-bold tracking-tighter text-orange-600">Why?</h2>
11
+
<ol class="list-disc">
12
+
<li>
13
+
free yourself from Big Tech™️; don't let them tell you what music you
14
+
can and can't make
15
+
</li>
16
+
<li>
17
+
Listen to music in full* quality, without being subject to horrendous
18
+
sounding data compression.
19
+
</li>
20
+
<li>
21
+
if I die, all your data is yours, and can be used by other projects!
22
+
</li>
23
+
<li>
24
+
Integrate your playback with <span class="text-teal-800 underline">
25
+
teal.fm
26
+
</span>
27
+
and let everyone else know what you're listening to!
28
+
</li>
29
+
<li>borpa</li>
30
+
</ol>
31
+
</div>
32
+
</section>