diff --git a/README.md b/README.md
index a0955fb..3246eae 100644
--- a/README.md
+++ b/README.md
@@ -118,16 +118,20 @@ BRANCH=master bash scripts/deploy.sh
## Админка загрузки (по токену)
-Новый контур на MySQL:
+Новый контур на MySQL (основной):
-- `admin-mysql.php?token=...` — админка
-- `index-mysql.php` — публичная витрина + комментарии
+- `admin.php?token=...` — админка
+- `index.php` — публичная витрина + комментарии
+
+Совместимость:
+- `admin-mysql.php` и `index-mysql.php` оставлены как алиасы на новые основные файлы.
Что уже есть в MySQL-контуре:
- создание разделов,
-- загрузка фото "до" + опционально "после",
+- сценарий загрузки: сначала выбор раздела, затем массовая загрузка только фото "до",
+- после загрузки автоматический prefill имени (code_name) из имени файла,
+- для каждой карточки фото можно отредактировать: имя, сортировку, комментарий и добавить/заменить фото "после",
- запись в таблицы `sections`, `photos`, `photo_files`,
-- просмотр разделов и загруженных фото,
- персональные комментаторы (генерация ссылок),
- плоские комментарии к фото,
- удаление комментариев админом,
diff --git a/admin-mysql.php b/admin-mysql.php
index 6620e55..e63d760 100644
--- a/admin-mysql.php
+++ b/admin-mysql.php
@@ -1,257 +1,3 @@
getMessage());
-}
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- $action = (string)($_POST['action'] ?? '');
- try {
- if ($action === 'create_section') {
- $name = trim((string)($_POST['name'] ?? ''));
- $sort = (int)($_POST['sort_order'] ?? 1000);
- if ($name === '') throw new RuntimeException('Название раздела пустое');
- sectionCreate($name, $sort);
- $message = 'Раздел создан';
- }
-
- if ($action === 'upload_photo') {
- $sectionId = (int)($_POST['section_id'] ?? 0);
- $codeName = trim((string)($_POST['code_name'] ?? ''));
- $sortOrder = (int)($_POST['sort_order'] ?? 1000);
- $description = trim((string)($_POST['description'] ?? ''));
- $description = $description !== '' ? $description : null;
-
- if ($sectionId < 1) throw new RuntimeException('Выбери раздел');
- if ($codeName === '') throw new RuntimeException('Укажи код фото (например АВФ1)');
- if (!isset($_FILES['before'])) throw new RuntimeException('Файл "до" обязателен');
- if (!sectionById($sectionId)) throw new RuntimeException('Раздел не найден');
-
- $photoId = photoCreate($sectionId, $codeName, $description, $sortOrder);
-
- $before = saveImageUpload($_FILES['before'], $codeName, 'before', $sectionId);
- photoFileUpsert($photoId, 'before', $before['path'], $before['mime'], $before['size']);
-
- if (isset($_FILES['after']) && (int)($_FILES['after']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
- $after = saveImageUpload($_FILES['after'], $codeName . 'р', 'after', $sectionId);
- photoFileUpsert($photoId, 'after', $after['path'], $after['mime'], $after['size']);
- }
-
- $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') . '/index-mysql.php?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) {
- $errors[] = $e->getMessage();
- }
-}
-
-$sections = sectionsAll();
-$activeSectionId = (int)($_GET['section_id'] ?? ($sections[0]['id'] ?? 0));
-$photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : [];
-$commenters = commentersAll();
-$latestComments = commentsLatest(80);
-
-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 saveImageUpload(array $file, string $baseName, string $kind, 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("Ошибка загрузки ({$kind})");
- $size = (int)($file['size'] ?? 0);
- if ($size < 1 || $size > MAX_UPLOAD_BYTES) throw new RuntimeException("Файл {$kind}: превышен лимит 3 МБ");
-
- $tmp = (string)($file['tmp_name'] ?? '');
- if (!is_uploaded_file($tmp)) throw new RuntimeException("Файл {$kind}: некорректный источник");
-
- $mime = mime_content_type($tmp) ?: '';
- if (!isset($allowedMime[$mime])) throw new RuntimeException("Файл {$kind}: недопустимый mime {$mime}");
-
- $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) && !is_dir($dir)) throw new RuntimeException('Не удалось создать папку раздела');
-
- $final = uniqueName($dir, $safeBase, $ext);
- $dest = $dir . '/' . $final;
- if (!move_uploaded_file($tmp, $dest)) throw new RuntimeException('Не удалось сохранить файл');
-
- return [
- 'path' => 'photos/section_' . $sectionId . '/' . $final,
- '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;
-}
-?>
-
-
-
-
- Админка (MySQL)
-
-
-
-
-
-
Админка (MySQL)
-
Открыть публичную MySQL-галерею
-
= h($message) ?>
-
= h($e) ?>
-
-
-
-
-
-
-
-
-
-
-
- Фото раздела
- | ID | Код | Превью | Описание | Порядок |
-
-
- | = (int)$p['id'] ?> |
- = h((string)$p['code_name']) ?> |
- $p['before_file_id'] ?>) |
- = h((string)($p['description'] ?? '')) ?> |
- = (int)$p['sort_order'] ?> |
-
-
-
-
-
-
- Последние комментарии
- | ID | Фото | Пользователь | Комментарий | |
-
-
- | = (int)$c['id'] ?> |
- = h((string)$c['code_name']) ?> |
- = h((string)($c['display_name'] ?? '—')) ?> |
- = h((string)$c['comment_text']) ?> |
-
-
- |
-
-
-
-
-
-
+// Backward-compat alias
+require __DIR__ . '/admin.php';
diff --git a/admin.php b/admin.php
index eac33ec..e3e681d 100644
--- a/admin.php
+++ b/admin.php
@@ -2,330 +2,348 @@
declare(strict_types=1);
+require_once __DIR__ . '/lib/db_gallery.php';
+
const MAX_UPLOAD_BYTES = 3 * 1024 * 1024;
-$rootDir = __DIR__;
-$photosDir = $rootDir . '/photos';
-$thumbsDir = $rootDir . '/thumbs';
-$dataDir = $rootDir . '/data';
-$sortFile = $dataDir . '/sort.json';
-
-@mkdir($photosDir, 0775, true);
-@mkdir($thumbsDir, 0775, true);
-@mkdir($dataDir, 0775, true);
-
$configPath = __DIR__ . '/deploy-config.php';
if (!is_file($configPath)) {
http_response_code(500);
- echo 'deploy-config.php not found';
- exit;
+ exit('deploy-config.php not found');
}
$config = require $configPath;
$tokenExpected = (string)($config['token'] ?? '');
$tokenIncoming = (string)($_REQUEST['token'] ?? '');
-
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
http_response_code(403);
- echo 'Forbidden';
- exit;
+ exit('Forbidden');
}
-$sortData = loadSortData($sortFile);
-$sortData = reconcileSortData($photosDir, $sortData);
-saveSortData($sortFile, $sortData);
$message = '';
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string)($_POST['action'] ?? '');
- if ($action === 'create_category') {
- $name = sanitizeCategoryName((string)($_POST['category_name'] ?? ''));
- if ($name === '') {
- $errors[] = 'Некорректное имя папки.';
- } else {
- $dir = $photosDir . '/' . $name;
- if (!is_dir($dir) && !mkdir($dir, 0775, true)) {
- $errors[] = 'Не удалось создать папку.';
- } else {
- $message = 'Папка создана: ' . $name;
- $sortData['categories'][$name] = nextSortIndex($sortData['categories']);
- }
+ try {
+ if ($action === 'create_section') {
+ $name = trim((string)($_POST['name'] ?? ''));
+ $sort = (int)($_POST['sort_order'] ?? 1000);
+ if ($name === '') throw new RuntimeException('Название раздела пустое');
+ sectionCreate($name, $sort);
+ $message = 'Раздел создан';
}
- }
- if ($action === 'category_update') {
- $current = sanitizeCategoryName((string)($_POST['category_current'] ?? ''));
- $newName = sanitizeCategoryName((string)($_POST['category_new_name'] ?? ''));
- $sortIndex = (int)($_POST['category_sort'] ?? 1000);
+ 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('Файлы не переданы');
- if ($current === '' || !is_dir($photosDir . '/' . $current)) {
- $errors[] = 'Категория не найдена.';
- } else {
- if ($newName !== '' && $newName !== $current) {
- $oldDir = $photosDir . '/' . $current;
- $newDir = $photosDir . '/' . $newName;
- $oldThumb = $thumbsDir . '/' . $current;
- $newThumb = $thumbsDir . '/' . $newName;
-
- if (is_dir($newDir)) {
- $errors[] = 'Категория с таким именем уже существует.';
- } else {
- rename($oldDir, $newDir);
- if (is_dir($oldThumb)) {
- @rename($oldThumb, $newThumb);
- }
-
- if (isset($sortData['categories'][$current])) {
- $sortData['categories'][$newName] = $sortData['categories'][$current];
- unset($sortData['categories'][$current]);
- }
- if (isset($sortData['photos'][$current])) {
- $sortData['photos'][$newName] = $sortData['photos'][$current];
- unset($sortData['photos'][$current]);
- }
- $current = $newName;
- $message = 'Категория переименована.';
- }
- }
-
- $sortData['categories'][$current] = $sortIndex;
- $message = $message ?: 'Категория обновлена.';
- }
- }
-
- if ($action === 'category_delete') {
- $category = sanitizeCategoryName((string)($_POST['category_current'] ?? ''));
- if ($category === '' || !is_dir($photosDir . '/' . $category)) {
- $errors[] = 'Категория не найдена.';
- } else {
- rrmdir($photosDir . '/' . $category);
- rrmdir($thumbsDir . '/' . $category);
- unset($sortData['categories'][$category], $sortData['photos'][$category]);
- $message = 'Категория удалена: ' . $category;
- }
- }
-
- if ($action === 'upload') {
- $category = sanitizeCategoryName((string)($_POST['category'] ?? ''));
- if ($category === '' || !is_dir($photosDir . '/' . $category)) {
- $errors[] = 'Выберите существующую категорию.';
- } elseif (!isset($_FILES['photos'])) {
- $errors[] = 'Файлы не переданы.';
- } else {
- $result = handleUploads($_FILES['photos'], $photosDir . '/' . $category, $sortData, $category);
+ $result = saveBulkBefore($_FILES['before_bulk'], $sectionId);
+ $message = 'Загружено: ' . $result['ok'];
$errors = array_merge($errors, $result['errors']);
- if ($result['ok'] > 0) {
- $message = 'Загружено: ' . $result['ok'];
- }
}
- }
- if ($action === 'photo_update') {
- $category = sanitizeCategoryName((string)($_POST['category'] ?? ''));
- $currentFile = basename((string)($_POST['photo_current'] ?? ''));
- $newBase = sanitizeFileBase((string)($_POST['photo_new_name'] ?? ''));
- $sortIndex = (int)($_POST['photo_sort'] ?? 1000);
+ 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;
- $src = $photosDir . '/' . $category . '/' . $currentFile;
- if ($category === '' || $currentFile === '' || !is_file($src)) {
- $errors[] = 'Фото не найдено.';
- } else {
- $finalName = $currentFile;
- if ($newBase !== '') {
- $ext = strtolower(pathinfo($currentFile, PATHINFO_EXTENSION));
- $candidate = uniqueFileNameForRename($photosDir . '/' . $category, $newBase, $ext, $currentFile);
- if ($candidate !== $currentFile) {
- $dst = $photosDir . '/' . $category . '/' . $candidate;
- if (@rename($src, $dst)) {
- $oldThumb = $thumbsDir . '/' . $category . '/' . pathinfo($currentFile, PATHINFO_FILENAME) . '.jpg';
- $newThumb = $thumbsDir . '/' . $category . '/' . pathinfo($candidate, PATHINFO_FILENAME) . '.jpg';
- if (is_file($oldThumb)) {
- @rename($oldThumb, $newThumb);
+ 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 ($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);
}
- if (isset($sortData['photos'][$category][$currentFile])) {
- $sortData['photos'][$category][$candidate] = $sortData['photos'][$category][$currentFile];
- unset($sortData['photos'][$category][$currentFile]);
- }
- $finalName = $candidate;
}
}
+ $st = db()->prepare('DELETE FROM photos WHERE id=:id');
+ $st->execute(['id' => $photoId]);
+ $message = 'Фото удалено';
}
-
- $sortData['photos'][$category][$finalName] = $sortIndex;
- $message = 'Фото обновлено.';
}
- }
- if ($action === 'photo_delete') {
- $category = sanitizeCategoryName((string)($_POST['category'] ?? ''));
- $file = basename((string)($_POST['photo_current'] ?? ''));
- $src = $photosDir . '/' . $category . '/' . $file;
- if ($category === '' || $file === '' || !is_file($src)) {
- $errors[] = 'Фото не найдено.';
- } else {
- @unlink($src);
- $thumb = $thumbsDir . '/' . $category . '/' . pathinfo($file, PATHINFO_FILENAME) . '.jpg';
- if (is_file($thumb)) {
- @unlink($thumb);
+ 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 = 'Комментатор удалён (доступ отозван)';
}
- unset($sortData['photos'][$category][$file]);
- $message = 'Фото удалено.';
+ }
+
+ if ($action === 'delete_comment') {
+ $id = (int)($_POST['id'] ?? 0);
+ if ($id > 0) {
+ commentDelete($id);
+ $message = 'Комментарий удалён';
+ }
+ }
+ } catch (Throwable $e) {
+ $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);
+
+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();
}
}
- saveSortData($sortFile, $sortData);
+ return ['ok' => $ok, 'errors' => $errors];
}
-$categories = listCategories($photosDir, $sortData);
-$selectedCategory = sanitizeCategoryName((string)($_GET['edit_category'] ?? ($_POST['category'] ?? '')));
-$photos = $selectedCategory !== '' ? listPhotos($photosDir, $thumbsDir, $selectedCategory, $sortData) : [];
+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 МБ');
-?>
-
-
-Админка галереи
-
-
-
-
-
Админка галереи
-
← В галерею
-
= h($message) ?>
-
= h($e) ?>
+ $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';
-
- Фото в категории: = h($selectedCategory ?: '—') ?>
-
-
- Только JPG/PNG/WEBP/GIF, максимум 3 МБ на файл.
-
-
- Сначала выбери категорию в блоке выше (клик по её названию).
-
- В категории пока нет фото.
-
-
-
-
-
-[],'photos'=>[]]; $d=json_decode((string)file_get_contents($file),true); return is_array($d)?['categories'=>(array)($d['categories']??[]),'photos'=>(array)($d['photos']??[])]:['categories'=>[],'photos'=>[]]; }
-function saveSortData(string $file, array $data): void { file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); }
-function nextSortIndex(array $map): int { return $map===[]?10:((int)max(array_map('intval',$map))+10); }
-function listCategories(string $photosDir, array $sortData): array { $out=[]; foreach((@scandir($photosDir)?:[]) as $x){ if($x==='.'||$x==='..')continue; if(is_dir($photosDir.'/'.$x))$out[]=$x; } usort($out, fn($a,$b)=>((int)($sortData['categories'][$a]??1000)<=> (int)($sortData['categories'][$b]??1000)) ?: strnatcasecmp($a,$b)); return $out; }
-function listPhotos(string $photosDir, string $thumbsDir, string $category, array $sortData): array { $out=[]; $dir=$photosDir.'/'.$category; foreach((@scandir($dir)?:[]) as $f){ if($f==='.'||$f==='..')continue; $p=$dir.'/'.$f; if(!is_file($p)||!isImageExt($f)) continue; $thumbAbs=$thumbsDir.'/'.$category.'/'.pathinfo($f, PATHINFO_FILENAME).'.jpg'; $thumb=is_file($thumbAbs)?('thumbs/'.rawurlencode($category).'/'.rawurlencode(pathinfo($f, PATHINFO_FILENAME).'.jpg')):''; $out[]=['file'=>$f,'sort'=>(int)($sortData['photos'][$category][$f]??1000),'thumb'=>$thumb]; } usort($out, fn($a,$b)=>($a['sort']<=>$b['sort']) ?: strnatcasecmp($a['file'],$b['file'])); return $out; }
+ $ext = $allowedMime[$mime];
+ $dir = __DIR__ . '/photos/section_' . $sectionId;
+ if (!is_dir($dir)) mkdir($dir, 0775, true);
+ $name = uniqueName($dir, $safeBase, $ext);
+ $dest = $dir . '/' . $name;
-function reconcileSortData(string $photosDir, array $sortData): array {
- $clean=['categories'=>[],'photos'=>[]];
- $cats=[];
- foreach((@scandir($photosDir)?:[]) as $c){
- if($c==='.'||$c==='..') continue;
- if(!is_dir($photosDir.'/'.$c)) continue;
- $cats[]=$c;
- }
- foreach($cats as $c){
- $clean['categories'][$c]=(int)($sortData['categories'][$c] ?? 1000);
- $clean['photos'][$c]=[];
- foreach((@scandir($photosDir.'/'.$c)?:[]) as $f){
- if($f==='.'||$f==='..') continue;
- if(!is_file($photosDir.'/'.$c.'/'.$f) || !isImageExt($f)) continue;
- $clean['photos'][$c][$f]=(int)($sortData['photos'][$c][$f] ?? 1000);
+ 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++;
}
- return $clean;
-}
-function isImageExt(string $file): bool { return in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), ['jpg','jpeg','png','webp','gif'], true); }
-function rrmdir(string $dir): void { if(!is_dir($dir)) return; $it=scandir($dir)?:[]; foreach($it as $x){ if($x==='.'||$x==='..')continue; $p=$dir.'/'.$x; if(is_dir($p)) rrmdir($p); else @unlink($p);} @rmdir($dir); }
-function uniqueFileNameForRename(string $dir,string $base,string $ext,string $current): string{ $n=0; do{ $cand=$n===0?"{$base}.{$ext}":"{$base}_{$n}.{$ext}"; if($cand===$current||!file_exists($dir.'/'.$cand)) return $cand; $n++; }while(true); }
-function handleUploads(array $files, string $targetDir, array &$sortData, string $category): array {
- $allowedMime=['image/jpeg','image/png','image/webp','image/gif']; $allowedExt=['jpg','jpeg','png','webp','gif']; $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]; }
- $finfo=finfo_open(FILEINFO_MIME_TYPE);
- foreach($names as $i=>$orig){ if((int)($errs[$i]??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_OK){$errors[]="{$orig}: ошибка загрузки";continue;}
- $size=(int)($sizes[$i]??0); if($size<1||$size>MAX_UPLOAD_BYTES){$errors[]="{$orig}: >3MB";continue;}
- $tmpFile=(string)($tmp[$i]??''); if($tmpFile===''||!is_uploaded_file($tmpFile)){ $errors[]="{$orig}: источник"; continue;}
- $mime=$finfo?(string)finfo_file($finfo,$tmpFile):''; if(!in_array($mime,$allowedMime,true)){ $errors[]="{$orig}: тип {$mime}"; continue;}
- $ext=strtolower(pathinfo((string)$orig, PATHINFO_EXTENSION)); if(!in_array($ext,$allowedExt,true)){ $errors[]="{$orig}: расширение"; continue;}
- $base=sanitizeFileBase(pathinfo((string)$orig, PATHINFO_FILENAME)); if($base==='')$base='photo'; $name=uniqueFileNameForRename($targetDir,$base,$ext,'');
- if(!move_uploaded_file($tmpFile,$targetDir.'/'.$name)){ $errors[]="{$orig}: не сохранить"; continue; }
- $sortData['photos'][$category][$name]=nextSortIndex((array)($sortData['photos'][$category]??[])); $ok++;
- }
- if($finfo)finfo_close($finfo);
- return ['ok'=>$ok,'errors'=>$errors];
}
+?>
+
+
+
+
+ Админка
+
+
+
+
+
+
Админка
+
= h($message) ?>
+
= h($e) ?>
+
+
+
+
+
+
+ Загрузка фото “до” в выбранный раздел
+ 0): ?>
+
+ После загрузки имя (code_name) заполняется автоматически из имени файла — затем можно отредактировать.
+
+ Сначала выбери раздел слева.
+
+
+
+
+
+
+ Комментаторы и комментарии
+ | Пользователь | Действие |
+
+ | = h((string)$u['display_name']) ?> |
+
+ |
+
+
+
+ | Фото | Пользователь | Комментарий | |
+
+
+ | = h((string)$c['code_name']) ?> |
+ = h((string)($c['display_name'] ?? '—')) ?> |
+ = h((string)$c['comment_text']) ?> |
+
+
+ |
+
+
+
+
+
+
+
diff --git a/index-mysql.php b/index-mysql.php
index 006b9f5..871b49b 100644
--- a/index-mysql.php
+++ b/index-mysql.php
@@ -1,211 +1,3 @@
0 && $text !== '') {
- $u = commenterByToken($token);
- if ($u) {
- commentAdd($photoId, (int)$u['id'], mb_substr($text, 0, 1000));
- }
- }
-
- $redirect = './index-mysql.php?photo_id=' . $photoId;
- if ($token !== '') {
- $redirect .= '&viewer=' . urlencode($token);
- }
- header('Location: ' . $redirect);
- exit;
-}
-
-$sections = sectionsAll();
-$activeSectionId = (int)($_GET['section_id'] ?? 0);
-$activePhotoId = (int)($_GET['photo_id'] ?? 0);
-
-if ($activePhotoId > 0) {
- $photo = photoById($activePhotoId);
- if (!$photo) {
- http_response_code(404);
- $photo = null;
- }
- $comments = $photo ? commentsByPhoto($activePhotoId) : [];
-} else {
- $photo = null;
- $comments = [];
-}
-
-$photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : [];
-
-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 serveImage(): never
-{
- $fileId = (int)($_GET['file_id'] ?? 0);
- if ($fileId < 1) {
- http_response_code(404);
- exit;
- }
-
- $f = photoFileById($fileId);
- if (!$f) {
- http_response_code(404);
- exit;
- }
-
- $abs = __DIR__ . '/' . ltrim((string)$f['file_path'], '/');
- if (!is_file($abs)) {
- http_response_code(404);
- exit;
- }
-
- $kind = (string)$f['kind'];
- if ($kind !== 'after') {
- header('Content-Type: ' . ((string)$f['mime_type'] ?: 'application/octet-stream'));
- header('Content-Length: ' . (string)filesize($abs));
- header('Cache-Control: private, max-age=60');
- header('X-Robots-Tag: noindex, nofollow');
- readfile($abs);
- exit;
- }
-
- outputWatermarked($abs, (string)$f['mime_type']);
-}
-
-function outputWatermarked(string $path, string $mime): never
-{
- $text = 'photo.andr33v.ru';
-
- if (extension_loaded('imagick')) {
- $im = new Imagick($path);
- $draw = new ImagickDraw();
- $draw->setFillColor(new ImagickPixel('rgba(255,255,255,0.22)'));
- $draw->setFontSize(max(18, (int)($im->getImageWidth() / 24)));
- $draw->setGravity(Imagick::GRAVITY_SOUTHEAST);
- $im->annotateImage($draw, 20, 24, -15, $text);
- header('Content-Type: ' . ($mime !== '' ? $mime : 'image/jpeg'));
- $im->setImageCompressionQuality(88);
- echo $im;
- $im->clear();
- $im->destroy();
- exit;
- }
-
- [$w, $h, $type] = @getimagesize($path) ?: [0,0,0];
- if ($w < 1 || $h < 1) {
- readfile($path);
- exit;
- }
-
- $img = match ($type) {
- IMAGETYPE_JPEG => imagecreatefromjpeg($path),
- IMAGETYPE_PNG => imagecreatefrompng($path),
- IMAGETYPE_GIF => imagecreatefromgif($path),
- IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? imagecreatefromwebp($path) : null,
- default => null,
- };
-
- if (!$img) {
- readfile($path);
- exit;
- }
-
- $font = 5;
- $color = imagecolorallocatealpha($img, 255, 255, 255, 90);
- $x = max(5, $w - (imagefontwidth($font) * strlen($text)) - 15);
- $y = max(5, $h - imagefontheight($font) - 12);
- imagestring($img, $font, $x, $y, $text, $color);
-
- header('Content-Type: image/jpeg');
- imagejpeg($img, null, 88);
- imagedestroy($img);
- exit;
-}
-?>
-
-
-
-
- Фотогалерея (MySQL)
-
-
-
-
-
-
-
-
-
-
- 0 && $photo): ?>
-
- ← к разделу
- = h((string)$photo['code_name']) ?>
- = h((string)($photo['description'] ?? '')) ?>
-
-
До обработки
$photo['before_file_id'] ?>)
-
После обработки (watermark)
$photo['after_file_id'] ?>)
-
-
- Комментарии
-
-
-
- Комментарии может оставлять только пользователь с персональной ссылкой.
-
-
-
- = h((string)($c['display_name'] ?? 'Пользователь')) ?> · = h((string)$c['created_at']) ?>
= nl2br(h((string)$c['comment_text'])) ?>
-
-
-
-
- Фотографии
-
- Выберите раздел слева.
-
- В разделе пока нет фотографий.
-
-
-
-
-
-
-
-
-
-
+// Backward-compat alias
+require __DIR__ . '/index.php';
diff --git a/index.php b/index.php
index 44c2d9a..ccb80a2 100644
--- a/index.php
+++ b/index.php
@@ -2,376 +2,192 @@
declare(strict_types=1);
-const THUMB_WIDTH = 360;
-const THUMB_HEIGHT = 240;
+require_once __DIR__ . '/lib/db_gallery.php';
-$baseDir = __DIR__;
-$photosDir = $baseDir . '/photos';
-$thumbsDir = $baseDir . '/thumbs';
-$dataDir = $baseDir . '/data';
-$lastIndexedFile = $dataDir . '/last_indexed.txt';
-$sortFile = $dataDir . '/sort.json';
-
-ensureDirectories([$photosDir, $thumbsDir, $dataDir]);
-$sortData = loadSortData($sortFile);
-
-$action = $_GET['action'] ?? null;
+$action = (string)($_GET['action'] ?? '');
if ($action === 'image') {
- serveImage($photosDir);
+ serveImage();
}
-$lastIndexedTimestamp = readLastIndexedTimestamp($lastIndexedFile);
-$maxTimestamp = $lastIndexedTimestamp;
+$viewerToken = trim((string)($_GET['viewer'] ?? ''));
+$viewer = $viewerToken !== '' ? commenterByToken($viewerToken) : null;
-$categories = scanCategories($photosDir, $sortData);
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'add_comment') {
+ $token = trim((string)($_POST['viewer'] ?? ''));
+ $photoId = (int)($_POST['photo_id'] ?? 0);
+ $text = trim((string)($_POST['comment_text'] ?? ''));
-foreach ($categories as $categoryName => &$images) {
- $categoryThumbDir = $thumbsDir . '/' . $categoryName;
- if (!is_dir($categoryThumbDir)) {
- mkdir($categoryThumbDir, 0775, true);
- }
-
- foreach ($images as &$image) {
- $sourcePath = $image['abs_path'];
- $sourceMtime = (int) filemtime($sourcePath);
- $maxTimestamp = max($maxTimestamp, $sourceMtime);
-
- $thumbExt = 'jpg';
- $thumbName = pathinfo($image['filename'], PATHINFO_FILENAME) . '.jpg';
- $thumbAbsPath = $categoryThumbDir . '/' . $thumbName;
- $thumbWebPath = 'thumbs/' . rawurlencode($categoryName) . '/' . rawurlencode($thumbName);
-
- $needsThumb = !file_exists($thumbAbsPath)
- || filemtime($thumbAbsPath) < $sourceMtime
- || $sourceMtime > $lastIndexedTimestamp;
-
- if ($needsThumb) {
- createThumbnail($sourcePath, $thumbAbsPath, THUMB_WIDTH, THUMB_HEIGHT);
+ if ($token !== '' && $photoId > 0 && $text !== '') {
+ $u = commenterByToken($token);
+ if ($u) {
+ commentAdd($photoId, (int)$u['id'], mb_substr($text, 0, 1000));
}
-
- $image['thumb_path'] = $thumbWebPath;
- $image['full_path'] = '?action=image&category=' . rawurlencode($categoryName) . '&file=' . rawurlencode($image['filename']);
- $image['title'] = titleFromFilename($image['filename']);
- $image['mtime'] = $sourceMtime;
}
- usort($images, static function (array $a, array $b): int {
- $bySort = ($a['sort_index'] ?? 0) <=> ($b['sort_index'] ?? 0);
- if ($bySort !== 0) {
- return $bySort;
- }
-
- return ($b['mtime'] ?? 0) <=> ($a['mtime'] ?? 0);
- });
-}
-unset($images, $image);
-
-if ($maxTimestamp > $lastIndexedTimestamp) {
- file_put_contents($lastIndexedFile, (string)$maxTimestamp);
-}
-
-$selectedCategory = isset($_GET['category']) ? trim((string)$_GET['category']) : null;
-if ($selectedCategory !== null && $selectedCategory !== '' && !isset($categories[$selectedCategory])) {
- http_response_code(404);
- $selectedCategory = null;
-}
-
-?>
-
-
-
-
- Фотогалерея
-
-
-
-
-
-
-
-
-
- Категории
-
- Пока нет папок с фото. Загрузите файлы в photos/<категория>/ через FTP.
-
-
-
-
-
-
-
-
-
-
- В этой категории пока нет изображений.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
![]()
-
-
-
-
-
-
-
- 0 ? photoById($activePhotoId) : null;
+$comments = $photo ? commentsByPhoto($activePhotoId) : [];
+$photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : [];
+
+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 serveImage(): never
{
- $name = pathinfo($filename, PATHINFO_FILENAME);
- $name = str_replace(['_', '-'], ' ', $name);
- $name = preg_replace('/\s+/', ' ', $name) ?? $name;
- $name = trim($name);
-
- if ($name === '') {
- return $filename;
+ $fileId = (int)($_GET['file_id'] ?? 0);
+ if ($fileId < 1) {
+ http_response_code(404);
+ exit;
}
- if (function_exists('mb_convert_case')) {
- return mb_convert_case($name, MB_CASE_TITLE, 'UTF-8');
+ $f = photoFileById($fileId);
+ if (!$f) {
+ http_response_code(404);
+ exit;
}
- return ucwords(strtolower($name));
+ $abs = __DIR__ . '/' . ltrim((string)$f['file_path'], '/');
+ if (!is_file($abs)) {
+ http_response_code(404);
+ exit;
+ }
+
+ if ((string)$f['kind'] !== 'after') {
+ header('Content-Type: ' . ((string)$f['mime_type'] ?: 'application/octet-stream'));
+ header('Content-Length: ' . (string)filesize($abs));
+ header('Cache-Control: private, max-age=60');
+ header('X-Robots-Tag: noindex, nofollow');
+ readfile($abs);
+ exit;
+ }
+
+ outputWatermarked($abs, (string)$f['mime_type']);
}
-function ensureDirectories(array $dirs): void
+function outputWatermarked(string $path, string $mime): never
{
- foreach ($dirs as $dir) {
- if (!is_dir($dir)) {
- mkdir($dir, 0775, true);
- }
- }
-}
+ $text = 'photo.andr33v.ru';
-function readLastIndexedTimestamp(string $path): int
-{
- if (!file_exists($path)) {
- return 0;
- }
-
- $value = trim((string) file_get_contents($path));
- return ctype_digit($value) ? (int)$value : 0;
-}
-
-function scanCategories(string $photosDir, array $sortData): array
-{
- $result = [];
- $categorySortMap = (array)($sortData['categories'] ?? []);
- $photoSortMap = (array)($sortData['photos'] ?? []);
-
- $entries = @scandir($photosDir) ?: [];
- foreach ($entries as $entry) {
- if ($entry === '.' || $entry === '..') {
- continue;
- }
-
- $categoryPath = $photosDir . '/' . $entry;
- if (!is_dir($categoryPath)) {
- continue;
- }
-
- $images = [];
- $files = @scandir($categoryPath) ?: [];
- foreach ($files as $filename) {
- if ($filename === '.' || $filename === '..') {
- continue;
- }
-
- $absPath = $categoryPath . '/' . $filename;
- if (!is_file($absPath) || !isImage($absPath)) {
- continue;
- }
-
- $images[] = [
- 'filename' => $filename,
- 'abs_path' => $absPath,
- 'sort_index' => (int)(($photoSortMap[$entry][$filename] ?? 1000)),
- ];
- }
-
- $result[$entry] = $images;
- }
-
- uksort($result, static function (string $a, string $b) use ($categorySortMap): int {
- $aSort = (int)($categorySortMap[$a] ?? 1000);
- $bSort = (int)($categorySortMap[$b] ?? 1000);
-
- if ($aSort !== $bSort) {
- return $aSort <=> $bSort;
- }
-
- return strnatcasecmp($a, $b);
- });
-
- return $result;
-}
-
-function assetUrl(string $relativePath): string
-{
- $file = __DIR__ . '/' . ltrim($relativePath, '/');
- $v = is_file($file) ? (string)filemtime($file) : (string)time();
- return $relativePath . '?v=' . rawurlencode($v);
-}
-
-function loadSortData(string $sortFile): array
-{
- if (!is_file($sortFile)) {
- return ['categories' => [], 'photos' => []];
- }
-
- $json = file_get_contents($sortFile);
- if ($json === false || trim($json) === '') {
- return ['categories' => [], 'photos' => []];
- }
-
- $data = json_decode($json, true);
- if (!is_array($data)) {
- return ['categories' => [], 'photos' => []];
- }
-
- return [
- 'categories' => is_array($data['categories'] ?? null) ? $data['categories'] : [],
- 'photos' => is_array($data['photos'] ?? null) ? $data['photos'] : [],
- ];
-}
-
-function isImage(string $path): bool
-{
- $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
- return in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'], true);
-}
-
-function createThumbnail(string $srcPath, string $thumbPath, int $targetWidth, int $targetHeight): void
-{
if (extension_loaded('imagick')) {
- createThumbnailWithImagick($srcPath, $thumbPath, $targetWidth, $targetHeight);
- return;
+ $im = new Imagick($path);
+ $draw = new ImagickDraw();
+ $draw->setFillColor(new ImagickPixel('rgba(255,255,255,0.22)'));
+ $draw->setFontSize(max(18, (int)($im->getImageWidth() / 24)));
+ $draw->setGravity(Imagick::GRAVITY_SOUTHEAST);
+ $im->annotateImage($draw, 20, 24, -15, $text);
+ header('Content-Type: ' . ($mime !== '' ? $mime : 'image/jpeg'));
+ $im->setImageCompressionQuality(88);
+ echo $im;
+ $im->clear();
+ $im->destroy();
+ exit;
}
- createThumbnailWithGd($srcPath, $thumbPath, $targetWidth, $targetHeight);
-}
-
-function createThumbnailWithImagick(string $srcPath, string $thumbPath, int $targetWidth, int $targetHeight): void
-{
- $imagick = new Imagick($srcPath);
- $imagick->setIteratorIndex(0);
- $imagick->setImageOrientation(Imagick::ORIENTATION_UNDEFINED);
- $imagick->thumbnailImage($targetWidth, $targetHeight, true, true);
- $imagick->setImageFormat('jpeg');
- $imagick->setImageCompressionQuality(82);
- $imagick->writeImage($thumbPath);
- $imagick->clear();
- $imagick->destroy();
-}
-
-function createThumbnailWithGd(string $srcPath, string $thumbPath, int $targetWidth, int $targetHeight): void
-{
- [$srcW, $srcH, $type] = @getimagesize($srcPath) ?: [0, 0, 0];
- if ($srcW < 1 || $srcH < 1) {
- return;
- }
-
- $src = match ($type) {
- IMAGETYPE_JPEG => @imagecreatefromjpeg($srcPath),
- IMAGETYPE_PNG => @imagecreatefrompng($srcPath),
- IMAGETYPE_GIF => @imagecreatefromgif($srcPath),
- IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($srcPath) : null,
+ [$w, $h, $type] = @getimagesize($path) ?: [0,0,0];
+ $img = match ($type) {
+ IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
+ IMAGETYPE_PNG => @imagecreatefrompng($path),
+ IMAGETYPE_GIF => @imagecreatefromgif($path),
+ IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : null,
default => null,
};
-
- if (!$src) {
- return;
+ if (!$img) {
+ readfile($path);
+ exit;
}
- $scale = min($targetWidth / $srcW, $targetHeight / $srcH);
- $dstW = max(1, (int) floor($srcW * $scale));
- $dstH = max(1, (int) floor($srcH * $scale));
+ $font = 5;
+ $color = imagecolorallocatealpha($img, 255, 255, 255, 90);
+ $x = max(5, $w - (imagefontwidth($font) * strlen($text)) - 15);
+ $y = max(5, $h - imagefontheight($font) - 12);
+ imagestring($img, $font, $x, $y, $text, $color);
- $dst = imagecreatetruecolor($dstW, $dstH);
- imagecopyresampled($dst, $src, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);
-
- imagejpeg($dst, $thumbPath, 82);
-
- imagedestroy($src);
- imagedestroy($dst);
+ header('Content-Type: image/jpeg');
+ imagejpeg($img, null, 88);
+ imagedestroy($img);
+ exit;
}
+?>
+
+
+
+
+ Фотогалерея
+
+
+
+
+
+
+
+
+
+
+ 0 && $photo): ?>
+
+ ← к разделу
+ = h((string)$photo['code_name']) ?>
+ = h((string)($photo['description'] ?? '')) ?>
+
+
До обработки
$photo['before_file_id'] ?>)
+
После обработки (watermark)
$photo['after_file_id'] ?>)
+
+
+ Комментарии
+
+
+
+ Комментарии может оставлять только пользователь с персональной ссылкой.
+
+
+
+ = h((string)($c['display_name'] ?? 'Пользователь')) ?> · = h((string)$c['created_at']) ?>
= nl2br(h((string)$c['comment_text'])) ?>
+
+
+
+
+ Фотографии
+
+ Выберите раздел слева.
+
+ В разделе пока нет фотографий.
+
+
+
+
+
+
+
+
+
+