<?php
declare(strict_types=1);

/* ============================================================
   پنل شخصی مدیریت فایل  |  فقط همین یک فایل را روی هاست بگذارید
   ============================================================ */

const ADMIN_PASSWORD = 'Mj138574';          // ← رمز ورود پنل و آپلود (حتماً عوض کنید)
const SITE_TITLE     = 'پنل فایل‌ها';
const STORAGE_DIR    = __DIR__ . '/storage';     // بهتر است خارج از public_html باشد (مثلاً dirname(__DIR__).'/storage')
const MAX_FILE_SIZE  = 0;                        // حداکثر حجم هر فایل به بایت؛ 0 یعنی نامحدود
const PRETTY_LINKS   = false;                    // true فقط وقتی htaccess.txt را به .htaccess تغییر نام دادید

/* ====================== راه‌اندازی ====================== */
mb_internal_encoding('UTF-8');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: same-origin');
header('X-Frame-Options: SAMEORIGIN');

$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
session_name('fhp_sess');
session_set_cookie_params(['lifetime' => 0, 'path' => '/', 'secure' => $https, 'httponly' => true, 'samesite' => 'Lax']);
session_start();
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16));

foreach ([STORAGE_DIR, STORAGE_DIR . '/files', STORAGE_DIR . '/tmp'] as $d) {
    if (!is_dir($d)) @mkdir($d, 0755, true);
}
if (!is_file(STORAGE_DIR . '/.htaccess')) {
    @file_put_contents(STORAGE_DIR . '/.htaccess', "Require all denied\nDeny from all\n");
    @file_put_contents(STORAGE_DIR . '/index.html', '');
}

const INLINE_MIME = [
    'mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'webm' => 'video/webm', 'ogv' => 'video/ogg',
    'mp3' => 'audio/mpeg', 'm4a' => 'audio/mp4', 'ogg' => 'audio/ogg', 'wav' => 'audio/wav',
    'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp',
];

/* ====================== توابع کمکی ====================== */
function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }
function is_admin(): bool { return ($_SESSION['admin'] ?? false) === true; }

function meta_read(): array {
    $f = STORAGE_DIR . '/meta.php';
    if (!is_file($f)) return [];
    $raw = (string)file_get_contents($f);
    $pos = strpos($raw, "\n");
    $d = json_decode($pos === false ? '' : substr($raw, $pos + 1), true);
    return is_array($d) ? $d : [];
}
function meta_update(callable $fn) {
    $lock = fopen(STORAGE_DIR . '/.lock', 'c');
    flock($lock, LOCK_EX);
    $d = meta_read();
    $r = $fn($d);
    $f = STORAGE_DIR . '/meta.php';
    file_put_contents($f . '.tmp', "<?php http_response_code(404); exit; ?>\n" . json_encode($d, JSON_UNESCAPED_UNICODE));
    rename($f . '.tmp', $f);
    flock($lock, LOCK_UN);
    fclose($lock);
    return $r;
}
function get_file(string $id): ?array {
    if (!preg_match('/^[a-f0-9]{12}$/', $id)) return null;
    return meta_read()[$id] ?? null;
}
function can_access(array $f): bool {
    return is_admin() || empty($f['pass']) || !empty($_SESSION['unlocked'][$f['id']]);
}
function origin(): string {
    global $https;
    return ($https ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
}
function link_dl(array $f): string {
    if (PRETTY_LINKS) return origin() . rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/d/' . $f['id'] . '/' . rawurlencode($f['name']);
    return origin() . $_SERVER['SCRIPT_NAME'] . '?d=' . $f['id'];
}
function link_page(array $f): string {
    if (PRETTY_LINKS) return origin() . rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/f/' . $f['id'];
    return origin() . $_SERVER['SCRIPT_NAME'] . '?f=' . $f['id'];
}
function kind_of(array $f): string {
    $m = $f['mime'] ?? '';
    if (!in_array($m, INLINE_MIME, true)) return '';
    return explode('/', $m)[0];
}
function clean_name(string $n): string {
    $n = basename(str_replace('\\', '/', $n));
    $n = preg_replace('/[\x00-\x1F\x7F"<>|:*?]/u', '', $n) ?? '';
    $n = trim($n);
    if (mb_strlen($n) > 180) {
        $ext = pathinfo($n, PATHINFO_EXTENSION);
        $n = mb_substr($n, 0, 170) . ($ext ? '.' . $ext : '');
    }
    return $n === '' ? 'file' : $n;
}
function ini_bytes(string $k): int {
    $v = trim((string)ini_get($k));
    if ($v === '') return 0;
    $n = (float)$v;
    switch (strtolower(substr($v, -1))) { case 'g': $n *= 1024; case 'm': $n *= 1024; case 'k': $n *= 1024; }
    return (int)$n;
}
function json_out(array $d, int $code = 200): void {
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($d, JSON_UNESCAPED_UNICODE);
    exit;
}
function fail(string $msg, array $extra = [], int $code = 400): void {
    json_out(['ok' => false, 'error' => $msg] + $extra, $code);
}
function human(int $b): string {
    $u = ['بایت', 'کیلوبایت', 'مگابایت', 'گیگابایت', 'ترابایت'];
    $i = 0; $n = (float)$b;
    while ($n >= 1024 && $i < 4) { $n /= 1024; $i++; }
    return number_format($n, $i ? 1 : 0) . ' ' . $u[$i];
}

function serve_file(array $f, bool $inline, bool $count): void {
    $path = STORAGE_DIR . '/files/' . $f['stored'];
    if (!is_file($path)) { http_response_code(404); exit('فایل پیدا نشد.'); }
    $size = filesize($path);
    $start = 0; $end = $size - 1; $status = 200;
    if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $m) && ($m[1] !== '' || $m[2] !== '')) {
        if ($m[1] === '') { $start = max(0, $size - (int)$m[2]); }
        else { $start = (int)$m[1]; if ($m[2] !== '') $end = min($end, (int)$m[2]); }
        if ($start > $end || $start >= $size) { http_response_code(416); header("Content-Range: bytes */$size"); exit; }
        $status = 206;
    }
    if ($count && $start === 0) {
        meta_update(function (array &$d) use ($f) { if (isset($d[$f['id']])) $d[$f['id']]['downloads'] = ($d[$f['id']]['downloads'] ?? 0) + 1; });
    }
    $ascii = preg_replace('/[^\x20-\x7E]/', '_', $f['name']);
    $ascii = str_replace(['"', '\\'], '_', (string)$ascii);
    session_write_close();
    while (ob_get_level()) ob_end_clean();
    @set_time_limit(0);
    http_response_code($status);
    header('Content-Type: ' . ($inline ? $f['mime'] : 'application/octet-stream'));
    header('Content-Disposition: ' . ($inline ? 'inline' : 'attachment') . '; filename="' . $ascii . '"; filename*=UTF-8\'\'' . rawurlencode($f['name']));
    header('Accept-Ranges: bytes');
    header('Content-Length: ' . ($end - $start + 1));
    header('Cache-Control: private, max-age=0');
    if ($status === 206) header("Content-Range: bytes $start-$end/$size");
    $fp = fopen($path, 'rb');
    fseek($fp, $start);
    $left = $end - $start + 1;
    while ($left > 0 && !feof($fp) && !connection_aborted()) {
        $buf = fread($fp, (int)min(1048576, $left));
        if ($buf === false) break;
        echo $buf;
        $left -= strlen($buf);
        flush();
    }
    fclose($fp);
    exit;
}

/* ====================== ظاهر مشترک ====================== */
function head(string $title): void { ?>
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title><?= e($title) ?></title>
<style>
:root{
  --bg:#eceff4; --card:#fff; --ink:#16213a; --muted:#68738d; --line:#dce1ea;
  --brand:#0d6f8f; --brand-d:#0a5a75; --brand-soft:#e1f1f6; --danger:#c23b52; --ok:#1d8a5a; --warn:#b7791f;
  --nav:#16213a;
  font-family:Tahoma,"Segoe UI","Vazirmatn","Noto Naskh Arabic",sans-serif;
}
@media (prefers-color-scheme:dark){
  :root{--bg:#0f1524;--card:#18203a;--ink:#e8ecf6;--muted:#93a0bf;--line:#2a3558;--brand:#39a9cc;--brand-d:#2d90b0;--brand-soft:#16344a;--nav:#0a0f1d}
}
*{box-sizing:border-box}
html,body{margin:0}
body{background:var(--bg);color:var(--ink);line-height:1.7;font-size:15px}
button,input{font:inherit;color:inherit}
a{color:var(--brand)}
:focus-visible{outline:3px solid var(--brand);outline-offset:2px;border-radius:6px}
.btn{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);background:var(--card);padding:9px 16px;border-radius:10px;cursor:pointer;text-decoration:none;color:var(--ink);transition:background .15s,border-color .15s}
.btn:hover{border-color:var(--brand)}
.btn.primary{background:var(--brand);border-color:var(--brand);color:#fff}
.btn.primary:hover{background:var(--brand-d)}
.btn.danger{background:var(--danger);border-color:var(--danger);color:#fff}
.btn.big{padding:12px 22px;font-size:16px}
.btn[disabled]{opacity:.55;cursor:not-allowed}
.field{width:100%;padding:11px 14px;border:1px solid var(--line);border-radius:10px;background:var(--card)}
.field:focus{border-color:var(--brand);outline:2px solid var(--brand-soft)}
svg.i{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex:none}
.center{min-height:100vh;display:grid;place-items:center;padding:20px}
.box{width:100%;max-width:420px;background:var(--card);border:1px solid var(--line);border-radius:18px;padding:30px}
.box.wide{max-width:720px}
.err{background:#fbe9ec;color:#8f2236;border-radius:10px;padding:10px 14px;margin:0 0 14px}
@media (prefers-color-scheme:dark){.err{background:#3b1822;color:#ffb3c0}}
</style>
</head>
<body>
<?php }
function foot(): void { echo "</body></html>"; }
const ICON_LOCK = '<svg class="i" viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>';
const ICON_DL   = '<svg class="i" viewBox="0 0 24 24"><path d="M12 4v11m0 0l-4-4m4 4l4-4M5 20h14"/></svg>';

/* ====================== مسیرهای عمومی (لینک دانلود) ====================== */

// دانلود مستقیم
if (isset($_GET['d'])) {
    $f = get_file((string)$_GET['d']);
    if (!$f) { http_response_code(404); exit('فایل پیدا نشد.'); }
    if (!can_access($f)) { header('Location: ' . link_page($f)); exit; }
    serve_file($f, false, true);
}

// پخش آنلاین (برای پیش‌نمایش)
if (isset($_GET['v'])) {
    $f = get_file((string)$_GET['v']);
    if (!$f) { http_response_code(404); exit; }
    if (!can_access($f)) { http_response_code(403); exit('دسترسی ندارید.'); }
    if (kind_of($f) === '') { http_response_code(415); exit; }
    serve_file($f, true, false);
}

// باز کردن قفل فایل رمزدار
$unlockError = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'unlock') {
    $f = get_file((string)($_POST['id'] ?? ''));
    if ($f && hash_equals($_SESSION['csrf'], (string)($_POST['csrf'] ?? ''))) {
        $key = 'tries_' . $f['id'];
        $_SESSION[$key] = ($_SESSION[$key] ?? 0) + 1;
        if (($_SESSION[$key] ?? 0) > 8) { sleep(3); }
        if (!empty($f['pass']) && password_verify((string)($_POST['password'] ?? ''), $f['pass'])) {
            $_SESSION['unlocked'][$f['id']] = true;
            $_SESSION[$key] = 0;
        } else {
            usleep(500000);
            $unlockError = 'رمز اشتباه است.';
        }
        $_GET['f'] = $f['id'];
    }
}

// صفحهٔ فایل (عمومی)
if (isset($_GET['f'])) {
    $f = get_file((string)$_GET['f']);
    if (!$f) { http_response_code(404); head('پیدا نشد'); echo '<div class="center"><div class="box"><h2>فایل پیدا نشد</h2><p>ممکن است این لینک حذف شده باشد.</p></div></div>'; foot(); exit; }
    $ok = can_access($f);
    $kind = kind_of($f);
    head($f['name']); ?>
<style>
.fname{font-size:20px;margin:0 0 4px;word-break:break-word}
.fmeta{color:var(--muted);margin:0 0 22px;display:flex;gap:18px;flex-wrap:wrap}
.player{width:100%;max-height:70vh;border-radius:12px;background:#000;margin-bottom:20px;display:block}
img.player{object-fit:contain;background:var(--bg)}
audio.player{background:none;height:54px}
.lockrow{display:flex;gap:10px;flex-wrap:wrap}
.lockrow .field{flex:1;min-width:180px}
</style>
<div class="center"><div class="box wide">
  <h1 class="fname"><?= e($f['name']) ?></h1>
  <p class="fmeta"><span><?= e(human((int)$f['size'])) ?></span><span><?= (int)($f['downloads'] ?? 0) ?> دانلود</span><?php if (!empty($f['pass'])): ?><span style="display:inline-flex;gap:5px;align-items:center"><?= ICON_LOCK ?> رمزدار</span><?php endif; ?></p>
<?php if (!$ok): ?>
  <?php if ($unlockError): ?><p class="err"><?= e($unlockError) ?></p><?php endif; ?>
  <p>برای دسترسی به این فایل رمز را وارد کنید.</p>
  <form method="post" class="lockrow">
    <input type="hidden" name="action" value="unlock">
    <input type="hidden" name="id" value="<?= e($f['id']) ?>">
    <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
    <input class="field" type="password" name="password" placeholder="رمز فایل" autofocus required>
    <button class="btn primary" type="submit">باز کردن</button>
  </form>
<?php else: ?>
  <?php if ($kind === 'video'): ?><video class="player" controls playsinline preload="metadata" src="<?= e($_SERVER['SCRIPT_NAME']) ?>?v=<?= e($f['id']) ?>"></video>
  <?php elseif ($kind === 'audio'): ?><audio class="player" controls preload="metadata" src="<?= e($_SERVER['SCRIPT_NAME']) ?>?v=<?= e($f['id']) ?>"></audio>
  <?php elseif ($kind === 'image'): ?><img class="player" alt="" src="<?= e($_SERVER['SCRIPT_NAME']) ?>?v=<?= e($f['id']) ?>"><?php endif; ?>
  <a class="btn primary big" href="<?= e(link_dl($f)) ?>"><?= ICON_DL ?> دانلود فایل</a>
<?php endif; ?>
</div></div>
<?php foot(); exit;
}

/* ====================== ورود و خروج ====================== */
$loginError = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'login') {
    $ipf = STORAGE_DIR . '/lock_' . md5($_SERVER['REMOTE_ADDR'] ?? 'x') . '.json';
    $st = is_file($ipf) ? (json_decode((string)file_get_contents($ipf), true) ?: []) : [];
    if (($st['until'] ?? 0) > time()) {
        $loginError = 'تلاش‌های ناموفق زیاد بود. چند دقیقه بعد دوباره امتحان کنید.';
    } elseif (hash_equals(ADMIN_PASSWORD, (string)($_POST['password'] ?? ''))) {
        @unlink($ipf);
        session_regenerate_id(true);
        $_SESSION['admin'] = true;
        header('Location: ' . $_SERVER['SCRIPT_NAME']); exit;
    } else {
        $n = ($st['n'] ?? 0) + 1;
        file_put_contents($ipf, json_encode(['n' => $n >= 5 ? 0 : $n, 'until' => $n >= 5 ? time() + 600 : 0]));
        usleep(700000);
        $loginError = 'رمز اشتباه است.';
    }
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'logout' && hash_equals($_SESSION['csrf'], (string)($_POST['csrf'] ?? ''))) {
    unset($_SESSION['admin']);
    header('Location: ' . $_SERVER['SCRIPT_NAME']); exit;
}

/* ====================== API پنل (فقط مدیر) ====================== */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && is_admin()) {
    if (!hash_equals($_SESSION['csrf'], (string)($_SERVER['HTTP_X_CSRF'] ?? ''))) fail('نشست منقضی شده؛ صفحه را تازه کنید.', [], 403);
    $act = (string)$_POST['action'];

    if ($act === 'list') {
        $all = meta_read();
        uasort($all, fn($a, $b) => ($b['time'] ?? 0) <=> ($a['time'] ?? 0));
        $out = []; $tot = 0; $dls = 0;
        foreach ($all as $f) {
            $tot += (int)$f['size']; $dls += (int)($f['downloads'] ?? 0);
            $out[] = ['id' => $f['id'], 'name' => $f['name'], 'size' => (int)$f['size'], 'time' => (int)$f['time'],
                'downloads' => (int)($f['downloads'] ?? 0), 'locked' => !empty($f['pass']), 'kind' => kind_of($f),
                'dl' => link_dl($f), 'page' => link_page($f), 'view' => $_SERVER['SCRIPT_NAME'] . '?v=' . $f['id']];
        }
        $lim = array_filter([ini_bytes('upload_max_filesize'), ini_bytes('post_max_size')]);
        $chunk = $lim ? (int)min(8 * 1048576, floor(min($lim) * 0.85)) : 1048576;
        json_out(['ok' => true, 'files' => $out, 'total' => $tot, 'downloads' => $dls, 'chunk' => max(65536, $chunk), 'max' => MAX_FILE_SIZE]);
    }

    if ($act === 'chunk') {
        $uid = (string)($_POST['uid'] ?? '');
        if (!preg_match('/^[a-f0-9]{16,32}$/', $uid)) fail('شناسه آپلود نامعتبر است.');
        $idx = (int)($_POST['idx'] ?? 0);
        $total = (int)($_POST['total'] ?? 1);
        $offset = (int)($_POST['offset'] ?? 0);
        $fsize = (int)($_POST['size'] ?? 0);
        $name = clean_name((string)($_POST['name'] ?? 'file'));
        if (MAX_FILE_SIZE > 0 && $fsize > MAX_FILE_SIZE) fail('حجم فایل از حد مجاز بیشتر است.');
        $up = $_FILES['chunk'] ?? null;
        if (!$up || $up['error'] !== UPLOAD_ERR_OK) fail('دریافت قطعه ناموفق بود (کد ' . ($up['error'] ?? '?') . '). محدودیت upload_max_filesize هاست را بررسی کنید.');
        $part = STORAGE_DIR . '/tmp/' . $uid . '.part';
        if ($idx === 0) {
            foreach (glob(STORAGE_DIR . '/tmp/*.part') ?: [] as $old) if (filemtime($old) < time() - 86400) @unlink($old);
            @unlink($part);
        }
        $have = is_file($part) ? filesize($part) : 0;
        if ($have !== $offset) fail('ترتیب قطعه‌ها به‌هم خورد.', ['have' => $have]);
        $in = fopen($up['tmp_name'], 'rb'); $outp = fopen($part, 'ab');
        stream_copy_to_stream($in, $outp);
        fclose($in); fclose($outp);
        clearstatcache(true, $part);
        if ($idx < $total - 1) json_out(['ok' => true]);

        if (filesize($part) !== $fsize) { @unlink($part); fail('حجم نهایی با حجم اصلی نمی‌خواند؛ دوباره آپلود کنید.'); }
        $stored = bin2hex(random_bytes(16));
        if (!rename($part, STORAGE_DIR . '/files/' . $stored)) fail('ذخیرهٔ فایل ناموفق بود.', [], 500);
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $pw = (string)($_POST['password'] ?? '');
        $rec = ['id' => bin2hex(random_bytes(6)), 'name' => $name, 'stored' => $stored, 'size' => $fsize,
            'mime' => INLINE_MIME[$ext] ?? 'application/octet-stream', 'time' => time(), 'downloads' => 0,
            'pass' => $pw !== '' ? password_hash($pw, PASSWORD_DEFAULT) : null];
        meta_update(function (array &$d) use ($rec) { $d[$rec['id']] = $rec; });
        json_out(['ok' => true, 'file' => ['name' => $name, 'dl' => link_dl($rec), 'page' => link_page($rec), 'locked' => $pw !== '']]);
    }

    $id = (string)($_POST['id'] ?? '');
    if (!get_file($id)) fail('فایل پیدا نشد.', [], 404);

    if ($act === 'setpass') {
        $pw = (string)($_POST['password'] ?? '');
        meta_update(function (array &$d) use ($id, $pw) { $d[$id]['pass'] = $pw !== '' ? password_hash($pw, PASSWORD_DEFAULT) : null; });
        json_out(['ok' => true]);
    }
    if ($act === 'rename') {
        $n = clean_name((string)($_POST['name'] ?? ''));
        $ext = strtolower(pathinfo($n, PATHINFO_EXTENSION));
        meta_update(function (array &$d) use ($id, $n, $ext) {
            $d[$id]['name'] = $n;
            $d[$id]['mime'] = INLINE_MIME[$ext] ?? 'application/octet-stream';
        });
        json_out(['ok' => true]);
    }
    if ($act === 'delete') {
        $stored = meta_update(function (array &$d) use ($id) { $s = $d[$id]['stored']; unset($d[$id]); return $s; });
        @unlink(STORAGE_DIR . '/files/' . $stored);
        json_out(['ok' => true]);
    }
    fail('درخواست نامعتبر.');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && !is_admin() && !in_array($_POST['action'], ['login', 'unlock', 'logout'], true)) {
    fail('ابتدا وارد شوید.', [], 401);
}

/* ====================== صفحهٔ ورود ====================== */
if (!is_admin()) {
    head(SITE_TITLE . ' | ورود'); ?>
<div class="center"><form class="box" method="post" autocomplete="off">
  <div style="width:54px;height:54px;border-radius:14px;background:var(--brand-soft);color:var(--brand);display:grid;place-items:center;margin-bottom:16px"><?= str_replace('class="i"', 'class="i" style="width:26px;height:26px"', ICON_LOCK) ?></div>
  <h1 style="font-size:22px;margin:0 0 6px"><?= e(SITE_TITLE) ?></h1>
  <p style="color:var(--muted);margin:0 0 20px">برای ورود، رمز پنل را وارد کنید.</p>
  <?php if ($loginError): ?><p class="err"><?= e($loginError) ?></p><?php endif; ?>
  <input type="hidden" name="action" value="login">
  <input class="field" type="password" name="password" placeholder="رمز عبور" autofocus required style="margin-bottom:14px">
  <button class="btn primary big" style="width:100%;justify-content:center" type="submit">ورود</button>
</form></div>
<?php foot(); exit;
}

/* ====================== پنل مدیریت ====================== */
head(SITE_TITLE); ?>
<style>
header.top{background:var(--nav);color:#fff}
header.top .in{max-width:1000px;margin:auto;padding:14px 20px;display:flex;align-items:center;justify-content:space-between;gap:12px}
header.top h1{font-size:18px;margin:0}
header.top .btn{background:transparent;color:#fff;border-color:rgba(255,255,255,.3);padding:6px 14px}
main{max-width:1000px;margin:auto;padding:24px 20px 60px}
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:22px}
.stat{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 18px}
.stat b{display:block;font-size:22px}
.stat span{color:var(--muted);font-size:13px}
.bar{display:flex;gap:10px;margin-bottom:14px}
.bar .field{flex:1}
.list{background:var(--card);border:1px solid var(--line);border-radius:14px;overflow:hidden}
.row{display:flex;align-items:center;gap:14px;padding:14px 16px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.row:last-child{border-bottom:0}
.badge{width:46px;height:46px;border-radius:12px;background:var(--brand-soft);color:var(--brand);display:grid;place-items:center;font-weight:700;font-size:12px;direction:ltr;flex:none;overflow:hidden}
.badge.video{background:#fdeed7;color:#8a5207}
.badge.audio{background:#e7e5fb;color:#4a3fb0}
.badge.image{background:#e0f3e8;color:#1d6b45}
.info{flex:1;min-width:180px}
.nm{font-weight:600;word-break:break-word;display:flex;align-items:center;gap:6px}
.nm .lk{color:var(--warn);display:inline-flex}
.mt{color:var(--muted);font-size:13px;display:flex;gap:16px;flex-wrap:wrap}
.acts{display:flex;gap:4px}
.ib{width:38px;height:38px;border-radius:10px;border:0;background:transparent;display:grid;place-items:center;cursor:pointer;color:var(--muted)}
.ib:hover{background:var(--brand-soft);color:var(--brand)}
.ib.del:hover{background:#fbe9ec;color:var(--danger)}
.empty{padding:50px 20px;text-align:center;color:var(--muted)}
dialog.modal{border:0;border-radius:18px;padding:0;width:min(560px,94vw);background:var(--card);color:var(--ink);box-shadow:0 30px 80px rgba(0,0,0,.35)}
dialog.modal.lg{width:min(860px,96vw)}
dialog::backdrop{background:rgba(10,15,29,.55)}
.mh{display:flex;justify-content:space-between;align-items:center;padding:16px 22px;border-bottom:1px solid var(--line)}
.mh h3{margin:0;font-size:17px}
.x{border:0;background:none;font-size:26px;line-height:1;cursor:pointer;color:var(--muted)}
.mb{padding:20px 22px}
.mf{padding:14px 22px;border-top:1px solid var(--line);display:flex;gap:10px;justify-content:flex-start}
.drop{border:2px dashed var(--line);border-radius:14px;padding:30px 16px;text-align:center;color:var(--muted);cursor:pointer;transition:.15s}
.drop.on,.drop:hover{border-color:var(--brand);background:var(--brand-soft);color:var(--brand)}
.q{margin-top:14px;display:grid;gap:10px;max-height:240px;overflow:auto}
.qi{font-size:13px}
.qi .t{display:flex;justify-content:space-between;gap:10px}
.qi .t span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pg{height:6px;background:var(--line);border-radius:6px;overflow:hidden;margin-top:4px}
.pg i{display:block;height:100%;width:0;background:var(--brand);transition:width .2s}
.pg.done i{background:var(--ok)}
.pg.bad i{background:var(--danger)}
.lbl{display:block;margin:14px 0 6px;font-size:13px;color:var(--muted)}
.linkrow{display:flex;gap:8px;margin-bottom:12px;align-items:center}
.linkrow .field{direction:ltr;text-align:left;font-size:13px}
.pv{width:100%;max-height:68vh;background:#000;border-radius:12px;display:block}
img.pv{object-fit:contain;background:var(--bg)}
audio.pv{background:none}
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--bg);padding:10px 20px;border-radius:12px;z-index:99;opacity:0;pointer-events:none;transition:opacity .2s}
.toast.on{opacity:1}
@media (max-width:640px){.stats{grid-template-columns:1fr 1fr 1fr;gap:8px}.stat{padding:10px 12px}.stat b{font-size:17px}.acts{width:100%;justify-content:flex-end}}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
</style>

<header class="top"><div class="in">
  <h1><?= e(SITE_TITLE) ?></h1>
  <form method="post"><input type="hidden" name="action" value="logout"><input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>"><button class="btn" type="submit">خروج</button></form>
</div></header>

<main>
  <div class="stats">
    <div class="stat"><b id="sCount">-</b><span>تعداد فایل</span></div>
    <div class="stat"><b id="sSize">-</b><span>حجم کل</span></div>
    <div class="stat"><b id="sDl">-</b><span>تعداد دانلود</span></div>
  </div>
  <div class="bar">
    <input class="field" id="search" type="search" placeholder="جستجو در نام فایل‌ها">
    <button class="btn primary" id="addBtn"><svg class="i" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg> افزودن فایل</button>
  </div>
  <div class="list" id="list"><div class="empty">در حال بارگذاری...</div></div>
</main>
<div class="toast" id="toast"></div>

<script>const CSRF = <?= json_encode($_SESSION['csrf']) ?>;</script>
<script>
const $ = s => document.querySelector(s);
const nf = new Intl.NumberFormat('fa-IR', {maximumFractionDigits: 1});
const IC = {
  play:'<polygon points="6 4 20 12 6 20 6 4"/>',
  copy:'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/>',
  lock:'<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>',
  edit:'<path d="M4 20h4L19 9l-4-4L4 16v4z"/>',
  trash:'<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>'
};
const icon = n => { const s = document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('class','i'); s.setAttribute('viewBox','0 0 24 24'); s.innerHTML = IC[n]; return s; };
function h(tag, attrs = {}, ...kids) {
  const el = document.createElement(tag);
  for (const [k, v] of Object.entries(attrs)) {
    if (k.startsWith('on')) el.addEventListener(k.slice(2), v);
    else if (v !== false && v != null) el.setAttribute(k, v);
  }
  for (const c of kids.flat()) if (c != null) el.append(c.nodeType ? c : document.createTextNode(c));
  return el;
}
function fmtSize(b) {
  const u = ['بایت','کیلوبایت','مگابایت','گیگابایت','ترابایت']; let i = 0, n = b;
  while (n >= 1024 && i < 4) { n /= 1024; i++; }
  return nf.format(n) + ' ' + u[i];
}
const fmtDate = t => new Date(t * 1000).toLocaleDateString('fa-IR', {year:'numeric', month:'short', day:'numeric'});
function toast(m) { const t = $('#toast'); t.textContent = m; t.classList.add('on'); clearTimeout(t._t); t._t = setTimeout(() => t.classList.remove('on'), 1800); }
async function copy(text) {
  try { await navigator.clipboard.writeText(text); }
  catch { const i = h('textarea', {}, text); document.body.append(i); i.select(); document.execCommand('copy'); i.remove(); }
  toast('لینک کپی شد');
}
async function api(action, data = {}) {
  const fd = new FormData(); fd.append('action', action);
  for (const k in data) fd.append(k, data[k]);
  const r = await fetch(location.pathname, {method:'POST', headers:{'X-CSRF': CSRF}, body: fd});
  let j; try { j = await r.json(); } catch { throw new Error('پاسخ نامعتبر از سرور'); }
  if (!j.ok) throw new Error(j.error || 'خطا');
  return j;
}
function modal(title, content, buttons = [], cls = '') {
  const d = h('dialog', {class:'modal ' + cls},
    h('div', {class:'mh'}, h('h3', {}, title), h('button', {class:'x', 'aria-label':'بستن', onclick:() => d.close()}, '×')),
    h('div', {class:'mb'}, content),
    buttons.length ? h('div', {class:'mf'}, buttons) : null);
  d.addEventListener('close', () => d.remove());
  document.body.append(d); d.showModal(); return d;
}

let state = {files: [], chunk: 1048576, max: 0};
async function load() {
  try {
    const j = await api('list');
    state = j;
    $('#sCount').textContent = nf.format(j.files.length);
    $('#sSize').textContent = fmtSize(j.total);
    $('#sDl').textContent = nf.format(j.downloads);
    render();
  } catch (e) { $('#list').replaceChildren(h('div', {class:'empty'}, e.message)); }
}
function render() {
  const q = $('#search').value.trim().toLowerCase();
  const files = state.files.filter(f => f.name.toLowerCase().includes(q));
  const list = $('#list'); list.replaceChildren();
  if (!files.length) {
    list.append(h('div', {class:'empty'}, state.files.length ? 'فایلی با این نام پیدا نشد.' : 'هنوز فایلی اضافه نکرده‌اید. با «افزودن فایل» شروع کنید.'));
    return;
  }
  for (const f of files) {
    const ext = (f.name.split('.').pop() || '').slice(0, 4).toUpperCase();
    list.append(h('div', {class:'row'},
      h('div', {class:'badge ' + f.kind}, ext),
      h('div', {class:'info'},
        h('div', {class:'nm'}, f.name, f.locked ? h('span', {class:'lk', title:'رمزدار'}, icon('lock')) : null),
        h('div', {class:'mt'}, h('span', {}, fmtSize(f.size)), h('span', {}, fmtDate(f.time)), h('span', {}, nf.format(f.downloads) + ' دانلود'))),
      h('div', {class:'acts'},
        f.kind ? h('button', {class:'ib', title:'پیش‌نمایش', 'aria-label':'پیش‌نمایش', onclick:() => preview(f)}, icon('play')) : null,
        h('button', {class:'ib', title:'کپی لینک مستقیم', 'aria-label':'کپی لینک مستقیم', onclick:() => linkDialog(f)}, icon('copy')),
        h('button', {class:'ib', title:'رمز فایل', 'aria-label':'رمز فایل', onclick:() => passDialog(f)}, icon('lock')),
        h('button', {class:'ib', title:'تغییر نام', 'aria-label':'تغییر نام', onclick:() => renameDialog(f)}, icon('edit')),
        h('button', {class:'ib del', title:'حذف', 'aria-label':'حذف', onclick:() => deleteDialog(f)}, icon('trash')))));
  }
}
$('#search').addEventListener('input', render);

function linkBlock(label, url) {
  const inp = h('input', {class:'field', readonly:'', value:url, onfocus:e => e.target.select()});
  return [h('label', {class:'lbl'}, label), h('div', {class:'linkrow'}, inp, h('button', {class:'btn', onclick:() => copy(url)}, 'کپی'))];
}
function linkDialog(f) {
  modal('لینک‌های فایل', [h('p', {style:'margin:0;word-break:break-word'}, f.name),
    ...linkBlock('لینک مستقیم دانلود', f.dl), ...linkBlock('صفحهٔ فایل (پیش‌نمایش و دانلود)', f.page),
    f.locked ? h('p', {style:'color:var(--muted);font-size:13px'}, 'این فایل رمزدار است؛ لینک مستقیم هنگام باز شدن، رمز را می‌پرسد.') : null]);
}
function preview(f) {
  const el = f.kind === 'video' ? h('video', {class:'pv', controls:'', autoplay:'', playsinline:'', src:f.view})
    : f.kind === 'audio' ? h('audio', {class:'pv', controls:'', autoplay:'', src:f.view})
    : h('img', {class:'pv', alt:f.name, src:f.view});
  const dl = h('a', {class:'btn primary', href:f.dl}, 'دانلود فایل');
  modal(f.name, [el], [dl], 'lg');
}
function passDialog(f) {
  const inp = h('input', {class:'field', type:'text', placeholder:'رمز جدید', autocomplete:'off'});
  const d = modal('رمز فایل', [h('p', {style:'margin:0 0 4px'}, f.locked ? 'این فایل رمز دارد. رمز جدید بنویسید یا آن را حذف کنید.' : 'برای رمزدار کردن این فایل، رمز را بنویسید.'), inp],
    [h('button', {class:'btn primary', onclick: async () => {
        if (!inp.value) return toast('رمز را وارد کنید');
        await api('setpass', {id:f.id, password:inp.value}); d.close(); toast('رمز ذخیره شد'); load();
      }}, 'ذخیره رمز'),
     f.locked ? h('button', {class:'btn', onclick: async () => { await api('setpass', {id:f.id, password:''}); d.close(); toast('رمز حذف شد'); load(); }}, 'حذف رمز') : null]);
  inp.focus();
}
function renameDialog(f) {
  const inp = h('input', {class:'field', value:f.name});
  const d = modal('تغییر نام', [inp], [h('button', {class:'btn primary', onclick: async () => {
    await api('rename', {id:f.id, name:inp.value}); d.close(); toast('نام تغییر کرد'); load();
  }}, 'ذخیره')]);
  inp.focus();
}
function deleteDialog(f) {
  const d = modal('حذف فایل', [h('p', {style:'margin:0;word-break:break-word'}, '«' + f.name + '» برای همیشه حذف می‌شود و لینک‌های آن از کار می‌افتد.')],
    [h('button', {class:'btn danger', onclick: async () => { await api('delete', {id:f.id}); d.close(); toast('حذف شد'); load(); }}, 'حذف فایل'),
     h('button', {class:'btn', onclick:() => d.close()}, 'انصراف')]);
}

/* ---------- آپلود چندقسمتی ---------- */
const sleep = ms => new Promise(r => setTimeout(r, ms));
function sendChunk(fields, blob, onProg) {
  return new Promise((resolve, reject) => {
    const x = new XMLHttpRequest(), fd = new FormData();
    for (const k in fields) fd.append(k, fields[k]);
    fd.append('chunk', blob, 'c');
    x.open('POST', location.pathname); x.setRequestHeader('X-CSRF', CSRF);
    x.upload.onprogress = e => e.lengthComputable && onProg(e.loaded);
    x.onerror = x.ontimeout = () => reject({retry:true, message:'قطع شدن اتصال'});
    x.onload = () => {
      let j; try { j = JSON.parse(x.responseText); } catch { return reject({retry:x.status >= 500, message:'پاسخ نامعتبر از سرور (' + x.status + ')'}); }
      j.ok ? resolve(j) : reject({retry:false, message:j.error, have:j.have});
    };
    x.send(fd);
  });
}
async function uploadOne(file, password, setProg) {
  const uid = [...crypto.getRandomValues(new Uint8Array(12))].map(b => b.toString(16).padStart(2, '0')).join('');
  const cs = state.chunk, total = Math.max(1, Math.ceil(file.size / cs));
  let last = null;
  for (let i = 0; i < total; i++) {
    const offset = i * cs, blob = file.slice(offset, offset + cs);
    const fields = {action:'chunk', uid, idx:i, total, offset, size:file.size, name:file.name, password};
    for (let tries = 0; ; tries++) {
      try { last = await sendChunk(fields, blob, l => setProg((offset + l) / file.size)); break; }
      catch (e) {
        if (e.have === offset + blob.size && i < total - 1) break;
        if (!e.retry || tries >= 3) throw new Error(e.message);
        await sleep(1200 * (tries + 1));
      }
    }
    setProg((offset + blob.size) / file.size);
  }
  return last.file;
}
function uploadDialog() {
  const input = h('input', {type:'file', multiple:'', hidden:''});
  const drop = h('div', {class:'drop', tabindex:'0', role:'button'}, 'فایل‌ها را اینجا بکشید یا برای انتخاب کلیک کنید');
  const queue = h('div', {class:'q'});
  const pass = h('input', {class:'field', type:'text', placeholder:'بدون رمز', autocomplete:'off'});
  let files = [];
  const startBtn = h('button', {class:'btn primary', disabled:''}, 'شروع آپلود');
  const addFiles = list => {
    for (const f of list) {
      if (!f.size) { toast('«' + f.name + '» خالی است'); continue; }
      if (state.max && f.size > state.max) { toast('«' + f.name + '» از حد مجاز بزرگ‌تر است'); continue; }
      const pg = h('div', {class:'pg'}, h('i')), st = h('span', {}, fmtSize(f.size));
      queue.append(h('div', {class:'qi'}, h('div', {class:'t'}, h('span', {}, f.name), st), pg));
      files.push({f, pg, st});
    }
    startBtn.disabled = !files.length;
  };
  drop.onclick = () => input.click();
  drop.onkeydown = e => (e.key === 'Enter' || e.key === ' ') && input.click();
  input.onchange = () => { addFiles(input.files); input.value = ''; };
  ['dragenter','dragover'].forEach(n => drop.addEventListener(n, e => { e.preventDefault(); drop.classList.add('on'); }));
  ['dragleave','drop'].forEach(n => drop.addEventListener(n, e => { e.preventDefault(); drop.classList.remove('on'); }));
  drop.addEventListener('drop', e => addFiles(e.dataTransfer.files));
  const d = modal('افزودن فایل', [drop, input, queue, h('label', {class:'lbl'}, 'رمز فایل (اختیاری)'), pass], [startBtn]);
  startBtn.onclick = async () => {
    startBtn.disabled = true; pass.disabled = true; drop.style.pointerEvents = 'none';
    const done = [];
    for (const it of files) {
      try {
        const r = await uploadOne(it.f, pass.value, p => { it.pg.firstChild.style.width = Math.round(p * 100) + '%'; it.st.textContent = nf.format(Math.round(p * 100)) + '٪'; });
        it.pg.classList.add('done'); it.st.textContent = 'تمام شد'; done.push(r);
      } catch (e) { it.pg.classList.add('bad'); it.st.textContent = e.message; }
    }
    await load();
    if (done.length) {
      d.close();
      modal(done.length > 1 ? 'فایل‌ها اضافه شدند' : 'فایل اضافه شد',
        done.flatMap(r => [h('p', {style:'margin:0;font-weight:600;word-break:break-word'}, r.name), ...linkBlock('لینک مستقیم دانلود', r.dl)]));
    } else { startBtn.disabled = false; pass.disabled = false; drop.style.pointerEvents = ''; }
  };
}
$('#addBtn').onclick = uploadDialog;
load();
</script>
<?php foot();
