- Media player. Multiple formats

This commit is contained in:
Ty Clifford
2026-06-03 01:28:23 -04:00
parent 1962390bb8
commit acfb4b3a05
16 changed files with 2346 additions and 0 deletions
+317
View File
@@ -0,0 +1,317 @@
<?php
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/layout.php';
// Determine which video to play
$slug = $_GET['v'] ?? '';
$video = $slug ? get_video_by_slug($slug) : null;
// Latest video as default if none specified
if (!$video) {
$db = get_db();
$latest = $db->query("SELECT slug FROM videos WHERE published=1 ORDER BY sort_order ASC, id DESC LIMIT 1")->fetch();
if ($latest) $video = get_video_by_slug($latest['slug']);
}
$per_page = (int)setting('catalogue_per_page', '10');
$page = max(1, (int)($_GET['page'] ?? 1));
$paged = get_videos_paginated($page, $per_page);
render_head('Media — TyClifford.com', '
/* ── Catalogue grid ── */
.cat-section { display:flex; flex-direction:column; gap:1rem; }
.cat-header { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; }
.cat-grid {
display:grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap:.85rem;
}
@media(min-width:600px) { .cat-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); } }
@media(min-width:1000px) { .cat-grid { grid-template-columns: repeat(5, 1fr); } }
.cat-thumb {
background:var(--surface-2);
border:1px solid var(--border);
border-radius:calc(var(--r)+2px);
overflow:hidden;
text-decoration:none;
color:var(--text);
display:flex; flex-direction:column;
transition:border-color .2s, transform .15s, box-shadow .2s;
position:relative;
cursor:pointer;
}
.cat-thumb:hover { border-color:var(--purple); transform:translateY(-2px); box-shadow:0 6px 24px rgba(176,96,255,.18); }
.cat-thumb.active { border-color:var(--green); box-shadow:0 0 0 1px var(--green), 0 4px 20px rgba(0,232,122,.2); }
.cat-thumb-img {
position:relative; width:100%; padding-top:56.25%; background:#000;
overflow:hidden;
}
.cat-thumb-img img {
position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
transition:transform .3s;
}
.cat-thumb:hover .cat-thumb-img img { transform:scale(1.04); }
.cat-thumb-img .no-thumb {
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); letter-spacing:.1em;
background:var(--surface);
}
.cat-thumb-dur {
position:absolute; bottom:4px; right:6px;
background:rgba(0,0,0,.75); backdrop-filter:blur(4px);
font-family:"Share Tech Mono",monospace; font-size:.55rem; letter-spacing:.05em;
color:#fff; padding:.15em .45em; border-radius:3px;
}
.play-overlay {
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,.35); opacity:0; transition:opacity .2s;
}
.cat-thumb:hover .play-overlay { opacity:1; }
.play-overlay svg { filter:drop-shadow(0 2px 8px rgba(0,0,0,.5)); }
.cat-thumb-body { padding:.55rem .65rem; display:flex; flex-direction:column; gap:.2rem; flex:1; }
.cat-thumb-title { font-size:.78rem; font-weight:600; color:var(--text); line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.cat-thumb-meta { font-family:"Share Tech Mono",monospace; font-size:.58rem; color:var(--text-3); letter-spacing:.06em; margin-top:auto; padding-top:.35rem; }
.cat-active-indicator {
position:absolute; top:0; left:0; right:0; height:2px;
background:var(--green); opacity:0; transition:opacity .2s;
}
.cat-thumb.active .cat-active-indicator { opacity:1; }
/* ── Video.js custom skin ── */
.video-js {
width:100% !important;
height:100% !important;
position:absolute !important;
inset:0 !important;
}
.video-js .vjs-big-play-button {
background:rgba(176,96,255,.75) !important;
border:2px solid rgba(176,96,255,.9) !important;
border-radius:50% !important;
width:64px !important; height:64px !important;
top:50% !important; left:50% !important;
transform:translate(-50%,-50%) !important;
margin:0 !important;
line-height:64px !important;
transition:background .2s !important;
}
.video-js:hover .vjs-big-play-button { background:rgba(176,96,255,.95) !important; }
.video-js .vjs-control-bar {
background:linear-gradient(transparent,rgba(0,0,0,.85)) !important;
height:3.2em !important;
}
.video-js .vjs-play-progress { background:var(--green) !important; }
.video-js .vjs-volume-level { background:var(--green) !important; }
.video-js .vjs-slider { background:rgba(255,255,255,.15) !important; }
.video-js .vjs-load-progress { background:rgba(255,255,255,.1) !important; }
.video-js .vjs-current-time,
.video-js .vjs-duration,
.video-js .vjs-time-divider { color:rgba(255,255,255,.8) !important; font-size:.7em !important; }
/* ── Lower grid ── */
.lower-grid { display:grid; grid-template-columns:1fr; gap:1rem; }
@media(min-width:640px) { .lower-grid { grid-template-columns:1fr 1fr; } }
@media(min-width:900px) { .lower-grid { grid-template-columns:2fr 1fr 1fr; } }
.card-about { background:var(--surface-2); }
.card-social { background:var(--surface-2); }
.social-list { list-style:none; display:flex; flex-direction:column; gap:.5rem; }
.social-list a {
display:flex; align-items:center; gap:.65rem;
padding:.6rem .8rem; border:1px solid var(--border-2); border-radius:var(--r);
font-family:"Share Tech Mono",monospace; font-size:.78rem; color:var(--text-2);
text-decoration:none; transition:border-color .2s, color .2s, background .2s;
}
.social-list a:hover { border-color:var(--green); color:var(--text); background:rgba(0,232,122,.04); }
.social-list a svg { flex-shrink:0; color:var(--text-3); transition:color .2s; }
.social-list a:hover svg { color:var(--green); }
/* empty state */
.empty-state {
padding:3rem 1rem; text-align:center; display:flex; flex-direction:column;
align-items:center; gap:1rem;
}
.empty-state svg { color:var(--text-3); opacity:.4; }
.empty-state p { font-family:"Share Tech Mono",monospace; font-size:.75rem; color:var(--text-3); letter-spacing:.1em; }
/* no-video placeholder */
.no-video { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.75rem; }
.no-video p { font-family:"Share Tech Mono",monospace; font-size:.7rem; color:var(--text-3); letter-spacing:.1em; }
/* pagination row */
.cat-footer { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; }
.cat-count { font-family:"Share Tech Mono",monospace; font-size:.65rem; color:var(--text-3); letter-spacing:.06em; }
');
?>
<main class="page" role="main">
<?php render_topbar('home'); ?>
<!-- ═══════════════════════════ PLAYER CARD ════════════════════════════════ -->
<section class="stream-card" aria-label="Media player">
<div class="stream-ratio">
<?php if ($video && !empty($video['sources'])): ?>
<video id="main-player"
class="video-js vjs-default-skin vjs-big-play-centered"
controls preload="metadata"
<?php if ($video['thumbnail']): ?>
poster="<?= h(MEDIA_URL . $video['thumbnail']) ?>"
<?php endif; ?>
data-setup='{"fluid":false,"responsive":true,"playbackRates":[0.5,1,1.25,1.5,2]}'>
<?php foreach ($video['sources'] as $src): ?>
<source src="<?= h(MEDIA_URL . $src['file_path']) ?>" type="<?= h($src['mime_type']) ?>" label="<?= h($src['quality'] ?: $src['label']) ?>">
<?php endforeach; ?>
<p class="vjs-no-js">Your browser does not support HTML video.</p>
</video>
<?php else: ?>
<div class="no-video">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity=".25"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<p>no video selected</p>
</div>
<?php endif; ?>
</div>
<div class="stream-footer">
<p class="eyebrow eyebrow-green"><?= $video ? h($video['title']) : 'Media Player' ?></p>
<?php if ($video && $video['description']): ?>
<p class="stream-meta" style="font-family:Inter,sans-serif;font-size:.78rem;color:var(--text-2);letter-spacing:0;max-width:60ch;line-height:1.5"><?= h($video['description']) ?></p>
<?php endif; ?>
<p class="stream-meta">
<?php if ($video && $video['duration']): ?>
<span><?= format_duration((int)$video['duration']) ?></span>
<?php endif; ?>
</p>
</div>
</section>
<!-- ═══════════════════════════ VIDEO CATALOGUE ═══════════════════════════ -->
<section class="cat-section">
<div class="cat-header">
<p class="eyebrow eyebrow-purple">Video Catalogue</p>
<a href="catalogue.php" class="btn btn-secondary btn-sm">Browse all →</a>
</div>
<?php if (empty($paged['videos'])): ?>
<div class="card">
<div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/></svg>
<p>no videos yet</p>
</div>
</div>
<?php else: ?>
<div class="cat-grid">
<?php foreach ($paged['videos'] as $v):
$active = $video && $video['id'] == $v['id'];
$vslug = $v['slug'];
$vtitle = $v['title'];
$vthumb = $v['thumbnail'];
$vdur = (int)$v['duration'];
$created = substr($v['created_at'], 0, 10);
?>
<a class="cat-thumb <?= $active ? 'active' : '' ?>"
href="?v=<?= urlencode($vslug) ?>"
title="<?= h($vtitle) ?>">
<div class="cat-active-indicator"></div>
<div class="cat-thumb-img">
<?php if ($vthumb): ?>
<img src="<?= h(MEDIA_URL . $vthumb) ?>" alt="<?= h($vtitle) ?>" loading="lazy">
<?php else: ?>
<div class="no-thumb">NO THUMB</div>
<?php endif; ?>
<?php if ($vdur): ?>
<span class="cat-thumb-dur"><?= format_duration($vdur) ?></span>
<?php endif; ?>
<div class="play-overlay">
<svg width="28" height="28" viewBox="0 0 24 24" fill="white"><circle cx="12" cy="12" r="12" fill="rgba(0,0,0,.4)"/><polygon points="10,8 18,12 10,16" fill="white"/></svg>
</div>
</div>
<div class="cat-thumb-body">
<span class="cat-thumb-title"><?= h($vtitle) ?></span>
<span class="cat-thumb-meta"><?= h($created) ?></span>
</div>
</a>
<?php endforeach; ?>
</div>
<?php if ($paged['total_pages'] > 1): ?>
<div class="cat-footer">
<span class="cat-count">
<?= $paged['total'] ?> video<?= $paged['total'] != 1 ? 's' : '' ?>
· page <?= $paged['page'] ?> of <?= $paged['total_pages'] ?>
</span>
<?php
$url_v = $video ? '&v=' . urlencode($video['slug']) : '';
render_pagination($page, $paged['total_pages'], '?page=%d' . $url_v);
?>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
<!-- ═══════════════════════════ LOWER GRID ════════════════════════════════ -->
<div class="lower-grid">
<div class="card card-about">
<p class="eyebrow eyebrow-green" style="margin-bottom:.1rem">About</p>
<h2 class="card-title">Ty Clifford</h2>
<p class="card-body">
Based in <strong>Keyser, West Virginia</strong>. Nerd, tinkerer, and creator.
Hobbies are my hobby — from tech and podcasting to whatever rabbit hole I fell into this week.
</p>
</div>
<div class="card card-social">
<p class="eyebrow eyebrow-green" style="margin-bottom:.1rem">Links</p>
<h2 class="card-title">Find me online</h2>
<ul class="social-list" aria-label="Social media links">
<li>
<a href="https://github.com/snick512" target="_blank" rel="noopener">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 00-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0020 4.77 5.07 5.07 0 0019.91 1S18.73.65 16 2.48a13.38 13.38 0 00-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 005 4.77a5.44 5.44 0 00-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 009 18.13V22"/></svg>
github.com/snick512
</a>
</li>
<li>
<a href="mailto:ty@tyclifford.com">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="2,4 12,13 22,4"/></svg>
ty@tyclifford.com
</a>
</li>
<li>
<a href="https://go.tyclifford.com/patreon" target="_blank" rel="noopener">
<svg width="14" height="14" viewBox="0 0 12 14" fill="currentColor"><ellipse cx="7.5" cy="4.8" rx="4.5" ry="4.5"/><rect x="0" y="0" width="2.8" height="14" rx="1.2"/></svg>
patreon.com/tyclifford
</a>
</li>
</ul>
</div>
</div>
<?php render_footer(); ?>
</main>
<script>
// Init Video.js if player exists
document.addEventListener('DOMContentLoaded', function() {
var el = document.getElementById('main-player');
if (el) {
var player = videojs('main-player', {
controls: true,
autoplay: false,
preload: 'metadata',
fluid: false,
responsive: true,
playbackRates: [0.5, 1, 1.25, 1.5, 2],
controlBar: {
children: [
'playToggle','volumePanel','currentTimeDisplay',
'timeDivider','durationDisplay','progressControl',
'playbackRateMenuButton','fullscreenToggle'
]
}
});
}
});
</script>
</body>
</html>