- Share conversations as Nostr kind:30023 articles with NIP-44 encryption - Read-only conversation viewer page at /view/:nostrAddr - Collaborative playlists via NIP-51 kind:30004 lists - Conversation templates (6 built-in + custom) - Audio podcast export via Web Speech API - Community content packs with registry and import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
import { ref, computed } from 'vue'
|
|
|
|
export interface ContentPackItem {
|
|
type: string
|
|
title: string
|
|
data: Record<string, unknown>
|
|
}
|
|
|
|
export interface ContentPack {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
version: string
|
|
author: string
|
|
items: ContentPackItem[]
|
|
installedAt?: number
|
|
source?: string
|
|
}
|
|
|
|
const STORAGE_KEY = 'aiui-content-packs'
|
|
|
|
const installedPacks = ref<ContentPack[]>([])
|
|
|
|
function loadPacks() {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored) installedPacks.value = JSON.parse(stored)
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
function savePacks() {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(installedPacks.value))
|
|
}
|
|
|
|
loadPacks()
|
|
|
|
// Built-in registry of available packs
|
|
const registryPacks: Omit<ContentPack, 'installedAt'>[] = [
|
|
{
|
|
id: 'pack-best-films-2024',
|
|
name: '2024 Best Films',
|
|
description: 'Curated selection of the best films released in 2024.',
|
|
version: '1.0.0',
|
|
author: 'AIUI Community',
|
|
source: 'builtin',
|
|
items: [
|
|
{ type: 'film', title: 'Dune: Part Two', data: { year: 2024, director: 'Denis Villeneuve', rating: 8.3 } },
|
|
{ type: 'film', title: 'The Substance', data: { year: 2024, director: 'Coralie Fargeat', rating: 7.5 } },
|
|
{ type: 'film', title: 'Conclave', data: { year: 2024, director: 'Edward Berger', rating: 7.8 } },
|
|
{ type: 'film', title: 'Anora', data: { year: 2024, director: 'Sean Baker', rating: 7.9 } },
|
|
{ type: 'film', title: 'The Brutalist', data: { year: 2024, director: 'Brady Corbet', rating: 7.6 } },
|
|
],
|
|
},
|
|
{
|
|
id: 'pack-bitcoin-music',
|
|
name: 'Bitcoin Music Playlist',
|
|
description: 'Songs celebrating Bitcoin, sound money, and sovereignty.',
|
|
version: '1.0.0',
|
|
author: 'AIUI Community',
|
|
source: 'builtin',
|
|
items: [
|
|
{ type: 'song', title: 'Value 4 Value', data: { artist: 'Ainsley Costello', year: 2023 } },
|
|
{ type: 'song', title: 'Bitcoin Thunder', data: { artist: 'Mandrik', year: 2022 } },
|
|
{ type: 'song', title: 'Bitcoin is Dead', data: { artist: 'HODL Band', year: 2024 } },
|
|
{ type: 'song', title: 'Sound Money', data: { artist: 'Cory Klippsten', year: 2023 } },
|
|
{ type: 'song', title: 'Stack Sats', data: { artist: 'Pleb Music', year: 2024 } },
|
|
],
|
|
},
|
|
{
|
|
id: 'pack-essential-nostr',
|
|
name: 'Essential Nostr Reads',
|
|
description: 'Must-read resources for understanding and building on Nostr.',
|
|
version: '1.0.0',
|
|
author: 'AIUI Community',
|
|
source: 'builtin',
|
|
items: [
|
|
{ type: 'book', title: 'The Nostr Protocol', data: { author: 'fiatjaf', year: 2023, category: 'Protocol Spec' } },
|
|
{ type: 'book', title: 'NIP-01: Basic Protocol', data: { author: 'fiatjaf', year: 2022, category: 'NIP' } },
|
|
{ type: 'book', title: 'Decentralized Social Media', data: { author: 'Various', year: 2023, category: 'Guide' } },
|
|
{ type: 'book', title: 'Building Censorship-Resistant Apps', data: { author: 'Nostr Community', year: 2024, category: 'Tutorial' } },
|
|
],
|
|
},
|
|
]
|
|
|
|
export function useContentPacks() {
|
|
const availablePacks = computed(() => {
|
|
const installedIds = new Set(installedPacks.value.map((p) => p.id))
|
|
return registryPacks.filter((p) => !installedIds.has(p.id))
|
|
})
|
|
|
|
function installPack(packId: string): boolean {
|
|
const pack = registryPacks.find((p) => p.id === packId)
|
|
if (!pack) return false
|
|
|
|
const installed: ContentPack = {
|
|
...pack,
|
|
installedAt: Date.now(),
|
|
}
|
|
installedPacks.value.push(installed)
|
|
savePacks()
|
|
return true
|
|
}
|
|
|
|
function uninstallPack(packId: string) {
|
|
installedPacks.value = installedPacks.value.filter((p) => p.id !== packId)
|
|
savePacks()
|
|
}
|
|
|
|
async function importFromUrl(url: string): Promise<ContentPack | null> {
|
|
try {
|
|
const response = await fetch(url)
|
|
if (!response.ok) return null
|
|
const data = (await response.json()) as ContentPack
|
|
if (!data.id || !data.name || !data.items) return null
|
|
|
|
const existing = installedPacks.value.find((p) => p.id === data.id)
|
|
if (existing) {
|
|
// Update existing
|
|
Object.assign(existing, data, { installedAt: Date.now(), source: url })
|
|
} else {
|
|
installedPacks.value.push({ ...data, installedAt: Date.now(), source: url })
|
|
}
|
|
savePacks()
|
|
return data
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function importFromJson(json: string): ContentPack | null {
|
|
try {
|
|
const data = JSON.parse(json) as ContentPack
|
|
if (!data.id || !data.name || !data.items) return null
|
|
|
|
const existing = installedPacks.value.find((p) => p.id === data.id)
|
|
if (existing) {
|
|
Object.assign(existing, data, { installedAt: Date.now() })
|
|
} else {
|
|
installedPacks.value.push({ ...data, installedAt: Date.now() })
|
|
}
|
|
savePacks()
|
|
return data
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function getPackItems(packId: string): ContentPackItem[] {
|
|
const pack = installedPacks.value.find((p) => p.id === packId)
|
|
return pack?.items ?? []
|
|
}
|
|
|
|
function getAllInstalledItems(type?: string): ContentPackItem[] {
|
|
const all = installedPacks.value.flatMap((p) => p.items)
|
|
return type ? all.filter((i) => i.type === type) : all
|
|
}
|
|
|
|
return {
|
|
installedPacks: computed(() => installedPacks.value),
|
|
availablePacks,
|
|
registryPacks,
|
|
installPack,
|
|
uninstallPack,
|
|
importFromUrl,
|
|
importFromJson,
|
|
getPackItems,
|
|
getAllInstalledItems,
|
|
}
|
|
}
|