@atmo-dev/kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/at-tags.d.ts +18 -0
- package/dist/at-tags.js +18 -0
- package/dist/components/AtTags.svelte +17 -0
- package/dist/components/AtTags.svelte.d.ts +7 -0
- package/dist/events/DatePicker.svelte +222 -0
- package/dist/events/DatePicker.svelte.d.ts +12 -0
- package/dist/events/DateTimePicker.svelte +112 -0
- package/dist/events/DateTimePicker.svelte.d.ts +10 -0
- package/dist/events/EventAttendees.svelte +199 -0
- package/dist/events/EventAttendees.svelte.d.ts +19 -0
- package/dist/events/EventCard.svelte +148 -0
- package/dist/events/EventCard.svelte.d.ts +8 -0
- package/dist/events/EventComments.svelte +98 -0
- package/dist/events/EventComments.svelte.d.ts +8 -0
- package/dist/events/EventEditor.svelte +540 -0
- package/dist/events/EventEditor.svelte.d.ts +18 -0
- package/dist/events/EventRsvp.svelte +178 -0
- package/dist/events/EventRsvp.svelte.d.ts +16 -0
- package/dist/events/EventView.svelte +488 -0
- package/dist/events/EventView.svelte.d.ts +23 -0
- package/dist/events/ImageDropper.svelte +66 -0
- package/dist/events/ImageDropper.svelte.d.ts +7 -0
- package/dist/events/Map.svelte +36 -0
- package/dist/events/Map.svelte.d.ts +8 -0
- package/dist/events/PostToBlueskyModal.svelte +246 -0
- package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/events/ShareModal.svelte +174 -0
- package/dist/events/ShareModal.svelte.d.ts +23 -0
- package/dist/events/StarterPackButton.svelte +72 -0
- package/dist/events/StarterPackButton.svelte.d.ts +10 -0
- package/dist/events/ThemeApply.svelte +30 -0
- package/dist/events/ThemeApply.svelte.d.ts +7 -0
- package/dist/events/ThemeBackground.svelte +33 -0
- package/dist/events/ThemeBackground.svelte.d.ts +7 -0
- package/dist/events/ThemePicker.svelte +104 -0
- package/dist/events/ThemePicker.svelte.d.ts +7 -0
- package/dist/events/ThumbnailPresets.svelte +71 -0
- package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/events/TimePicker.svelte +117 -0
- package/dist/events/TimePicker.svelte.d.ts +10 -0
- package/dist/events/TimezonePicker.svelte +67 -0
- package/dist/events/TimezonePicker.svelte.d.ts +6 -0
- package/dist/events/VodPlayer.svelte +154 -0
- package/dist/events/VodPlayer.svelte.d.ts +14 -0
- package/dist/events/VodTranscript.svelte +75 -0
- package/dist/events/VodTranscript.svelte.d.ts +8 -0
- package/dist/events/atproto-helpers.d.ts +21 -0
- package/dist/events/atproto-helpers.js +61 -0
- package/dist/events/cal/helper.d.ts +1 -0
- package/dist/events/cal/helper.js +20 -0
- package/dist/events/cal/ical.d.ts +22 -0
- package/dist/events/cal/ical.js +188 -0
- package/dist/events/cal/sanitize.d.ts +3 -0
- package/dist/events/cal/sanitize.js +25 -0
- package/dist/events/conference.d.ts +65 -0
- package/dist/events/conference.js +61 -0
- package/dist/events/contrail.d.ts +49 -0
- package/dist/events/contrail.js +12 -0
- package/dist/events/date-format.d.ts +22 -0
- package/dist/events/date-format.js +43 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
- package/dist/events/editor/LinksSection.svelte +154 -0
- package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
- package/dist/events/editor/LocationSection.svelte +163 -0
- package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/events/editor/RecurringModal.svelte +313 -0
- package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/events/editor/ThemeSection.svelte +24 -0
- package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/events/editor/ThumbnailSection.svelte +181 -0
- package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/events/editor/adapter.d.ts +73 -0
- package/dist/events/editor/adapter.js +9 -0
- package/dist/events/editor/location.d.ts +16 -0
- package/dist/events/editor/location.js +50 -0
- package/dist/events/editor/save.d.ts +43 -0
- package/dist/events/editor/save.js +168 -0
- package/dist/events/editor/types.d.ts +39 -0
- package/dist/events/editor/types.js +9 -0
- package/dist/events/event-media.d.ts +4 -0
- package/dist/events/event-media.js +4 -0
- package/dist/events/event-types.d.ts +79 -0
- package/dist/events/event-types.js +11 -0
- package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
- package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/events/event-view/EventBadges.svelte +20 -0
- package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/events/event-view/EventDateBlock.svelte +43 -0
- package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventHostedBy.svelte +61 -0
- package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/events/event-view/EventLinksList.svelte +24 -0
- package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/events/event-view/EventLocationBlock.svelte +62 -0
- package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventLocationMap.svelte +66 -0
- package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/events/event-view/format.d.ts +10 -0
- package/dist/events/event-view/format.js +112 -0
- package/dist/events/event-view/locations.d.ts +36 -0
- package/dist/events/event-view/locations.js +243 -0
- package/dist/events/event-view/plyr-icons.d.ts +3 -0
- package/dist/events/event-view/plyr-icons.js +46 -0
- package/dist/events/linked-records.d.ts +18 -0
- package/dist/events/linked-records.js +11 -0
- package/dist/events/profile-url.d.ts +3 -0
- package/dist/events/profile-url.js +11 -0
- package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
- package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
- package/dist/events/schedule/DaySchedule.svelte +298 -0
- package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
- package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
- package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
- package/dist/events/schedule/schedule-utils.d.ts +44 -0
- package/dist/events/schedule/schedule-utils.js +166 -0
- package/dist/events/starter-pack-url.d.ts +3 -0
- package/dist/events/starter-pack-url.js +16 -0
- package/dist/events/theme.d.ts +9 -0
- package/dist/events/theme.js +36 -0
- package/dist/events/themes/Blobs.svelte +64 -0
- package/dist/events/themes/Blobs.svelte.d.ts +18 -0
- package/dist/events/themes/Butterflies.svelte +183 -0
- package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/events/themes/Fireflies.svelte +134 -0
- package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/events/themes/Kaleidoscope.svelte +183 -0
- package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/events/themes/Matrix.svelte +153 -0
- package/dist/events/themes/Matrix.svelte.d.ts +3 -0
- package/dist/events/themes/Stars.svelte +98 -0
- package/dist/events/themes/Stars.svelte.d.ts +3 -0
- package/dist/events/thumbnails/designs.d.ts +18 -0
- package/dist/events/thumbnails/designs.js +316 -0
- package/dist/events/view-data.d.ts +38 -0
- package/dist/events/view-data.js +1 -0
- package/dist/events-data.d.ts +15 -0
- package/dist/events-data.js +9 -0
- package/dist/events-editor.d.ts +2 -0
- package/dist/events-editor.js +2 -0
- package/dist/events.d.ts +19 -0
- package/dist/events.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +50 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Icon } from '../../index.js';
|
|
3
|
+
import { Link, MapPin } from '@sketchyicons/svelte';
|
|
4
|
+
import { getPhysicalLocationLines, type LocationData } from './locations.js';
|
|
5
|
+
|
|
6
|
+
let { locations }: { locations: LocationData[] } = $props();
|
|
7
|
+
let physicalLines = $derived(getPhysicalLocationLines(locations));
|
|
8
|
+
let links = $derived(locations.filter((location) => location.kind === 'uri'));
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
{#if physicalLines.length || links.length}
|
|
12
|
+
<ul class="mb-6 space-y-4" aria-label="Event locations">
|
|
13
|
+
{#if physicalLines.length}
|
|
14
|
+
<li class="flex min-w-0 items-center gap-4">
|
|
15
|
+
<div
|
|
16
|
+
class="flex size-12 shrink-0 items-center justify-center rounded-ui bg-recess text-muted [box-shadow:var(--ui-recess-shadow)]"
|
|
17
|
+
>
|
|
18
|
+
<Icon img={MapPin} size={22} />
|
|
19
|
+
</div>
|
|
20
|
+
<div class="min-w-0 [overflow-wrap:anywhere]">
|
|
21
|
+
{#each physicalLines as line, i (i)}
|
|
22
|
+
<p class={line.muted ? 'text-sm text-muted' : 'font-semibold text-ink'}>
|
|
23
|
+
<svelte:element
|
|
24
|
+
this={line.href ? 'a' : 'span'}
|
|
25
|
+
href={line.href}
|
|
26
|
+
target={line.href ? '_blank' : undefined}
|
|
27
|
+
rel={line.href ? 'noopener noreferrer' : undefined}
|
|
28
|
+
class={line.href ? 'transition-opacity hover:opacity-80' : undefined}
|
|
29
|
+
>{line.label}</svelte:element
|
|
30
|
+
>
|
|
31
|
+
</p>
|
|
32
|
+
{/each}
|
|
33
|
+
</div>
|
|
34
|
+
</li>
|
|
35
|
+
{/if}
|
|
36
|
+
{#each links as location, i (i)}
|
|
37
|
+
<li>
|
|
38
|
+
<svelte:element
|
|
39
|
+
this={location.href ? 'a' : 'div'}
|
|
40
|
+
href={location.href}
|
|
41
|
+
target={location.href ? '_blank' : undefined}
|
|
42
|
+
rel={location.href ? 'noopener noreferrer' : undefined}
|
|
43
|
+
class="flex min-w-0 items-center gap-4 rounded-ui {location.href
|
|
44
|
+
? 'transition-opacity hover:opacity-80'
|
|
45
|
+
: ''}"
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
class="flex size-12 shrink-0 items-center justify-center rounded-ui bg-recess text-muted [box-shadow:var(--ui-recess-shadow)]"
|
|
49
|
+
>
|
|
50
|
+
<Icon img={Link} size={22} />
|
|
51
|
+
</div>
|
|
52
|
+
<div class="min-w-0 [overflow-wrap:anywhere]">
|
|
53
|
+
<p class="font-semibold text-ink">{location.label}</p>
|
|
54
|
+
{#if location.detail}
|
|
55
|
+
<p class="text-sm text-muted">{location.detail}</p>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
</svelte:element>
|
|
59
|
+
</li>
|
|
60
|
+
{/each}
|
|
61
|
+
</ul>
|
|
62
|
+
{/if}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LocationData } from './locations.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
locations: LocationData[];
|
|
4
|
+
};
|
|
5
|
+
declare const EventLocationBlock: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type EventLocationBlock = ReturnType<typeof EventLocationBlock>;
|
|
7
|
+
export default EventLocationBlock;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Badge } from '../../index.js';
|
|
3
|
+
import Map from '../Map.svelte';
|
|
4
|
+
import type { GeoLocation } from './locations.js';
|
|
5
|
+
|
|
6
|
+
let { geoLocation }: { geoLocation: GeoLocation | null } = $props();
|
|
7
|
+
|
|
8
|
+
let copied = $state(false);
|
|
9
|
+
|
|
10
|
+
async function copyCoords() {
|
|
11
|
+
if (!geoLocation) return;
|
|
12
|
+
const text = `${geoLocation.lat.toFixed(5)}, ${geoLocation.lng.toFixed(5)}`;
|
|
13
|
+
try {
|
|
14
|
+
await navigator.clipboard.writeText(text);
|
|
15
|
+
copied = true;
|
|
16
|
+
setTimeout(() => (copied = false), 2000);
|
|
17
|
+
} catch {
|
|
18
|
+
// Clipboard access is optional; the coordinates remain visible.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#if geoLocation}
|
|
24
|
+
<div class="mt-8 mb-8">
|
|
25
|
+
<div class="mb-3 flex items-baseline gap-2">
|
|
26
|
+
<p class="text-xs font-semibold tracking-wider text-muted uppercase">Location:</p>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
onclick={copyCoords}
|
|
30
|
+
class="ml-auto cursor-pointer transition-opacity active:opacity-60"
|
|
31
|
+
title="Copy coordinates"
|
|
32
|
+
aria-label="Copy coordinates"
|
|
33
|
+
>
|
|
34
|
+
<Badge variant="outline" class="font-mono">
|
|
35
|
+
{copied ? 'Copied!' : `${geoLocation.lat.toFixed(5)}, ${geoLocation.lng.toFixed(5)}`}
|
|
36
|
+
</Badge>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="h-64 w-full overflow-hidden rounded-xl">
|
|
40
|
+
<Map lat={geoLocation.lat} lng={geoLocation.lng} />
|
|
41
|
+
</div>
|
|
42
|
+
<p class="mt-3 text-sm [overflow-wrap:anywhere] text-base-700 dark:text-base-200">
|
|
43
|
+
{geoLocation.label}
|
|
44
|
+
</p>
|
|
45
|
+
<p class="mt-1 text-xs text-muted">
|
|
46
|
+
Open in
|
|
47
|
+
<a
|
|
48
|
+
href={geoLocation.googleMapsUrl}
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noopener noreferrer"
|
|
51
|
+
class="text-base-700 hover:underline dark:text-base-300"
|
|
52
|
+
>
|
|
53
|
+
Google Maps
|
|
54
|
+
</a>
|
|
55
|
+
|
|
|
56
|
+
<a
|
|
57
|
+
href={geoLocation.osmUrl}
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
class="text-base-700 hover:underline dark:text-base-300"
|
|
61
|
+
>
|
|
62
|
+
OpenStreetMap
|
|
63
|
+
</a>
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GeoLocation } from './locations.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
geoLocation: GeoLocation | null;
|
|
4
|
+
};
|
|
5
|
+
declare const EventLocationMap: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type EventLocationMap = ReturnType<typeof EventLocationMap>;
|
|
7
|
+
export default EventLocationMap;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ArrowUpRight } from '@sketchyicons/svelte';
|
|
3
|
+
import { Button, Icon } from '../../index.js';
|
|
4
|
+
|
|
5
|
+
// Shown in place of the RSVP controls for imported events whose host opted out
|
|
6
|
+
// of atmo RSVPs (additionalData.externalSource.rsvpMode === 'external_only').
|
|
7
|
+
// Directs attendees to the original event page instead.
|
|
8
|
+
let { url }: { url: string } = $props();
|
|
9
|
+
|
|
10
|
+
let host = $derived.by(() => {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div
|
|
20
|
+
class="mt-8 mb-2 flex flex-col gap-3 rounded-2xl border border-base-200 bg-base-100 p-4 sm:flex-row sm:items-center sm:justify-between dark:border-base-800 dark:bg-base-950/50"
|
|
21
|
+
>
|
|
22
|
+
<div class="min-w-0">
|
|
23
|
+
<p class="text-sm font-semibold text-base-900 dark:text-base-50">RSVP on the original page</p>
|
|
24
|
+
<p class="mt-0.5 text-xs text-base-600 dark:text-base-400">
|
|
25
|
+
{host
|
|
26
|
+
? `This event is hosted on ${host}. Head there to RSVP.`
|
|
27
|
+
: 'This event is hosted elsewhere. Open the original page to RSVP.'}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
<Button href={url} target="_blank" rel="noopener noreferrer" class="shrink-0">
|
|
31
|
+
{host ? `RSVP on ${host}` : 'Open original event'}
|
|
32
|
+
<Icon img={ArrowUpRight} size={16} class="shrink-0" />
|
|
33
|
+
</Button>
|
|
34
|
+
</div>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Icon } from '../../index.js';
|
|
4
|
+
import { ExternalLink } from '@sketchyicons/svelte';
|
|
5
|
+
import 'plyr/dist/plyr.css';
|
|
6
|
+
import type PlyrType from 'plyr';
|
|
7
|
+
import { sketchyPlyrIcons } from './plyr-icons.js';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
handle,
|
|
11
|
+
title
|
|
12
|
+
}: {
|
|
13
|
+
handle: string;
|
|
14
|
+
title: string;
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
let videoEl: HTMLVideoElement | undefined = $state();
|
|
18
|
+
let overlayEl: HTMLAnchorElement | undefined = $state();
|
|
19
|
+
let error = $state(false);
|
|
20
|
+
let controlsVisible = $state(true);
|
|
21
|
+
|
|
22
|
+
let pc: RTCPeerConnection | null = null;
|
|
23
|
+
let plyr: PlyrType | null = null;
|
|
24
|
+
|
|
25
|
+
const posterUrl = $derived(
|
|
26
|
+
`https://stream.place/api/playback/${encodeURIComponent(handle)}/stream.jpg`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
onMount(() => {
|
|
30
|
+
init();
|
|
31
|
+
return () => {
|
|
32
|
+
if (pc) {
|
|
33
|
+
pc.close();
|
|
34
|
+
pc = null;
|
|
35
|
+
}
|
|
36
|
+
if (videoEl) {
|
|
37
|
+
videoEl.srcObject = null;
|
|
38
|
+
}
|
|
39
|
+
plyr?.destroy();
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function waitForIceGathering(peer: RTCPeerConnection, timeoutMs = 2000): Promise<void> {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
if (peer.iceGatheringState === 'complete') {
|
|
46
|
+
resolve();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let done = false;
|
|
51
|
+
const finish = () => {
|
|
52
|
+
if (done) return;
|
|
53
|
+
done = true;
|
|
54
|
+
peer.removeEventListener('icegatheringstatechange', onStateChange);
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const onStateChange = () => {
|
|
60
|
+
if (peer.iceGatheringState === 'complete') {
|
|
61
|
+
finish();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
peer.addEventListener('icegatheringstatechange', onStateChange);
|
|
66
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function init() {
|
|
71
|
+
if (!videoEl) return;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
pc = new RTCPeerConnection({
|
|
75
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
76
|
+
bundlePolicy: 'max-bundle'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
pc.addTransceiver('video', { direction: 'recvonly' });
|
|
80
|
+
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
81
|
+
|
|
82
|
+
pc.addEventListener('track', (event) => {
|
|
83
|
+
if (!videoEl) return;
|
|
84
|
+
if (event.streams && event.streams[0]) {
|
|
85
|
+
videoEl.srcObject = event.streams[0];
|
|
86
|
+
} else {
|
|
87
|
+
let stream = videoEl.srcObject as MediaStream | null;
|
|
88
|
+
if (!stream) {
|
|
89
|
+
stream = new MediaStream();
|
|
90
|
+
videoEl.srcObject = stream;
|
|
91
|
+
}
|
|
92
|
+
stream.addTrack(event.track);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const offer = await pc.createOffer();
|
|
97
|
+
await pc.setLocalDescription(offer);
|
|
98
|
+
await waitForIceGathering(pc, 2000);
|
|
99
|
+
|
|
100
|
+
const sdp = pc.localDescription?.sdp;
|
|
101
|
+
if (!sdp) {
|
|
102
|
+
error = true;
|
|
103
|
+
pc.close();
|
|
104
|
+
pc = null;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const response = await fetch(
|
|
109
|
+
`https://stream.place/api/playback/${encodeURIComponent(handle)}/webrtc?rendition=source`,
|
|
110
|
+
{
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/sdp' },
|
|
113
|
+
body: sdp
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
error = true;
|
|
119
|
+
pc.close();
|
|
120
|
+
pc = null;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const answerSdp = await response.text();
|
|
125
|
+
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
|
|
126
|
+
|
|
127
|
+
const { default: Plyr } = await import('plyr');
|
|
128
|
+
|
|
129
|
+
plyr = new Plyr(videoEl, {
|
|
130
|
+
...sketchyPlyrIcons(),
|
|
131
|
+
controls: ['play', 'mute', 'volume', 'fullscreen'],
|
|
132
|
+
settings: [],
|
|
133
|
+
ratio: '16:9'
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
plyr.on('controlshidden', () => {
|
|
137
|
+
controlsVisible = false;
|
|
138
|
+
});
|
|
139
|
+
plyr.on('controlsshown', () => {
|
|
140
|
+
controlsVisible = true;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Move the overlay link inside Plyr's container so mousemove over the
|
|
144
|
+
// button bubbles into Plyr's activity tracker — otherwise hovering it
|
|
145
|
+
// hides the controls, which hides the button, which reactivates Plyr
|
|
146
|
+
// (a flicker loop).
|
|
147
|
+
const plyrEl = videoEl.closest('.plyr');
|
|
148
|
+
if (plyrEl && overlayEl) plyrEl.appendChild(overlayEl);
|
|
149
|
+
} catch {
|
|
150
|
+
error = true;
|
|
151
|
+
if (pc) {
|
|
152
|
+
pc.close();
|
|
153
|
+
pc = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
{#snippet watchOnStreamPlace(hidden = false)}
|
|
160
|
+
<a
|
|
161
|
+
bind:this={overlayEl}
|
|
162
|
+
href="https://stream.place/{handle}"
|
|
163
|
+
target="_blank"
|
|
164
|
+
rel="noopener noreferrer"
|
|
165
|
+
onclick={() => plyr?.pause()}
|
|
166
|
+
class="absolute top-3 right-3 z-10 inline-flex items-center gap-1.5 rounded-full bg-black/55 px-2.5 py-1 text-xs font-medium text-white backdrop-blur-sm transition-opacity duration-200 hover:bg-black/75 {hidden
|
|
167
|
+
? 'pointer-events-none opacity-0'
|
|
168
|
+
: 'opacity-100'}"
|
|
169
|
+
>
|
|
170
|
+
Watch on stream.place
|
|
171
|
+
<Icon img={ExternalLink} size={12} />
|
|
172
|
+
</a>
|
|
173
|
+
{/snippet}
|
|
174
|
+
|
|
175
|
+
{#if error}
|
|
176
|
+
<div
|
|
177
|
+
class="relative flex aspect-video w-full items-center justify-center overflow-hidden rounded-xl border border-base-200 bg-base-100 dark:border-base-800 dark:bg-base-900"
|
|
178
|
+
>
|
|
179
|
+
<div
|
|
180
|
+
class="absolute inset-0 bg-cover bg-center"
|
|
181
|
+
style="background-image: url({posterUrl});"
|
|
182
|
+
></div>
|
|
183
|
+
<div class="absolute inset-0 bg-black/60"></div>
|
|
184
|
+
{@render watchOnStreamPlace()}
|
|
185
|
+
<p class="relative text-sm text-base-100">Stream is offline</p>
|
|
186
|
+
</div>
|
|
187
|
+
{:else}
|
|
188
|
+
<div
|
|
189
|
+
class="relative aspect-video w-full max-w-full overflow-hidden rounded-xl border border-base-300 dark:border-base-400/40"
|
|
190
|
+
>
|
|
191
|
+
{@render watchOnStreamPlace(!controlsVisible)}
|
|
192
|
+
<video
|
|
193
|
+
bind:this={videoEl}
|
|
194
|
+
class="h-full w-full"
|
|
195
|
+
aria-label={title}
|
|
196
|
+
poster={posterUrl}
|
|
197
|
+
autoplay
|
|
198
|
+
playsinline
|
|
199
|
+
muted
|
|
200
|
+
crossorigin="anonymous"
|
|
201
|
+
></video>
|
|
202
|
+
</div>
|
|
203
|
+
{/if}
|
|
204
|
+
|
|
205
|
+
<style>
|
|
206
|
+
* {
|
|
207
|
+
--plyr-color-main: var(--color-accent-500);
|
|
208
|
+
}
|
|
209
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import 'plyr/dist/plyr.css';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
handle: string;
|
|
4
|
+
title: string;
|
|
5
|
+
};
|
|
6
|
+
declare const StreamPlacePlayer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type StreamPlacePlayer = ReturnType<typeof StreamPlacePlayer>;
|
|
8
|
+
export default StreamPlacePlayer;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PreferredSocialClient } from '../../social.js';
|
|
2
|
+
export declare function formatMonth(date: Date): string;
|
|
3
|
+
export declare function formatDay(date: Date): number;
|
|
4
|
+
export declare function formatWeekday(date: Date): string;
|
|
5
|
+
export declare function formatFullDate(date: Date): string;
|
|
6
|
+
export declare function formatTime(date: Date): string;
|
|
7
|
+
export declare function getModeLabel(mode: string): string;
|
|
8
|
+
/** Optional local accent class for an event mode. */
|
|
9
|
+
export declare function getModeColor(mode: string): string;
|
|
10
|
+
export declare function buildDescriptionHtml(description: string | undefined, facets: unknown, socialClient?: PreferredSocialClient | null): string | null;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import { tokenize } from '@atcute/bluesky-richtext-parser';
|
|
3
|
+
import { sanitize } from '../cal/sanitize.js';
|
|
4
|
+
import { getLocalProfileUrl } from '../profile-url.js';
|
|
5
|
+
import { socialHashtagUrl } from '../../social.js';
|
|
6
|
+
export function formatMonth(date) {
|
|
7
|
+
return date.toLocaleDateString('en-US', { month: 'short' }).toUpperCase();
|
|
8
|
+
}
|
|
9
|
+
export function formatDay(date) {
|
|
10
|
+
return date.getDate();
|
|
11
|
+
}
|
|
12
|
+
export function formatWeekday(date) {
|
|
13
|
+
return date.toLocaleDateString('en-US', { weekday: 'long' });
|
|
14
|
+
}
|
|
15
|
+
export function formatFullDate(date) {
|
|
16
|
+
const options = { month: 'long', day: 'numeric' };
|
|
17
|
+
if (date.getFullYear() !== new Date().getFullYear()) {
|
|
18
|
+
options.year = 'numeric';
|
|
19
|
+
}
|
|
20
|
+
return date.toLocaleDateString('en-US', options);
|
|
21
|
+
}
|
|
22
|
+
export function formatTime(date) {
|
|
23
|
+
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
|
24
|
+
}
|
|
25
|
+
export function getModeLabel(mode) {
|
|
26
|
+
if (mode.includes('virtual'))
|
|
27
|
+
return 'Virtual';
|
|
28
|
+
if (mode.includes('hybrid'))
|
|
29
|
+
return 'Hybrid';
|
|
30
|
+
if (mode.includes('inperson'))
|
|
31
|
+
return 'In-Person';
|
|
32
|
+
return 'Event';
|
|
33
|
+
}
|
|
34
|
+
/** Optional local accent class for an event mode. */
|
|
35
|
+
export function getModeColor(mode) {
|
|
36
|
+
if (mode.includes('virtual'))
|
|
37
|
+
return 'cyan';
|
|
38
|
+
if (mode.includes('hybrid'))
|
|
39
|
+
return 'purple';
|
|
40
|
+
if (mode.includes('inperson'))
|
|
41
|
+
return 'amber';
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
const renderer = new marked.Renderer();
|
|
45
|
+
renderer.link = ({ href, text }) => {
|
|
46
|
+
const attributes = href.startsWith('/')
|
|
47
|
+
? ''
|
|
48
|
+
: ' target="_blank" rel="noopener noreferrer nofollow"';
|
|
49
|
+
const safeHref = href.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
50
|
+
return `<a${attributes} href="${safeHref}" class="text-accent-600 dark:text-accent-400 hover:underline">${text}</a>`;
|
|
51
|
+
};
|
|
52
|
+
function renderDescription(text, facets, socialClient) {
|
|
53
|
+
let result = text;
|
|
54
|
+
// Imported descriptions may have no facets. Keep existing Markdown links and
|
|
55
|
+
// code intact; only turn detected standalone mentions and hashtags into links.
|
|
56
|
+
if (!facets?.length) {
|
|
57
|
+
result = tokenize(text)
|
|
58
|
+
.map((token) => {
|
|
59
|
+
if (token.type === 'mention')
|
|
60
|
+
return `[${token.raw}](${getLocalProfileUrl(token.handle)})`;
|
|
61
|
+
if (token.type === 'topic')
|
|
62
|
+
return `[${token.raw}](<${socialHashtagUrl(token.name, socialClient)}>)`;
|
|
63
|
+
return token.raw;
|
|
64
|
+
})
|
|
65
|
+
.join('');
|
|
66
|
+
}
|
|
67
|
+
if (facets && facets.length > 0) {
|
|
68
|
+
const encoded = new TextEncoder().encode(text);
|
|
69
|
+
const decoder = new TextDecoder();
|
|
70
|
+
const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart);
|
|
71
|
+
const parts = [];
|
|
72
|
+
let cursor = 0;
|
|
73
|
+
for (const facet of sorted) {
|
|
74
|
+
const feature = facet.features?.[0];
|
|
75
|
+
if (!feature)
|
|
76
|
+
continue;
|
|
77
|
+
if (facet.index.byteStart < cursor)
|
|
78
|
+
continue;
|
|
79
|
+
const segmentText = decoder.decode(encoded.slice(facet.index.byteStart, facet.index.byteEnd));
|
|
80
|
+
let mdLink = null;
|
|
81
|
+
switch (feature.$type) {
|
|
82
|
+
case 'app.bsky.richtext.facet#mention': {
|
|
83
|
+
const handle = segmentText.startsWith('@') ? segmentText.slice(1) : segmentText;
|
|
84
|
+
mdLink = `[${segmentText}](${getLocalProfileUrl(feature.did || handle)})`;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'app.bsky.richtext.facet#link':
|
|
88
|
+
mdLink = `[${segmentText}](${feature.uri})`;
|
|
89
|
+
break;
|
|
90
|
+
case 'app.bsky.richtext.facet#tag':
|
|
91
|
+
if (feature.tag)
|
|
92
|
+
mdLink = `[${segmentText}](<${socialHashtagUrl(feature.tag, socialClient)}>)`;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (mdLink) {
|
|
96
|
+
parts.push(decoder.decode(encoded.slice(cursor, facet.index.byteStart)));
|
|
97
|
+
parts.push(mdLink);
|
|
98
|
+
cursor = facet.index.byteEnd;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
parts.push(decoder.decode(encoded.slice(cursor)));
|
|
102
|
+
result = parts.join('');
|
|
103
|
+
}
|
|
104
|
+
return marked.parse(result, { renderer });
|
|
105
|
+
}
|
|
106
|
+
export function buildDescriptionHtml(description, facets, socialClient) {
|
|
107
|
+
if (!description)
|
|
108
|
+
return null;
|
|
109
|
+
return sanitize(renderDescription(description, facets, socialClient), {
|
|
110
|
+
ADD_ATTR: ['target']
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EventLocationVariant } from '../event-types.js';
|
|
2
|
+
export type Coordinates = {
|
|
3
|
+
lat: number;
|
|
4
|
+
lng: number;
|
|
5
|
+
};
|
|
6
|
+
export type LocationData = {
|
|
7
|
+
kind: 'address' | 'geo' | 'fsq' | 'hthree' | 'place' | 'uri';
|
|
8
|
+
name?: string;
|
|
9
|
+
label: string;
|
|
10
|
+
detail?: string;
|
|
11
|
+
href?: string;
|
|
12
|
+
/** Compact label for event cards; addresses keep the existing city-only treatment. */
|
|
13
|
+
summary?: string;
|
|
14
|
+
coordinates?: Coordinates;
|
|
15
|
+
h3Cell?: string;
|
|
16
|
+
/** Only street addresses may be sent to the geocoder. */
|
|
17
|
+
geocodeQuery?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Preserve record order and supported variants, keeping only the first entry for each URI. */
|
|
20
|
+
export declare function getLocations(locations: EventLocationVariant[] | undefined): LocationData[];
|
|
21
|
+
export type LocationLine = {
|
|
22
|
+
label: string;
|
|
23
|
+
href?: string;
|
|
24
|
+
muted: boolean;
|
|
25
|
+
};
|
|
26
|
+
/** One pin for the place: venue names above addresses, coordinates only as a fallback. */
|
|
27
|
+
export declare function getPhysicalLocationLines(locations: LocationData[]): LocationLine[];
|
|
28
|
+
/** Cards stay compact: prefer cities, falling back to the other physical variants. */
|
|
29
|
+
export declare function getLocationSummary(locations: LocationData[]): string | undefined;
|
|
30
|
+
export type GeoLocation = Coordinates & {
|
|
31
|
+
label: string;
|
|
32
|
+
googleMapsUrl: string;
|
|
33
|
+
osmUrl: string;
|
|
34
|
+
};
|
|
35
|
+
/** A single overview map: prefer explicit coordinates; otherwise geocode the first address. */
|
|
36
|
+
export declare function resolveGeoLocation(locations: LocationData[]): Promise<GeoLocation | null>;
|