Files
videoPlayer/pages/index.vue
2025-10-02 01:52:03 -06:00

810 lines
16 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<div class="background-effect">
<div class="gradient-orb orb-1"></div>
<div class="gradient-orb orb-2"></div>
<div class="gradient-orb orb-3"></div>
<div class="grid-overlay"></div>
</div>
<header class="header">
<h1 class="logo">
<span class="logo-icon"></span>
<span class="logo-text">Quantum Player</span>
</h1>
<div class="header-stats">
<div class="stat-item">
<span class="stat-label">Videos</span>
<span class="stat-value">{{ videos.length }}</span>
</div>
<button @click="showSettings = true" class="settings-btn" title="Configuración">
</button>
</div>
</header>
<SettingsModal :is-open="showSettings" @close="showSettings = false" />
<main class="main-content">
<div v-if="selectedVideo" class="player-section">
<div class="player-header">
<h2 class="video-title">{{ selectedVideo.name }}</h2>
<div class="player-actions">
<div v-if="selectedVideo.qualities && selectedVideo.qualities.length > 0" class="download-menu">
<button @click="toggleDownloadMenu" class="download-btn">
Descargar
</button>
<div v-if="downloadMenuOpen" class="download-dropdown">
<a
v-for="quality in selectedVideo.qualities"
:key="quality.quality"
:href="quality.url"
:download="selectedVideo.name + '_' + quality.quality + selectedVideo.extension"
class="download-option"
@click="downloadMenuOpen = false"
>
{{ quality.label }}
</a>
</div>
</div>
<button @click="selectedVideo = null" class="close-btn"></button>
</div>
</div>
<VideoPlayer :video-url="selectedVideo.url" :qualities="selectedVideo.qualities" />
</div>
<section class="videos-section">
<div class="section-header">
<h2 class="section-title">
<span class="title-line"></span>
Biblioteca
<span class="title-line"></span>
</h2>
</div>
<div v-if="videos.length === 0" class="empty-state">
<div class="empty-icon">📁</div>
<h3>No hay videos disponibles</h3>
<p>Agrega archivos de video a la carpeta <code>/videos</code></p>
</div>
<div v-else class="videos-grid">
<div
v-for="(video, index) in videos"
:key="video.id"
class="video-card"
:style="{ animationDelay: `${index * 0.1}s` }"
>
<div class="card-inner" @click="selectVideo(video)">
<div class="card-icon">
<span></span>
</div>
<div class="card-content">
<h3 class="card-title">{{ video.name }}</h3>
<div class="card-meta">
<span class="meta-badge">{{ video.extension }}</span>
</div>
</div>
<div class="card-hover-effect"></div>
</div>
<!-- Botón de descarga directa -->
<div class="card-actions">
<a
:href="getOriginalVideoUrl(video)"
:download="video.name + video.extension"
class="download-direct-btn"
@click.stop
title="Descargar video original"
>
<span class="download-icon"></span>
<span class="download-text">Descargar Original</span>
</a>
</div>
</div>
</div>
</section>
</main>
</div>
</template>
<script setup lang="ts">
interface Quality {
quality: string
label: string
url: string
file: string
}
interface Video {
id: string
name: string
url: string
extension: string
isHLS?: boolean
qualities?: Quality[]
}
const selectedVideo = ref<Video | null>(null)
const videos = ref<Video[]>([])
const downloadMenuOpen = ref(false)
const showSettings = ref(false)
const { data } = await useFetch('/api/videos')
if (data.value?.videos) {
videos.value = data.value.videos
}
const selectVideo = (video: Video) => {
selectedVideo.value = video
downloadMenuOpen.value = false
// Scroll to top suavemente
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const toggleDownloadMenu = () => {
downloadMenuOpen.value = !downloadMenuOpen.value
}
const getOriginalVideoUrl = (video: Video) => {
// Si tiene HLS, el video original está en /videos/nombre.mp4
if (video.isHLS) {
return `/videos/${video.name}${video.extension}`
}
// Si no tiene HLS, buscar la calidad "auto" (original)
const originalQuality = video.qualities?.find(q => q.quality === 'auto')
if (originalQuality) {
return originalQuality.url
}
// Fallback al URL principal
return video.url
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.app-container {
min-height: 100vh;
background: #0a0a0f;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
position: relative;
overflow-x: hidden;
}
.background-effect {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.gradient-orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.4;
animation: float 20s ease-in-out infinite;
}
.orb-1 {
width: 500px;
height: 500px;
background: radial-gradient(circle, #00ffff, transparent);
top: -200px;
left: -200px;
animation-delay: 0s;
}
.orb-2 {
width: 400px;
height: 400px;
background: radial-gradient(circle, #ff00ff, transparent);
top: 50%;
right: -150px;
animation-delay: -7s;
}
.orb-3 {
width: 450px;
height: 450px;
background: radial-gradient(circle, #00ff88, transparent);
bottom: -200px;
left: 50%;
animation-delay: -14s;
}
@keyframes float {
0%, 100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(50px, -50px) scale(1.1);
}
66% {
transform: translate(-30px, 30px) scale(0.9);
}
}
.grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(0, 255, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 255, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
opacity: 0.3;
}
.header {
position: relative;
z-index: 10;
padding: 30px 40px;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.logo {
display: flex;
align-items: center;
gap: 15px;
font-size: 28px;
font-weight: 700;
}
.logo-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
background: linear-gradient(135deg, #00ffff, #ff00ff);
border-radius: 12px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
animation: glow 2s ease-in-out infinite;
}
@keyframes glow {
0%, 100% {
box-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
}
50% {
box-shadow: 0 0 50px rgba(255, 0, 255, 0.7);
}
}
.logo-text {
background: linear-gradient(135deg, #00ffff, #ff00ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-stats {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
background: linear-gradient(135deg, #00ffff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.settings-btn {
width: 45px;
height: 45px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
color: #fff;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.settings-btn:hover {
background: linear-gradient(135deg, rgba(0, 255, 255, 0.2), rgba(255, 0, 255, 0.2));
border-color: rgba(0, 255, 255, 0.5);
box-shadow: 0 0 20px rgba(0, 255, 255, 0.4);
transform: rotate(90deg);
}
.main-content {
position: relative;
z-index: 10;
padding: 40px;
max-width: 1400px;
margin: 0 auto;
}
.player-section {
margin-bottom: 60px;
animation: slideDown 0.5s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.player-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.video-title {
font-size: 32px;
font-weight: 700;
background: linear-gradient(135deg, #fff, #00ffff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.player-actions {
display: flex;
align-items: center;
gap: 15px;
}
.download-menu {
position: relative;
}
.download-btn {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 0.4);
color: #fff;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
gap: 8px;
}
.download-btn:hover {
background: rgba(0, 255, 255, 0.3);
border-color: rgba(0, 255, 255, 0.6);
box-shadow: 0 0 20px rgba(0, 255, 255, 0.5);
transform: translateY(-2px);
}
.download-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 10px;
background: rgba(0, 0, 0, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 12px;
padding: 8px;
min-width: 160px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.4);
animation: slideDownMenu 0.2s ease;
z-index: 100;
}
@keyframes slideDownMenu {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.download-option {
display: block;
padding: 12px 16px;
color: #fff;
text-decoration: none;
border-radius: 8px;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
}
.download-option:hover {
background: rgba(0, 255, 255, 0.2);
transform: translateX(5px);
}
.close-btn {
background: rgba(255, 0, 100, 0.2);
border: 1px solid rgba(255, 0, 100, 0.4);
color: #fff;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.close-btn:hover {
background: rgba(255, 0, 100, 0.4);
transform: rotate(90deg);
box-shadow: 0 0 20px rgba(255, 0, 100, 0.6);
}
.videos-section {
margin-top: 40px;
}
.section-header {
margin-bottom: 30px;
}
.section-title {
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
gap: 20px;
text-transform: uppercase;
letter-spacing: 2px;
}
.title-line {
flex: 1;
height: 2px;
background: linear-gradient(90deg, transparent, #00ffff, transparent);
}
.empty-state {
text-align: center;
padding: 80px 20px;
background: rgba(255, 255, 255, 0.02);
border-radius: 20px;
border: 2px dashed rgba(255, 255, 255, 0.1);
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 24px;
margin-bottom: 10px;
color: rgba(255, 255, 255, 0.8);
}
.empty-state p {
color: rgba(255, 255, 255, 0.5);
font-size: 16px;
}
.empty-state code {
background: rgba(0, 255, 255, 0.1);
padding: 4px 8px;
border-radius: 4px;
color: #00ffff;
font-family: monospace;
}
.videos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 25px;
}
.video-card {
animation: fadeInUp 0.6s ease both;
cursor: pointer;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-inner {
position: relative;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 25px;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
overflow: hidden;
}
.card-inner:hover {
transform: translateY(-8px);
border-color: rgba(0, 255, 255, 0.5);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.5),
0 0 40px rgba(0, 255, 255, 0.3);
}
.card-inner:hover .card-hover-effect {
opacity: 1;
}
.card-hover-effect {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(0, 255, 255, 0.1),
transparent
);
transform: rotate(45deg);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.card-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, rgba(0, 255, 255, 0.2), rgba(255, 0, 255, 0.2));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 15px;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.card-inner:hover .card-icon {
background: linear-gradient(135deg, rgba(0, 255, 255, 0.4), rgba(255, 0, 255, 0.4));
transform: scale(1.1) rotate(10deg);
}
.card-content {
position: relative;
z-index: 1;
}
.card-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
color: #fff;
word-break: break-word;
}
.card-meta {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.meta-badge {
display: inline-block;
padding: 4px 12px;
background: rgba(0, 255, 255, 0.1);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 20px;
font-size: 12px;
text-transform: uppercase;
color: #00ffff;
font-weight: 600;
letter-spacing: 0.5px;
}
/* Botón de descarga directa */
.card-actions {
margin-top: 15px;
padding: 0 15px 15px 15px;
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.video-card:hover .card-actions {
opacity: 1;
transform: translateY(0);
}
.download-direct-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px 20px;
background: linear-gradient(135deg, rgba(0, 255, 255, 0.15), rgba(255, 0, 255, 0.15));
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 10px;
color: #fff;
text-decoration: none;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
.download-direct-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s ease;
}
.download-direct-btn:hover::before {
left: 100%;
}
.download-direct-btn:hover {
background: linear-gradient(135deg, rgba(0, 255, 255, 0.3), rgba(255, 0, 255, 0.3));
border-color: rgba(0, 255, 255, 0.6);
box-shadow: 0 0 20px rgba(0, 255, 255, 0.4);
transform: translateY(-2px);
}
.download-icon {
font-size: 18px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
.download-text {
font-size: 12px;
}
@media (max-width: 1024px) {
.videos-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
}
@media (max-width: 768px) {
.header {
padding: 20px;
flex-direction: column;
gap: 20px;
}
.logo {
font-size: 24px;
}
.logo-icon {
width: 40px;
height: 40px;
}
.main-content {
padding: 20px;
}
.video-title {
font-size: 24px;
}
.section-title {
font-size: 20px;
}
.videos-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.title-line {
display: none;
}
/* Mostrar botón de descarga siempre en móvil */
.card-actions {
opacity: 1;
transform: translateY(0);
}
.download-text {
font-size: 11px;
}
}
@media (max-width: 480px) {
.header-stats {
width: 100%;
justify-content: center;
}
.logo {
font-size: 20px;
}
.empty-state {
padding: 40px 20px;
}
.empty-icon {
font-size: 60px;
}
}
</style>