<?php
/**
 * SteamOS-style Launcher (single-file)
 * - Lists .html, .php, and .htm in the same folder as this PHP file
 * - If both base.html and base.htm exist, show ONLY base.html
 * - If base.html doesn't exist, show base.htm
 * - Extracts display title from <title> tag inside the file source (HTML/HTM/PHP)
 * - Poster image uses same base name: base.png/jpg/jpeg/webp/svg/gif/avif
 */

declare(strict_types=1);

function h(string $s): string {
  return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function getTitleFromSourceFile(string $path): string {
  $maxBytes = 524288; // 512KB
  $fh = @fopen($path, 'rb');
  if (!$fh) return '';
  $chunk = @fread($fh, $maxBytes);
  @fclose($fh);
  if ($chunk === false || $chunk === '') return '';

  if (preg_match('~<title\b[^>]*>(.*?)</title>~is', $chunk, $m)) {
    $title = trim(html_entity_decode(strip_tags($m[1]), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
    $title = preg_replace('/\s+/u', ' ', $title ?? '') ?? '';
    return trim($title);
  }
  return '';
}

function findPoster(string $dir, string $baseName): ?array {
  $exts = ['png','jpg','jpeg','webp','svg','gif','avif'];
  foreach ($exts as $ext) {
    $candidate = $dir . DIRECTORY_SEPARATOR . $baseName . '.' . $ext;
    if (is_file($candidate)) {
      return ['file' => $baseName . '.' . $ext, 'mtime' => @filemtime($candidate) ?: null];
    }
  }

  // Case-insensitive fallback scan (odd casing)
  $pattern = $dir . DIRECTORY_SEPARATOR . $baseName . '.*';
  foreach (glob($pattern, GLOB_NOSORT) ?: [] as $file) {
    if (!is_file($file)) continue;
    $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
    if (in_array($ext, $exts, true)) {
      return ['file' => basename($file), 'mtime' => @filemtime($file) ?: null];
    }
  }

  return null;
}

$dir = __DIR__;
$self = basename(__FILE__);

// Gather candidates
$html = []; // base => filename (.html)
$htm  = []; // base => filename (.htm)
$php  = []; // base => filename (.php) (multiple possible if weird duplicates; we store by filename later)

foreach (glob($dir . DIRECTORY_SEPARATOR . '*.{html,htm,php}', GLOB_BRACE) ?: [] as $path) {
  if (!is_file($path)) continue;
  $file = basename($path);

  // Exclude this launcher itself
  if (strcasecmp($file, $self) === 0) continue;

  $ext  = strtolower(pathinfo($file, PATHINFO_EXTENSION));
  $base = pathinfo($file, PATHINFO_FILENAME);

  if ($ext === 'html') $html[$base] = $file;
  elseif ($ext === 'htm') $htm[$base] = $file;
  elseif ($ext === 'php') $php[] = $file;
}

// Build final list:
// - Include all .html
// - Include .htm only if no matching .html exists
// - Include all .php
$finalFiles = [];

// 1) HTML
foreach ($html as $base => $file) {
  $finalFiles[] = $file;
}

// 2) HTM only where HTML doesn't exist
foreach ($htm as $base => $file) {
  if (!isset($html[$base])) {
    $finalFiles[] = $file;
  }
}

// 3) PHP (always include)
foreach ($php as $file) {
  $finalFiles[] = $file;
}

// Build entries
$games = [];
foreach ($finalFiles as $file) {
  $path = $dir . DIRECTORY_SEPARATOR . $file;
  $base = pathinfo($file, PATHINFO_FILENAME);

  $title = getTitleFromSourceFile($path);
  if ($title === '') $title = $base;

  $posterInfo = findPoster($dir, $base);
  $poster = $posterInfo['file'] ?? null;
  $posterMtime = $posterInfo['mtime'] ?? null;

  $games[] = [
    'file' => $file,
    'title' => $title,
    'base' => $base,
    'poster' => $poster,
    'poster_mtime' => $posterMtime,
  ];
}

// Sort by title natural (case-insensitive)
usort($games, function($a, $b) {
  return strnatcasecmp((string)$a['title'], (string)$b['title']);
});

$total = count($games);
?><!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="color-scheme" content="dark" />
  <title>Library (<?= (int)$total ?>)</title>
  <style>
    :root{
      --bg0:#070a12;
      --bg1:#0b1020;
      --card: rgba(255,255,255,.06);
      --card2: rgba(255,255,255,.10);
      --stroke: rgba(255,255,255,.10);
      --stroke2: rgba(255,255,255,.18);
      --text:#eaf0ff;
      --muted: rgba(234,240,255,.72);
      --accent:#6ee7ff;
      --shadow: 0 12px 40px rgba(0,0,0,.45);
      --radius: 22px;
    }

    *{box-sizing:border-box}
    html,body{height:100%}
    body{
      margin:0;
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
      background:
        radial-gradient(1200px 800px at 20% 10%, rgba(110,231,255,.12), transparent 60%),
        radial-gradient(900px 700px at 80% 30%, rgba(70,246,168,.10), transparent 55%),
        linear-gradient(180deg, var(--bg0), var(--bg1));
      color:var(--text);
      overflow-x:hidden;
    }

    .topbar{
      position: sticky;
      top: 0;
      z-index: 50;
      backdrop-filter: blur(14px);
      background: linear-gradient(180deg, rgba(7,10,18,.92), rgba(11,16,32,.72));
      border-bottom: 1px solid var(--stroke);
    }

    .topbar-inner{
      max-width: 1400px;
      margin: 0 auto;
      padding: 16px 18px;
      display:flex;
      align-items:center;
      gap:14px;
    }

    .brand{
      display:flex;
      align-items:center;
      gap:12px;
      min-width: 210px;
    }

    .logo{
      width:42px;height:42px;
      border-radius: 14px;
      background: radial-gradient(circle at 30% 25%, rgba(110,231,255,.85), rgba(110,231,255,.12) 55%, rgba(255,255,255,.06));
      border: 1px solid var(--stroke2);
      box-shadow: var(--shadow);
      position:relative;
      overflow:hidden;
      flex: 0 0 auto;
    }
    .logo:after{
      content:"";
      position:absolute; inset:-40% -40%;
      background: conic-gradient(from 180deg, transparent, rgba(110,231,255,.22), transparent, rgba(70,246,168,.18), transparent);
      animation: spin 6s linear infinite;
    }
    @keyframes spin { to { transform: rotate(360deg); } }

    .brand h1{
      margin:0;
      font-size: 16px;
      letter-spacing: .3px;
      font-weight: 800;
      line-height: 1.2;
    }
    .brand .sub{
      font-size: 12px;
      color: var(--muted);
      margin-top: 2px;
    }

    .search{
      flex: 1;
      display:flex;
      align-items:center;
      gap:10px;
    }

    .search input{
      width:100%;
      padding: 12px 14px;
      border-radius: 16px;
      border: 1px solid var(--stroke);
      background: rgba(255,255,255,.05);
      color: var(--text);
      outline: none;
      font-size: 14px;
    }
    .search input:focus{
      border-color: rgba(110,231,255,.45);
      box-shadow: 0 0 0 4px rgba(110,231,255,.12);
    }

    .pill{
      padding: 10px 12px;
      border-radius: 999px;
      border: 1px solid var(--stroke);
      background: rgba(255,255,255,.04);
      color: var(--muted);
      font-size: 12px;
      white-space: nowrap;
    }

    .wrap{
      max-width: 1400px;
      margin: 0 auto;
      padding: 18px;
    }

    .grid{
      display:grid;
      grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
      gap: 14px;
    }

    /* SteamOS-ish poster cards (poster + title only) */
    .card{
      display:flex;
      flex-direction:column;
      gap: 10px;
      text-decoration:none;
      color: inherit;
      background: var(--card);
      border: 1px solid var(--stroke);
      border-radius: var(--radius);
      padding: 12px;
      box-shadow: 0 10px 28px rgba(0,0,0,.25);
      transition: transform .18s ease, background .18s ease, border-color .18s ease, box-shadow .18s ease;
      position:relative;
      overflow:hidden;
      min-height: 272px;
      will-change: transform;
      outline: none;
    }

    /* Hover/Focus animation (mouse + keyboard) */
    .card:hover,
    .card:focus-visible,
    .card.is-hover{
      transform: translateY(-4px) scale(1.02);
      background: var(--card2);
      border-color: rgba(110,231,255,.28);
      box-shadow: 0 18px 44px rgba(0,0,0,.36);
    }

    .poster{
      width: 100%;
      aspect-ratio: 2 / 3;
      border-radius: 18px;
      overflow:hidden;
      border: 1px solid rgba(255,255,255,.10);
      background:
        radial-gradient(500px 260px at 40% 30%, rgba(110,231,255,.16), transparent 65%),
        linear-gradient(140deg, rgba(255,255,255,.10), rgba(255,255,255,.02));
      display:flex;
      align-items:center;
      justify-content:center;
      position:relative;
    }

    .poster img{
      width:100%;
      height:100%;
      object-fit: cover;
      display:block;
      transform: scale(1.02);
      transition: transform .22s ease;
      will-change: transform;
    }

    /* Poster enlargement on hover */
    .card:hover .poster img,
    .card:focus-visible .poster img,
    .card.is-hover .poster img{
      transform: scale(1.10);
    }

    /* Touch press feedback */
    .card:active{
      transform: translateY(-1px) scale(1.01);
    }
    .card:active .poster img{
      transform: scale(1.08);
    }

    .fallback{
      padding: 12px;
      text-align:center;
      color: rgba(234,240,255,.86);
      font-weight: 900;
      line-height: 1.2;
    }
    .fallback small{
      display:block;
      color: var(--muted);
      font-weight: 600;
      margin-top: 8px;
    }

    .title{
      font-weight: 900;
      font-size: 14px;
      letter-spacing: .2px;
      margin-top: 2px;
      text-align: center;
      display:-webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow:hidden;
      min-height: 38px;
      padding: 0 4px;
    }

    .empty{
      margin-top: 20px;
      padding: 18px;
      border-radius: var(--radius);
      border: 1px dashed var(--stroke2);
      background: rgba(255,255,255,.04);
      color: var(--muted);
    }

    @media (max-width: 520px){
      .brand{min-width:unset}
      .pill{display:none}
      .grid{grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));}
      .card{min-height: 252px;}
    }

    @media (prefers-reduced-motion: reduce){
      .card, .poster img, .logo:after { transition:none !important; animation:none !important; }
    }
  </style>
</head>
<body>
  <div class="topbar">
    <div class="topbar-inner">
      <div class="brand" aria-label="Library">
        <div class="logo" aria-hidden="true"></div>
        <div>
          <h1>Library</h1>
          <div class="sub"><?= (int)$total ?> item<?= $total === 1 ? '' : 's' ?></div>
        </div>
      </div>

      <div class="search">
        <input id="q" type="search" placeholder="Search titles…" autocomplete="off" />
      </div>

      <div class="pill" id="countPill"><?= (int)$total ?> shown</div>
    </div>
  </div>

  <div class="wrap">
    <?php if ($total === 0): ?>
      <div class="empty">
        No <code>.html</code>, <code>.htm</code>, or <code>.php</code> files found in this folder.
      </div>
    <?php else: ?>
      <div class="grid" id="grid">
        <?php foreach ($games as $g): ?>
          <?php
            $file  = $g['file'];
            $title = $g['title'];
            $poster = $g['poster'];

            $href = rawurlencode($file);

            $posterUrl = '';
            if ($poster) {
              $posterUrl = rawurlencode($poster);
              if (!empty($g['poster_mtime'])) $posterUrl .= '?v=' . (int)$g['poster_mtime'];
            }
          ?>
          <a class="card" href="<?= h($href) ?>" data-title="<?= h(mb_strtolower($title, 'UTF-8')) ?>" aria-label="<?= h($title) ?>">
            <div class="poster">
              <?php if ($poster): ?>
                <img src="<?= h($posterUrl) ?>" alt="<?= h($title) ?> poster" loading="lazy" />
              <?php else: ?>
                <div class="fallback">
                  <?= h($title) ?>
                  <small>No poster found</small>
                </div>
              <?php endif; ?>
            </div>
            <div class="title"><?= h($title) ?></div>
          </a>
        <?php endforeach; ?>
      </div>
    <?php endif; ?>
  </div>

  <script>
    (function(){
      const q = document.getElementById('q');
      const grid = document.getElementById('grid');
      const pill = document.getElementById('countPill');
      if (!q || !grid) return;

      const cards = Array.from(grid.querySelectorAll('.card'));
      const total = cards.length;

      function update(){
        const term = (q.value || '').trim().toLowerCase();
        let shown = 0;

        for (const c of cards){
          const t = c.getAttribute('data-title') || '';
          const ok = !term || t.includes(term);
          c.style.display = ok ? '' : 'none';
          if (ok) shown++;
        }
        if (pill) pill.textContent = shown + ' shown';
      }

      q.addEventListener('input', update);

      // "/" focuses search, ESC clears
      window.addEventListener('keydown', (e) => {
        if (e.key === '/' && document.activeElement !== q) {
          e.preventDefault();
          q.focus();
        }
        if (e.key === 'Escape' && document.activeElement === q) {
          q.value = '';
          q.blur();
          update();
        }
      });

      // Touch-friendly "hover" animation:
      // On touchstart, add .is-hover briefly so you still get the hover feel.
      let hoverTimer = null;
      grid.addEventListener('touchstart', (e) => {
        const card = e.target.closest && e.target.closest('.card');
        if (!card) return;

        // Clear existing
        for (const c of cards) c.classList.remove('is-hover');
        card.classList.add('is-hover');

        clearTimeout(hoverTimer);
        hoverTimer = setTimeout(() => card.classList.remove('is-hover'), 850);
      }, {passive:true});

      // Remove touch-hover when scrolling starts
      window.addEventListener('touchmove', () => {
        for (const c of cards) c.classList.remove('is-hover');
      }, {passive:true});

      update();
    })();
  </script>
</body>
</html>
``