Samlingsnoder med `metadata.traits` rendres nå som egne sider på /collection/[id]. Hvert trait-navn mappes til en dedikert Svelte-komponent som viser relevant UI. Traits uten egen komponent vises med et generisk panel. Komponenter for 9 traits: editor, chat, kanban, podcast, publishing, rss, calendar, recording, transcription. Mottak-siden viser traits som pills og lenker til samlingssiden. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
80 lines
2.3 KiB
Svelte
80 lines
2.3 KiB
Svelte
<script lang="ts">
|
|
import type { Node } from '$lib/spacetime';
|
|
import { edgeStore, nodeStore, nodeVisibility } from '$lib/spacetime';
|
|
import { casUrl } from '$lib/api';
|
|
import AudioPlayer from '$lib/components/AudioPlayer.svelte';
|
|
import TraitPanel from './TraitPanel.svelte';
|
|
|
|
interface Props {
|
|
collection: Node;
|
|
config: Record<string, unknown>;
|
|
userId?: string;
|
|
accessToken?: string;
|
|
}
|
|
|
|
let { collection, config, userId, accessToken }: Props = $props();
|
|
|
|
/** Media nodes (episodes) belonging to this collection */
|
|
const episodes = $derived.by(() => {
|
|
const nodes: Node[] = [];
|
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
|
if (edge.edgeType !== 'belongs_to') continue;
|
|
const node = nodeStore.get(edge.sourceId);
|
|
if (!node || nodeVisibility(node, userId) === 'hidden') continue;
|
|
if (node.nodeKind === 'media') {
|
|
try {
|
|
const meta = JSON.parse(node.metadata ?? '{}');
|
|
if (typeof meta.mime === 'string' && meta.mime.startsWith('audio/')) {
|
|
nodes.push(node);
|
|
}
|
|
} catch { /* skip */ }
|
|
}
|
|
}
|
|
nodes.sort((a, b) => {
|
|
const ta = a.createdAt?.microsSinceUnixEpoch ?? 0n;
|
|
const tb = b.createdAt?.microsSinceUnixEpoch ?? 0n;
|
|
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
|
});
|
|
return nodes;
|
|
});
|
|
|
|
function audioSrc(node: Node): string {
|
|
try {
|
|
const meta = JSON.parse(node.metadata ?? '{}');
|
|
return casUrl(meta.cas_hash);
|
|
} catch { return ''; }
|
|
}
|
|
|
|
function audioDuration(node: Node): number | undefined {
|
|
try {
|
|
const meta = JSON.parse(node.metadata ?? '{}');
|
|
return meta.transcription?.duration_ms ? meta.transcription.duration_ms / 1000 : undefined;
|
|
} catch { return undefined; }
|
|
}
|
|
</script>
|
|
|
|
<TraitPanel name="podcast" label="Podcast" icon="🎙️">
|
|
{#snippet children()}
|
|
{#if episodes.length === 0}
|
|
<p class="text-sm text-gray-400">Ingen episoder ennå.</p>
|
|
{:else}
|
|
<ul class="space-y-3">
|
|
{#each episodes as ep (ep.id)}
|
|
<li class="rounded border border-gray-100 p-3">
|
|
<h4 class="text-sm font-medium text-gray-900">{ep.title || 'Uten tittel'}</h4>
|
|
{#if ep.content}
|
|
<p class="mt-0.5 text-xs text-gray-500">{ep.content.slice(0, 100)}</p>
|
|
{/if}
|
|
<div class="mt-2">
|
|
<AudioPlayer
|
|
src={audioSrc(ep)}
|
|
duration={audioDuration(ep)}
|
|
compact
|
|
/>
|
|
</div>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
{/snippet}
|
|
</TraitPanel>
|