true, 'message' => $message], JSON_UNESCAPED_UNICODE); exit; } } if ($action === 'delete_section') { $sectionId = (int)($_POST['section_id'] ?? 0); if ($sectionId < 1) throw new RuntimeException('Некорректный раздел'); if (!sectionById($sectionId)) throw new RuntimeException('Раздел не найден'); removeSectionImageFiles($sectionId); sectionDelete($sectionId); deleteSectionStorage($sectionId); $message = 'Раздел удалён'; } if ($action === 'update_welcome') { $text = trim((string)($_POST['welcome_text'] ?? '')); settingSet('welcome_text', $text); $message = 'Приветственное сообщение сохранено'; } if ($action === 'upload_before_bulk') { $sectionId = (int)($_POST['section_id'] ?? 0); if ($sectionId < 1 || !sectionById($sectionId)) throw new RuntimeException('Выбери раздел'); if (!isset($_FILES['before_bulk'])) throw new RuntimeException('Файлы не переданы'); $result = saveBulkBefore($_FILES['before_bulk'], $sectionId); $message = 'Загружено: ' . $result['ok']; $errors = array_merge($errors, $result['errors']); } if ($action === 'photo_update') { $photoId = (int)($_POST['photo_id'] ?? 0); $code = trim((string)($_POST['code_name'] ?? '')); $sort = (int)($_POST['sort_order'] ?? 1000); $descr = trim((string)($_POST['description'] ?? '')); $descr = $descr !== '' ? $descr : null; if ($photoId < 1) throw new RuntimeException('Некорректный photo_id'); if ($code === '') throw new RuntimeException('Код фото пустой'); $st = db()->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 === 'rotate_photo_file') { $photoId = (int)($_POST['photo_id'] ?? 0); $kind = (string)($_POST['kind'] ?? ''); $direction = (string)($_POST['direction'] ?? 'right'); if ($photoId < 1) throw new RuntimeException('Некорректный photo_id'); if (!in_array($kind, ['before', 'after'], true)) throw new RuntimeException('Некорректный тип файла'); $photo = photoById($photoId); if (!$photo) throw new RuntimeException('Фото не найдено'); $pathKey = $kind === 'before' ? 'before_path' : 'after_path'; $relPath = (string)($photo[$pathKey] ?? ''); if ($relPath === '') throw new RuntimeException('Файл отсутствует'); $absPath = __DIR__ . '/' . ltrim($relPath, '/'); if (!is_file($absPath)) throw new RuntimeException('Файл не найден на диске'); $degrees = $direction === 'left' ? -90 : 90; rotateImageOnDisk($absPath, $degrees); $st = db()->prepare('UPDATE photo_files SET updated_at=CURRENT_TIMESTAMP WHERE photo_id=:pid AND kind=:kind'); $st->execute(['pid' => $photoId, 'kind' => $kind]); $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 === 'regenerate_commenter_token') { $id = (int)($_POST['id'] ?? 0); if ($id > 0) { $token = commenterRegenerateToken($id); $link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/?viewer=' . urlencode($token); $message = 'Токен обновлён | ссылка: ' . $link; } } 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))); $activeSection = $activeSectionId > 0 ? sectionById($activeSectionId) : null; if (!$activeSection && $sections !== []) { $activeSectionId = (int)$sections[0]['id']; $activeSection = sectionById($activeSectionId); } $photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : []; $commenters = commentersAll(); $latestComments = commentsLatest(80); $welcomeText = settingGet('welcome_text', 'Добро пожаловать в галерею. Выберите раздел слева, чтобы посмотреть фотографии.'); $adminMode = (string)($_GET['mode'] ?? 'photos'); if ($adminMode === 'media') { $adminMode = 'photos'; } if (!in_array($adminMode, ['sections', 'photos', 'comments', 'welcome'], true)) { $adminMode = 'photos'; } $previewVersion = (string)time(); 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 deleteSectionStorage(int $sectionId): void { $dir = __DIR__ . '/photos/section_' . $sectionId; if (!is_dir($dir)) { return; } deleteDirRecursive($dir); } function deleteDirRecursive(string $dir): void { $items = scandir($dir); if (!is_array($items)) { return; } foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $dir . '/' . $item; if (is_dir($path)) { deleteDirRecursive($path); continue; } @unlink($path); } @rmdir($dir); } function removeSectionImageFiles(int $sectionId): void { $st = db()->prepare('SELECT pf.file_path FROM photo_files pf JOIN photos p ON p.id = pf.photo_id WHERE p.section_id = :sid'); $st->execute(['sid' => $sectionId]); $paths = $st->fetchAll(PDO::FETCH_COLUMN); if (!is_array($paths)) { return; } foreach ($paths as $path) { if (!is_string($path) || $path === '') { continue; } $abs = __DIR__ . '/' . ltrim($path, '/'); if (is_file($abs)) { @unlink($abs); } } } function rotateImageOnDisk(string $path, int $degrees): void { $mime = mime_content_type($path) ?: ''; if (!in_array($mime, ['image/jpeg', 'image/png', 'image/webp', 'image/gif'], true)) { throw new RuntimeException('Недопустимый тип файла для поворота'); } if (extension_loaded('imagick')) { $im = new Imagick($path); $im->setImageOrientation(Imagick::ORIENTATION_TOPLEFT); $im->rotateImage(new ImagickPixel('none'), $degrees); $im->setImagePage(0, 0, 0, 0); if ($mime === 'image/jpeg') { $im->setImageCompressionQuality(92); } $im->writeImage($path); $im->clear(); $im->destroy(); return; } $src = match ($mime) { 'image/jpeg' => @imagecreatefromjpeg($path), 'image/png' => @imagecreatefrompng($path), 'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : false, 'image/gif' => @imagecreatefromgif($path), default => false, }; if (!$src) { throw new RuntimeException('Не удалось открыть изображение'); } $bgColor = 0; if ($mime === 'image/png' || $mime === 'image/webp') { $bgColor = imagecolorallocatealpha($src, 0, 0, 0, 127); } $rotated = imagerotate($src, -$degrees, $bgColor); if (!$rotated) { imagedestroy($src); throw new RuntimeException('Не удалось повернуть изображение'); } if ($mime === 'image/png' || $mime === 'image/webp') { imagealphablending($rotated, false); imagesavealpha($rotated, true); } $ok = match ($mime) { 'image/jpeg' => imagejpeg($rotated, $path, 92), 'image/png' => imagepng($rotated, $path), 'image/webp' => function_exists('imagewebp') ? imagewebp($rotated, $path, 92) : false, 'image/gif' => imagegif($rotated, $path), default => false, }; imagedestroy($src); imagedestroy($rotated); if (!$ok) { throw new RuntimeException('Не удалось сохранить повернутое изображение'); } } 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) заполняется автоматически из имени файла — затем можно отредактировать.

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

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

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

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

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

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

ПользовательСсылкаДействия
Нет сохранённой ссылки (старый пользователь)

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