// Vidgas — screens
// Exports: LibraryScreen, PlayerScreen, UploadScreen, CollectionsScreen, SearchScreen
const { useState: useS, useEffect: useE, useRef: useR, useMemo } = React;
/* ============== LIBRARY ============== */
function LibraryScreen({ data, navigate }) {
const [filter, setFilter] = useS('all');
const featured = data.VIDEOS.find(v => v.featured) || data.VIDEOS[0];
const rest = data.VIDEOS.filter(v => v.id !== featured.id);
const filters = [
{ value: 'all', label: 'Semua' },
{ value: 'recent', label: 'Terbaru' },
{ value: '4k', label: '4K' },
{ value: 'travel', label: 'Travel' },
{ value: 'family', label: 'Keluarga' },
{ value: 'friends', label: 'Anak-anak' },
];
const filtered = useMemo(() => {
if (filter === 'all') return rest;
if (filter === 'recent') return rest.slice(0, 6);
if (filter === '4k') return rest.filter(v => v.resolution === '4K');
return rest.filter(v => v.collection === filter);
}, [filter]);
const recentlyAdded = rest.slice(0, 6);
return (
{recentlyAdded.map(v => )}
{filtered.length} video
} />
{filtered.map(v => )}
);
}
/* ============== VIDEO PLAYER COMPONENT ============== */
// iOS Safari has the best native HLS implementation. Video.js VHS polyfill
// often stutters on iOS even in "native mode", so we bypass Video.js entirely
// for iOS and use a plain element with the HLS URL.
const IS_IOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
function VidgasPlayer({ videoUrl, poster, filename }) {
const videoRef = useR(null);
const playerRef = useR(null);
useE(() => {
if (!videoRef.current || !videoUrl) return;
// Determine HLS URL from filename
const hlsUrl = filename ? `/api/hls/${filename}/index.m3u8` : null;
// iOS path: use native element directly. No Video.js, no VHS.
// Prefer MP4 with HTTP Range over HLS — iOS Safari handles this most reliably
// for VOD content and avoids MPEG-TS HLS stutter.
if (IS_IOS) {
const v = videoRef.current;
v.src = videoUrl || hlsUrl;
if (poster) v.poster = poster;
v.controls = true;
v.preload = 'metadata';
return () => {
try { v.pause(); v.removeAttribute('src'); v.load(); } catch (e) {}
};
}
// Non-iOS path: Video.js with VHS polyfill
const playerOptions = {
controls: true,
responsive: true,
fluid: true,
playsinline: true,
preload: 'metadata',
poster: poster || '',
html5: {
vhs: {
overrideNative: !videojs.browser.IS_SAFARI,
enableLowInitialPlaylist: true,
},
nativeAudioTracks: videojs.browser.IS_SAFARI,
nativeVideoTracks: videojs.browser.IS_SAFARI,
},
sources: [],
};
if (hlsUrl) {
playerOptions.sources.push({
src: hlsUrl,
type: 'application/x-mpegURL',
});
}
if (videoUrl) {
playerOptions.sources.push({
src: videoUrl,
type: 'video/mp4',
});
}
const player = videojs(videoRef.current, playerOptions);
playerRef.current = player;
return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [videoUrl, filename]);
// iOS: plain video element, no video-js classes (avoid Video.js CSS interference)
if (IS_IOS) {
return (
);
}
// Non-iOS: Video.js wrapper
return (
);
}
/* ============== PLAYER ============== */
function PlayerScreen({ data, route, navigate }) {
const video = data.VIDEOS.find(v => v.id === route.id) || data.VIDEOS[0];
const related = data.VIDEOS.filter(v => v.id !== video.id && v.collection === video.collection).slice(0, 4);
const more = data.VIDEOS.filter(v => v.id !== video.id).slice(0, 8);
// Extract filename from video_url (e.g. "/api/stream/file.mp4" -> "file.mp4")
const filename = video.video_url ? video.video_url.split('/').pop() : null;
return (
navigate({ name: 'library' })}>
Kembali
{video.video_url ? (
) : (
)}
{video.title}
{video.resolution}
{video.duration}
{video.size}
{video.views} kali ditonton
{video.date}
Favoritkan
Bagikan link
Download
Pindahkan
{video.description}
{related.length > 0 && (
)}
);
}
/* ============== UPLOAD / DOWNLOAD FROM VIDARA ============== */
function UploadScreen({ data, navigate }) {
const [vidaraUrl, setVidaraUrl] = useS('');
const [queue, setQueue] = useS([]);
const [loading, setLoading] = useS(false);
// Poll download status
useE(() => {
const downloading = queue.filter(q => q.status === 'downloading');
if (!downloading.length) return;
const id = setInterval(() => {
downloading.forEach(item => {
fetch('/api/download/status/' + item.id)
.then(r => r.json())
.then(d => {
setQueue(q => q.map(qi => qi.id === item.id ? { ...qi, status: d.status, title: d.title || qi.title, size: d.size } : qi));
}).catch(() => {});
});
}, 3000);
return () => clearInterval(id);
}, [queue]);
const submitDownload = () => {
if (!vidaraUrl.trim()) return;
setLoading(true);
fetch('/api/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: vidaraUrl.trim() }),
})
.then(r => r.json())
.then(d => {
if (d.ok) {
setQueue(q => [{ id: d.video_id, title: vidaraUrl.split('/').pop(), status: d.status, size: 0 }, ...q]);
setVidaraUrl('');
}
})
.catch(() => {})
.finally(() => setLoading(false));
};
const totalVideos = data.VIDEOS.length;
const totalSize = data.VIDEOS.reduce((s, v) => {
const m = (v.size || '').match(/([\d.]+)\s*(GB|MB|KB)/i);
if (!m) return s;
const n = parseFloat(m[1]);
if (m[2].toUpperCase() === 'GB') return s + n;
if (m[2].toUpperCase() === 'MB') return s + n / 1024;
return s;
}, 0);
return (
Download video
Paste link Vidara.to untuk download dan simpan ke library.
Download dari Vidara
Paste link video dari vidara.to
setVidaraUrl(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && submitDownload()}
placeholder="https://vidara.to/v/..."
style={{
width: '100%', padding: '14px 16px',
background: 'var(--bg-1)', border: '1px solid var(--line-2)',
borderRadius: 10, color: 'var(--fg-0)',
fontSize: 14, fontFamily: 'var(--font-mono)',
outline: 'none', marginBottom: 12,
}}
/>
{loading ? '⏳ Memproses...' : '⬇️ Download & Simpan'}
vidara.to vidara.so HLS → MP4
Total size
{totalSize.toFixed(1)} GB
Total video
{totalVideos}
Antrian
{queue.filter(q => q.status === 'downloading').length}
Antrian download
{queue.length} file
{queue.length === 0 && (
Belum ada download
)}
{queue.map((u, i) => (
{u.title || u.id}
{u.size ? ((u.size / 1048576).toFixed(0) + ' MB') : '...'}
{u.status === 'ready' ? '✅ Selesai' : u.status === 'downloading' ? '⬇️ Downloading...' : u.status === 'error' ? '❌ Error' : u.status === 'already_exists' ? '✅ Sudah ada' : u.status}
{u.status === 'downloading' && (
)}
))}
);
}
function UploadOption({ label, defaultOn }) {
const [on, setOn] = useS(!!defaultOn);
return (
{label}
setOn(!on)} style={{
width: 36, height: 20, borderRadius: 99,
background: on ? 'var(--accent)' : 'var(--bg-4)',
border: '1px solid var(--line-1)',
position: 'relative', cursor: 'pointer', transition: 'background 160ms',
}}>
);
}
/* ============== COLLECTIONS ============== */
function CollectionsScreen({ data, navigate }) {
return (
Koleksi
Folder dan playlist buat ngerapiin video kamu.
Bikin koleksi baru
{data.COLLECTIONS.map(c => (
navigate({ name: 'library' })}>
{c.name}
{c.count} video
terakhir update 2 hari lalu
))}
Koleksi baru
Kelompokin video kamu
{data.VIDEOS.slice(0, 4).map(v => )}
);
}
/* ============== SEARCH ============== */
function SearchScreen({ data, navigate }) {
const [q, setQ] = useS('bali');
const [filter, setFilter] = useS('all');
const inputRef = useR(null);
useE(() => { inputRef.current && inputRef.current.focus(); }, []);
const filters = [
{ value: 'all', label: 'Semua' },
{ value: 'video', label: 'Video' },
{ value: 'collection', label: 'Koleksi' },
{ value: '4k', label: '4K' },
{ value: 'recent', label: 'Bulan ini' },
];
const results = useMemo(() => {
if (!q) return [];
const ql = q.toLowerCase();
let r = data.VIDEOS.filter(v =>
v.title.toLowerCase().includes(ql) ||
v.description.toLowerCase().includes(ql) ||
v.collection.toLowerCase().includes(ql)
);
if (filter === '4k') r = r.filter(v => v.resolution === '4K');
if (filter === 'recent') r = r.slice(0, 6);
return r;
}, [q, filter]);
const highlight = (text) => {
if (!q) return text;
const i = text.toLowerCase().indexOf(q.toLowerCase());
if (i < 0) return text;
return <>{text.slice(0, i)}{text.slice(i, i + q.length)} {text.slice(i + q.length)}>;
};
return (
setQ(e.target.value)}
placeholder="Cari judul, deskripsi, koleksi…"
style={{
width: '100%', padding: '16px 18px 16px 50px',
background: 'var(--bg-2)', border: '1px solid var(--line-2)',
borderRadius: 14, color: 'var(--fg-0)',
fontSize: 18, fontFamily: 'var(--font-display)', fontWeight: 500,
outline: 'none', letterSpacing: '-0.01em',
}}
/>
{q && (
setQ('')} style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)' }}>
)}
{q ? `${results.length} hasil untuk "${q}"` : 'Mulai ngetik buat nyari…'}
{!q ? (
Apa yang lagi kamu cari?
Coba "bali", "mama", atau "tokyo".
) : results.length === 0 ? (
Ga ada hasil
Coba kata kunci lain.
) : (
{results.map(v => (
navigate({ name: 'player', id: v.id })}>
{v.duration}
{highlight(v.title)}
{highlight(v.description)}
{v.resolution}
{v.size}
{v.views} kali ditonton
{v.date}
))}
)}
{q && (
{['bali sunset', 'mama masak', 'bromo', 'tokyo snow', 'lebaran 2025', 'studio session'].map(s => (
setQ(s)}>{s}
))}
)}
);
}
Object.assign(window, { LibraryScreen, PlayerScreen, UploadScreen, CollectionsScreen, SearchScreen });