Initial commit: IndeeHub decentralized streaming platform
Built a complete Netflix-style streaming interface for IndeeHub's decentralized media platform with real film content. Features: - Vue 3 + TypeScript + Vite setup with hot module reloading - Netflix-inspired UI with hero section and horizontal scrolling content rows - Glass morphism design system with custom Tailwind configuration - 20+ real IndeeHub films organized into 6 categories (Bitcoin, Documentaries, Drama, etc.) - Full-featured video player component with custom controls - Mobile-responsive design with bottom navigation - Nostr integration ready (nostr-tools, relay pool, NIP-71 support) - Pinia state management for content - MCP tools configured (Filesystem, Memory, Nostr, Puppeteer) Components: - Browse.vue: Main streaming interface with hero and content rows - ContentRow.vue: Horizontal scrolling film cards with navigation arrows - VideoPlayer.vue: Custom video player with play/pause, seek, volume, fullscreen - MobileNav.vue: Bottom tab navigation for mobile devices Tech Stack: - Frontend: Vue 3 (Composition API), TypeScript - Build: Vite 7 - Styling: Tailwind CSS with custom theme - State: Pinia 3 - Router: Vue Router 4.6 - Protocol: Nostr (nostr-tools 2.22) Design: - 4px grid spacing system - Glass morphism UI components - Netflix-style hero section with featured content - Smooth animations and hover effects - Mobile-first responsive breakpoints - Dark theme with custom color palette Content: - 20+ IndeeHub films with titles, descriptions, categories - Bitcoin documentaries: God Bless Bitcoin, Dirty Coin, Searching for Satoshi - Independent films and documentaries - Working Unsplash CDN images for thumbnails and backdrops Ready for deployment to Umbrel, Start9, and Archy nodes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
97
src/utils/indeeHubApi.ts
Normal file
97
src/utils/indeeHubApi.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// Utility to fetch content from IndeeHub API
|
||||
// Update with your actual API endpoints
|
||||
|
||||
const INDEEDHUB_API = 'https://indeehub.studio/api'
|
||||
|
||||
export interface IndeeHubFilm {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
thumbnailUrl: string
|
||||
backdropUrl?: string
|
||||
type: 'film' | 'series' | 'short'
|
||||
duration?: number
|
||||
releaseYear?: number
|
||||
rating?: string
|
||||
creator?: {
|
||||
name: string
|
||||
npub?: string
|
||||
}
|
||||
nostrEventId?: string
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch films from IndeeHub screening room
|
||||
* TODO: Replace with actual API call when authenticated
|
||||
*/
|
||||
export async function fetchFilms(): Promise<IndeeHubFilm[]> {
|
||||
try {
|
||||
// TODO: Add authentication headers (NIP-98 for Nostr auth)
|
||||
const response = await fetch(`${INDEEHHUB_API}/screening-room?type=film`, {
|
||||
headers: {
|
||||
// Add your auth headers here
|
||||
// 'Authorization': 'Bearer ...'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch films')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error fetching films:', error)
|
||||
// Return mock data for development
|
||||
return getMockFilms()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock data for development
|
||||
* Replace with real IndeeHub data
|
||||
*/
|
||||
function getMockFilms(): IndeeHubFilm[] {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Sample Film 1',
|
||||
description: 'Replace with actual IndeeHub film data',
|
||||
thumbnailUrl: 'https://images.unsplash.com/photo-1518546305927-5a555bb7020d?w=400',
|
||||
backdropUrl: 'https://images.unsplash.com/photo-1518546305927-5a555bb7020d?w=1920',
|
||||
type: 'film',
|
||||
duration: 120,
|
||||
releaseYear: 2024,
|
||||
categories: ['Drama']
|
||||
},
|
||||
// Add more mock films...
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch featured content
|
||||
*/
|
||||
export async function fetchFeaturedContent(): Promise<IndeeHubFilm | null> {
|
||||
try {
|
||||
const response = await fetch(`${INDEEHHUB_API}/featured`)
|
||||
if (!response.ok) return null
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error fetching featured:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch films by category
|
||||
*/
|
||||
export async function fetchFilmsByCategory(category: string): Promise<IndeeHubFilm[]> {
|
||||
try {
|
||||
const response = await fetch(`${INDEEHHUB_API}/films?category=${category}`)
|
||||
if (!response.ok) return []
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error fetching category:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
100
src/utils/nostr.ts
Normal file
100
src/utils/nostr.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// Nostr relay pool and connection management
|
||||
import { SimplePool, Event as NostrEvent } from 'nostr-tools'
|
||||
|
||||
const DEFAULT_RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://relay.snort.social'
|
||||
]
|
||||
|
||||
// Kind numbers for IndeeHub content types
|
||||
export const NOSTR_KINDS = {
|
||||
VIDEO_HORIZONTAL: 34235, // NIP-71 Video horizontal
|
||||
VIDEO_VERTICAL: 34236, // NIP-71 Video vertical
|
||||
LONG_FORM: 30023, // NIP-23 Long-form content
|
||||
SHORT_FORM: 1, // Regular notes for shorts
|
||||
}
|
||||
|
||||
class NostrService {
|
||||
private pool: SimplePool
|
||||
private relays: string[]
|
||||
|
||||
constructor(relays: string[] = DEFAULT_RELAYS) {
|
||||
this.pool = new SimplePool()
|
||||
this.relays = relays
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch video content from Nostr
|
||||
* Using NIP-71 for video events
|
||||
*/
|
||||
async fetchVideos(limit: number = 50): Promise<NostrEvent[]> {
|
||||
try {
|
||||
const events = await this.pool.querySync(this.relays, {
|
||||
kinds: [NOSTR_KINDS.VIDEO_HORIZONTAL, NOSTR_KINDS.VIDEO_VERTICAL],
|
||||
limit
|
||||
})
|
||||
|
||||
return events
|
||||
} catch (error) {
|
||||
console.error('Error fetching videos from Nostr:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content by creator (pubkey)
|
||||
*/
|
||||
async fetchByCreator(pubkey: string, limit: number = 20): Promise<NostrEvent[]> {
|
||||
try {
|
||||
const events = await this.pool.querySync(this.relays, {
|
||||
kinds: [NOSTR_KINDS.VIDEO_HORIZONTAL, NOSTR_KINDS.VIDEO_VERTICAL],
|
||||
authors: [pubkey],
|
||||
limit
|
||||
})
|
||||
|
||||
return events
|
||||
} catch (error) {
|
||||
console.error('Error fetching creator content:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to new content events
|
||||
*/
|
||||
subscribeToContent(
|
||||
callback: (event: NostrEvent) => void,
|
||||
kinds: number[] = [NOSTR_KINDS.VIDEO_HORIZONTAL, NOSTR_KINDS.VIDEO_VERTICAL]
|
||||
) {
|
||||
const sub = this.pool.subscribeMany(
|
||||
this.relays,
|
||||
[{ kinds, limit: 10 }],
|
||||
{
|
||||
onevent(event) {
|
||||
callback(event)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return () => sub.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a view/watch event
|
||||
*/
|
||||
async publishView(videoEventId: string, userPrivkey: string) {
|
||||
// TODO: Implement NIP-XX for view tracking
|
||||
console.log('Publishing view for:', videoEventId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all connections
|
||||
*/
|
||||
close() {
|
||||
this.pool.close(this.relays)
|
||||
}
|
||||
}
|
||||
|
||||
export const nostrService = new NostrService()
|
||||
Reference in New Issue
Block a user