Enhance content management and user interaction features

- Introduced a new content source toggle in the profile and app header to switch between IndeeHub and TopDoc films.
- Updated the content fetching logic to dynamically load content based on the selected source.
- Enhanced the seeding process to include a combined catalog of IndeeHub and TopDoc films, ensuring diverse content availability.
- Improved user interaction by preventing duplicate reactions and ensuring a smoother voting experience across comments and content.
- Added support for Amber login (NIP-55) for Android users, integrating it into the existing authentication flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-12 14:24:52 +00:00
parent ab0560de00
commit 35bc78b890
38 changed files with 1107 additions and 185 deletions

View File

@@ -9,132 +9,168 @@
</svg>
</button>
<!-- Video Area (Dummy Player) -->
<div class="video-area">
<img
v-if="content?.backdrop || content?.thumbnail"
:src="content?.backdrop || content?.thumbnail"
:alt="content?.title"
class="w-full h-full object-cover"
/>
<!-- Play Overlay -->
<div class="video-overlay">
<button class="play-button" @click="togglePlay">
<svg v-if="!isPlaying" class="w-20 h-20" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
<svg v-else class="w-20 h-20" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
</svg>
</button>
<!-- Real YouTube Embed -->
<template v-if="embedUrl">
<div class="video-area iframe-area">
<iframe
:src="embedUrl"
class="w-full h-full"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
</div>
<!-- Dummy Notice -->
<div class="demo-notice">
<div class="demo-badge">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
<!-- Minimal info bar -->
<div class="stream-info-bar">
<div class="flex items-center gap-4 text-sm">
<span class="text-white font-medium">{{ content?.title }}</span>
<span v-if="content?.releaseYear" class="text-white/50">{{ content.releaseYear }}</span>
<span v-if="content?.duration" class="text-white/50">{{ content.duration }} min</span>
</div>
<a
:href="content?.streamingUrl?.replace('/embed/', '/watch?v=') ?? '#'"
target="_blank"
rel="noopener noreferrer"
class="yt-link"
>
Open on YouTube
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Demo Mode
</div>
<p class="text-sm">Video player preview - Full streaming coming soon</p>
</a>
</div>
</div>
</template>
<!-- Video Controls -->
<div class="video-controls">
<!-- Progress Bar -->
<div class="progress-container">
<div class="progress-bar">
<div class="progress-filled" :style="{ width: `${progress}%` }"></div>
<div class="progress-handle" :style="{ left: `${progress}%` }"></div>
</div>
<div class="time-display">
<span>{{ formatTime(currentTime) }}</span>
<span class="text-white/60">/</span>
<span class="text-white/60">{{ formatTime(duration) }}</span>
</div>
</div>
<!-- Control Buttons -->
<div class="control-buttons">
<!-- Left Side -->
<div class="flex items-center gap-4">
<button @click="togglePlay" class="control-btn">
<svg v-if="!isPlaying" class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<!-- Demo Player (no streaming URL) -->
<template v-else>
<div class="video-area">
<img
v-if="content?.backdrop || content?.thumbnail"
:src="content?.backdrop || content?.thumbnail"
:alt="content?.title"
class="w-full h-full object-cover"
/>
<!-- Play Overlay -->
<div class="video-overlay">
<button class="play-button" @click="togglePlay">
<svg v-if="!isPlaying" class="w-20 h-20" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
<svg v-else class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<svg v-else class="w-20 h-20" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
</svg>
</button>
<button class="control-btn">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M5 4v16l7-8z M12 4v16l7-8z" />
</svg>
</button>
<button class="control-btn">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
</button>
<div class="text-white font-medium text-sm">{{ content?.title }}</div>
</div>
<!-- Right Side -->
<div class="flex items-center gap-4">
<button class="control-btn">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<!-- Demo Notice -->
<div class="demo-notice">
<div class="demo-badge">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</button>
Demo Mode
</div>
<p class="text-sm">Video player preview - Full streaming coming soon</p>
</div>
</div>
<!-- Video Controls -->
<div class="video-controls">
<!-- Progress Bar -->
<div class="progress-container">
<div class="progress-bar">
<div class="progress-filled" :style="{ width: `${progress}%` }"></div>
<div class="progress-handle" :style="{ left: `${progress}%` }"></div>
</div>
<div class="time-display">
<span>{{ formatTime(currentTime) }}</span>
<span class="text-white/60">/</span>
<span class="text-white/60">{{ formatTime(duration) }}</span>
</div>
</div>
<!-- Control Buttons -->
<div class="control-buttons">
<!-- Left Side -->
<div class="flex items-center gap-4">
<button @click="togglePlay" class="control-btn">
<svg v-if="!isPlaying" class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
<svg v-else class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
</svg>
</button>
<div class="quality-selector">
<button class="control-btn">
{{ quality }}
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M5 4v16l7-8z M12 4v16l7-8z" />
</svg>
</button>
<button class="control-btn">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
</button>
<div class="text-white font-medium text-sm">{{ content?.title }}</div>
</div>
<!-- Right Side -->
<div class="flex items-center gap-4">
<button class="control-btn">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<div class="quality-selector">
<button class="control-btn">
{{ quality }}
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<button class="control-btn" @click="toggleFullscreen">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</button>
</div>
<button class="control-btn" @click="toggleFullscreen">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</button>
</div>
</div>
</div>
<!-- Content Info Panel -->
<div class="content-info-panel">
<h2 class="text-2xl font-bold text-white mb-2">{{ content?.title }}</h2>
<div class="flex items-center gap-4 text-sm text-white/80 mb-4">
<span v-if="content?.releaseYear" class="bg-white/10 px-2 py-0.5 rounded">{{ content.releaseYear }}</span>
<span v-if="content?.rating">{{ content.rating }}</span>
<span v-if="content?.duration">{{ content.duration }}min</span>
<span class="text-green-400 flex items-center gap-1">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
Cinephile Access
</span>
<!-- Content Info Panel -->
<div class="content-info-panel">
<h2 class="text-2xl font-bold text-white mb-2">{{ content?.title }}</h2>
<div class="flex items-center gap-4 text-sm text-white/80 mb-4">
<span v-if="content?.releaseYear" class="bg-white/10 px-2 py-0.5 rounded">{{ content.releaseYear }}</span>
<span v-if="content?.rating">{{ content.rating }}</span>
<span v-if="content?.duration">{{ content.duration }}min</span>
<span class="text-green-400 flex items-center gap-1">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
Cinephile Access
</span>
</div>
<p class="text-white/70 text-sm line-clamp-3">{{ content?.description }}</p>
</div>
<p class="text-white/70 text-sm line-clamp-3">{{ content?.description }}</p>
</div>
</template>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, computed, watch } from 'vue'
import type { Content } from '../types/content'
interface Props {
@@ -157,6 +193,17 @@ const quality = ref('4K')
let playInterval: number | null = null
/**
* Build the YouTube embed URL with autoplay when the player opens.
* Returns null when the content has no streamingUrl.
*/
const embedUrl = computed(() => {
if (!props.content?.streamingUrl) return null
const base = props.content.streamingUrl
const sep = base.includes('?') ? '&' : '?'
return `${base}${sep}autoplay=1&rel=0&modestbranding=1`
})
watch(() => props.isOpen, (newVal) => {
if (newVal) {
// Reset player state when opened
@@ -184,7 +231,6 @@ function togglePlay() {
function startPlay() {
isPlaying.value = true
console.log('▶️ Video playing (demo mode)')
// Simulate playback progress
playInterval = window.setInterval(() => {
@@ -208,7 +254,13 @@ function stopPlay() {
}
function toggleFullscreen() {
console.log('🖥️ Fullscreen toggled (demo)')
const el = document.querySelector('.video-player-overlay')
if (!el) return
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
el.requestFullscreen()
}
}
function formatTime(seconds: number): string {
@@ -275,6 +327,42 @@ function formatTime(seconds: number): string {
justify-content: center;
}
/* YouTube iframe fills the entire area */
.iframe-area iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: none;
}
/* Minimal info bar for YouTube embed mode */
.stream-info-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.yt-link {
display: flex;
align-items: center;
gap: 6px;
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: color 0.2s ease;
}
.yt-link:hover {
color: white;
}
.video-overlay {
position: absolute;
inset: 0;
@@ -477,5 +565,11 @@ function formatTime(seconds: number): string {
.control-buttons .text-white {
display: none;
}
.stream-info-bar {
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
}
</style>