Загрузить файлы в «/»
This commit is contained in:
+311
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
/**
|
||||
* Server-side photo download handler
|
||||
* Bypasses CORS restrictions by proxying through the server
|
||||
*/
|
||||
|
||||
// Prevent any output before headers
|
||||
ob_start();
|
||||
|
||||
session_start();
|
||||
|
||||
// Load configuration
|
||||
$configFile = __DIR__ . '/config.php';
|
||||
if (!file_exists($configFile)) {
|
||||
http_response_code(500);
|
||||
die('Configuration not found');
|
||||
}
|
||||
$config = require $configFile;
|
||||
|
||||
// Autoload classes
|
||||
spl_autoload_register(function ($class) {
|
||||
$file = __DIR__ . '/classes/' . $class . '.php';
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
}
|
||||
});
|
||||
|
||||
// Check authentication
|
||||
$auth = new Auth();
|
||||
if (!$auth->isAuthenticated()) {
|
||||
http_response_code(401);
|
||||
die('Not authenticated');
|
||||
}
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
/**
|
||||
* Fetch remote file content with proper error handling
|
||||
*/
|
||||
function fetchRemoteFile($url) {
|
||||
// Use cURL for more reliable fetching
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: image/*, */*',
|
||||
'Referer: https://www.flickr.com/',
|
||||
],
|
||||
]);
|
||||
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// Check for errors
|
||||
if ($content === false || $httpCode !== 200) {
|
||||
error_log("Flickr fetch failed: HTTP $httpCode, Error: $error, URL: $url");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate it's actually an image
|
||||
if (strlen($content) < 1000) {
|
||||
error_log("Flickr fetch: Content too small (" . strlen($content) . " bytes), likely error page");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check magic bytes for image formats
|
||||
$magicBytes = substr($content, 0, 8);
|
||||
$isJpeg = (substr($magicBytes, 0, 2) === "\xFF\xD8");
|
||||
$isPng = (substr($magicBytes, 0, 4) === "\x89PNG");
|
||||
$isGif = (substr($magicBytes, 0, 3) === "GIF");
|
||||
$isWebp = (substr($magicBytes, 0, 4) === "RIFF" && substr($content, 8, 4) === "WEBP");
|
||||
|
||||
if (!$isJpeg && !$isPng && !$isGif && !$isWebp) {
|
||||
error_log("Flickr fetch: Not a valid image format. First bytes: " . bin2hex(substr($content, 0, 16)));
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine content type from magic bytes if needed
|
||||
if (empty($contentType) || strpos($contentType, 'image/') !== 0) {
|
||||
if ($isJpeg) $contentType = 'image/jpeg';
|
||||
elseif ($isPng) $contentType = 'image/png';
|
||||
elseif ($isGif) $contentType = 'image/gif';
|
||||
elseif ($isWebp) $contentType = 'image/webp';
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
'content_type' => $contentType,
|
||||
'size' => strlen($content),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean filename for download
|
||||
*/
|
||||
function cleanFilename($name, $ext = 'jpg') {
|
||||
$name = preg_replace('/[<>:"\/\\\\|?*]/', '_', $name);
|
||||
$name = substr($name, 0, 100);
|
||||
return $name . '.' . $ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension from URL or format
|
||||
*/
|
||||
function getExtension($url, $format = 'jpg') {
|
||||
// Check URL extension first
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
return $ext === 'jpeg' ? 'jpg' : $ext;
|
||||
}
|
||||
|
||||
return $format ?: 'jpg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if URL is a valid Flickr image URL
|
||||
*/
|
||||
function isFlickrUrl($url) {
|
||||
// Flickr uses several domain/URL formats:
|
||||
// - farm{N}.staticflickr.com (static images)
|
||||
// - live.staticflickr.com (static images, modern)
|
||||
// - www.flickr.com/photo_download.gne (download endpoint for originals)
|
||||
$patterns = [
|
||||
'/^https?:\/\/farm\d+\.staticflickr\.com\//',
|
||||
'/^https?:\/\/live\.staticflickr\.com\//',
|
||||
'/^https?:\/\/farm\d+\.static\.flickr\.com\//',
|
||||
'/^https?:\/\/staticflickr\.com\//',
|
||||
'/^https?:\/\/c\d+\.staticflickr\.com\//',
|
||||
'/^https?:\/\/(www\.)?flickr\.com\/photo_download\.gne\?/',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
|
||||
// Download single photo
|
||||
case 'photo':
|
||||
$url = $_GET['url'] ?? '';
|
||||
$filename = $_GET['filename'] ?? 'photo';
|
||||
$format = $_GET['format'] ?? 'jpg';
|
||||
|
||||
if (!$url) {
|
||||
http_response_code(400);
|
||||
die('URL required');
|
||||
}
|
||||
|
||||
// Validate URL is from Flickr (multiple domain formats)
|
||||
if (!isFlickrUrl($url)) {
|
||||
http_response_code(400);
|
||||
die('Invalid URL - only Flickr URLs allowed: ' . htmlspecialchars($url));
|
||||
}
|
||||
|
||||
$file = fetchRemoteFile($url);
|
||||
|
||||
if (!$file) {
|
||||
http_response_code(502);
|
||||
die('Failed to fetch image from Flickr');
|
||||
}
|
||||
|
||||
$ext = getExtension($url, $format);
|
||||
$safeFilename = cleanFilename($filename, $ext);
|
||||
|
||||
// Clear ALL output buffers (there might be multiple levels)
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
header('Content-Type: ' . $file['content_type']);
|
||||
header('Content-Length: ' . $file['size']);
|
||||
header('Content-Disposition: attachment; filename="' . $safeFilename . '"');
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Pragma: public');
|
||||
|
||||
echo $file['content'];
|
||||
exit;
|
||||
|
||||
// Download multiple photos as ZIP
|
||||
case 'zip':
|
||||
// Get photo data from POST
|
||||
$photosJson = $_POST['photos'] ?? '';
|
||||
$albumName = $_POST['album_name'] ?? '';
|
||||
|
||||
if (!$photosJson) {
|
||||
http_response_code(400);
|
||||
die('Photos data required');
|
||||
}
|
||||
|
||||
$photos = json_decode($photosJson, true);
|
||||
|
||||
if (!$photos || !is_array($photos) || count($photos) === 0) {
|
||||
http_response_code(400);
|
||||
die('Invalid photos data');
|
||||
}
|
||||
|
||||
// Limit to prevent server overload
|
||||
if (count($photos) > 500) {
|
||||
http_response_code(400);
|
||||
die('Maximum 500 photos per archive');
|
||||
}
|
||||
|
||||
// Create temp file for ZIP
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'vh_photos_');
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
http_response_code(500);
|
||||
die('Failed to create ZIP archive');
|
||||
}
|
||||
|
||||
// Create folder name
|
||||
$folderName = $albumName ? preg_replace('/[<>:"\/\\\\|?*]/', '_', $albumName) : 'flickr_photos_' . date('Y-m-d');
|
||||
$folderName = substr($folderName, 0, 100);
|
||||
|
||||
$usedNames = [];
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
foreach ($photos as $photo) {
|
||||
$url = $photo['url'] ?? '';
|
||||
$title = $photo['title'] ?? $photo['id'] ?? 'photo';
|
||||
$format = $photo['format'] ?? 'jpg';
|
||||
$photoId = $photo['id'] ?? uniqid();
|
||||
|
||||
if (!$url) {
|
||||
$failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate URL - use same function as single photo
|
||||
if (!isFlickrUrl($url)) {
|
||||
$failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$file = fetchRemoteFile($url);
|
||||
|
||||
if (!$file) {
|
||||
$failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$ext = getExtension($url, $format);
|
||||
$filename = cleanFilename($title, $ext);
|
||||
|
||||
// Ensure unique filename
|
||||
if (in_array($filename, $usedNames)) {
|
||||
$baseName = pathinfo($filename, PATHINFO_FILENAME);
|
||||
$filename = $baseName . '_' . $photoId . '.' . $ext;
|
||||
}
|
||||
$usedNames[] = $filename;
|
||||
|
||||
$zip->addFromString($folderName . '/' . $filename, $file['content']);
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
if ($successCount === 0) {
|
||||
// Clean up temp file if it exists
|
||||
if (file_exists($tempFile)) {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
http_response_code(502);
|
||||
die('Failed to download any photos. Check that photo URLs are valid.');
|
||||
}
|
||||
|
||||
// Send ZIP file
|
||||
$zipFilename = $folderName . '.zip';
|
||||
$zipSize = filesize($tempFile);
|
||||
|
||||
// Clear ALL output buffers
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Length: ' . $zipSize);
|
||||
header('Content-Disposition: attachment; filename="' . $zipFilename . '"');
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('X-Photos-Downloaded: ' . $successCount);
|
||||
header('X-Photos-Failed: ' . $failCount);
|
||||
|
||||
readfile($tempFile);
|
||||
unlink($tempFile);
|
||||
exit;
|
||||
|
||||
default:
|
||||
ob_end_clean();
|
||||
http_response_code(400);
|
||||
die('Unknown action');
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
/**
|
||||
* Flickr OAuth Authorization Handler
|
||||
*
|
||||
* Usage:
|
||||
* 1. Visit flickr_auth.php to start authorization
|
||||
* 2. Authorize on Flickr
|
||||
* 3. Callback returns here with tokens saved
|
||||
*/
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/classes/FlickrOAuth.php';
|
||||
|
||||
// Load config
|
||||
$configFile = __DIR__ . '/config.php';
|
||||
if (!file_exists($configFile)) {
|
||||
die('Configuration not found');
|
||||
}
|
||||
$config = require $configFile;
|
||||
|
||||
if (empty($config['flickr']['api_key']) || empty($config['flickr']['api_secret'])) {
|
||||
die('Flickr API credentials not configured');
|
||||
}
|
||||
|
||||
$oauth = new FlickrOAuth(
|
||||
$config['flickr']['api_key'],
|
||||
$config['flickr']['api_secret']
|
||||
);
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
|
||||
. '://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['SCRIPT_NAME']);
|
||||
|
||||
// Handle OAuth callback from Flickr
|
||||
if (isset($_GET['oauth_token']) && isset($_GET['oauth_verifier'])) {
|
||||
try {
|
||||
$result = $oauth->handleCallback($_GET['oauth_token'], $_GET['oauth_verifier']);
|
||||
|
||||
$message = "Авторизация успешна! Пользователь: " . htmlspecialchars($result['username'] ?? $result['user_nsid']);
|
||||
$success = true;
|
||||
} catch (Exception $e) {
|
||||
$message = "Ошибка авторизации: " . htmlspecialchars($e->getMessage());
|
||||
$success = false;
|
||||
}
|
||||
|
||||
// Show result
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Flickr OAuth</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 40px; background: #f5f5f7; }
|
||||
.card { background: white; padding: 30px; border-radius: 16px; max-width: 500px; margin: 0 auto; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
|
||||
h1 { margin-top: 0; }
|
||||
.success { color: #34C759; }
|
||||
.error { color: #FF3B30; }
|
||||
.btn { display: inline-block; padding: 12px 24px; background: #007AFF; color: white; text-decoration: none; border-radius: 8px; margin-top: 20px; }
|
||||
.btn:hover { background: #0056CC; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1 class="<?= $success ? 'success' : 'error' ?>"><?= $success ? '✓' : '✗' ?> <?= $message ?></h1>
|
||||
<?php if ($success): ?>
|
||||
<p>Теперь приложение может загружать фотографии в оригинальном качестве.</p>
|
||||
<?php endif; ?>
|
||||
<a href="index.php" class="btn">Вернуться в приложение</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle logout
|
||||
if ($action === 'logout') {
|
||||
$oauth->clearTokens();
|
||||
header('Location: flickr_auth.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check current status or start auth
|
||||
$isAuthorized = $oauth->isAuthorized();
|
||||
|
||||
if ($action === 'authorize' && !$isAuthorized) {
|
||||
try {
|
||||
$callbackUrl = $baseUrl . '/flickr_auth.php';
|
||||
$authUrl = $oauth->getAuthorizationUrl($callbackUrl);
|
||||
header('Location: ' . $authUrl);
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Flickr OAuth Authorization</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
padding: 40px;
|
||||
background: #f5f5f7;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 16px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 { margin-top: 0; font-size: 1.5rem; }
|
||||
.status {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.status.authorized { background: #d4edda; color: #155724; }
|
||||
.status.not-authorized { background: #fff3cd; color: #856404; }
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.btn:hover { background: #0056CC; }
|
||||
.btn-danger { background: #FF3B30; }
|
||||
.btn-danger:hover { background: #CC2F27; }
|
||||
.btn-secondary { background: #8E8E93; }
|
||||
.error { color: #FF3B30; margin: 20px 0; }
|
||||
ul { padding-left: 20px; }
|
||||
li { margin: 8px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>🔐 Flickr OAuth Authorization</h1>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="error">Ошибка: <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isAuthorized): ?>
|
||||
<div class="status authorized">
|
||||
✓ <strong>Авторизован</strong> — доступ к оригиналам фото включён
|
||||
</div>
|
||||
<p>Приложение авторизовано и может загружать фотографии в оригинальном качестве.</p>
|
||||
<p>
|
||||
<a href="index.php" class="btn">Открыть приложение</a>
|
||||
<a href="?action=logout" class="btn btn-danger" style="margin-left: 10px;">Выйти</a>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<div class="status not-authorized">
|
||||
⚠ <strong>Не авторизован</strong> — доступны только уменьшенные версии
|
||||
</div>
|
||||
|
||||
<p>Для загрузки фотографий в оригинальном качестве необходимо авторизовать приложение:</p>
|
||||
|
||||
<ul>
|
||||
<li>Вы будете перенаправлены на Flickr</li>
|
||||
<li>Flickr запросит разрешение на <strong>чтение</strong> ваших фото</li>
|
||||
<li>После подтверждения вернётесь обратно</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<a href="?action=authorize" class="btn">Авторизовать через Flickr</a>
|
||||
<a href="index.php" class="btn btn-secondary" style="margin-left: 10px;">Назад</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,735 @@
|
||||
<?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="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>
|
||||
<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="Код"></></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: 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>Как получить пользовательский токен (для загрузки фото)?</summary>
|
||||
<ol style="margin: 10px 0; padding-left: 20px; font-size: 0.9em;">
|
||||
<li>Перейдите на <a href="https://vkhost.github.io/" target="_blank">vkhost.github.io</a></li>
|
||||
<li>Нажмите <strong>"VK Admin"</strong></li>
|
||||
<li>Разрешите доступ приложению</li>
|
||||
<li>Скопируйте <code>access_token</code> из адресной строки браузера</li>
|
||||
<li>Вставьте токен в поле выше и нажмите "Сохранить"</li>
|
||||
</ol>
|
||||
<p style="font-size: 0.85em; color: var(--text-secondary); margin-top: 10px;">
|
||||
<strong>Важно:</strong> Пользовательский токен позволяет загружать фото напрямую в VK.
|
||||
Community-токен (ключ сообщества) может только постить текст и ссылки.
|
||||
</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>
|
||||
|
||||
<!-- 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">×</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>
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/**
|
||||
* Страница входа
|
||||
*/
|
||||
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/classes/Auth.php';
|
||||
|
||||
$auth = new Auth();
|
||||
|
||||
// If no users, redirect to setup
|
||||
if (!$auth->hasUsers()) {
|
||||
header('Location: setup.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// If already logged in, redirect to main page
|
||||
if ($auth->isAuthenticated()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$message = '';
|
||||
|
||||
// Handle login form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$ip = Auth::getClientIP();
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$error = 'Введите имя пользователя и пароль';
|
||||
} else {
|
||||
$result = $auth->login($username, $password, $ip);
|
||||
|
||||
if ($result['success']) {
|
||||
$auth->startSession($result['username'], $result['token']);
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['message'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSRF token
|
||||
$csrfToken = bin2hex(random_bytes(32));
|
||||
$_SESSION['csrf_token'] = $csrfToken;
|
||||
?>
|
||||
<!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
|
||||
(function() {
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<img src="image.png" alt="VH Logo" class="login-logo-img" style="width:100px;height:100px;max-width:100px;max-height:100px;">
|
||||
</div>
|
||||
<h1>VH Posting System</h1>
|
||||
<h2>Вход в систему</h2>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="login.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Имя пользователя:</label>
|
||||
<input type="text" id="username" name="username" required autofocus
|
||||
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-large btn-block">Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user