| +<?php | ||
| +require_once 'File.php'; | ||
| +require_once 'FileRenderer.php'; | ||
| + | ||
| +abstract class BasePage implements Page { | ||
| + protected $repositories; | ||
| + protected $title; | ||
| + | ||
| + public function __construct(array $repositories) { | ||
| + $this->repositories = $repositories; | ||
| + } | ||
| + | ||
| + protected function renderLayout($contentCallback, $currentRepo = null) { | ||
| + ?> | ||
| + <!DOCTYPE html> | ||
| + <html lang="en"> | ||
| + <head> | ||
| + <meta charset="UTF-8"> | ||
| + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| + <title><?php echo Config::SITE_TITLE . ($this->title ? ' - ' . htmlspecialchars($this->title) : ''); ?></title> | ||
| + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | ||
| + <link rel="stylesheet" href="repo.css"> | ||
| + </head> | ||
| + <body> | ||
| + <div class="container"> | ||
| + <header> | ||
| + <h1><?php echo Config::SITE_TITLE; ?></h1> | ||
| + <nav class="nav"> | ||
| + <a href="?">Home</a> | ||
| + <?php if ($currentRepo): | ||
| + $safeName = urlencode($currentRepo['safe_name']); ?> | ||
| + <a href="?repo=<?php echo $safeName; ?>">Files</a> | ||
| + <a href="?action=commits&repo=<?php echo $safeName; ?>">Commits</a> | ||
| + <a href="?action=refs&repo=<?php echo $safeName; ?>">Branches</a> | ||
| + <?php endif; ?> | ||
| + | ||
| + <?php if ($currentRepo): ?> | ||
| + <div class="repo-selector"> | ||
| + <label>Repository:</label> | ||
| + <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)"> | ||
| + <option value="">Select repository...</option> | ||
| + <?php foreach ($this->repositories as $r): ?> | ||
| + <option value="<?php echo htmlspecialchars($r['safe_name']); ?>" | ||
| + <?php echo $r['safe_name'] === $currentRepo['safe_name'] ? 'selected' : ''; ?>> | ||
| + <?php echo htmlspecialchars($r['name']); ?> | ||
| + </option> | ||
| + <?php endforeach; ?> | ||
| + </select> | ||
| + </div> | ||
| + <?php endif; ?> | ||
| + </nav> | ||
| + | ||
| + <?php if ($currentRepo): ?> | ||
| + <div style="margin-top: 15px;"> | ||
| + <span class="current-repo">Current: <strong><?php echo htmlspecialchars($currentRepo['name']); ?></strong></span> | ||
| + </div> | ||
| + <?php endif; ?> | ||
| + </header> | ||
| + | ||
| + <?php call_user_func($contentCallback); ?> | ||
| + | ||
| + </div> | ||
| + </body> | ||
| + </html> | ||
| + <?php | ||
| + } | ||
| +} | ||
| +<?php | ||
| +class CommitsPage extends BasePage { | ||
| + private $currentRepo; | ||
| + private $git; | ||
| + private $hash; | ||
| + | ||
| + public function __construct($allRepos, $currentRepo, $git, $hash) { | ||
| + parent::__construct($allRepos); | ||
| + $this->currentRepo = $currentRepo; | ||
| + $this->git = $git; | ||
| + $this->hash = $hash; | ||
| + $this->title = $currentRepo['name']; | ||
| + } | ||
| + | ||
| + public function render() { | ||
| + $this->renderLayout(function() { | ||
| + $main = $this->git->getMainBranch(); | ||
| + if (!$main) { | ||
| + echo '<div class="empty-state"><h3>No branches</h3><p>Empty repository.</p></div>'; | ||
| + return; | ||
| + } | ||
| + | ||
| + $this->renderBreadcrumbs(); | ||
| + echo '<h2>Commit History <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; | ||
| + echo '<div class="commit-list">'; | ||
| + | ||
| + $start = $this->hash ?: $main['hash']; | ||
| + $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| + | ||
| + $this->git->history($start, 100, function($commit) use ($repoParam) { | ||
| + $msg = htmlspecialchars(explode("\n", $commit->message)[0]); | ||
| + echo '<div class="commit-row">'; | ||
| + echo '<a href="?action=commit&hash=' . $commit->sha . $repoParam . '" class="sha">' . substr($commit->sha, 0, 7) . '</a>'; | ||
| + echo '<span class="message">' . $msg . '</span>'; | ||
| + echo '<span class="meta">' . htmlspecialchars($commit->author) . ' • ' . date('Y-m-d', $commit->date) . '</span>'; | ||
| + echo '</div>'; | ||
| + }); | ||
| + echo '</div>'; | ||
| + }, $this->currentRepo); | ||
| + } | ||
| + | ||
| + private function renderBreadcrumbs() { | ||
| + echo '<div class="breadcrumb">'; | ||
| + echo '<a href="?">Repositories</a><span>/</span>'; | ||
| + echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a><span>/</span>'; | ||
| + echo '<span>Commits</span></div>'; | ||
| + } | ||
| +} | ||
| +<?php | ||
| + | ||
| +class FilePage extends BasePage { | ||
| + private $currentRepo; | ||
| + private $git; | ||
| + private $hash; | ||
| + | ||
| + public function __construct($allRepos, $currentRepo, $git, $hash) { | ||
| + parent::__construct($allRepos); | ||
| + $this->currentRepo = $currentRepo; | ||
| + $this->git = $git; | ||
| + $this->hash = $hash; | ||
| + $this->title = $currentRepo['name']; | ||
| + } | ||
| + | ||
| + public function render() { | ||
| + $this->renderLayout(function() { | ||
| + $main = $this->git->getMainBranch(); | ||
| + if (!$main) { | ||
| + echo '<div class="empty-state"><h3>No branches</h3></div>'; | ||
| + return; | ||
| + } | ||
| + | ||
| + $target = $this->hash ?: $main['hash']; | ||
| + $entries = []; | ||
| + | ||
| + // Entries are now File objects | ||
| + $this->git->walk($target, function($file) use (&$entries) { | ||
| + $entries[] = $file; | ||
| + }); | ||
| + | ||
| + if (!empty($entries)) { | ||
| + $this->renderTree($main, $target, $entries); | ||
| + } else { | ||
| + $this->renderBlob($target); | ||
| + } | ||
| + }, $this->currentRepo); | ||
| + } | ||
| + | ||
| + private function renderTree($main, $targetHash, $entries) { | ||
| + $this->renderBreadcrumbs($targetHash, 'Tree'); | ||
| + echo '<h2>' . htmlspecialchars($this->currentRepo['name']) . ' <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; | ||
| + | ||
| + // Encapsulated sorting via File::compare | ||
| + usort($entries, function($a, $b) { | ||
| + return $a->compare($b); | ||
| + }); | ||
| + | ||
| + echo '<div class="file-list">'; | ||
| + $renderer = new HtmlFileRenderer($this->currentRepo['safe_name']); | ||
| + | ||
| + foreach ($entries as $file) { | ||
| + $file->render($renderer); | ||
| + } | ||
| + | ||
| + echo '</div>'; | ||
| + } | ||
| + | ||
| + private function renderBlob($targetHash) { | ||
| + $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| + | ||
| + $size = $this->git->getObjectSize($targetHash); | ||
| + | ||
| + $buffer = ''; | ||
| + $this->git->stream($targetHash, function($d) use (&$buffer) { | ||
| + if (strlen($buffer) < 12) $buffer .= $d; | ||
| + }); | ||
| + | ||
| + $filename = $_GET['name'] ?? ''; | ||
| + $category = MediaTypeSniffer::isCategory($buffer, $filename); | ||
| + $mimeType = MediaTypeSniffer::isMediaType($buffer, $filename); | ||
| + | ||
| + $this->renderBreadcrumbs($targetHash, 'File'); | ||
| + | ||
| + // UPDATED: Handle empty files | ||
| + if ($size === 0) { | ||
| + $this->renderDownloadState($targetHash, "This file is empty."); | ||
| + return; | ||
| + } | ||
| + | ||
| + $rawUrl = '?action=raw&hash=' . $targetHash . $repoParam . '&name=' . urlencode($filename); | ||
| + | ||
| + if ($category === MediaTypeSniffer::CAT_VIDEO) { | ||
| + echo '<div class="blob-content" style="text-align:center; padding: 20px; background: #000;">'; | ||
| + echo '<video controls style="max-width: 100%; max-height: 80vh;">'; | ||
| + echo '<source src="' . $rawUrl . '" type="' . $mimeType . '">'; | ||
| + echo 'Your browser does not support the video element.'; | ||
| + echo '</video>'; | ||
| + echo '</div>'; | ||
| + | ||
| + } elseif ($category === MediaTypeSniffer::CAT_AUDIO) { | ||
| + echo '<div class="blob-content" style="text-align:center; padding: 40px; background: #f6f8fa;">'; | ||
| + echo '<audio controls style="width: 100%; max-width: 600px;">'; | ||
| + echo '<source src="' . $rawUrl . '" type="' . $mimeType . '">'; | ||
| + echo 'Your browser does not support the audio element.'; | ||
| + echo '</audio>'; | ||
| + echo '</div>'; | ||
| + | ||
| + } elseif ($category === MediaTypeSniffer::CAT_IMAGE) { | ||
| + echo '<div class="blob-content" style="text-align:center; padding: 20px; background: #f6f8fa;">'; | ||
| + echo '<img src="' . $rawUrl . '" style="max-width: 100%; border: 1px solid #dfe2e5;">'; | ||
| + echo '</div>'; | ||
| + | ||
| + } elseif ($category === MediaTypeSniffer::CAT_TEXT) { | ||
| + if ($size > 524288) { | ||
| + $this->renderDownloadState($targetHash, "File is too large to display (" . $this->formatSize($size) . ")."); | ||
| + } else { | ||
| + $content = ''; | ||
| + $this->git->stream($targetHash, function($d) use (&$content) { $content .= $d; }); | ||
| + echo '<div class="blob-content"><pre class="blob-code">' . htmlspecialchars($content) . '</pre></div>'; | ||
| + } | ||
| + | ||
| + } else { | ||
| + $this->renderDownloadState($targetHash, "This is a binary file."); | ||
| + } | ||
| + } | ||
| + | ||
| + private function renderDownloadState($hash, $reason) { | ||
| + $url = '?action=raw&hash=' . $hash . '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| + echo '<div class="empty-state" style="text-align: center; padding: 40px; border: 1px solid #e1e4e8; border-radius: 6px; margin-top: 10px;">'; | ||
| + echo '<p style="margin-bottom: 20px; color: #586069;">' . htmlspecialchars($reason) . '</p>'; | ||
| + echo '<a href="' . $url . '" style="display: inline-block; padding: 6px 16px; background: #0366d6; color: white; text-decoration: none; border-radius: 6px; font-weight: 600;">Download Raw File</a>'; | ||
| + echo '</div>'; | ||
| + } | ||
| + | ||
| + private function formatSize($size) { | ||
| + if ($size <= 0) return '0 B'; | ||
| + $units = ['B', 'KB', 'MB', 'GB']; | ||
| + $i = (int)floor(log($size, 1024)); | ||
| + return round($size / pow(1024, $i), 1) . ' ' . $units[$i]; | ||
| + } | ||
| + | ||
| + private function renderBreadcrumbs($hash, $type) { | ||
| + echo '<div class="breadcrumb">'; | ||
| + echo '<a href="?">Repositories</a><span>/</span>'; | ||
| + echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>'; | ||
| + if ($this->hash) echo '<span>/</span><span>' . $type . ' ' . substr($hash, 0, 7) . '</span>'; | ||
| + echo '</div>'; | ||
| + } | ||
| +} | ||
| +<?php | ||
| +class HomePage extends BasePage { | ||
| + public function render() { | ||
| + $this->renderLayout(function() { | ||
| + echo '<h2>Repositories</h2>'; | ||
| + if (empty($this->repositories)) { | ||
| + echo '<div class="empty-state">No repositories found in ' . htmlspecialchars(Config::getReposPath()) . '</div>'; | ||
| + return; | ||
| + } | ||
| + echo '<div class="repo-grid">'; | ||
| + foreach ($this->repositories as $repo) { | ||
| + $this->renderRepoCard($repo); | ||
| + } | ||
| + echo '</div>'; | ||
| + }); | ||
| + } | ||
| + | ||
| + private function renderRepoCard($repo) { | ||
| + $git = new Git($repo['path']); | ||
| + $main = $git->getMainBranch(); | ||
| + | ||
| + $stats = ['branches' => 0, 'tags' => 0]; | ||
| + $git->eachBranch(function() use (&$stats) { $stats['branches']++; }); | ||
| + $git->eachTag(function() use (&$stats) { $stats['tags']++; }); | ||
| + | ||
| + echo '<a href="?repo=' . urlencode($repo['safe_name']) . '" class="repo-card">'; | ||
| + echo '<h3>' . htmlspecialchars($repo['name']) . '</h3>'; | ||
| + if ($main) echo '<p>Branch: ' . htmlspecialchars($main['name']) . '</p>'; | ||
| + echo '<p>' . $stats['branches'] . ' branches, ' . $stats['tags'] . ' tags</p>'; | ||
| + | ||
| + if ($main) { | ||
| + $git->history('HEAD', 1, function($c) use ($repo) { | ||
| + $renderer = new HtmlFileRenderer($repo['safe_name']); | ||
| + echo '<p style="margin-top: 8px; color: #58a6ff;">'; | ||
| + $renderer->renderTime($c->date); | ||
| + echo '</p>'; | ||
| + }); | ||
| + } | ||
| + echo '</a>'; | ||
| + } | ||
| +} | ||
| +<?php | ||
| + | ||
| +class RawPage implements Page { | ||
| + private $git; | ||
| + private $hash; | ||
| + | ||
| + public function __construct($git, $hash) { | ||
| + $this->git = $git; | ||
| + $this->hash = $hash; | ||
| + } | ||
| + | ||
| + public function render() { | ||
| + while (ob_get_level()) ob_end_clean(); | ||
| + | ||
| + $size = $this->git->getObjectSize($this->hash); | ||
| + $filename = $_GET['name'] ?? 'file'; | ||
| + | ||
| + $buffer = ''; | ||
| + $this->git->stream($this->hash, function($d) use (&$buffer) { | ||
| + if (strlen($buffer) < 12) $buffer .= $d; | ||
| + }); | ||
| + | ||
| + $mime = MediaTypeSniffer::isMediaType($buffer, $filename); | ||
| + if (!$mime) $mime = 'application/octet-stream'; | ||
| + | ||
| + header('Content-Type: ' . $mime); | ||
| + header('Content-Length: ' . $size); | ||
| + header('Content-Disposition: inline; filename="' . basename($filename) . '"'); | ||
| + | ||
| + $this->git->stream($this->hash, function($data) { | ||
| + echo $data; | ||
| + }); | ||
| + | ||
| + exit; | ||
| + } | ||
| +} | ||
| require_once 'FileRenderer.php'; | ||
| -abstract class BasePage implements Page { | ||
| - protected $repositories; | ||
| - protected $title; | ||
| - | ||
| - public function __construct(array $repositories) { | ||
| - $this->repositories = $repositories; | ||
| - } | ||
| - | ||
| - protected function renderLayout($contentCallback, $currentRepo = null) { | ||
| - ?> | ||
| - <!DOCTYPE html> | ||
| - <html lang="en"> | ||
| - <head> | ||
| - <meta charset="UTF-8"> | ||
| - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| - <title><?php echo Config::SITE_TITLE . ($this->title ? ' - ' . htmlspecialchars($this->title) : ''); ?></title> | ||
| - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | ||
| - <link rel="stylesheet" href="repo.css"> | ||
| - </head> | ||
| - <body> | ||
| - <div class="container"> | ||
| - <header> | ||
| - <h1><?php echo Config::SITE_TITLE; ?></h1> | ||
| - <nav class="nav"> | ||
| - <a href="?">Home</a> | ||
| - <?php if ($currentRepo): | ||
| - $safeName = urlencode($currentRepo['safe_name']); ?> | ||
| - <a href="?repo=<?php echo $safeName; ?>">Files</a> | ||
| - <a href="?action=commits&repo=<?php echo $safeName; ?>">Commits</a> | ||
| - <a href="?action=refs&repo=<?php echo $safeName; ?>">Branches</a> | ||
| - <?php endif; ?> | ||
| - | ||
| - <?php if ($currentRepo): ?> | ||
| - <div class="repo-selector"> | ||
| - <label>Repository:</label> | ||
| - <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)"> | ||
| - <option value="">Select repository...</option> | ||
| - <?php foreach ($this->repositories as $r): ?> | ||
| - <option value="<?php echo htmlspecialchars($r['safe_name']); ?>" | ||
| - <?php echo $r['safe_name'] === $currentRepo['safe_name'] ? 'selected' : ''; ?>> | ||
| - <?php echo htmlspecialchars($r['name']); ?> | ||
| - </option> | ||
| - <?php endforeach; ?> | ||
| - </select> | ||
| - </div> | ||
| - <?php endif; ?> | ||
| - </nav> | ||
| - | ||
| - <?php if ($currentRepo): ?> | ||
| - <div style="margin-top: 15px;"> | ||
| - <span class="current-repo">Current: <strong><?php echo htmlspecialchars($currentRepo['name']); ?></strong></span> | ||
| - </div> | ||
| - <?php endif; ?> | ||
| - </header> | ||
| - | ||
| - <?php call_user_func($contentCallback); ?> | ||
| - | ||
| - </div> | ||
| - </body> | ||
| - </html> | ||
| - <?php | ||
| - } | ||
| -} | ||
| - | ||
| -class HomePage extends BasePage { | ||
| - public function render() { | ||
| - $this->renderLayout(function() { | ||
| - echo '<h2>Repositories</h2>'; | ||
| - if (empty($this->repositories)) { | ||
| - echo '<div class="empty-state">No repositories found in ' . htmlspecialchars(Config::getReposPath()) . '</div>'; | ||
| - return; | ||
| - } | ||
| - echo '<div class="repo-grid">'; | ||
| - foreach ($this->repositories as $repo) { | ||
| - $this->renderRepoCard($repo); | ||
| - } | ||
| - echo '</div>'; | ||
| - }); | ||
| - } | ||
| - | ||
| - private function renderRepoCard($repo) { | ||
| - $git = new Git($repo['path']); | ||
| - $main = $git->getMainBranch(); | ||
| - | ||
| - $stats = ['branches' => 0, 'tags' => 0]; | ||
| - $git->eachBranch(function() use (&$stats) { $stats['branches']++; }); | ||
| - $git->eachTag(function() use (&$stats) { $stats['tags']++; }); | ||
| - | ||
| - echo '<a href="?repo=' . urlencode($repo['safe_name']) . '" class="repo-card">'; | ||
| - echo '<h3>' . htmlspecialchars($repo['name']) . '</h3>'; | ||
| - if ($main) echo '<p>Branch: ' . htmlspecialchars($main['name']) . '</p>'; | ||
| - echo '<p>' . $stats['branches'] . ' branches, ' . $stats['tags'] . ' tags</p>'; | ||
| - | ||
| - if ($main) { | ||
| - $git->history('HEAD', 1, function($c) use ($repo) { | ||
| - $renderer = new HtmlFileRenderer($repo['safe_name']); | ||
| - echo '<p style="margin-top: 8px; color: #58a6ff;">'; | ||
| - $renderer->renderTime($c->date); | ||
| - echo '</p>'; | ||
| - }); | ||
| - } | ||
| - echo '</a>'; | ||
| - } | ||
| -} | ||
| - | ||
| -class CommitsPage extends BasePage { | ||
| - private $currentRepo; | ||
| - private $git; | ||
| - private $hash; | ||
| - | ||
| - public function __construct($allRepos, $currentRepo, $git, $hash) { | ||
| - parent::__construct($allRepos); | ||
| - $this->currentRepo = $currentRepo; | ||
| - $this->git = $git; | ||
| - $this->hash = $hash; | ||
| - $this->title = $currentRepo['name']; | ||
| - } | ||
| - | ||
| - public function render() { | ||
| - $this->renderLayout(function() { | ||
| - $main = $this->git->getMainBranch(); | ||
| - if (!$main) { | ||
| - echo '<div class="empty-state"><h3>No branches</h3><p>Empty repository.</p></div>'; | ||
| - return; | ||
| - } | ||
| - | ||
| - $this->renderBreadcrumbs(); | ||
| - echo '<h2>Commit History <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; | ||
| - echo '<div class="commit-list">'; | ||
| - | ||
| - $start = $this->hash ?: $main['hash']; | ||
| - $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - | ||
| - $this->git->history($start, 100, function($commit) use ($repoParam) { | ||
| - $msg = htmlspecialchars(explode("\n", $commit->message)[0]); | ||
| - echo '<div class="commit-row">'; | ||
| - echo '<a href="?action=commit&hash=' . $commit->sha . $repoParam . '" class="sha">' . substr($commit->sha, 0, 7) . '</a>'; | ||
| - echo '<span class="message">' . $msg . '</span>'; | ||
| - echo '<span class="meta">' . htmlspecialchars($commit->author) . ' • ' . date('Y-m-d', $commit->date) . '</span>'; | ||
| - echo '</div>'; | ||
| - }); | ||
| - echo '</div>'; | ||
| - }, $this->currentRepo); | ||
| - } | ||
| - | ||
| - private function renderBreadcrumbs() { | ||
| - echo '<div class="breadcrumb">'; | ||
| - echo '<a href="?">Repositories</a><span>/</span>'; | ||
| - echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a><span>/</span>'; | ||
| - echo '<span>Commits</span></div>'; | ||
| - } | ||
| -} | ||
| - | ||
| -class FilePage extends BasePage { | ||
| - private $currentRepo; | ||
| - private $git; | ||
| - private $hash; | ||
| - | ||
| - public function __construct($allRepos, $currentRepo, $git, $hash) { | ||
| - parent::__construct($allRepos); | ||
| - $this->currentRepo = $currentRepo; | ||
| - $this->git = $git; | ||
| - $this->hash = $hash; | ||
| - $this->title = $currentRepo['name']; | ||
| - } | ||
| - | ||
| - public function render() { | ||
| - $this->renderLayout(function() { | ||
| - $main = $this->git->getMainBranch(); | ||
| - if (!$main) { | ||
| - echo '<div class="empty-state"><h3>No branches</h3></div>'; | ||
| - return; | ||
| - } | ||
| - | ||
| - $target = $this->hash ?: $main['hash']; | ||
| - $entries = []; | ||
| - | ||
| - // Entries are now File objects | ||
| - $this->git->walk($target, function($file) use (&$entries) { | ||
| - $entries[] = $file; | ||
| - }); | ||
| - | ||
| - if (!empty($entries)) { | ||
| - $this->renderTree($main, $target, $entries); | ||
| - } else { | ||
| - $this->renderBlob($target); | ||
| - } | ||
| - }, $this->currentRepo); | ||
| - } | ||
| - | ||
| - private function renderTree($main, $targetHash, $entries) { | ||
| - $this->renderBreadcrumbs($targetHash, 'Tree'); | ||
| - echo '<h2>' . htmlspecialchars($this->currentRepo['name']) . ' <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; | ||
| - | ||
| - // Encapsulated sorting via File::compare | ||
| - usort($entries, function($a, $b) { | ||
| - return $a->compare($b); | ||
| - }); | ||
| - | ||
| - echo '<div class="file-list">'; | ||
| - $renderer = new HtmlFileRenderer($this->currentRepo['safe_name']); | ||
| - | ||
| - foreach ($entries as $file) { | ||
| - $file->render($renderer); | ||
| - } | ||
| - | ||
| - echo '</div>'; | ||
| - } | ||
| - | ||
| - private function renderBlob($targetHash) { | ||
| - $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - | ||
| - $size = $this->git->getObjectSize($targetHash); | ||
| - | ||
| - $buffer = ''; | ||
| - $this->git->stream($targetHash, function($d) use (&$buffer) { | ||
| - if (strlen($buffer) < 12) $buffer .= $d; | ||
| - }); | ||
| - | ||
| - $filename = $_GET['name'] ?? ''; | ||
| - $category = MediaTypeSniffer::isCategory($buffer, $filename); | ||
| - $mimeType = MediaTypeSniffer::isMediaType($buffer, $filename); | ||
| - | ||
| - $this->renderBreadcrumbs($targetHash, 'File'); | ||
| - | ||
| - // UPDATED: Handle empty files | ||
| - if ($size === 0) { | ||
| - $this->renderDownloadState($targetHash, "This file is empty."); | ||
| - return; | ||
| - } | ||
| - | ||
| - $rawUrl = '?action=raw&hash=' . $targetHash . $repoParam . '&name=' . urlencode($filename); | ||
| - | ||
| - if ($category === MediaTypeSniffer::CAT_VIDEO) { | ||
| - echo '<div class="blob-content" style="text-align:center; padding: 20px; background: #000;">'; | ||
| - echo '<video controls style="max-width: 100%; max-height: 80vh;">'; | ||
| - echo '<source src="' . $rawUrl . '" type="' . $mimeType . '">'; | ||
| - echo 'Your browser does not support the video element.'; | ||
| - echo '</video>'; | ||
| - echo '</div>'; | ||
| - | ||
| - } elseif ($category === MediaTypeSniffer::CAT_AUDIO) { | ||
| - echo '<div class="blob-content" style="text-align:center; padding: 40px; background: #f6f8fa;">'; | ||
| - echo '<audio controls style="width: 100%; max-width: 600px;">'; | ||
| - echo '<source src="' . $rawUrl . '" type="' . $mimeType . '">'; | ||
| - echo 'Your browser does not support the audio element.'; | ||
| - echo '</audio>'; | ||
| - echo '</div>'; | ||
| - | ||
| - } elseif ($category === MediaTypeSniffer::CAT_IMAGE) { | ||
| - echo '<div class="blob-content" style="text-align:center; padding: 20px; background: #f6f8fa;">'; | ||
| - echo '<img src="' . $rawUrl . '" style="max-width: 100%; border: 1px solid #dfe2e5;">'; | ||
| - echo '</div>'; | ||
| - | ||
| - } elseif ($category === MediaTypeSniffer::CAT_TEXT) { | ||
| - if ($size > 524288) { | ||
| - $this->renderDownloadState($targetHash, "File is too large to display (" . $this->formatSize($size) . ")."); | ||
| - } else { | ||
| - $content = ''; | ||
| - $this->git->stream($targetHash, function($d) use (&$content) { $content .= $d; }); | ||
| - echo '<div class="blob-content"><pre class="blob-code">' . htmlspecialchars($content) . '</pre></div>'; | ||
| - } | ||
| - | ||
| - } else { | ||
| - $this->renderDownloadState($targetHash, "This is a binary file."); | ||
| - } | ||
| - } | ||
| - | ||
| - private function renderDownloadState($hash, $reason) { | ||
| - $url = '?action=raw&hash=' . $hash . '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - echo '<div class="empty-state" style="text-align: center; padding: 40px; border: 1px solid #e1e4e8; border-radius: 6px; margin-top: 10px;">'; | ||
| - echo '<p style="margin-bottom: 20px; color: #586069;">' . htmlspecialchars($reason) . '</p>'; | ||
| - echo '<a href="' . $url . '" style="display: inline-block; padding: 6px 16px; background: #0366d6; color: white; text-decoration: none; border-radius: 6px; font-weight: 600;">Download Raw File</a>'; | ||
| - echo '</div>'; | ||
| - } | ||
| - | ||
| - private function formatSize($size) { | ||
| - if ($size <= 0) return '0 B'; | ||
| - $units = ['B', 'KB', 'MB', 'GB']; | ||
| - $i = (int)floor(log($size, 1024)); | ||
| - return round($size / pow(1024, $i), 1) . ' ' . $units[$i]; | ||
| - } | ||
| - | ||
| - private function renderBreadcrumbs($hash, $type) { | ||
| - echo '<div class="breadcrumb">'; | ||
| - echo '<a href="?">Repositories</a><span>/</span>'; | ||
| - echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>'; | ||
| - if ($this->hash) echo '<span>/</span><span>' . $type . ' ' . substr($hash, 0, 7) . '</span>'; | ||
| - echo '</div>'; | ||
| - } | ||
| -} | ||
| - | ||
| -class RawPage implements Page { | ||
| - private $git; | ||
| - private $hash; | ||
| - | ||
| - public function __construct($git, $hash) { | ||
| - $this->git = $git; | ||
| - $this->hash = $hash; | ||
| - } | ||
| - | ||
| - public function render() { | ||
| - while (ob_get_level()) ob_end_clean(); | ||
| - | ||
| - $size = $this->git->getObjectSize($this->hash); | ||
| - $filename = $_GET['name'] ?? 'file'; | ||
| - | ||
| - $buffer = ''; | ||
| - $this->git->stream($this->hash, function($d) use (&$buffer) { | ||
| - if (strlen($buffer) < 12) $buffer .= $d; | ||
| - }); | ||
| - | ||
| - $mime = MediaTypeSniffer::isMediaType($buffer, $filename); | ||
| - if (!$mime) $mime = 'application/octet-stream'; | ||
| - | ||
| - header('Content-Type: ' . $mime); | ||
| - header('Content-Length: ' . $size); | ||
| - header('Content-Disposition: inline; filename="' . basename($filename) . '"'); | ||
| - | ||
| - $this->git->stream($this->hash, function($data) { | ||
| - echo $data; | ||
| - }); | ||
| - | ||
| - exit; | ||
| - } | ||
| -} | ||
| - | ||
| +require_once 'BasePage.php'; | ||
| +require_once 'HomePage.php'; | ||
| +require_once 'CommitsPage.php'; | ||
| +require_once 'FilePage.php'; | ||
| +require_once 'RawPage.php'; | ||
| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-08 23:48:57 GMT-0800 |
| Commit | 43c6f066c73de4f6ccf5a00f96c210b0c58a9690 |
| Parent | 8f1ed88 |
| Delta | 337 lines added, 327 lines removed, 10-line increase |