tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
Initial template
Harry L
2 weeks ago
e13bca21
7f766672
+183
4 changed files
expand all
collapse all
unified
split
src
lib
cards
index.ts
media
RockskyPlaysCard
AlbumArt.svelte
RockskyPlaysCard.svelte
index.ts
+2
src/lib/cards/index.ts
···
23
23
import { GifCardDefinition } from './media/GIFCard';
24
24
import { PopfeedReviewsCardDefinition } from './media/PopfeedReviews';
25
25
import { TealFMPlaysCardDefinition } from './media/TealFMPlaysCard';
26
26
+
import { RockskyPlaysCardDefinition } from './media/RockskyPlaysCard';
26
27
import { PhotoGalleryCardDefinition } from './media/PhotoGalleryCard';
27
28
import { StandardSiteDocumentListCardDefinition } from './content/StandardSiteDocumentListCard';
28
29
import { StatusphereCardDefinition } from './media/StatusphereCard';
···
81
82
GifCardDefinition,
82
83
PopfeedReviewsCardDefinition,
83
84
TealFMPlaysCardDefinition,
85
85
+
RockskyPlaysCardDefinition,
84
86
PhotoGalleryCardDefinition,
85
87
StandardSiteDocumentListCardDefinition,
86
88
StatusphereCardDefinition,
+39
src/lib/cards/media/RockskyPlaysCard/AlbumArt.svelte
···
1
1
+
<script lang="ts">
2
2
+
let { releaseMbId, alt }: { releaseMbId?: string; alt: string } = $props();
3
3
+
4
4
+
let isLoading = $state(true);
5
5
+
let hasError = $state(false);
6
6
+
</script>
7
7
+
8
8
+
{#if isLoading}
9
9
+
<div class="bg-base-200 dark:bg-base-800 h-10 w-10 animate-pulse rounded-lg"></div>
10
10
+
{/if}
11
11
+
12
12
+
{#if hasError}
13
13
+
<div
14
14
+
class="bg-base-300 dark:bg-base-800 accent:bg-accent-700/50 flex h-10 w-10 items-center justify-center rounded-lg"
15
15
+
>
16
16
+
<svg
17
17
+
class="text-base-400 dark:text-base-600 accent:text-accent-900 h-5 w-5"
18
18
+
fill="currentColor"
19
19
+
viewBox="0 0 20 20"
20
20
+
>
21
21
+
<path
22
22
+
d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z"
23
23
+
/>
24
24
+
</svg>
25
25
+
</div>
26
26
+
{:else}
27
27
+
<img
28
28
+
src="https://coverartarchive.org/release/{releaseMbId}/front-250"
29
29
+
{alt}
30
30
+
class="h-10 w-10 rounded-lg object-cover {isLoading && 'hidden'}"
31
31
+
onload={() => {
32
32
+
isLoading = false;
33
33
+
}}
34
34
+
onerror={() => {
35
35
+
isLoading = false;
36
36
+
hasError = true;
37
37
+
}}
38
38
+
/>
39
39
+
{/if}
+111
src/lib/cards/media/RockskyPlaysCard/RockskyPlaysCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import type { Item } from '$lib/types';
3
3
+
import { onMount } from 'svelte';
4
4
+
import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context';
5
5
+
import { CardDefinitionsByType } from '../..';
6
6
+
// import AlbumArt from './AlbumArt.svelte';
7
7
+
import { RelativeTime } from '@foxui/time';
8
8
+
9
9
+
interface Artist {
10
10
+
artistName: string;
11
11
+
}
12
12
+
13
13
+
interface PlayValue {
14
14
+
releaseMbId?: string;
15
15
+
trackName: string;
16
16
+
playedTime?: string;
17
17
+
artists?: Artist[];
18
18
+
originUrl?: string;
19
19
+
}
20
20
+
21
21
+
interface Play {
22
22
+
uri: string;
23
23
+
value: PlayValue;
24
24
+
}
25
25
+
26
26
+
let { item }: { item: Item } = $props();
27
27
+
28
28
+
const data = getAdditionalUserData();
29
29
+
// svelte-ignore state_referenced_locally
30
30
+
let feed = $state(data[item.cardType] as Play[] | undefined);
31
31
+
32
32
+
let did = getDidContext();
33
33
+
let handle = getHandleContext();
34
34
+
35
35
+
onMount(async () => {
36
36
+
if (feed) return;
37
37
+
38
38
+
feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], {
39
39
+
did,
40
40
+
handle
41
41
+
})) as Play[] | undefined;
42
42
+
43
43
+
data[item.cardType] = feed;
44
44
+
});
45
45
+
46
46
+
function isNumeric(str: string) {
47
47
+
if (typeof str != 'string') return false;
48
48
+
return !isNaN(Number(str)) && !isNaN(parseFloat(str));
49
49
+
}
50
50
+
</script>
51
51
+
52
52
+
{#snippet musicItem(play: Play)}
53
53
+
<div class="flex w-full items-center gap-3">
54
54
+
<div class="size-10 shrink-0">
55
55
+
<AlbumArt releaseMbId={play.value.releaseMbId} alt="" />
56
56
+
</div>
57
57
+
<div class="min-w-0 flex-1">
58
58
+
<div class="inline-flex w-full max-w-full justify-between gap-2">
59
59
+
<div
60
60
+
class="text-accent-500 accent:text-accent-950 min-w-0 flex-1 shrink truncate font-semibold"
61
61
+
>
62
62
+
{play.value.trackName}
63
63
+
</div>
64
64
+
65
65
+
{#if play.value.playedTime}
66
66
+
<div class="shrink-0 text-xs">
67
67
+
<RelativeTime
68
68
+
date={new Date(
69
69
+
isNumeric(play.value.playedTime)
70
70
+
? parseInt(play.value.playedTime) * 1000
71
71
+
: play.value.playedTime
72
72
+
)}
73
73
+
locale="en-US"
74
74
+
/> ago
75
75
+
</div>
76
76
+
{:else}
77
77
+
<div></div>
78
78
+
{/if}
79
79
+
</div>
80
80
+
<div class="my-1 min-w-0 gap-2 truncate text-xs whitespace-nowrap">
81
81
+
{(play?.value?.artists ?? []).map((a) => a.artistName).join(', ')}
82
82
+
</div>
83
83
+
</div>
84
84
+
</div>
85
85
+
{/snippet}
86
86
+
87
87
+
<div class="z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4">
88
88
+
{#if feed && feed.length > 0}
89
89
+
{#each feed as play (play.uri)}
90
90
+
{#if play.value.originUrl}
91
91
+
<a href={play.value.originUrl} target="_blank" rel="noopener noreferrer" class="w-full">
92
92
+
{@render musicItem(play)}
93
93
+
</a>
94
94
+
{:else}
95
95
+
{@render musicItem(play)}
96
96
+
{/if}
97
97
+
{/each}
98
98
+
{:else if feed}
99
99
+
<div
100
100
+
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm"
101
101
+
>
102
102
+
No recent plays found.
103
103
+
</div>
104
104
+
{:else}
105
105
+
<div
106
106
+
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm"
107
107
+
>
108
108
+
Loading plays...
109
109
+
</div>
110
110
+
{/if}
111
111
+
</div>
+31
src/lib/cards/media/RockskyPlaysCard/index.ts
···
1
1
+
import type { CardDefinition } from '../../types';
2
2
+
import { listRecords } from '$lib/atproto';
3
3
+
import RockskyPlaysCard from './RockskyPlaysCard.svelte';
4
4
+
5
5
+
export const RockskyPlaysCardDefinition = {
6
6
+
type: 'recentRockskyPlays',
7
7
+
contentComponent: RockskyPlaysCard,
8
8
+
createNew: (card) => {
9
9
+
card.w = 4;
10
10
+
card.mobileW = 8;
11
11
+
card.h = 3;
12
12
+
card.mobileH = 6;
13
13
+
},
14
14
+
loadData: async (items, { did }) => {
15
15
+
const data = await listRecords({
16
16
+
did,
17
17
+
collection: 'app.rocksky.scrobble',
18
18
+
limit: 99
19
19
+
});
20
20
+
21
21
+
return data;
22
22
+
},
23
23
+
minW: 4,
24
24
+
canHaveLabel: true,
25
25
+
26
26
+
keywords: ['music', 'scrobble', 'listening', 'songs'],
27
27
+
name: 'Rocksky Plays',
28
28
+
29
29
+
groups: ['Media'],
30
30
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m9 9 10.5-3m0 6.553v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 1 1-.99-3.467l2.31-.66a2.25 2.25 0 0 0 1.632-2.163Zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 0 1-.99-3.467l2.31-.66A2.25 2.25 0 0 0 9 15.553Z" /></svg>`
31
31
+
} as CardDefinition & { type: 'recentRockskyPlays' };