Files
zuevav 91be1b1751 Fix album scroll loading, lift 9-photo limit, badge import & mobile UX
Four user-reported fixes:

1. Albums infinite scroll: after a successful page load the
   IntersectionObserver wouldn't refire when the sentinel stayed in
   view (small thumbnails or large viewport), so subsequent pages
   needed a manual refresh. Re-check sentinel position after each
   load and add a scroll-event fallback for older mobile browsers.

2. 9-photo limit: add a "Больше 9 фото" toggle next to the photo
   source buttons. When checked, the cap lifts to 99 (covers Telegram
   auto-album splitting). VK still has its 9-attachment limit — if
   posting to VK with more selected, prompt a confirm and truncate
   the VK media payload to the first 9 client-side. Counters and
   notifications all read the dynamic max.

3. Badge tab: gallery selections now auto-import into the badge list
   on tab activation, so the photos appear right away with the first
   one opened in the editor. The "Выбранные в галерее Flickr" button
   reuses the same path for clarity.

4. Mobile UX: main navigation now scrolls horizontally on screens
   ≤720px instead of breaking into a tall stack — tabs stay on one
   row, scroll-snap on. In the badge editor the canvas wrapper is
   `position: sticky; top: 0` on screens ≤800px so the photo stays
   visible while scrolling through the controls below it; canvas
   also shrinks to ~62vw on phones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:16:23 +03:00

966 lines
59 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* VH Posting System - Главная страница
* Управление фотографиями Flickr и публикация в соцсети
*/
session_start();
// Load configuration
$configFile = __DIR__ . '/config.php';
if (!file_exists($configFile)) {
die('Файл конфигурации не найден. Скопируйте config.example.php в config.php и настройте его.');
}
$config = require $configFile;
// Autoload classes
spl_autoload_register(function ($class) {
$file = __DIR__ . '/classes/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// Initialize Auth
$auth = new Auth();
// Handle setup if no users exist
if (!$auth->hasUsers()) {
header('Location: setup.php');
exit;
}
// Check authentication
if (!$auth->isAuthenticated()) {
header('Location: login.php');
exit;
}
// Get current user
$currentUser = $auth->getCurrentUser();
// Handle logout
if (isset($_GET['logout'])) {
$auth->logout();
header('Location: login.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VH Posting System</title>
<link rel="icon" type="image/png" href="image.png">
<link rel="stylesheet" href="css/style.css?v=<?= filemtime(__DIR__ . '/css/style.css') ?>">
<script>
// Apply saved theme immediately to prevent flash
(function() {
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="app-header">
<h1>VH Posting System</h1>
<div class="user-menu">
<button class="theme-toggle" id="theme-toggle" title="Переключить тему"></button>
<span class="username"><?= htmlspecialchars($currentUser) ?></span>
<a href="?logout=1" class="btn btn-small btn-secondary">Выход</a>
</div>
</header>
<!-- Main Navigation -->
<nav class="main-nav">
<button class="nav-btn" data-tab="gallery">Галерея</button>
<button class="nav-btn active" data-tab="posting">Публикация</button>
<button class="nav-btn" data-tab="badge">Цифровой бейдж</button>
<button class="nav-btn" data-tab="converter">Конвертер</button>
<button class="nav-btn" data-tab="widget">Виджет</button>
<button class="nav-btn" data-tab="settings">Настройки</button>
</nav>
<!-- Tab: Flickr Gallery -->
<section id="tab-gallery" class="tab-content">
<div class="panel">
<!-- OAuth Status Banner -->
<div id="oauth-banner" class="oauth-banner hidden">
<div class="oauth-banner-content">
<span class="oauth-banner-icon">🔐</span>
<span class="oauth-banner-text">
<strong>Оригиналы недоступны</strong> — требуется авторизация Flickr
</span>
<a href="flickr_auth.php" class="btn btn-small btn-primary">Авторизовать</a>
</div>
</div>
<!-- Albums View (default) -->
<div id="albums-view" class="gallery-view">
<div class="gallery-header">
<h2>Альбомы Flickr</h2>
<div class="gallery-toolbar">
<button id="btn-load-albums" class="btn btn-secondary">
<span class="btn-icon-text">↻</span> Обновить
</button>
<div class="toolbar-search">
<input type="text" id="search-albums" placeholder="Поиск альбомов...">
</div>
</div>
</div>
<p class="drag-hint" id="drag-hint">💡 Перетащите альбомы для изменения порядка</p>
<div id="albums-grid" class="albums-grid">
<p class="placeholder">Загрузка альбомов...</p>
</div>
</div>
<!-- Photos View (when inside album) -->
<div id="photos-view" class="gallery-view hidden">
<div class="gallery-header">
<div class="breadcrumb">
<button id="btn-back-to-albums" class="btn btn-text">
← Назад к альбомам
</button>
<span class="breadcrumb-separator">/</span>
<span id="current-album-title" class="breadcrumb-current">Альбом</span>
</div>
<div class="gallery-toolbar">
<div class="toolbar-search">
<input type="text" id="search-photos" placeholder="Поиск фото...">
</div>
<span id="photos-count" class="photos-count"></span>
<button id="btn-download-album" class="btn btn-small" onclick="downloadAllPhotos()" title="Скачать все фото альбома">
↓ Скачать альбом
</button>
</div>
</div>
<!-- Photo Grid -->
<div id="photo-gallery" class="photo-gallery">
<p class="placeholder">Загрузка фотографий...</p>
</div>
<!-- Pagination -->
<div class="pagination">
<button id="btn-prev-page" class="btn btn-small" disabled>←</button>
<span id="page-info">1</span>
<button id="btn-next-page" class="btn btn-small" disabled>→</button>
</div>
</div>
</div>
<!-- Floating Action Bar (appears when photos selected) -->
<div id="selection-bar" class="floating-action-bar hidden">
<div class="action-bar-left">
<span id="selected-count" class="selection-count">0</span>
<span class="selection-label">выбрано</span>
</div>
<div class="action-bar-center">
<button id="btn-select-all" class="action-btn" title="Выбрать все">
<span class="action-icon">☑</span>
<span class="action-text">Все</span>
</button>
<button id="btn-deselect-all" class="action-btn" title="Снять выбор">
<span class="action-icon">☐</span>
<span class="action-text">Сброс</span>
</button>
</div>
<div class="action-bar-right">
<button id="btn-download-selected" class="action-btn" title="Скачать оригиналы">
<span class="action-icon">↓</span>
<span class="action-text">Скачать</span>
</button>
<button id="btn-convert-selected" class="action-btn action-secondary" title="Конвертировать в BBCode/HTML">
<span class="action-icon">{ }</span>
<span class="action-text">Код</span>
</button>
<button id="btn-telegram-selected" class="action-btn action-primary" title="Опубликовать в соцсети">
<span class="action-icon">↗</span>
<span class="action-text">Опубликовать</span>
</button>
</div>
</div>
</section>
<!-- Tab: Multi-Platform Posting -->
<section id="tab-posting" class="tab-content active">
<div class="panel">
<h2>Публикация в социальные сети</h2>
<p class="help-text">Выберите платформы и опубликуйте одним нажатием</p>
<!-- Photos Section -->
<div class="form-group">
<label>Фото и видео: <span id="photo-counter" class="photo-counter">0/9</span></label>
<div class="photo-source-buttons">
<button type="button" class="btn btn-secondary" id="btn-select-from-flickr">
<span class="btn-icon-text">🖼</span> Выбрать с Flickr
</button>
<input type="file" id="file-upload" multiple accept="image/*,video/*" style="display: none;">
<button type="button" class="btn btn-secondary" id="btn-upload-files">
<span class="btn-icon-text">📤</span> Загрузить
</button>
</div>
<label class="checkbox-label compact" style="margin-top:8px;">
<input type="checkbox" id="chk-unlock-photo-limit">
<span>Больше 9 фото (для Telegram). В VK уйдут только первые 9.</span>
</label>
<div id="post-photos-preview" class="photos-preview combined-preview">
<p class="placeholder">Нажмите кнопку выше чтобы добавить фото</p>
</div>
</div>
<!-- Post Text -->
<div class="form-group">
<label for="post-text">Текст публикации:</label>
<div class="text-editor">
<div class="editor-toolbar">
<button type="button" class="toolbar-btn" data-format="bold" title="Жирный"><b>B</b></button>
<button type="button" class="toolbar-btn" data-format="italic" title="Курсив"><i>I</i></button>
<button type="button" class="toolbar-btn" data-format="underline" title="Подчёркнутый"><u>U</u></button>
<button type="button" class="toolbar-btn" data-format="strike" title="Зачёркнутый"><s>S</s></button>
<span class="toolbar-separator"></span>
<button type="button" class="toolbar-btn" data-format="link" title="Ссылка">🔗</button>
<button type="button" class="toolbar-btn" data-format="code" title="Код">&lt;/&gt;</button>
</div>
<textarea id="post-text" rows="4" placeholder="Введите текст для публикации..."></textarea>
</div>
</div>
<!-- Tags -->
<div class="form-group">
<label>Теги:</label>
<div class="tags-container" id="post-tags-container">
<div class="tags-list" id="post-tags-list"></div>
<div class="tags-input-wrapper">
<input type="text" id="post-tags-input" class="tags-input" placeholder="Добавить тег...">
<div class="tags-suggestions" id="post-tags-suggestions"></div>
</div>
</div>
<div class="tags-presets">
<span class="tags-presets-label">Быстрые теги:</span>
<div class="presets-list" id="post-presets-list"></div>
<button type="button" class="preset-add-btn" id="post-preset-add" title="Добавить пресет">+</button>
<button type="button" class="preset-manage-btn" id="post-preset-manage" title="Управление пресетами">⚙</button>
</div>
</div>
<!-- Platform Selection -->
<div class="form-group">
<label>Выберите платформы:</label>
<div class="platforms-grid">
<!-- Telegram -->
<div class="platform-card">
<div class="platform-header">
<label class="platform-checkbox">
<input type="checkbox" id="chk-telegram" checked>
<span class="checkmark"></span>
</label>
<div class="platform-info">
<span class="platform-name">Telegram</span>
<span id="tg-status-mini" class="status-mini">Не подключён</span>
</div>
</div>
<select id="tg-channel" class="platform-target">
<option value="">Выберите канал...</option>
</select>
</div>
<!-- VK -->
<div class="platform-card">
<div class="platform-header">
<label class="platform-checkbox">
<input type="checkbox" id="chk-vk" checked>
<span class="checkmark"></span>
</label>
<div class="platform-info">
<span class="platform-name">ВКонтакте</span>
<span id="vk-status-mini" class="status-mini">Не подключён</span>
</div>
</div>
<select id="vk-group" class="platform-target">
<option value="">Выберите группу...</option>
</select>
</div>
<!-- Instagram (info only) -->
<div class="platform-card platform-disabled">
<div class="platform-header">
<label class="platform-checkbox">
<input type="checkbox" id="chk-instagram" disabled>
<span class="checkmark"></span>
</label>
<div class="platform-info">
<span class="platform-name">Instagram</span>
<span class="status-mini">Требует настройки</span>
</div>
</div>
<p class="platform-note">Требуется Facebook Business API</p>
</div>
</div>
</div>
<!-- Post Options -->
<div class="post-options-grid">
<div class="form-group">
<label for="post-parse-mode">Формат:</label>
<select id="post-parse-mode">
<option value="HTML">HTML</option>
<option value="Markdown">Markdown</option>
<option value="">Текст</option>
</select>
</div>
<div class="form-group">
<label class="checkbox-label compact">
<input type="checkbox" id="chk-cross-promo" checked>
<span>Кросс-промо</span>
</label>
</div>
<div class="form-group schedule-toggle">
<label class="checkbox-label compact">
<input type="checkbox" id="chk-schedule">
<span>Отложить</span>
</label>
</div>
</div>
<!-- Schedule Options (hidden by default) -->
<div id="schedule-options" class="schedule-options hidden">
<label class="schedule-label">📅 Когда опубликовать:</label>
<div class="schedule-presets">
<button type="button" class="preset-btn" data-preset="1h">Через 1 час</button>
<button type="button" class="preset-btn" data-preset="3h">Через 3 часа</button>
<button type="button" class="preset-btn" data-preset="tomorrow-10">Завтра 10:00</button>
<button type="button" class="preset-btn" data-preset="tomorrow-18">Завтра 18:00</button>
</div>
<div class="schedule-custom">
<div class="schedule-date-row">
<div class="schedule-field">
<label for="schedule-date">Дата:</label>
<input type="date" id="schedule-date" class="schedule-input">
</div>
<div class="schedule-field">
<label for="schedule-time">Время:</label>
<input type="time" id="schedule-time" class="schedule-input">
</div>
</div>
<input type="hidden" id="scheduled-datetime">
</div>
</div>
<!-- Action Buttons -->
<div class="post-actions">
<button id="btn-send-post" class="btn btn-primary btn-large">
🚀 Опубликовать
</button>
</div>
<div id="post-result" class="result-message"></div>
<!-- Scheduled Posts List -->
<div class="scheduled-section">
<div class="scheduled-header">
<h3>📅 Отложенные публикации</h3>
<span id="scheduled-count" class="badge">0</span>
</div>
<div id="scheduled-posts-list" class="scheduled-posts-list">
<p class="placeholder">Нет запланированных постов</p>
</div>
</div>
<!-- Published Posts Archive -->
<div class="archive-section">
<div class="archive-header">
<h3>✓ Архив публикаций</h3>
<button type="button" class="btn btn-small btn-secondary" id="btn-refresh-archive">↻</button>
</div>
<div id="published-posts-list" class="published-posts-list">
<p class="placeholder">Загрузка...</p>
</div>
</div>
</div>
</section>
<!-- Tab: Link Converter -->
<section id="tab-converter" class="tab-content">
<div class="panel">
<h2>Конвертер ссылок Flickr</h2>
<p class="help-text">Преобразование ссылок в различные форматы для форумов и соцсетей</p>
<div class="converter-grid">
<!-- Left Column: Input -->
<div class="converter-input-section">
<div class="form-group">
<label for="input-urls">Ссылки Flickr:</label>
<textarea id="input-urls" rows="5" placeholder="https://www.flickr.com/photos/username/12345678901/
https://flic.kr/p/ABC123
https://live.staticflickr.com/65535/12345678901_abcdef1234_b.jpg"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="image-size">Размер:</label>
<select id="image-size">
<option value="Large" selected>Большой (1024px)</option>
<option value="Large1600">1600px</option>
<option value="Large2048">2048px</option>
<option value="Original">Оригинал</option>
<option value="Medium640">640px</option>
</select>
</div>
<div class="form-group">
<label for="output-format">Формат:</label>
<select id="output-format">
<optgroup label="Кукольные форумы">
<option value="bjdclub">BJDClub.ru</option>
<option value="babiki">Babiki.ru</option>
<option value="babiki_simple">Babiki (простой)</option>
<option value="doll_forum">Универсальный</option>
</optgroup>
<optgroup label="BBCode">
<option value="bbcode">BBCode</option>
<option value="bbcode_linked">BBCode + ссылка</option>
</optgroup>
<optgroup label="HTML / Markdown">
<option value="html">HTML</option>
<option value="markdown">Markdown</option>
<option value="url_only">Только URL</option>
</optgroup>
</select>
</div>
</div>
<button id="btn-convert" class="btn btn-primary btn-block">Конвертировать</button>
</div>
<!-- Right Column: Text & Tags -->
<div class="converter-text-section">
<div class="form-group">
<label for="converter-title">Заголовок:</label>
<input type="text" id="converter-title" placeholder="Название поста...">
</div>
<div class="form-group">
<label for="converter-text">Текст к фотографиям:</label>
<div class="text-editor">
<div class="editor-toolbar">
<button type="button" class="toolbar-btn" data-format="bold" data-target="converter-text" title="Жирный"><b>B</b></button>
<button type="button" class="toolbar-btn" data-format="italic" data-target="converter-text" title="Курсив"><i>I</i></button>
<button type="button" class="toolbar-btn" data-format="underline" data-target="converter-text" title="Подчёркнутый"><u>U</u></button>
<span class="toolbar-separator"></span>
<button type="button" class="toolbar-btn" data-format="link" data-target="converter-text" title="Ссылка">🔗</button>
</div>
<textarea id="converter-text" rows="3" placeholder="Описание, комментарии к фото..."></textarea>
</div>
</div>
<div class="form-group">
<label>Теги:</label>
<div class="tags-container" id="converter-tags-container">
<div class="tags-list" id="converter-tags-list"></div>
<div class="tags-input-wrapper">
<input type="text" id="converter-tags-input" class="tags-input" placeholder="Добавить тег...">
<div class="tags-suggestions" id="converter-tags-suggestions"></div>
</div>
</div>
<div class="tags-presets">
<span class="tags-presets-label">Быстрые:</span>
<div class="presets-list" id="converter-presets-list"></div>
<button type="button" class="preset-add-btn" id="converter-preset-add" title="Добавить пресет">+</button>
<button type="button" class="preset-manage-btn" id="converter-preset-manage" title="Управление пресетами">⚙</button>
</div>
</div>
</div>
</div>
<!-- Output Section -->
<div class="converter-output-section">
<div class="form-group">
<label for="output-result">Результат:</label>
<textarea id="output-result" rows="6" readonly placeholder="Результат появится здесь после конвертации..."></textarea>
<div class="output-actions">
<button id="btn-copy" class="btn btn-secondary">📋 Скопировать</button>
<button id="btn-copy-with-tags" class="btn btn-secondary">📋 С тегами</button>
<span id="copy-status" class="copy-status"></span>
</div>
</div>
</div>
</div>
</section>
<!-- Tab: Widget Settings -->
<section id="tab-widget" class="tab-content">
<div class="panel">
<h2>Виджет для WordPress</h2>
<p class="help-text">Настройте мозаику фотографий для отображения на вашем сайте</p>
<!-- Widget Status -->
<div class="settings-section">
<h3>Статус виджета</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="widget-enabled" checked>
<span>Виджет включён</span>
</label>
</div>
<div class="form-group">
<label>API URL для WordPress:</label>
<input type="text" id="widget-api-url" readonly>
<button type="button" class="btn btn-small btn-secondary" onclick="copyWidgetUrl()">Копировать</button>
</div>
</div>
<!-- Album Selection -->
<div class="settings-section">
<h3>Выбор альбомов</h3>
<p class="help-text">Выберите альбомы для отображения в виджете. Если ничего не выбрано — показываются последние фото.</p>
<div class="form-group">
<button type="button" id="btn-load-widget-albums" class="btn btn-secondary">Загрузить альбомы</button>
</div>
<div id="widget-albums-list" class="widget-albums-grid">
<p class="placeholder">Нажмите «Загрузить альбомы» для выбора</p>
</div>
</div>
<!-- Widget Options -->
<div class="settings-section">
<h3>Параметры</h3>
<div class="form-row">
<div class="form-group">
<label for="widget-max-photos">Максимум фото:</label>
<input type="number" id="widget-max-photos" value="30" min="5" max="100">
</div>
<div class="form-group">
<label for="widget-cache-time">Кэш (секунд):</label>
<input type="number" id="widget-cache-time" value="3600" min="60" max="86400">
<span class="hint">3600 = 1 час</span>
</div>
</div>
</div>
<button id="btn-save-widget-settings" class="btn btn-primary btn-large">Сохранить настройки виджета</button>
<span id="widget-save-status" class="save-status"></span>
<!-- WordPress Installation -->
<div class="settings-section" style="margin-top: 32px;">
<h3>Установка на WordPress</h3>
<div class="code-block">
<p>1. Скачайте плагин:</p>
<a href="vh-flickr-mosaic.zip" class="btn btn-secondary" id="download-plugin-btn">Скачать плагин</a>
</div>
<div class="code-block">
<p>2. Установите плагин в WordPress (Плагины → Добавить новый → Загрузить)</p>
</div>
<div class="code-block">
<p>3. В настройках плагина укажите API URL:</p>
<code id="widget-api-url-code"></code>
</div>
</div>
</div>
</section>
<!-- Tab: Digital Badge (round display) -->
<section id="tab-badge" class="tab-content">
<div class="panel">
<h2>Цифровой бейдж</h2>
<p class="help-text">Подготовьте фотографии под круглые дисплеи 240×240 пикселей. Кадрируйте, добавьте ценник или подпись никнеймом дугой снизу.</p>
<!-- Photo sources -->
<div class="form-group">
<label>Источник фотографий:</label>
<div class="photo-source-buttons">
<input type="file" id="badge-file-upload" multiple accept="image/*" style="display: none;">
<button type="button" class="btn btn-secondary" id="btn-badge-upload">
<span class="btn-icon-text">📤</span> Загрузить с устройства
</button>
<button type="button" class="btn btn-secondary" id="btn-badge-from-flickr">
<span class="btn-icon-text">🖼</span> Выбранные в галерее Flickr
</button>
</div>
<p class="hint">Размер итогового кружка — 240×240 PNG с прозрачным фоном.</p>
</div>
<!-- Items grid (selected photos for badge processing) -->
<div class="form-group">
<label>Фотографии для бейджа: <span id="badge-items-count" class="photo-counter">0</span></label>
<div id="badge-items-grid" class="badge-items-grid">
<p class="placeholder">Выберите фото из галереи или загрузите с устройства</p>
</div>
</div>
<!-- Editor (visible when a photo is selected) -->
<div id="badge-editor" class="badge-editor hidden">
<div class="badge-editor-layout">
<!-- Canvas preview -->
<div class="badge-canvas-wrapper">
<canvas id="badge-canvas" width="480" height="480"></canvas>
<div class="badge-canvas-hint">Один палец — сдвиг, два пальца — масштаб. На компьютере — мышь и колесо.</div>
</div>
<!-- Controls -->
<div class="badge-controls">
<div class="form-group">
<label>Масштаб: <span id="badge-zoom-value">100%</span></label>
<input type="range" id="badge-zoom" min="100" max="400" step="1" value="100">
</div>
<div class="form-group">
<button type="button" id="btn-badge-reset" class="btn btn-secondary btn-small">Сбросить кадрирование</button>
</div>
<hr style="border:none; border-top:1px solid var(--border-color, #ddd); margin:12px 0;">
<!-- Price tag -->
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="badge-price-enabled">
<span>Добавить ценник</span>
</label>
</div>
<div id="badge-price-options" class="badge-sub-options hidden">
<div class="form-group">
<label for="badge-price-value">Цена (₽):</label>
<input type="number" inputmode="numeric" pattern="[0-9]*" id="badge-price-value" min="0" step="1" placeholder="2500">
</div>
<div class="form-group">
<label>Стиль плашки:</label>
<div class="badge-radio-row">
<label class="radio-label">
<input type="radio" name="badge-price-style" value="arc" checked>
<span>Дуга по низу</span>
</label>
<label class="radio-label">
<input type="radio" name="badge-price-style" value="rect">
<span>Прямоугольник</span>
</label>
</div>
</div>
<div class="form-group">
<label>Положение по вертикали: <span id="badge-price-y-value">9</span></label>
<input type="range" id="badge-price-y" min="-50" max="20" step="1" value="9">
</div>
<div class="form-group">
<label>Размер плашки: <span id="badge-price-size-value">60%</span></label>
<input type="range" id="badge-price-size" min="60" max="160" step="5" value="60">
</div>
<div class="form-group">
<label>Цвет ленты:</label>
<div class="badge-color-row" data-target="bg">
<button type="button" class="badge-color-swatch" data-color="#FFCC00" style="background:#FFCC00" title="Жёлтый"></button>
<button type="button" class="badge-color-swatch" data-color="#FF3B30" style="background:#FF3B30" title="Красный"></button>
<button type="button" class="badge-color-swatch" data-color="#34C759" style="background:#34C759" title="Зелёный"></button>
<button type="button" class="badge-color-swatch" data-color="#007AFF" style="background:#007AFF" title="Синий"></button>
<button type="button" class="badge-color-swatch" data-color="#000000" style="background:#000000" title="Чёрный"></button>
<button type="button" class="badge-color-swatch" data-color="#FFFFFF" style="background:#FFFFFF;border-color:#ccc" title="Белый"></button>
<input type="color" id="badge-price-bg" value="#FFCC00" title="Свой цвет ленты">
</div>
</div>
<div class="form-group">
<label>Цвет цифр:</label>
<div class="badge-color-row" data-target="fg">
<button type="button" class="badge-color-swatch" data-color="#000000" style="background:#000000" title="Чёрный"></button>
<button type="button" class="badge-color-swatch" data-color="#FFFFFF" style="background:#FFFFFF;border-color:#ccc" title="Белый"></button>
<button type="button" class="badge-color-swatch" data-color="#FFCC00" style="background:#FFCC00" title="Жёлтый"></button>
<button type="button" class="badge-color-swatch" data-color="#FF3B30" style="background:#FF3B30" title="Красный"></button>
<input type="color" id="badge-price-fg" value="#000000" title="Свой цвет цифр">
</div>
</div>
</div>
<hr style="border:none; border-top:1px solid var(--border-color, #ddd); margin:12px 0;">
<!-- Nickname -->
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="badge-nickname-enabled">
<span>Добавить никнейм (дугой снизу)</span>
</label>
</div>
<div id="badge-nickname-options" class="badge-sub-options hidden">
<div class="form-group">
<label for="badge-nickname-value">Текст:</label>
<input type="text" id="badge-nickname-value" maxlength="40" placeholder="Никнейм">
<span class="hint">По умолчанию — из настроек.</span>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="badge-nick-as-price">
<span>Использовать как цену — добавить ₽</span>
</label>
</div>
<div class="form-group">
<label>Положение надписи:</label>
<div class="badge-radio-row">
<label class="radio-label">
<input type="radio" name="badge-nick-position" value="auto" checked>
<span>Авто</span>
</label>
<label class="radio-label">
<input type="radio" name="badge-nick-position" value="bottom">
<span>Снизу</span>
</label>
<label class="radio-label">
<input type="radio" name="badge-nick-position" value="top">
<span>Сверху</span>
</label>
</div>
</div>
<div class="form-group">
<label>Размер текста: <span id="badge-nick-size-value">100%</span></label>
<input type="range" id="badge-nick-size" min="60" max="160" step="5" value="100">
</div>
<div class="form-group">
<label>Отступ от края: <span id="badge-nick-edge-value">14</span></label>
<input type="range" id="badge-nick-edge" min="0" max="30" step="1" value="14">
</div>
<div class="form-group">
<label>Цвет текста:</label>
<div class="badge-color-row" data-target="nick">
<button type="button" class="badge-color-swatch" data-color="#FFFFFF" style="background:#FFFFFF;border-color:#ccc" title="Белый"></button>
<button type="button" class="badge-color-swatch" data-color="#000000" style="background:#000000" title="Чёрный"></button>
<button type="button" class="badge-color-swatch" data-color="#FFCC00" style="background:#FFCC00" title="Жёлтый"></button>
<button type="button" class="badge-color-swatch" data-color="#FF3B30" style="background:#FF3B30" title="Красный"></button>
<button type="button" class="badge-color-swatch" data-color="#34C759" style="background:#34C759" title="Зелёный"></button>
<button type="button" class="badge-color-swatch" data-color="#007AFF" style="background:#007AFF" title="Синий"></button>
<input type="color" id="badge-nick-color" value="#FFFFFF" title="Свой цвет текста">
</div>
</div>
</div>
<hr style="border:none; border-top:1px solid var(--border-color, #ddd); margin:12px 0;">
<div class="badge-actions">
<button type="button" id="btn-badge-download" class="btn btn-primary">
<span class="btn-icon-text">↓</span> <span id="badge-download-label">Сохранить PNG</span>
</button>
<button type="button" id="btn-badge-remove" class="btn btn-secondary btn-small">Убрать из списка</button>
</div>
</div>
</div>
</div>
<!-- Batch actions -->
<div id="badge-batch-actions" class="form-group hidden" style="margin-top: 16px;">
<button type="button" id="btn-badge-download-all" class="btn btn-secondary">
<span class="btn-icon-text">⬇</span> Скачать все бейджи
</button>
</div>
<!-- History -->
<div class="settings-section" style="margin-top: 24px;">
<h3>История сохранённых бейджей</h3>
<div id="badge-history-grid" class="badge-history-grid">
<p class="placeholder">История пуста — сгенерированные бейджи будут появляться здесь.</p>
</div>
</div>
</div>
</section>
<!-- Tab: Settings -->
<section id="tab-settings" class="tab-content">
<div class="panel">
<h2>Настройки</h2>
<!-- Flickr Settings -->
<div class="settings-section">
<h3>Flickr API</h3>
<div class="form-group">
<label>API ключ:</label>
<input type="text" value="<?= !empty($config['flickr']['api_key']) ? '••••••••' . substr($config['flickr']['api_key'], -4) : '' ?>" readonly>
<span class="status <?= !empty($config['flickr']['api_key']) ? 'connected' : 'disconnected' ?>">
<?= !empty($config['flickr']['api_key']) ? 'Настроен' : 'Не настроен' ?>
</span>
</div>
<div class="form-group">
<label>ID пользователя:</label>
<input type="text" value="<?= htmlspecialchars($config['flickr_user_id'] ?? '') ?>" readonly>
</div>
<div class="form-group">
<label>OAuth авторизация:</label>
<span id="flickr-oauth-status" class="status">Проверка...</span>
<a href="flickr_auth.php" id="flickr-oauth-btn" class="btn btn-small btn-primary" style="margin-left: 10px;">Авторизовать</a>
<span class="hint">Требуется для загрузки оригиналов</span>
</div>
</div>
<!-- Telegram Settings -->
<div class="settings-section">
<h3>Telegram</h3>
<div class="form-group">
<label>Статус бота:</label>
<span id="tg-bot-status" class="status">Проверка...</span>
</div>
<div class="form-group">
<label for="tg-channels-list">Каналы (по одному на строку):</label>
<textarea id="tg-channels-list" rows="3" placeholder="@channel_username
-1001234567890"><?php
$channels = $config['telegram']['channels'] ?? [];
foreach ($channels as $ch) {
echo htmlspecialchars($ch['id'] ?? $ch) . "\n";
}
?></textarea>
</div>
</div>
<!-- VK Settings -->
<div class="settings-section">
<h3>ВКонтакте</h3>
<div class="form-group">
<label>Статус:</label>
<span id="vk-status" class="status">Проверка...</span>
</div>
<div class="form-group">
<label for="vk-token-input">Access Token:</label>
<input type="password" id="vk-token-input" placeholder="Вставьте токен сюда..." value="<?= !empty($config['vk']['access_token']) ? $config['vk']['access_token'] : '' ?>">
<button id="btn-save-vk-token" class="btn btn-primary btn-small" style="margin-left: 10px;">Сохранить</button>
<button id="btn-toggle-vk-token" class="btn btn-secondary btn-small" style="margin-left: 5px;">👁</button>
</div>
<div class="form-group">
<span id="vk-token-save-status" class="save-status"></span>
</div>
<div class="form-group">
<details class="vk-help" open>
<summary>Как получить access token для VK?</summary>
<p style="font-size: 0.9em; margin: 10px 0;"><strong>VK API имеет ограничение:</strong> загружать фото на стену может только <em>пользовательский</em> токен. Токен сообщества постит только текст (фото уйдут как ссылки).</p>
<p style="font-size: 0.92em; margin: 12px 0 6px;"><strong>Вариант A — пользовательский токен через Kate Mobile (рекомендуется, фото работают)</strong></p>
<ol style="margin: 6px 0 10px; padding-left: 20px; font-size: 0.9em;">
<li>Перейдите по ссылке (откроется страница авторизации VK): <br>
<a href="https://oauth.vk.com/authorize?client_id=2685278&scope=photos,wall,groups,offline&redirect_uri=https://oauth.vk.com/blank.html&display=mobile&response_type=token&revoke=1&v=5.199" target="_blank" rel="noopener" style="word-break: break-all;">oauth.vk.com/authorize?client_id=2685278&scope=photos,wall,groups,offline&...</a>
</li>
<li>Нажмите <strong>«Разрешить»</strong> от лица Kate Mobile</li>
<li>В адресной строке после <code>#</code> найдите <code>access_token=...</code> — скопируйте значение до символа <code>&amp;</code></li>
<li>Вставьте в поле выше → «Сохранить»</li>
</ol>
<p style="font-size: 0.82em; color: var(--text-secondary); margin: 6px 0 14px;">
Скоуп <code>offline</code> делает токен бессрочным. Kate Mobile (<code>app_id=2685278</code>) пока не заблокирован VK, в отличие от VK Admin.
</p>
<p style="font-size: 0.92em; margin: 12px 0 6px;"><strong>Вариант B — токен сообщества (только текст, без фото)</strong></p>
<ol style="margin: 6px 0 10px; padding-left: 20px; font-size: 0.9em;">
<li>Группа VK → <strong>Управление</strong> → <strong>Работа с API</strong> → <strong>Создать ключ</strong></li>
<li>Права: Управление, Стена, Фотографии, Сообщения сообщества</li>
<li>Вставьте ключ в поле выше → «Сохранить»</li>
</ol>
<p style="font-size: 0.82em; color: var(--text-secondary); margin-top: 6px;">
Бессрочный, но из-за ограничения VK <code>photos.getWallUploadServer</code> не работает с такими токенами — фото уходят ссылками в тексте.
</p>
<p style="font-size: 0.78em; color: var(--text-secondary); margin-top: 10px;">
Старый способ через <a href="https://vkhost.github.io/" target="_blank" rel="noopener">vkhost.github.io</a> (VK Admin) больше не работает — VK заблокировал это приложение (<code>[8] Application is blocked</code>).
</p>
</details>
</div>
</div>
<!-- Cross-Promo Settings -->
<div class="settings-section">
<h3>Кросс-промо каналов</h3>
<p class="help-text">Ссылки на ваши каналы для автоматического добавления в посты</p>
<div class="form-group">
<label for="cross-promo-telegram">Ссылка на Telegram канал:</label>
<input type="text" id="cross-promo-telegram" placeholder="https://t.me/your_channel">
<span class="hint">Будет добавлена в посты VK</span>
</div>
<div class="form-group">
<label for="cross-promo-vk">Ссылка на ВКонтакте:</label>
<input type="text" id="cross-promo-vk" placeholder="https://vk.com/your_group">
<span class="hint">Будет добавлена в посты Telegram</span>
</div>
<div class="form-group">
<label for="cross-promo-text-tg">Текст для Telegram:</label>
<input type="text" id="cross-promo-text-tg" placeholder="Мой канал ВКонтакте" value="Мой канал ВКонтакте">
</div>
<div class="form-group">
<label for="cross-promo-text-vk">Текст для ВКонтакте:</label>
<input type="text" id="cross-promo-text-vk" placeholder="Мой канал в Telegram" value="Мой канал в Telegram">
</div>
<button id="btn-save-cross-promo" class="btn btn-primary">Сохранить настройки кросс-промо</button>
<span id="cross-promo-save-status" class="save-status"></span>
</div>
<!-- Digital Badge Settings -->
<div class="settings-section">
<h3>Цифровой бейдж</h3>
<p class="help-text">Никнейм по умолчанию для подписи дугой снизу.</p>
<div class="form-group">
<label for="badge-nickname-default">Никнейм для бейджа:</label>
<input type="text" id="badge-nickname-default" maxlength="40" placeholder="Ваш никнейм">
<button id="btn-save-badge-nickname" class="btn btn-primary btn-small" style="margin-left: 10px;">Сохранить</button>
<span id="badge-nickname-save-status" class="save-status"></span>
</div>
</div>
<!-- Theme -->
<div class="settings-section">
<h3>Оформление</h3>
<div class="form-group">
<label>Тема интерфейса:</label>
<button id="btn-toggle-theme" class="btn btn-secondary">Переключить тему</button>
</div>
</div>
<!-- Password Change -->
<div class="settings-section">
<h3>Смена пароля</h3>
<div class="form-group">
<label for="current-password">Текущий пароль:</label>
<input type="password" id="current-password">
</div>
<div class="form-group">
<label for="new-password">Новый пароль:</label>
<input type="password" id="new-password">
<span class="hint">Минимум 8 символов</span>
</div>
<div class="form-group">
<label for="confirm-password">Подтвердите пароль:</label>
<input type="password" id="confirm-password">
</div>
<button id="btn-change-password" class="btn btn-primary">Сменить пароль</button>
</div>
</div>
</section>
</div>
<!-- Preset Management Modal -->
<div id="preset-modal" class="modal-overlay" style="display: none;">
<div class="modal-content preset-modal">
<div class="modal-header">
<h3 id="preset-modal-title">Управление пресетами</h3>
<button type="button" class="modal-close" id="preset-modal-close">&times;</button>
</div>
<div class="modal-body">
<!-- Add/Edit Form -->
<div id="preset-form-section" style="display: none;">
<div class="form-group">
<label for="preset-name">Название пресета:</label>
<input type="text" id="preset-name" class="form-control" placeholder="Например: BJD">
</div>
<div class="form-group">
<label for="preset-tags">Теги (через запятую):</label>
<input type="text" id="preset-tags" class="form-control" placeholder="bjd, doll, куклы">
</div>
<div class="preset-form-actions">
<button type="button" id="preset-save" class="btn btn-primary">Сохранить</button>
<button type="button" id="preset-cancel" class="btn btn-secondary">Отмена</button>
</div>
</div>
<!-- Presets List -->
<div id="preset-list-section">
<div class="preset-manager-list" id="preset-manager-list"></div>
<button type="button" id="preset-add-new" class="btn btn-primary btn-block">+ Добавить пресет</button>
</div>
</div>
</div>
</div>
<script src="js/app.js?v=<?= filemtime(__DIR__ . '/js/app.js') ?>"></script>
</body>
</html>