prepare('UPDATE photos SET code_name=:c, sort_order=:s, description=:d WHERE id=:id'); $st->execute(['c' => $code, 's' => $sort, 'd' => $descr, 'id' => $photoId]); if (isset($_FILES['after']) && (int)($_FILES['after']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) { $p = photoById($photoId); if (!$p) throw new RuntimeException('Фото не найдено'); $up = saveSingleImage($_FILES['after'], $code . 'р', (int)$p['section_id']); photoFileUpsert($photoId, 'after', $up['path'], $up['mime'], $up['size']); } $message = 'Фото обновлено'; if ($isAjax) { header('Content-Type: application/json; charset=utf-8'); echo json_encode(['ok' => true, 'message' => $message], JSON_UNESCAPED_UNICODE); exit; } } if ($action === 'photo_delete') { $photoId = (int)($_POST['photo_id'] ?? 0); if ($photoId > 0) { $p = photoById($photoId); if ($p) { foreach (['before_path', 'after_path'] as $k) { if (!empty($p[$k])) { $abs = __DIR__ . '/' . ltrim((string)$p[$k], '/'); if (is_file($abs)) @unlink($abs); } } } $st = db()->prepare('DELETE FROM photos WHERE id=:id'); $st->execute(['id' => $photoId]); $message = 'Фото удалено'; } } if ($action === 'create_commenter') { $displayName = trim((string)($_POST['display_name'] ?? '')); if ($displayName === '') throw new RuntimeException('Укажи имя комментатора'); $u = commenterCreate($displayName); $link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/?viewer=' . urlencode($u['token']); $message = 'Комментатор создан: ' . $u['display_name'] . ' | ссылка: ' . $link; } if ($action === 'delete_commenter') { $id = (int)($_POST['id'] ?? 0); if ($id > 0) { commenterDelete($id); $message = 'Комментатор удалён (доступ отозван)'; } } if ($action === 'delete_comment') { $id = (int)($_POST['id'] ?? 0); if ($id > 0) { commentDelete($id); $message = 'Комментарий удалён'; } } } catch (Throwable $e) { if ($isAjax) { header('Content-Type: application/json; charset=utf-8'); http_response_code(400); echo json_encode(['ok' => false, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE); exit; } $errors[] = $e->getMessage(); } } $sections = sectionsAll(); $activeSectionId = (int)($_GET['section_id'] ?? ($_POST['section_id'] ?? ($sections[0]['id'] ?? 0))); $photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : []; $commenters = commentersAll(); $latestComments = commentsLatest(80); $welcomeText = settingGet('welcome_text', 'Добро пожаловать в галерею. Выберите раздел слева, чтобы посмотреть фотографии.'); $adminMode = (string)($_GET['mode'] ?? 'media'); if (!in_array($adminMode, ['media', 'comments'], true)) { $adminMode = 'media'; } function h(string $v): string { return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $v=is_file($f)?(string)filemtime($f):(string)time(); return $path . '?v=' . rawurlencode($v); } function saveBulkBefore(array $files, int $sectionId): array { $ok = 0; $errors = []; $names = $files['name'] ?? []; $tmp = $files['tmp_name'] ?? []; $sizes = $files['size'] ?? []; $errs = $files['error'] ?? []; if (!is_array($names)) { $names = [$names]; $tmp = [$tmp]; $sizes = [$sizes]; $errs = [$errs]; } foreach ($names as $i => $orig) { $file = [ 'name' => $orig, 'tmp_name' => $tmp[$i] ?? '', 'size' => $sizes[$i] ?? 0, 'error' => $errs[$i] ?? UPLOAD_ERR_NO_FILE, ]; try { $base = (string)pathinfo((string)$orig, PATHINFO_FILENAME); $base = trim(preg_replace('/[^\p{L}\p{N}._-]+/u', '_', $base) ?? 'photo', '._-'); if ($base === '') $base = 'photo'; $codeName = nextUniqueCodeName($base); $photoId = photoCreate($sectionId, $codeName, null, nextSortOrderForSection($sectionId)); $saved = saveSingleImage($file, $codeName, $sectionId); photoFileUpsert($photoId, 'before', $saved['path'], $saved['mime'], $saved['size']); $ok++; } catch (Throwable $e) { $errors[] = (string)$orig . ': ' . $e->getMessage(); } } return ['ok' => $ok, 'errors' => $errors]; } function saveSingleImage(array $file, string $baseName, int $sectionId): array { $allowedMime = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif']; $err = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE); if ($err !== UPLOAD_ERR_OK) throw new RuntimeException('Ошибка загрузки'); $size = (int)($file['size'] ?? 0); if ($size < 1 || $size > MAX_UPLOAD_BYTES) throw new RuntimeException('Превышен лимит 3 МБ'); $tmp = (string)($file['tmp_name'] ?? ''); if (!is_uploaded_file($tmp)) throw new RuntimeException('Некорректный источник'); $mime = mime_content_type($tmp) ?: ''; if (!isset($allowedMime[$mime])) throw new RuntimeException('Недопустимый тип файла'); $safeBase = preg_replace('/[^\p{L}\p{N}._-]+/u', '_', $baseName) ?? 'photo'; $safeBase = trim($safeBase, '._-'); if ($safeBase === '') $safeBase = 'photo'; $ext = $allowedMime[$mime]; $dir = __DIR__ . '/photos/section_' . $sectionId; if (!is_dir($dir)) mkdir($dir, 0775, true); $name = uniqueName($dir, $safeBase, $ext); $dest = $dir . '/' . $name; if (!move_uploaded_file($tmp, $dest)) throw new RuntimeException('Не удалось сохранить файл'); return [ 'path' => 'photos/section_' . $sectionId . '/' . $name, 'mime' => $mime, 'size' => $size, ]; } function uniqueName(string $dir, string $base, string $ext): string { $i = 0; do { $name = $i === 0 ? "{$base}.{$ext}" : "{$base}_{$i}.{$ext}"; $i++; } while (is_file($dir . '/' . $name)); return $name; } function nextSortOrderForSection(int $sectionId): int { $st = db()->prepare('SELECT COALESCE(MAX(sort_order),0)+10 FROM photos WHERE section_id=:sid'); $st->execute(['sid' => $sectionId]); return (int)$st->fetchColumn(); } function nextUniqueCodeName(string $base): string { $candidate = $base; $i = 1; while (true) { $st = db()->prepare('SELECT 1 FROM photos WHERE code_name=:c LIMIT 1'); $st->execute(['c' => $candidate]); if (!$st->fetchColumn()) { return $candidate; } $candidate = $base . '_' . $i; $i++; } } ?> Админка

Админка

Приветственное сообщение (публичная часть)

Загрузка фото “до” в выбранный раздел

0): ?>

После загрузки имя (code_name) заполняется автоматически из имени файла — затем можно отредактировать.

Сначала выбери раздел слева.

Фото в разделе

ДоПослеПоляДействия

Фото после (опционально):

Сохраняется автоматически при выходе из карточки.

Комментаторы и комментарии

ПользовательДействие

ФотоПользовательКомментарий