-
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* db.php — SQLite database bootstrap & helpers
|
||||
*/
|
||||
|
||||
define('DB_PATH', __DIR__ . '/../data/media.db');
|
||||
define('MEDIA_DIR', __DIR__ . '/../media/');
|
||||
define('MEDIA_URL', 'media/');
|
||||
|
||||
function get_db(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo) return $pdo;
|
||||
|
||||
$dir = dirname(DB_PATH);
|
||||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||
|
||||
$pdo = new PDO('sqlite:' . DB_PATH);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$pdo->exec('PRAGMA journal_mode=WAL');
|
||||
$pdo->exec('PRAGMA foreign_keys=ON');
|
||||
|
||||
// Schema
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS videos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL DEFAULT 'Untitled',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
thumbnail TEXT NOT NULL DEFAULT '',
|
||||
duration INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
published INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS video_sources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
|
||||
label TEXT NOT NULL DEFAULT '',
|
||||
file_path TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL DEFAULT 'video/mp4',
|
||||
quality TEXT NOT NULL DEFAULT '720p',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_videos_published ON videos(published);
|
||||
CREATE INDEX IF NOT EXISTS idx_videos_sort ON videos(sort_order, id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sources_video ON video_sources(video_id);
|
||||
");
|
||||
|
||||
// Default settings
|
||||
$defaults = [
|
||||
'catalogue_per_page' => '10',
|
||||
'admin_password' => password_hash('admin', PASSWORD_DEFAULT),
|
||||
'site_title' => 'Ty Clifford',
|
||||
'site_sub' => 'tyclifford.com / media',
|
||||
];
|
||||
$ins = $pdo->prepare("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)");
|
||||
foreach ($defaults as $k => $v) $ins->execute([$k, $v]);
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function setting(string $key, string $fallback = ''): string {
|
||||
$row = get_db()->prepare("SELECT value FROM settings WHERE key=?");
|
||||
$row->execute([$key]);
|
||||
$r = $row->fetch();
|
||||
return $r ? $r['value'] : $fallback;
|
||||
}
|
||||
|
||||
function set_setting(string $key, string $value): void {
|
||||
get_db()->prepare("INSERT OR REPLACE INTO settings (key,value) VALUES (?,?)")
|
||||
->execute([$key, $value]);
|
||||
}
|
||||
|
||||
function make_slug(string $title, int $id = 0): string {
|
||||
$slug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $title), '-'));
|
||||
if ($id) {
|
||||
$exists = get_db()->prepare("SELECT id FROM videos WHERE slug=? AND id!=?");
|
||||
$exists->execute([$slug, $id]);
|
||||
} else {
|
||||
$exists = get_db()->prepare("SELECT id FROM videos WHERE slug=?");
|
||||
$exists->execute([$slug]);
|
||||
}
|
||||
if ($exists->fetch()) $slug .= '-' . ($id ?: time());
|
||||
return $slug ?: 'video-' . time();
|
||||
}
|
||||
|
||||
function mime_from_ext(string $path): string {
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
return match($ext) {
|
||||
'mp4' => 'video/mp4',
|
||||
'webm' => 'video/webm',
|
||||
'ogv' => 'video/ogg',
|
||||
'mov' => 'video/quicktime',
|
||||
'mkv' => 'video/x-matroska',
|
||||
'avi' => 'video/x-msvideo',
|
||||
default => 'video/mp4',
|
||||
};
|
||||
}
|
||||
|
||||
function format_duration(int $seconds): string {
|
||||
if ($seconds <= 0) return '';
|
||||
$h = intdiv($seconds, 3600);
|
||||
$m = intdiv($seconds % 3600, 60);
|
||||
$s = $seconds % 60;
|
||||
if ($h) return sprintf('%d:%02d:%02d', $h, $m, $s);
|
||||
return sprintf('%d:%02d', $m, $s);
|
||||
}
|
||||
|
||||
function h(string $s): string {
|
||||
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function get_videos_paginated(int $page, int $per_page, bool $published_only = true): array {
|
||||
$db = get_db();
|
||||
$where = $published_only ? 'WHERE v.published=1' : '';
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$total = $db->query("SELECT COUNT(*) FROM videos v $where")->fetchColumn();
|
||||
$rows = $db->prepare("
|
||||
SELECT v.*, GROUP_CONCAT(vs.file_path,'||') AS source_paths
|
||||
FROM videos v
|
||||
LEFT JOIN video_sources vs ON vs.video_id = v.id
|
||||
$where
|
||||
GROUP BY v.id
|
||||
ORDER BY v.sort_order ASC, v.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
");
|
||||
$rows->execute([$per_page, $offset]);
|
||||
return [
|
||||
'videos' => $rows->fetchAll(),
|
||||
'total' => (int)$total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => (int)ceil($total / $per_page),
|
||||
];
|
||||
}
|
||||
|
||||
function get_video_by_slug(string $slug): ?array {
|
||||
$db = get_db();
|
||||
$row = $db->prepare("SELECT * FROM videos WHERE slug=? AND published=1");
|
||||
$row->execute([$slug]);
|
||||
$v = $row->fetch();
|
||||
if (!$v) return null;
|
||||
$sources = $db->prepare("SELECT * FROM video_sources WHERE video_id=? ORDER BY sort_order ASC");
|
||||
$sources->execute([$v['id']]);
|
||||
$v['sources'] = $sources->fetchAll();
|
||||
return $v;
|
||||
}
|
||||
|
||||
function get_video_by_id(int $id): ?array {
|
||||
$db = get_db();
|
||||
$row = $db->prepare("SELECT * FROM videos WHERE id=?");
|
||||
$row->execute([$id]);
|
||||
$v = $row->fetch();
|
||||
if (!$v) return null;
|
||||
$sources = $db->prepare("SELECT * FROM video_sources WHERE video_id=? ORDER BY sort_order ASC");
|
||||
$sources->execute([$v['id']]);
|
||||
$v['sources'] = $sources->fetchAll();
|
||||
return $v;
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* layout.php — shared HTML head, header, and footer partials
|
||||
*/
|
||||
|
||||
function render_head(string $title = 'Media — TyClifford.com', string $extra_css = ''): void { ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= h($title) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||
<!-- Video.js -->
|
||||
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
|
||||
<script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
|
||||
<style>
|
||||
/* ── Reset & design tokens (mirrors gate.php exactly) ──────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #09090f;
|
||||
--surface: #0f0f1a;
|
||||
--surface-2: #141425;
|
||||
--border: #1e1e38;
|
||||
--border-2: #2a2a48;
|
||||
--text: #e8e8f8;
|
||||
--text-2: #9898c0;
|
||||
--text-3: #58587a;
|
||||
|
||||
--green: #00e87a;
|
||||
--blue: #00c8ff;
|
||||
--purple: #b060ff;
|
||||
--red: #ff3355;
|
||||
--orange: #ff8800;
|
||||
--yellow: #ffd040;
|
||||
|
||||
--pat: #ff424d;
|
||||
--pat-lite: rgba(255,66,77,.10);
|
||||
--pat-border:rgba(255,66,77,.32);
|
||||
--pat-glow: rgba(255,66,77,.18);
|
||||
|
||||
--r: 6px;
|
||||
--gap: 1.25rem;
|
||||
--page-pad: 1rem;
|
||||
}
|
||||
@media (min-width:600px) { :root { --page-pad: 1.5rem; } }
|
||||
@media (min-width:1000px) { :root { --page-pad: 2rem; } }
|
||||
|
||||
html { font-size:16px; -webkit-text-size-adjust:100%; scroll-behavior:smooth; }
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
background: var(--bg);
|
||||
background-image:
|
||||
radial-gradient(ellipse 70% 50% at 20% 10%, rgba(176,96,255,.05) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(0,200,255,.04) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 50% 60% at 50% 50%, rgba(255,66,77,.03) 0%, transparent 70%);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
body::after {
|
||||
content:''; position:fixed; inset:0; pointer-events:none; z-index:999;
|
||||
background: repeating-linear-gradient(0deg,transparent 0px,transparent 3px,rgba(0,0,0,.06) 3px,rgba(0,0,0,.06) 4px);
|
||||
}
|
||||
|
||||
@keyframes rise { from{opacity:0;transform:translateY(14px) scale(.98)} to{opacity:1;transform:translateY(0) scale(1)} }
|
||||
@keyframes shimmer{ from{background-position:0% 50%} to{background-position:200% 50%} }
|
||||
@keyframes pulse-border { 0%,100%{box-shadow:0 0 0 0 var(--pat-glow)} 50%{box-shadow:0 0 0 6px transparent} }
|
||||
@keyframes live-pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
|
||||
|
||||
/* ── Page wrapper ── */
|
||||
.page { width:100%; max-width:1100px; padding:var(--page-pad); display:flex; flex-direction:column; gap:1.5rem; animation:rise .45s cubic-bezier(.22,.68,0,1.2) both; }
|
||||
|
||||
/* ── Top bar ── */
|
||||
.topbar { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; }
|
||||
.topbar-brand { display:flex; align-items:center; gap:.75rem; }
|
||||
.brand-name { font-size:1rem; font-weight:700; letter-spacing:-.01em; color:var(--text); text-decoration:none; }
|
||||
.brand-sub { font-family:'Share Tech Mono',monospace; font-size:.62rem; letter-spacing:.18em; text-transform:uppercase; color:var(--text-3); }
|
||||
.topbar-social { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; }
|
||||
.social-pill {
|
||||
display:inline-flex; align-items:center; gap:.4rem;
|
||||
padding:.35rem .75rem;
|
||||
background:rgba(255,255,255,.03);
|
||||
border:1px solid var(--border-2);
|
||||
border-radius:99px;
|
||||
font-family:'Share Tech Mono',monospace; font-size:.65rem; letter-spacing:.08em;
|
||||
color:var(--text-3); text-decoration:none;
|
||||
transition:border-color .2s, color .2s, background .2s;
|
||||
}
|
||||
.social-pill:hover { color:var(--text-2); background:rgba(255,255,255,.06); }
|
||||
.social-pill svg { flex-shrink:0; }
|
||||
.social-pill.active { border-color:var(--purple); color:var(--purple); }
|
||||
|
||||
/* ── Cards ── */
|
||||
.card {
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:calc(var(--r) + 2px);
|
||||
padding:1.4rem 1.5rem;
|
||||
display:flex; flex-direction:column; gap:.9rem;
|
||||
position:relative; overflow:hidden;
|
||||
}
|
||||
.card::after {
|
||||
content:''; position:absolute; bottom:-1px; right:-1px;
|
||||
width:14px; height:14px;
|
||||
border-bottom:2px solid var(--purple); border-right:2px solid var(--purple);
|
||||
border-radius:0 0 calc(var(--r)+2px) 0; opacity:.5;
|
||||
}
|
||||
.card-title { font-size:.95rem; font-weight:700; letter-spacing:-.01em; color:var(--text); line-height:1.2; }
|
||||
.card-body { font-size:.85rem; color:var(--text-2); line-height:1.7; }
|
||||
.card-body strong { color:var(--text); font-weight:600; }
|
||||
.card-body a { color:var(--pat); text-decoration:none; border-bottom:1px solid rgba(255,66,77,.25); transition:border-color .15s; }
|
||||
.card-body a:hover { border-color:rgba(255,66,77,.6); }
|
||||
|
||||
/* ── Eyebrow ── */
|
||||
.eyebrow { font-family:'Share Tech Mono',monospace; font-size:.62rem; letter-spacing:.2em; text-transform:uppercase; }
|
||||
.eyebrow-green { color:var(--green); text-shadow:0 0 10px rgba(0,232,122,.5); }
|
||||
.eyebrow-green::before { content:'> '; opacity:.5; }
|
||||
.eyebrow-purple { color:var(--purple); }
|
||||
.eyebrow-blue { color:var(--blue); }
|
||||
|
||||
/* ── Stream card / player card ── */
|
||||
.stream-card {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:calc(var(--r)+2px); overflow:hidden; position:relative;
|
||||
box-shadow:0 0 0 1px rgba(255,255,255,.03), 0 4px 50px rgba(0,0,0,.6);
|
||||
}
|
||||
.stream-card::before {
|
||||
content:''; position:absolute; top:0; left:0; right:0; height:2px; z-index:2;
|
||||
background:linear-gradient(90deg,var(--pat),var(--purple),var(--blue),var(--green),var(--blue),var(--purple),var(--pat));
|
||||
background-size:300% 100%; animation:shimmer 5s linear infinite;
|
||||
}
|
||||
.stream-ratio { position:relative; width:100%; padding-top:56.25%; background:#000; }
|
||||
.stream-ratio > * { position:absolute; inset:0; width:100%; height:100%; border:0; display:block; }
|
||||
.stream-footer {
|
||||
display:flex; align-items:center; justify-content:space-between; gap:.75rem;
|
||||
padding:.75rem 1.1rem; border-top:1px solid var(--border); flex-wrap:wrap;
|
||||
}
|
||||
.stream-meta { font-family:'Share Tech Mono',monospace; font-size:.65rem; letter-spacing:.1em; color:var(--text-3); }
|
||||
.stream-meta span { color:var(--text-2); }
|
||||
|
||||
/* ── Footer ── */
|
||||
.page-footer {
|
||||
padding:.5rem 0 1rem; display:flex; align-items:center; justify-content:center;
|
||||
gap:1.5rem; flex-wrap:wrap; border-top:1px solid var(--border);
|
||||
}
|
||||
.footer-note { font-family:'Share Tech Mono',monospace; font-size:.63rem; color:var(--text-3); letter-spacing:.06em; text-align:center; }
|
||||
.footer-note::before { content:'// '; opacity:.4; }
|
||||
.footer-note a { color:var(--text-3); text-decoration:none; transition:color .15s; }
|
||||
.footer-note a:hover { color:var(--text-2); }
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; gap:.45rem;
|
||||
padding:.6rem 1.1rem; border-radius:var(--r);
|
||||
font-family:'Inter',sans-serif; font-size:.82rem; font-weight:600;
|
||||
text-decoration:none; cursor:pointer; border:none;
|
||||
transition:transform .15s, filter .15s, background .2s;
|
||||
}
|
||||
.btn:hover { transform:translateY(-1px); filter:brightness(1.08); }
|
||||
.btn:active { transform:scale(.98); }
|
||||
.btn-primary { background:var(--purple); color:#fff; box-shadow:0 4px 18px rgba(176,96,255,.25); }
|
||||
.btn-secondary { background:rgba(255,255,255,.06); color:var(--text-2); border:1px solid var(--border-2); }
|
||||
.btn-secondary:hover { color:var(--text); background:rgba(255,255,255,.1); }
|
||||
.btn-danger { background:rgba(255,51,85,.15); color:var(--red); border:1px solid rgba(255,51,85,.3); }
|
||||
.btn-danger:hover { background:rgba(255,51,85,.25); }
|
||||
.btn-green { background:rgba(0,232,122,.12); color:var(--green); border:1px solid rgba(0,232,122,.3); }
|
||||
.btn-green:hover { background:rgba(0,232,122,.2); }
|
||||
.btn-sm { padding:.38rem .75rem; font-size:.75rem; }
|
||||
|
||||
/* ── Form controls ── */
|
||||
.form-group { display:flex; flex-direction:column; gap:.4rem; }
|
||||
.form-label { font-family:'Share Tech Mono',monospace; font-size:.65rem; letter-spacing:.12em; text-transform:uppercase; color:var(--text-3); }
|
||||
.form-input, .form-select, .form-textarea {
|
||||
background:var(--surface-2); border:1px solid var(--border-2); border-radius:var(--r);
|
||||
color:var(--text); padding:.6rem .85rem; font-family:'Inter',sans-serif; font-size:.85rem;
|
||||
transition:border-color .2s;
|
||||
width:100%;
|
||||
}
|
||||
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||
outline:none; border-color:var(--purple);
|
||||
box-shadow:0 0 0 2px rgba(176,96,255,.15);
|
||||
}
|
||||
.form-textarea { resize:vertical; min-height:80px; }
|
||||
.form-select option { background:var(--surface-2); }
|
||||
|
||||
/* ── Notice/alert ── */
|
||||
.notice {
|
||||
padding:.75rem 1rem; border-radius:var(--r); font-size:.82rem;
|
||||
border:1px solid; margin-bottom:1rem;
|
||||
}
|
||||
.notice-success { background:rgba(0,232,122,.08); border-color:rgba(0,232,122,.3); color:var(--green); }
|
||||
.notice-error { background:rgba(255,51,85,.08); border-color:rgba(255,51,85,.3); color:var(--red); }
|
||||
.notice-info { background:rgba(0,200,255,.08); border-color:rgba(0,200,255,.3); color:var(--blue); }
|
||||
|
||||
/* ── Pagination ── */
|
||||
.pagination { display:flex; align-items:center; gap:.4rem; flex-wrap:wrap; }
|
||||
.pagination a, .pagination span {
|
||||
display:inline-flex; align-items:center; justify-content:center;
|
||||
min-width:2rem; height:2rem; padding:0 .5rem;
|
||||
border-radius:var(--r); font-family:'Share Tech Mono',monospace; font-size:.72rem;
|
||||
text-decoration:none; transition:background .2s, color .2s, border-color .2s;
|
||||
border:1px solid var(--border-2); color:var(--text-3);
|
||||
}
|
||||
.pagination a:hover { border-color:var(--purple); color:var(--purple); background:rgba(176,96,255,.08); }
|
||||
.pagination .current { border-color:var(--purple); color:var(--purple); background:rgba(176,96,255,.15); }
|
||||
.pagination .disabled { opacity:.3; pointer-events:none; }
|
||||
|
||||
<?= $extra_css ?>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php }
|
||||
|
||||
function render_topbar(string $active = ''): void {
|
||||
$title = setting('site_title', 'Ty Clifford');
|
||||
$sub = setting('site_sub', 'tyclifford.com / player');
|
||||
?>
|
||||
<header class="topbar">
|
||||
<div class="topbar-brand">
|
||||
<div>
|
||||
<a href="index.php" class="brand-name"><?= h($title) ?></a>
|
||||
<div class="brand-sub"><?= h($sub) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="topbar-social" aria-label="Navigation">
|
||||
<a class="social-pill <?= $active==='home' ? 'active' : '' ?>" href="index.php">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
<a class="social-pill <?= $active==='catalogue' ? 'active' : '' ?>" href="catalogue.php">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
||||
Videos
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
<?php }
|
||||
|
||||
function render_footer(): void { ?>
|
||||
<footer class="page-footer" role="contentinfo">
|
||||
<p class="footer-note">TyClifford.com · player</p>
|
||||
<p class="footer-note"><a href="https://git.tyclifford.com/">open source</a></p>
|
||||
<p class="footer-note">hobbies are my hobby</p>
|
||||
<p class="footer-note"><a href="mailto:ty@tyclifford.com">ty@tyclifford.com</a></p>
|
||||
</footer>
|
||||
<?php }
|
||||
|
||||
function render_pagination(int $current, int $total, string $url_pattern): void {
|
||||
if ($total <= 1) return;
|
||||
echo '<div class="pagination">';
|
||||
$prev = $current - 1;
|
||||
$next = $current + 1;
|
||||
if ($prev >= 1) {
|
||||
echo '<a href="' . sprintf($url_pattern, $prev) . '">‹</a>';
|
||||
} else {
|
||||
echo '<span class="disabled">‹</span>';
|
||||
}
|
||||
$range = range(max(1, $current-2), min($total, $current+2));
|
||||
if (!in_array(1, $range)) { echo '<a href="' . sprintf($url_pattern, 1) . '">1</a><span>…</span>'; }
|
||||
foreach ($range as $p) {
|
||||
if ($p === $current) echo '<span class="current">' . $p . '</span>';
|
||||
else echo '<a href="' . sprintf($url_pattern, $p) . '">' . $p . '</a>';
|
||||
}
|
||||
if (!in_array($total, $range)) { echo '<span>…</span><a href="' . sprintf($url_pattern, $total) . '">' . $total . '</a>'; }
|
||||
if ($next <= $total) {
|
||||
echo '<a href="' . sprintf($url_pattern, $next) . '">›</a>';
|
||||
} else {
|
||||
echo '<span class="disabled">›</span>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
Reference in New Issue
Block a user