Files
VH_posting_system/js/app.js
T
zuevav e5a88665cd mailn
2026-04-30 15:14:09 +03:00

4211 lines
165 KiB
JavaScript
Raw 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.
/**
* VH Posting System - Frontend JavaScript
* Apple Style UI | Multi-platform posting
*/
document.addEventListener('DOMContentLoaded', function() {
// ============ THEME MANAGEMENT ============
function initTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
applyTheme(savedTheme);
}
function applyTheme(theme) {
if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
document.getElementById('theme-toggle')?.classList.add('dark');
} else {
document.documentElement.removeAttribute('data-theme');
document.getElementById('theme-toggle')?.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}
function toggleTheme() {
const currentTheme = localStorage.getItem('theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
}
document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
document.getElementById('btn-toggle-theme')?.addEventListener('click', toggleTheme);
initTheme();
// ============ CROSS-PROMO SETTINGS ============
const CROSS_PROMO_KEY = 'vh_cross_promo';
function getCrossPromoSettings() {
try {
const data = localStorage.getItem(CROSS_PROMO_KEY);
return data ? JSON.parse(data) : {
telegramLink: '',
vkLink: '',
textForTg: 'Мой канал ВКонтакте',
textForVk: 'Мой канал в Telegram'
};
} catch (e) {
return { telegramLink: '', vkLink: '', textForTg: 'Мой канал ВКонтакте', textForVk: 'Мой канал в Telegram' };
}
}
function saveCrossPromoSettings(settings) {
localStorage.setItem(CROSS_PROMO_KEY, JSON.stringify(settings));
}
async function initCrossPromoSettings() {
// Try to load from server first, fallback to localStorage
try {
const formData = new FormData();
formData.append('action', 'get_cross_promo');
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.settings) {
saveCrossPromoSettings(data.settings);
}
} catch (e) {
console.log('Using local cross-promo settings');
}
const settings = getCrossPromoSettings();
const tgInput = document.getElementById('cross-promo-telegram');
const vkInput = document.getElementById('cross-promo-vk');
const textTgInput = document.getElementById('cross-promo-text-tg');
const textVkInput = document.getElementById('cross-promo-text-vk');
if (tgInput) tgInput.value = settings.telegramLink || '';
if (vkInput) vkInput.value = settings.vkLink || '';
if (textTgInput) textTgInput.value = settings.textForTg || 'Мой канал ВКонтакте';
if (textVkInput) textVkInput.value = settings.textForVk || 'Мой канал в Telegram';
}
document.getElementById('btn-save-cross-promo')?.addEventListener('click', async () => {
const settings = {
telegramLink: document.getElementById('cross-promo-telegram')?.value.trim() || '',
vkLink: document.getElementById('cross-promo-vk')?.value.trim() || '',
textForTg: document.getElementById('cross-promo-text-tg')?.value.trim() || 'Мой канал ВКонтакте',
textForVk: document.getElementById('cross-promo-text-vk')?.value.trim() || 'Мой канал в Telegram'
};
// Save to localStorage
saveCrossPromoSettings(settings);
// Save to server for cron
try {
const formData = new FormData();
formData.append('action', 'save_cross_promo');
formData.append('telegramLink', settings.telegramLink);
formData.append('vkLink', settings.vkLink);
formData.append('textForTg', settings.textForTg);
formData.append('textForVk', settings.textForVk);
await fetch('api.php', { method: 'POST', body: formData });
} catch (e) {
console.warn('Failed to save cross-promo to server:', e);
}
const status = document.getElementById('cross-promo-save-status');
if (status) {
status.textContent = '✓ Сохранено';
status.className = 'save-status success';
setTimeout(() => { status.textContent = ''; }, 2000);
}
showNotification('Настройки кросс-промо сохранены', 'success');
});
// Initialize cross-promo on load
initCrossPromoSettings();
// ============ AUTO-SAVE POST DRAFT (SERVER-BASED) ============
let serverDraft = null;
let draftSaveTimer = null;
// Save draft to server (debounced - for text input)
function savePostDraft() {
clearTimeout(draftSaveTimer);
draftSaveTimer = setTimeout(savePostDraftToServer, 1500);
}
// Save draft immediately (for tags, photos)
function savePostDraftNow() {
clearTimeout(draftSaveTimer);
savePostDraftToServer();
}
// Actually save to server
async function savePostDraftToServer() {
const postText = document.getElementById('post-text')?.value || '';
const tags = typeof tagContexts !== 'undefined' ? tagContexts.post : [];
const photos = state?.selectedPhotos?.map(p => ({
id: p.id,
title: p.title,
urls: p.urls,
page_url: p.page_url,
is_video: p.is_video
})) || [];
const uploadedFiles = state?.uploadedFiles?.filter(f => !f.uploading && f.url).map(f => ({
id: f.id,
name: f.name,
type: f.type,
url: f.url
})) || [];
const formData = new FormData();
formData.append('action', 'save_draft');
formData.append('text', postText);
formData.append('tags', JSON.stringify(tags));
formData.append('photos', JSON.stringify(photos));
formData.append('uploaded_files', JSON.stringify(uploadedFiles));
try {
await fetch('api.php', { method: 'POST', body: formData });
} catch (e) {
console.warn('Could not save draft to server:', e);
}
}
// Load draft from server
async function loadPostDraftFromServer() {
try {
const response = await fetch('api.php?action=get_draft');
const data = await response.json();
if (data.success && data.draft) {
return data.draft;
}
} catch (e) {
console.error('Error loading draft from server:', e);
}
return null;
}
// Clear draft on server
async function clearPostDraftOnServer() {
try {
const formData = new FormData();
formData.append('action', 'clear_draft');
await fetch('api.php', { method: 'POST', body: formData });
} catch (e) {
console.warn('Could not clear draft on server:', e);
}
}
function clearPostDraft() {
clearPostDraftOnServer();
}
// Save photos and uploaded files immediately
function saveSelectedPhotos() {
savePostDraftNow();
}
function saveUploadedFiles() {
savePostDraftNow();
}
// Auto-save on text input (debounced)
document.getElementById('post-text')?.addEventListener('input', () => {
savePostDraft();
});
// ============ STATE ============
const MAX_PHOTOS = 9;
function getTotalPhotosCount() {
return state.selectedPhotos.length + state.uploadedFiles.length;
}
function canAddPhotos(count = 1) {
if (getTotalPhotosCount() + count > MAX_PHOTOS) {
showNotification(`Максимум ${MAX_PHOTOS} фото/видео`, 'error');
return false;
}
return true;
}
const state = {
selectedPhotos: [],
uploadedFiles: [],
currentPage: 1,
totalPages: 1,
currentAlbum: '',
isLoadingPhotos: false,
isLoadingAlbums: false,
photoRequestId: 0, // For request deduplication
albumRequestId: 0,
// Infinite scroll state for albums
albumsPage: 1,
albumsTotalPages: 1,
albumsTotal: 0,
isLoadingMoreAlbums: false,
allAlbums: [],
// Infinite scroll state for photos
isLoadingMorePhotos: false,
allPhotos: []
};
// Load draft from server on page load
let pendingDraft = null;
loadPostDraftFromServer().then(draft => {
if (!draft) return;
serverDraft = draft;
pendingDraft = draft;
// Restore text
const postText = document.getElementById('post-text');
if (postText && draft.text) {
postText.value = draft.text;
}
// Restore photos
if (draft.photos && draft.photos.length > 0) {
state.selectedPhotos = draft.photos;
}
// Restore uploaded files (without dataUrl, just URL)
if (draft.uploaded_files && draft.uploaded_files.length > 0) {
state.uploadedFiles = draft.uploaded_files.map(f => ({
...f,
dataUrl: f.url, // Use URL as dataUrl for preview
uploading: false
}));
}
// Update preview
updatePostingPreview();
// Try to restore tags (will work if tagContexts is already initialized)
restoreDraftTags();
});
// Function to restore tags after tagContexts is available
let tagsRestored = false;
function restoreDraftTags() {
if (tagsRestored) return;
if (!pendingDraft || !pendingDraft.tags || pendingDraft.tags.length === 0) return;
if (typeof tagContexts === 'undefined') return;
tagsRestored = true;
tagContexts.post = pendingDraft.tags;
const tagsList = document.getElementById('post-tags-list');
if (tagsList) {
tagsList.innerHTML = pendingDraft.tags.map(tag => `
<span class="tag-chip">
#${escapeHtml(tag)}
<button type="button" class="tag-remove" data-tag="${escapeHtml(tag)}">&times;</button>
</span>
`).join('');
// Re-attach remove handlers
tagsList.querySelectorAll('.tag-remove').forEach(btn => {
btn.addEventListener('click', () => {
const tag = btn.dataset.tag;
tagContexts.post = tagContexts.post.filter(t => t !== tag);
btn.parentElement.remove();
savePostDraftNow();
});
});
}
showNotification('Черновик восстановлен', 'info');
}
// ============ PHOTO SOURCE BUTTONS ============
// Select from Flickr - go to gallery tab
document.getElementById('btn-select-from-flickr')?.addEventListener('click', () => {
document.querySelector('.nav-btn[data-tab="gallery"]')?.click();
});
// Upload from device
document.getElementById('btn-upload-files')?.addEventListener('click', () => {
document.getElementById('file-upload')?.click();
});
document.getElementById('file-upload')?.addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
if (!files.length) return;
for (const file of files) {
// Check photo limit
if (getTotalPhotosCount() >= MAX_PHOTOS) {
showNotification(`Максимум ${MAX_PHOTOS} фото/видео`, 'error');
break;
}
// Check file size (max 50MB)
if (file.size > 50 * 1024 * 1024) {
showNotification(`Файл ${file.name} слишком большой (макс 50MB)`, 'error');
continue;
}
// Create preview immediately
const reader = new FileReader();
reader.onload = async (event) => {
const fileId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const fileData = {
id: fileId,
name: file.name,
type: file.type,
size: file.size,
dataUrl: event.target.result,
uploading: true,
url: null
};
state.uploadedFiles.push(fileData);
renderUploadedFiles();
// Upload to server
try {
const formData = new FormData();
formData.append('action', 'upload_file');
formData.append('file', file);
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
// Find and update the file in state
const idx = state.uploadedFiles.findIndex(f => f.id === fileId);
if (idx !== -1) {
if (data.error) {
showNotification(`Ошибка загрузки ${file.name}: ${data.error}`, 'error');
state.uploadedFiles.splice(idx, 1);
} else {
state.uploadedFiles[idx].url = data.url;
state.uploadedFiles[idx].uploading = false;
saveUploadedFiles(); // Save to localStorage after successful upload
}
renderUploadedFiles();
}
} catch (error) {
showNotification(`Ошибка загрузки ${file.name}`, 'error');
const idx = state.uploadedFiles.findIndex(f => f.id === fileId);
if (idx !== -1) {
state.uploadedFiles.splice(idx, 1);
renderUploadedFiles();
}
}
};
reader.readAsDataURL(file);
}
// Clear input for re-upload of same files
e.target.value = '';
});
function renderUploadedFiles() {
// Now uses the combined preview
updatePostingPreview();
}
// Drag and drop support for combined preview
const combinedPreview = document.getElementById('post-photos-preview');
if (combinedPreview) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
combinedPreview.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
});
});
['dragenter', 'dragover'].forEach(eventName => {
combinedPreview.addEventListener(eventName, () => {
combinedPreview.classList.add('drag-over');
});
});
['dragleave', 'drop'].forEach(eventName => {
combinedPreview.addEventListener(eventName, () => {
combinedPreview.classList.remove('drag-over');
});
});
combinedPreview.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
if (files.length) {
const fileInput = document.getElementById('file-upload');
if (fileInput) {
fileInput.files = files;
fileInput.dispatchEvent(new Event('change'));
}
}
});
}
// AbortController for cancelling in-flight requests
let photoAbortController = null;
let albumAbortController = null;
// ============ ALBUM PREFERENCES (localStorage) ============
const ALBUM_CACHE_KEY = 'vh_album_cache';
const ALBUM_PREFS_KEY = 'vh_album_prefs';
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
function getAlbumCache() {
try {
const cached = localStorage.getItem(ALBUM_CACHE_KEY);
if (!cached) return null;
const data = JSON.parse(cached);
if (Date.now() - data.timestamp > CACHE_TTL) {
localStorage.removeItem(ALBUM_CACHE_KEY);
return null;
}
// Handle both old format (array) and new format (object with albums property)
const albums = data.albums;
if (Array.isArray(albums)) {
return albums;
} else if (albums && Array.isArray(albums.albums)) {
return albums.albums;
}
return null;
} catch (e) {
return null;
}
}
function setAlbumCache(albumsOrData) {
try {
// Accept either array or object with albums property
const albums = Array.isArray(albumsOrData) ? albumsOrData : (albumsOrData.albums || []);
localStorage.setItem(ALBUM_CACHE_KEY, JSON.stringify({
albums: albums,
timestamp: Date.now()
}));
} catch (e) {
console.warn('Failed to cache albums:', e);
}
}
function getAlbumPrefs() {
try {
const prefs = localStorage.getItem(ALBUM_PREFS_KEY);
return prefs ? JSON.parse(prefs) : { favorites: [], order: [] };
} catch (e) {
return { favorites: [], order: [] };
}
}
function saveAlbumPrefs(prefs) {
try {
localStorage.setItem(ALBUM_PREFS_KEY, JSON.stringify(prefs));
} catch (e) {
console.warn('Failed to save album prefs:', e);
}
}
function toggleAlbumFavorite(albumId) {
const prefs = getAlbumPrefs();
const index = prefs.favorites.indexOf(albumId);
if (index === -1) {
prefs.favorites.push(albumId);
} else {
prefs.favorites.splice(index, 1);
}
saveAlbumPrefs(prefs);
renderAlbumDropdown(window._cachedAlbums || []);
}
function sortAlbumsByPreference(albums) {
const prefs = getAlbumPrefs();
const favorites = new Set(prefs.favorites);
// Sort: favorites first, then rest
return [...albums].sort((a, b) => {
const aFav = favorites.has(a.id);
const bFav = favorites.has(b.id);
if (aFav && !bFav) return -1;
if (!aFav && bFav) return 1;
return 0; // Keep original order within groups
});
}
// ============ DOM ELEMENTS ============
const selectionBar = document.getElementById('selection-bar');
const selectedCountEl = document.getElementById('selected-count');
const photoGallery = document.getElementById('photo-gallery');
const postPhotosPreview = document.getElementById('post-photos-preview');
// ============ UTILITY FUNCTIONS ============
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
border-radius: 12px;
font-weight: 500;
z-index: 10000;
animation: slideIn 0.3s ease;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
color: white;
background: ${type === 'success' ? 'rgba(52, 199, 89, 0.9)' :
type === 'error' ? 'rgba(255, 59, 48, 0.9)' :
'rgba(0, 122, 255, 0.9)'};
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Add animation styles
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
`;
document.head.appendChild(style);
// ============ SELECTION MANAGEMENT ============
function updateSelectionUI() {
const count = state.selectedPhotos.length;
const total = getTotalPhotosCount();
// Update counter with limit info (gallery floating bar)
if (selectedCountEl) {
selectedCountEl.textContent = `${total}/${MAX_PHOTOS}`;
}
// Update counter on posting page
const photoCounter = document.getElementById('photo-counter');
if (photoCounter) {
photoCounter.textContent = `${total}/${MAX_PHOTOS}`;
photoCounter.classList.toggle('at-limit', total >= MAX_PHOTOS);
}
// Show/hide floating action bar
if (selectionBar) {
if (count > 0) {
selectionBar.classList.remove('hidden');
} else {
selectionBar.classList.add('hidden');
}
}
}
function updatePostingPreview() {
// Always update photo counter
const total = getTotalPhotosCount();
const photoCounter = document.getElementById('photo-counter');
if (photoCounter) {
photoCounter.textContent = `${total}/${MAX_PHOTOS}`;
photoCounter.classList.toggle('at-limit', total >= MAX_PHOTOS);
}
if (!postPhotosPreview) return;
const hasFlickrPhotos = state.selectedPhotos.length > 0;
const hasUploadedFiles = state.uploadedFiles.length > 0;
if (!hasFlickrPhotos && !hasUploadedFiles) {
postPhotosPreview.innerHTML = '<p class="placeholder">Нажмите кнопку выше чтобы добавить фото</p>';
return;
}
postPhotosPreview.innerHTML = '';
// Render Flickr photos
state.selectedPhotos.forEach((photo, index) => {
const div = document.createElement('div');
div.className = 'preview-thumb';
div.innerHTML = `
<img src="${photo.urls.small || photo.urls.medium || photo.urls.square}" alt="${escapeHtml(photo.title)}">
<button class="remove-btn" data-index="${index}" data-source="flickr">×</button>
<span class="source-badge">flickr</span>
`;
postPhotosPreview.appendChild(div);
});
// Render uploaded files
state.uploadedFiles.forEach((file) => {
const div = document.createElement('div');
const isVideo = file.type.startsWith('video/');
div.className = 'preview-thumb' + (file.uploading ? ' uploading' : '');
div.innerHTML = `
${isVideo
? `<video src="${file.dataUrl}"></video><span class="video-badge">▶</span>`
: `<img src="${file.dataUrl}" alt="${escapeHtml(file.name)}">`
}
${file.uploading ? '<div class="upload-spinner"></div>' : ''}
<button class="remove-btn" data-id="${file.id}" data-source="upload">×</button>
<span class="source-badge">файл</span>
`;
postPhotosPreview.appendChild(div);
});
// Attach remove handlers for Flickr photos
postPhotosPreview.querySelectorAll('.remove-btn[data-source="flickr"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const index = parseInt(btn.dataset.index);
state.selectedPhotos.splice(index, 1);
updatePostingPreview();
updateSelectionUI();
syncGallerySelection();
saveSelectedPhotos();
});
});
// Attach remove handlers for uploaded files
postPhotosPreview.querySelectorAll('.remove-btn[data-source="upload"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.dataset.id;
state.uploadedFiles = state.uploadedFiles.filter(f => f.id !== id);
updatePostingPreview();
saveUploadedFiles();
});
});
}
function syncGallerySelection() {
document.querySelectorAll('.photo-item').forEach(item => {
const photoId = item.dataset.photoId;
if (state.selectedPhotos.find(p => p.id === photoId)) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
}
// ============ TAB NAVIGATION ============
const navBtns = document.querySelectorAll('.nav-btn');
const tabContents = document.querySelectorAll('.tab-content');
navBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tabId = btn.dataset.tab;
navBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(t => t.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + tabId)?.classList.add('active');
// Load data when switching tabs
if (tabId === 'gallery') {
// Albums are loaded on button click now
// But if we have cache, render it
if (window._cachedAlbums && albumsGrid) {
renderAlbumsGrid(window._cachedAlbums);
}
} else if (tabId === 'posting') {
updatePostingPreview();
loadTelegramStatus();
loadVKStatus();
} else if (tabId === 'settings') {
loadTelegramStatus();
loadVKStatus();
}
});
});
// ============ LINK CONVERTER ============
const btnConvert = document.getElementById('btn-convert');
const btnCopy = document.getElementById('btn-copy');
const inputUrls = document.getElementById('input-urls');
const outputResult = document.getElementById('output-result');
btnConvert?.addEventListener('click', async () => {
const urls = inputUrls?.value.trim();
if (!urls) {
showNotification('Введите ссылки Flickr', 'error');
return;
}
btnConvert.disabled = true;
btnConvert.textContent = 'Конвертация...';
try {
const formData = new FormData();
formData.append('action', 'convert');
formData.append('urls', urls);
formData.append('size', document.getElementById('image-size')?.value || 'Large');
formData.append('format', document.getElementById('output-format')?.value || 'bbcode');
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
} else {
if (outputResult) outputResult.value = data.output;
showNotification('Конвертация завершена', 'success');
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
} finally {
btnConvert.disabled = false;
btnConvert.textContent = 'Конвертировать';
}
});
btnCopy?.addEventListener('click', () => {
outputResult?.select();
document.execCommand('copy');
if (btnCopy) {
btnCopy.textContent = 'Скопировано!';
setTimeout(() => btnCopy.textContent = 'Скопировать', 2000);
}
});
// ============ FLICKR GALLERY (Albums Grid + Photos View) ============
// DOM Elements
const albumsView = document.getElementById('albums-view');
const photosView = document.getElementById('photos-view');
const albumsGrid = document.getElementById('albums-grid');
const btnLoadAlbums = document.getElementById('btn-load-albums');
const searchAlbums = document.getElementById('search-albums');
const searchPhotos = document.getElementById('search-photos');
const btnBackToAlbums = document.getElementById('btn-back-to-albums');
const currentAlbumTitle = document.getElementById('current-album-title');
const photosCountEl = document.getElementById('photos-count');
const btnPrevPage = document.getElementById('btn-prev-page');
const btnNextPage = document.getElementById('btn-next-page');
const pageInfo = document.getElementById('page-info');
const dragHint = document.getElementById('drag-hint');
// Drag state
let draggedAlbum = null;
let draggedElement = null;
// ============ ALBUMS GRID ============
function renderAlbumsGrid(albums, filterText = '') {
if (!albumsGrid) return;
// Apply order from preferences
const prefs = getAlbumPrefs();
let orderedAlbums = [...albums];
// Sort by saved order if available
if (prefs.order && prefs.order.length > 0) {
const orderMap = new Map(prefs.order.map((id, idx) => [id, idx]));
orderedAlbums.sort((a, b) => {
const aIdx = orderMap.has(a.id) ? orderMap.get(a.id) : 9999;
const bIdx = orderMap.has(b.id) ? orderMap.get(b.id) : 9999;
return aIdx - bIdx;
});
}
// Filter by search text
if (filterText) {
const lower = filterText.toLowerCase();
orderedAlbums = orderedAlbums.filter(a => {
const title = (a.title?._content || a.title || '').toLowerCase();
return title.includes(lower);
});
}
if (orderedAlbums.length === 0) {
albumsGrid.innerHTML = '<p class="placeholder">Альбомы не найдены</p>';
if (dragHint) dragHint.classList.add('hidden');
return;
}
if (dragHint) dragHint.classList.remove('hidden');
albumsGrid.innerHTML = '';
orderedAlbums.forEach(album => {
const card = createAlbumCard(album);
albumsGrid.appendChild(card);
});
}
function createAlbumCard(album) {
const card = document.createElement('div');
card.className = 'album-card';
card.dataset.albumId = album.id;
card.draggable = true;
const title = album.title?._content || album.title || 'Без названия';
const count = album.photos || 0;
const coverUrl = album.primary_photo_extras?.url_m ||
album.primary_photo_extras?.url_s ||
album.primary_photo_extras?.url_sq ||
null;
card.innerHTML = `
${coverUrl
? `<img class="album-card-cover" src="${coverUrl}" alt="${escapeHtml(title)}" loading="lazy">`
: `<div class="album-card-placeholder">📁</div>`
}
<div class="album-card-overlay">
<div class="album-card-title">${escapeHtml(title)}</div>
<div class="album-card-count">${count} фото</div>
</div>
<div class="album-card-drag-handle" title="Перетащите для изменения порядка">⋮⋮</div>
`;
// Click to open album
card.addEventListener('click', (e) => {
if (e.target.closest('.album-card-drag-handle')) return;
openAlbum(album);
});
// Drag events
card.addEventListener('dragstart', handleDragStart);
card.addEventListener('dragend', handleDragEnd);
card.addEventListener('dragover', handleDragOver);
card.addEventListener('dragenter', handleDragEnter);
card.addEventListener('dragleave', handleDragLeave);
card.addEventListener('drop', handleDrop);
return card;
}
// ============ DRAG AND DROP ============
function handleDragStart(e) {
draggedElement = e.currentTarget;
draggedAlbum = e.currentTarget.dataset.albumId;
e.currentTarget.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedAlbum);
}
function handleDragEnd(e) {
e.currentTarget.classList.remove('dragging');
document.querySelectorAll('.album-card.drag-over').forEach(el => {
el.classList.remove('drag-over');
});
draggedElement = null;
draggedAlbum = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function handleDragEnter(e) {
e.preventDefault();
if (e.currentTarget !== draggedElement) {
e.currentTarget.classList.add('drag-over');
}
}
function handleDragLeave(e) {
e.currentTarget.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
const targetCard = e.currentTarget;
targetCard.classList.remove('drag-over');
if (!draggedElement || targetCard === draggedElement) return;
const targetId = targetCard.dataset.albumId;
const sourceId = draggedAlbum;
// Get current order from DOM
const cards = Array.from(albumsGrid.querySelectorAll('.album-card'));
const currentOrder = cards.map(c => c.dataset.albumId);
const sourceIdx = currentOrder.indexOf(sourceId);
const targetIdx = currentOrder.indexOf(targetId);
if (sourceIdx === -1 || targetIdx === -1) return;
// Move element in DOM
if (sourceIdx < targetIdx) {
targetCard.parentNode.insertBefore(draggedElement, targetCard.nextSibling);
} else {
targetCard.parentNode.insertBefore(draggedElement, targetCard);
}
// Save new order
const newOrder = Array.from(albumsGrid.querySelectorAll('.album-card'))
.map(c => c.dataset.albumId);
saveAlbumOrder(newOrder);
showNotification('Порядок альбомов сохранён', 'success');
}
function saveAlbumOrder(order) {
const prefs = getAlbumPrefs();
prefs.order = order;
saveAlbumPrefs(prefs);
}
// ============ ALBUM NAVIGATION ============
function openAlbum(album) {
state.currentAlbum = album.id;
state.currentPage = 1;
// Update UI
if (currentAlbumTitle) {
currentAlbumTitle.textContent = album.title?._content || album.title || 'Альбом';
}
// Switch views
albumsView?.classList.add('hidden');
photosView?.classList.remove('hidden');
// Load photos
loadPhotos();
}
function closeAlbum() {
state.currentAlbum = '';
state.currentPage = 1;
// Switch views
photosView?.classList.add('hidden');
albumsView?.classList.remove('hidden');
// Clear search
if (searchPhotos) searchPhotos.value = '';
// Silently refresh albums in background
refreshAlbumsSilently();
}
// Refresh albums in background without showing loading to user
async function refreshAlbumsSilently() {
if (state.isLoadingAlbums) return;
try {
const response = await fetch('api.php?action=flickr_albums');
if (!response.ok) return;
const data = await response.json();
if (data.albums && data.albums.length > 0) {
setAlbumCache(data.albums);
window._cachedAlbums = data.albums;
// Only re-render if on albums view (photos view is hidden)
if (photosView?.classList.contains('hidden')) {
renderAlbumsGrid(data.albums);
}
}
} catch (error) {
console.log('Silent refresh failed:', error.message);
}
}
// ============ LOAD ALBUMS (with infinite scroll) ============
async function loadAlbums(forceRefresh = false) {
console.log('loadAlbums called, forceRefresh:', forceRefresh);
// Check if albumsGrid exists
if (!albumsGrid) {
console.error('albumsGrid element not found!');
showNotification('Ошибка: элемент галереи не найден', 'error');
return;
}
if (state.isLoadingAlbums) {
console.log('Albums already loading, skipping...');
showNotification('Альбомы уже загружаются...', 'info');
return;
}
// Reset pagination state
state.albumsPage = 1;
state.albumsTotalPages = 1;
state.allAlbums = [];
// Check cache first
if (!forceRefresh) {
const cached = getAlbumCache();
if (cached && cached.albums) {
console.log('Using cached albums:', cached.albums.length);
state.allAlbums = cached.albums;
state.albumsPage = cached.page || 1;
state.albumsTotalPages = cached.pages || 1;
state.albumsTotal = cached.total || cached.albums.length;
window._cachedAlbums = cached.albums;
renderAlbumsGrid(cached.albums);
showNotification(`Загружено ${cached.albums.length} альбомов (из кеша)`, 'success');
setupAlbumsInfiniteScroll();
return;
} else if (cached && Array.isArray(cached)) {
// Old cache format compatibility
console.log('Using old cached albums:', cached.length);
state.allAlbums = cached;
window._cachedAlbums = cached;
renderAlbumsGrid(cached);
showNotification(`Загружено ${cached.length} альбомов (из кеша)`, 'success');
return;
}
}
// Cancel previous request
if (albumAbortController) {
albumAbortController.abort();
}
albumAbortController = new AbortController();
state.isLoadingAlbums = true;
state.albumRequestId++;
const thisRequestId = state.albumRequestId;
// Show loading state
albumsGrid.innerHTML = `
<div class="loading" style="grid-column: 1/-1;">
<div class="loading-spinner"></div>
<span>Загрузка альбомов с Flickr...</span>
</div>
`;
if (dragHint) dragHint.classList.add('hidden');
// Also update button state
if (btnLoadAlbums) {
btnLoadAlbums.disabled = true;
btnLoadAlbums.innerHTML = '<span class="btn-icon-text">⏳</span> Загрузка...';
}
try {
console.log('Fetching albums from API...');
// Add timeout
const timeoutId = setTimeout(() => {
albumAbortController.abort();
console.error('Request timed out after 30 seconds');
}, 30000);
const response = await fetch(`api.php?action=flickr_albums&page=1&per_page=50`, {
signal: albumAbortController.signal
});
clearTimeout(timeoutId);
console.log('API response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (thisRequestId !== state.albumRequestId) {
console.log('Album request superseded');
return;
}
const data = await response.json();
console.log('API response data:', data);
if (data.error) {
throw new Error(data.error);
}
if (data.albums && data.albums.length > 0) {
state.allAlbums = data.albums;
state.albumsPage = data.page || 1;
state.albumsTotalPages = data.pages || 1;
state.albumsTotal = data.total || data.albums.length;
// Save to cache with pagination info
setAlbumCache({
albums: data.albums,
page: state.albumsPage,
pages: state.albumsTotalPages,
total: state.albumsTotal
});
window._cachedAlbums = data.albums;
renderAlbumsGrid(data.albums);
const hasMore = state.albumsPage < state.albumsTotalPages;
const totalInfo = state.albumsTotal > data.albums.length
? ` (${data.albums.length} из ${state.albumsTotal})`
: '';
showNotification(`Загружено ${data.albums.length} альбомов${totalInfo}`, 'success');
// Setup infinite scroll if there are more pages
if (hasMore) {
setupAlbumsInfiniteScroll();
}
} else {
albumsGrid.innerHTML = '<p class="placeholder">Альбомы не найдены. Проверьте настройки Flickr API.</p>';
showNotification('Альбомы не найдены', 'error');
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
albumsGrid.innerHTML = `
<div class="error-message" style="grid-column: 1/-1; text-align: center; padding: 40px;">
<p style="font-size: 2rem; margin-bottom: 10px;">⏱️</p>
<p class="placeholder">Превышено время ожидания. Flickr API не отвечает.</p>
<button class="btn btn-primary" style="margin-top: 15px;" onclick="location.reload()">Попробовать снова</button>
</div>
`;
showNotification('Таймаут: сервер не отвечает', 'error');
return;
}
console.error('Ошибка загрузки альбомов:', error);
albumsGrid.innerHTML = `
<div class="error-message" style="grid-column: 1/-1; text-align: center; padding: 40px;">
<p style="font-size: 2rem; margin-bottom: 10px;">⚠️</p>
<p class="placeholder">Ошибка загрузки: ${escapeHtml(error.message)}</p>
<button onclick="location.reload()" class="btn btn-small" style="margin-top: 15px;">Перезагрузить страницу</button>
</div>
`;
showNotification('Ошибка: ' + error.message, 'error');
} finally {
state.isLoadingAlbums = false;
if (btnLoadAlbums) {
btnLoadAlbums.disabled = false;
btnLoadAlbums.innerHTML = '<span class="btn-icon-text">↻</span> Загрузить альбомы';
}
}
}
// Load more albums (infinite scroll)
async function loadMoreAlbums() {
if (state.isLoadingMoreAlbums || state.albumsPage >= state.albumsTotalPages) {
return;
}
state.isLoadingMoreAlbums = true;
const nextPage = state.albumsPage + 1;
// Show loading indicator at bottom
const loadingEl = document.createElement('div');
loadingEl.className = 'albums-loading-more';
loadingEl.innerHTML = `
<div class="loading-spinner"></div>
<span>Загрузка альбомов...</span>
`;
albumsGrid.appendChild(loadingEl);
try {
const response = await fetch(`api.php?action=flickr_albums&page=${nextPage}&per_page=50`);
const data = await response.json();
// Remove loading indicator
loadingEl.remove();
if (data.error) {
throw new Error(data.error);
}
if (data.albums && data.albums.length > 0) {
state.albumsPage = data.page;
state.allAlbums = [...state.allAlbums, ...data.albums];
window._cachedAlbums = state.allAlbums;
// Update cache
setAlbumCache({
albums: state.allAlbums,
page: state.albumsPage,
pages: state.albumsTotalPages,
total: state.albumsTotal
});
// Append new albums to grid
appendAlbumsToGrid(data.albums);
console.log(`Loaded page ${nextPage}/${state.albumsTotalPages}, total albums: ${state.allAlbums.length}`);
}
} catch (error) {
loadingEl.remove();
console.error('Error loading more albums:', error);
showNotification('Ошибка загрузки альбомов: ' + error.message, 'error');
} finally {
state.isLoadingMoreAlbums = false;
}
}
// Append albums to existing grid
function appendAlbumsToGrid(albums) {
albums.forEach(album => {
const card = createAlbumCard(album);
albumsGrid.appendChild(card);
});
if (dragHint && state.allAlbums.length > 1) {
dragHint.classList.remove('hidden');
}
}
// Setup infinite scroll observer for albums
let albumsScrollObserver = null;
function setupAlbumsInfiniteScroll() {
// Remove existing observer
if (albumsScrollObserver) {
albumsScrollObserver.disconnect();
}
// Create sentinel element
let sentinel = document.getElementById('albums-scroll-sentinel');
if (!sentinel) {
sentinel = document.createElement('div');
sentinel.id = 'albums-scroll-sentinel';
sentinel.style.cssText = 'height: 20px; grid-column: 1/-1;';
}
// Append sentinel after grid
if (albumsGrid && albumsGrid.parentNode) {
// Insert after albumsGrid
albumsGrid.parentNode.insertBefore(sentinel, albumsGrid.nextSibling);
}
// Create intersection observer
albumsScrollObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !state.isLoadingMoreAlbums && state.albumsPage < state.albumsTotalPages) {
console.log('Albums sentinel visible, loading more...');
loadMoreAlbums();
}
});
}, {
root: null,
rootMargin: '200px',
threshold: 0
});
albumsScrollObserver.observe(sentinel);
}
// ============ LOAD PHOTOS (with infinite scroll) ============
async function loadPhotos() {
if (!photoGallery) return;
if (photoAbortController) {
photoAbortController.abort();
}
photoAbortController = new AbortController();
// Reset state for new load
state.currentPage = 1;
state.totalPages = 1;
state.allPhotos = [];
state.isLoadingPhotos = true;
state.photoRequestId++;
const thisRequestId = state.photoRequestId;
photoGallery.innerHTML = `
<div class="loading">
<div class="loading-spinner"></div>
<span>Загрузка фотографий...</span>
</div>
`;
try {
const params = new URLSearchParams({
action: 'flickr_photos',
page: 1,
per_page: 50
});
if (state.currentAlbum) params.append('album_id', state.currentAlbum);
if (searchPhotos?.value.trim()) params.append('search', searchPhotos.value.trim());
const response = await fetch('api.php?' + params, {
signal: photoAbortController.signal
});
if (thisRequestId !== state.photoRequestId) {
console.log('Photo request superseded');
return;
}
const data = await response.json();
if (data.error) {
photoGallery.innerHTML = `<p class="placeholder">Ошибка: ${data.error}</p>`;
return;
}
state.totalPages = data.pagination?.pages || 1;
state.currentPage = data.pagination?.page || 1;
const totalPhotos = data.pagination?.total || 0;
updatePagination();
if (photosCountEl) {
photosCountEl.textContent = `${totalPhotos} фото`;
}
if (!data.photos || data.photos.length === 0) {
photoGallery.innerHTML = '<p class="placeholder">Фотографии не найдены</p>';
return;
}
state.allPhotos = data.photos;
photoGallery.innerHTML = '';
renderPhotos(data.photos);
// Setup infinite scroll if there are more pages
if (state.currentPage < state.totalPages) {
setupPhotosInfiniteScroll();
}
} catch (error) {
if (error.name === 'AbortError') return;
photoGallery.innerHTML = `<p class="placeholder">Ошибка: ${error.message}</p>`;
showNotification('Ошибка загрузки фотографий', 'error');
} finally {
state.isLoadingPhotos = false;
}
}
// Render photos to gallery
function renderPhotos(photos, append = false) {
if (!append) {
photoGallery.innerHTML = '';
}
photos.forEach(photo => {
const div = document.createElement('div');
div.className = 'photo-item';
if (photo.is_video) {
div.classList.add('is-video');
}
div.dataset.photoId = photo.id;
div.dataset.photoData = JSON.stringify(photo);
if (state.selectedPhotos.find(p => p.id === photo.id)) {
div.classList.add('selected');
}
const videoBadge = photo.is_video ? '<div class="video-badge"><span class="video-icon">▶</span></div>' : '';
div.innerHTML = `
<div class="checkbox"></div>
${videoBadge}
<img src="${photo.urls.small || photo.urls.medium || photo.urls.square}" alt="${escapeHtml(photo.title)}" loading="lazy">
<div class="title">${escapeHtml(photo.title)}</div>
<button class="photo-preview-btn" title="Просмотр">${photo.is_video ? '▶' : '🔍'}</button>
`;
// Checkbox click = toggle selection
div.querySelector('.checkbox').addEventListener('click', (e) => {
e.stopPropagation();
togglePhotoSelection(div, photo);
});
// Image click = open lightbox
div.querySelector('img').addEventListener('click', (e) => {
e.stopPropagation();
openLightbox(photo);
});
// Preview button = open lightbox
div.querySelector('.photo-preview-btn').addEventListener('click', (e) => {
e.stopPropagation();
openLightbox(photo);
});
// Background click = toggle selection
div.addEventListener('click', () => togglePhotoSelection(div, photo));
photoGallery.appendChild(div);
});
}
// Load more photos (infinite scroll)
async function loadMorePhotos() {
if (state.isLoadingMorePhotos || state.currentPage >= state.totalPages) {
return;
}
state.isLoadingMorePhotos = true;
const nextPage = state.currentPage + 1;
// Show loading indicator at bottom
const loadingEl = document.createElement('div');
loadingEl.className = 'photos-loading-more';
loadingEl.innerHTML = `
<div class="loading-spinner"></div>
<span>Загрузка фотографий...</span>
`;
photoGallery.appendChild(loadingEl);
try {
const params = new URLSearchParams({
action: 'flickr_photos',
page: nextPage,
per_page: 50
});
if (state.currentAlbum) params.append('album_id', state.currentAlbum);
if (searchPhotos?.value.trim()) params.append('search', searchPhotos.value.trim());
const response = await fetch('api.php?' + params);
const data = await response.json();
// Remove loading indicator
loadingEl.remove();
if (data.error) {
throw new Error(data.error);
}
if (data.photos && data.photos.length > 0) {
state.currentPage = data.pagination?.page || nextPage;
state.allPhotos = [...state.allPhotos, ...data.photos];
// Append new photos
renderPhotos(data.photos, true);
updatePagination();
console.log(`Loaded photos page ${nextPage}/${state.totalPages}, total: ${state.allPhotos.length}`);
}
} catch (error) {
loadingEl.remove();
console.error('Error loading more photos:', error);
showNotification('Ошибка загрузки фотографий: ' + error.message, 'error');
} finally {
state.isLoadingMorePhotos = false;
}
}
// Setup infinite scroll observer for photos
let photosScrollObserver = null;
function setupPhotosInfiniteScroll() {
// Remove existing observer
if (photosScrollObserver) {
photosScrollObserver.disconnect();
}
// Create sentinel element
let sentinel = document.getElementById('photos-scroll-sentinel');
if (!sentinel) {
sentinel = document.createElement('div');
sentinel.id = 'photos-scroll-sentinel';
sentinel.style.cssText = 'height: 20px; width: 100%; clear: both;';
}
// Append sentinel after gallery
if (photoGallery && photoGallery.parentNode) {
photoGallery.parentNode.insertBefore(sentinel, photoGallery.nextSibling);
}
// Create intersection observer
photosScrollObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !state.isLoadingMorePhotos && state.currentPage < state.totalPages) {
console.log('Photos sentinel visible, loading more...');
loadMorePhotos();
}
});
}, {
root: null,
rootMargin: '300px',
threshold: 0
});
photosScrollObserver.observe(sentinel);
}
function togglePhotoSelection(element, photo) {
const index = state.selectedPhotos.findIndex(p => p.id === photo.id);
if (index === -1) {
if (!canAddPhotos(1)) return;
state.selectedPhotos.push(photo);
element.classList.add('selected');
} else {
state.selectedPhotos.splice(index, 1);
element.classList.remove('selected');
}
updateSelectionUI();
saveSelectedPhotos(); // Save to localStorage
}
function updatePagination() {
// Show loaded/total info for infinite scroll
const loaded = state.allPhotos.length;
const total = state.totalPages * 50; // Approximate total
if (pageInfo) {
if (state.currentPage < state.totalPages) {
pageInfo.textContent = `Загружено: ${loaded}`;
} else {
pageInfo.textContent = `Все фото загружены`;
}
}
// Hide old pagination buttons (infinite scroll replaces them)
if (btnPrevPage) btnPrevPage.style.display = 'none';
if (btnNextPage) btnNextPage.style.display = 'none';
}
// ============ EVENT LISTENERS ============
// Load albums button
if (btnLoadAlbums) {
console.log('btnLoadAlbums found, attaching click listener');
btnLoadAlbums.addEventListener('click', () => {
console.log('Load albums button clicked!');
loadAlbums(true);
});
} else {
console.error('btnLoadAlbums element not found! Check ID: btn-load-albums');
}
// Back to albums
btnBackToAlbums?.addEventListener('click', closeAlbum);
// Search albums
let albumSearchTimeout;
searchAlbums?.addEventListener('input', () => {
clearTimeout(albumSearchTimeout);
albumSearchTimeout = setTimeout(() => {
if (window._cachedAlbums) {
renderAlbumsGrid(window._cachedAlbums, searchAlbums.value.trim());
}
}, 300);
});
// Search photos
let photoSearchTimeout;
searchPhotos?.addEventListener('input', () => {
clearTimeout(photoSearchTimeout);
photoSearchTimeout = setTimeout(() => {
state.currentPage = 1;
loadPhotos();
}, 500);
});
// Pagination
btnPrevPage?.addEventListener('click', () => {
if (state.currentPage > 1) {
state.currentPage--;
loadPhotos();
}
});
btnNextPage?.addEventListener('click', () => {
if (state.currentPage < state.totalPages) {
state.currentPage++;
loadPhotos();
}
});
// ============ FLOATING ACTION BAR ============
document.getElementById('btn-select-all')?.addEventListener('click', () => {
document.querySelectorAll('.photo-item').forEach(item => {
if (getTotalPhotosCount() >= MAX_PHOTOS) return;
const photo = JSON.parse(item.dataset.photoData);
if (!state.selectedPhotos.find(p => p.id === photo.id)) {
state.selectedPhotos.push(photo);
item.classList.add('selected');
}
});
if (getTotalPhotosCount() >= MAX_PHOTOS) {
showNotification(`Выбрано максимум ${MAX_PHOTOS} фото`, 'info');
}
updateSelectionUI();
saveSelectedPhotos();
});
document.getElementById('btn-deselect-all')?.addEventListener('click', () => {
state.selectedPhotos = [];
document.querySelectorAll('.photo-item').forEach(item => item.classList.remove('selected'));
updateSelectionUI();
saveSelectedPhotos();
});
document.getElementById('btn-convert-selected')?.addEventListener('click', () => {
if (state.selectedPhotos.length === 0) {
showNotification('Сначала выберите фотографии', 'error');
return;
}
const urls = state.selectedPhotos.map(p => p.page_url).join('\n');
if (inputUrls) inputUrls.value = urls;
document.querySelector('.nav-btn[data-tab="converter"]')?.click();
});
document.getElementById('btn-telegram-selected')?.addEventListener('click', () => {
if (state.selectedPhotos.length === 0) {
showNotification('Сначала выберите фотографии', 'error');
return;
}
document.querySelector('.nav-btn[data-tab="posting"]')?.click();
});
// Get best quality URL for photo (prefer original, but only use actual URLs)
function getBestPhotoUrl(photo) {
// Priority: original > large2048 > large > medium640 > medium
// Only return URLs that actually exist (not null/undefined)
const urls = photo.urls || {};
return urls.original || urls.large2048 || urls.large || urls.medium640 || urls.medium;
}
// Get preview URL (medium size for lightbox)
function getPreviewUrl(photo) {
return photo.urls.large || photo.urls.medium640 || photo.urls.medium || photo.urls.original;
}
// Get file extension from photo
function getPhotoExtension(photo) {
return photo.original_format || 'jpg';
}
// ============ SERVER-SIDE DOWNLOAD (bypasses CORS) ============
// Download single photo via server proxy
function downloadSinglePhoto(photo) {
const url = getBestPhotoUrl(photo);
const ext = getPhotoExtension(photo);
const title = (photo.title || photo.id).replace(/[<>:"/\\|?*]/g, '_').substring(0, 100);
// Create download URL through our server proxy
const downloadUrl = `download.php?action=photo&url=${encodeURIComponent(url)}&filename=${encodeURIComponent(title)}&format=${ext}`;
// Trigger download via hidden iframe or link
const link = document.createElement('a');
link.href = downloadUrl;
link.download = `${title}.${ext}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Download multiple photos as individual files (sequential)
async function downloadPhotosIndividually(photos) {
showNotification(`Скачивание ${photos.length} фото...`, 'info');
for (let i = 0; i < photos.length; i++) {
downloadSinglePhoto(photos[i]);
// Small delay between downloads to avoid overwhelming browser
if (i < photos.length - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
showNotification(`✓ Загрузка ${photos.length} фото начата`, 'success');
}
// Download multiple photos as ZIP archive
function downloadPhotosAsZip(photos, albumName = '') {
showNotification(`Подготовка архива: ${photos.length} фото...`, 'info');
// Prepare photo data for server
const photoData = photos.map(photo => ({
id: photo.id,
url: getBestPhotoUrl(photo),
title: photo.title || photo.id,
format: getPhotoExtension(photo)
}));
try {
// Create form for POST request
const form = document.createElement('form');
form.method = 'POST';
form.action = 'download.php?action=zip';
form.style.display = 'none';
const photosInput = document.createElement('input');
photosInput.type = 'hidden';
photosInput.name = 'photos';
photosInput.value = JSON.stringify(photoData);
form.appendChild(photosInput);
const albumInput = document.createElement('input');
albumInput.type = 'hidden';
albumInput.name = 'album_name';
albumInput.value = albumName;
form.appendChild(albumInput);
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
showNotification('✓ Создание архива на сервере...', 'success');
} catch (error) {
console.error('Failed to create ZIP:', error);
showNotification('Ошибка создания архива', 'error');
}
}
// Show download format choice dialog
function showDownloadChoiceDialog(photos, albumName = '') {
// Remove existing dialog if any
const existing = document.getElementById('download-choice-dialog');
if (existing) existing.remove();
const dialog = document.createElement('div');
dialog.id = 'download-choice-dialog';
dialog.className = 'download-dialog-overlay';
dialog.innerHTML = `
<div class="download-dialog">
<h3>Скачать ${photos.length} фото</h3>
<p>Выберите формат загрузки:</p>
<div class="download-dialog-buttons">
<button class="btn btn-primary" data-action="zip">
<span class="btn-icon">📦</span>
ZIP архив
</button>
<button class="btn btn-secondary" data-action="individual">
<span class="btn-icon">📄</span>
Отдельные файлы
</button>
</div>
<button class="download-dialog-close" title="Отмена">×</button>
</div>
`;
dialog.querySelector('[data-action="zip"]').addEventListener('click', () => {
dialog.remove();
downloadPhotosAsZip(photos, albumName);
});
dialog.querySelector('[data-action="individual"]').addEventListener('click', () => {
dialog.remove();
downloadPhotosIndividually(photos);
});
dialog.querySelector('.download-dialog-close').addEventListener('click', () => {
dialog.remove();
});
dialog.querySelector('.download-dialog-overlay')?.addEventListener('click', (e) => {
if (e.target === dialog) dialog.remove();
});
// Close on backdrop click
dialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.remove();
});
document.body.appendChild(dialog);
}
// Download photos - single directly, multiple with choice
async function downloadPhotos(photos, albumName = '') {
if (photos.length === 0) {
showNotification('Нет фото для скачивания', 'error');
return;
}
// Single photo - download directly via server proxy
if (photos.length === 1) {
showNotification('Скачивание фото...', 'info');
downloadSinglePhoto(photos[0]);
showNotification('✓ Загрузка начата', 'success');
return;
}
// Multiple photos - show choice dialog
showDownloadChoiceDialog(photos, albumName);
}
// ============ PHOTO LIGHTBOX (Preview) ============
let lightboxPhoto = null;
function createLightbox() {
// Create lightbox container if not exists
if (document.getElementById('photo-lightbox')) return;
const lightbox = document.createElement('div');
lightbox.id = 'photo-lightbox';
lightbox.className = 'lightbox hidden';
lightbox.innerHTML = `
<div class="lightbox-backdrop"></div>
<div class="lightbox-content">
<button class="lightbox-close" title="Закрыть">×</button>
<div class="lightbox-image-container">
<img class="lightbox-image" src="" alt="">
<div class="lightbox-video-container" style="display: none;">
<iframe class="lightbox-video" src="" frameborder="0" allowfullscreen allow="autoplay"></iframe>
</div>
<div class="lightbox-loading">
<div class="loading-spinner"></div>
</div>
</div>
<div class="lightbox-info">
<h3 class="lightbox-title"></h3>
</div>
<div class="lightbox-actions">
<button class="btn btn-primary lightbox-btn-select">
<span class="btn-icon">✓</span> Выбрать
</button>
<button class="btn btn-secondary lightbox-btn-download">
<span class="btn-icon">↓</span> Скачать
</button>
<a class="btn btn-secondary lightbox-btn-flickr" href="#" target="_blank" title="Открыть на Flickr">
<span class="btn-icon">🔗</span> Flickr
</a>
</div>
<button class="lightbox-nav lightbox-prev" title="Предыдущее"></button>
<button class="lightbox-nav lightbox-next" title="Следующее"></button>
</div>
`;
document.body.appendChild(lightbox);
// Event listeners
lightbox.querySelector('.lightbox-backdrop').addEventListener('click', closeLightbox);
lightbox.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
lightbox.querySelector('.lightbox-btn-select').addEventListener('click', lightboxSelectPhoto);
lightbox.querySelector('.lightbox-btn-download').addEventListener('click', lightboxDownloadPhoto);
lightbox.querySelector('.lightbox-prev').addEventListener('click', lightboxPrevPhoto);
lightbox.querySelector('.lightbox-next').addEventListener('click', lightboxNextPhoto);
// Keyboard navigation
document.addEventListener('keydown', handleLightboxKeyboard);
}
function openLightbox(photo) {
createLightbox();
lightboxPhoto = photo;
const lightbox = document.getElementById('photo-lightbox');
const img = lightbox.querySelector('.lightbox-image');
const videoContainer = lightbox.querySelector('.lightbox-video-container');
const video = lightbox.querySelector('.lightbox-video');
const title = lightbox.querySelector('.lightbox-title');
const loading = lightbox.querySelector('.lightbox-loading');
const selectBtn = lightbox.querySelector('.lightbox-btn-select');
const flickrBtn = lightbox.querySelector('.lightbox-btn-flickr');
const downloadBtn = lightbox.querySelector('.lightbox-btn-download');
const isVideo = photo.is_video;
// Show loading
loading.classList.remove('hidden');
// Update Flickr link
if (flickrBtn) {
flickrBtn.href = photo.page_url || '#';
}
// Hide video container (not used due to Flickr restrictions)
videoContainer.style.display = 'none';
video.src = '';
// Show image (for both photos and video thumbnails)
img.style.display = 'block';
img.style.opacity = '0';
img.src = getPreviewUrl(photo);
img.alt = photo.title || '';
// Handle video play overlay
let playOverlay = lightbox.querySelector('.video-play-overlay');
if (!playOverlay) {
playOverlay = document.createElement('div');
playOverlay.className = 'video-play-overlay';
playOverlay.innerHTML = `
<div class="video-play-button">▶</div>
<div class="video-play-text">Смотреть на Flickr</div>
`;
lightbox.querySelector('.lightbox-image-container').appendChild(playOverlay);
}
if (isVideo) {
// Show play overlay for videos
playOverlay.style.display = 'flex';
playOverlay.onclick = () => window.open(photo.page_url, '_blank');
if (downloadBtn) downloadBtn.style.display = 'none';
} else {
// Hide play overlay for photos
playOverlay.style.display = 'none';
if (downloadBtn) downloadBtn.style.display = '';
}
// When image loads
img.onload = () => {
loading.classList.add('hidden');
img.style.opacity = '1';
};
img.onerror = () => {
loading.classList.add('hidden');
img.style.opacity = '1';
};
title.textContent = (isVideo ? '▶ ' : '') + (photo.title || 'Без названия');
// Update select button state
const isSelected = state.selectedPhotos.find(p => p.id === photo.id);
selectBtn.innerHTML = isSelected
? '<span class="btn-icon">✓</span> Выбрано'
: '<span class="btn-icon">+</span> Выбрать';
selectBtn.className = isSelected ? 'btn btn-success lightbox-btn-select' : 'btn btn-primary lightbox-btn-select';
// Show lightbox
lightbox.classList.remove('hidden');
document.body.style.overflow = 'hidden';
updateLightboxNavigation();
}
function closeLightbox() {
const lightbox = document.getElementById('photo-lightbox');
if (lightbox) {
// Stop video if playing
const video = lightbox.querySelector('.lightbox-video');
if (video) video.src = '';
lightbox.classList.add('hidden');
document.body.style.overflow = '';
lightboxPhoto = null;
}
}
function lightboxSelectPhoto() {
if (!lightboxPhoto) return;
const index = state.selectedPhotos.findIndex(p => p.id === lightboxPhoto.id);
if (index === -1) {
if (!canAddPhotos(1)) return;
state.selectedPhotos.push(lightboxPhoto);
} else {
state.selectedPhotos.splice(index, 1);
}
// Update button
const selectBtn = document.querySelector('.lightbox-btn-select');
const isSelected = state.selectedPhotos.find(p => p.id === lightboxPhoto.id);
selectBtn.innerHTML = isSelected
? '<span class="btn-icon">✓</span> Выбрано'
: '<span class="btn-icon">+</span> Выбрать';
selectBtn.className = isSelected ? 'btn btn-success lightbox-btn-select' : 'btn btn-primary lightbox-btn-select';
// Update gallery
updateSelectionUI();
syncGallerySelection();
saveSelectedPhotos();
showNotification(isSelected ? 'Фото добавлено' : 'Фото удалено', 'info');
}
function lightboxDownloadPhoto() {
if (!lightboxPhoto) return;
showNotification('Скачивание...', 'info');
downloadSinglePhoto(lightboxPhoto);
}
function getPhotosList() {
return Array.from(document.querySelectorAll('.photo-item')).map(item => {
try {
return JSON.parse(item.dataset.photoData);
} catch (e) {
return null;
}
}).filter(p => p !== null);
}
function lightboxPrevPhoto() {
if (!lightboxPhoto) return;
const photos = getPhotosList();
const currentIdx = photos.findIndex(p => p.id === lightboxPhoto.id);
if (currentIdx > 0) {
openLightbox(photos[currentIdx - 1]);
}
}
function lightboxNextPhoto() {
if (!lightboxPhoto) return;
const photos = getPhotosList();
const currentIdx = photos.findIndex(p => p.id === lightboxPhoto.id);
if (currentIdx < photos.length - 1) {
openLightbox(photos[currentIdx + 1]);
}
}
function updateLightboxNavigation() {
const lightbox = document.getElementById('photo-lightbox');
if (!lightbox || !lightboxPhoto) return;
const photos = getPhotosList();
const currentIdx = photos.findIndex(p => p.id === lightboxPhoto.id);
const prevBtn = lightbox.querySelector('.lightbox-prev');
const nextBtn = lightbox.querySelector('.lightbox-next');
prevBtn.style.visibility = currentIdx > 0 ? 'visible' : 'hidden';
nextBtn.style.visibility = currentIdx < photos.length - 1 ? 'visible' : 'hidden';
}
function handleLightboxKeyboard(e) {
const lightbox = document.getElementById('photo-lightbox');
if (!lightbox || lightbox.classList.contains('hidden')) return;
switch (e.key) {
case 'Escape':
closeLightbox();
break;
case 'ArrowLeft':
lightboxPrevPhoto();
break;
case 'ArrowRight':
lightboxNextPhoto();
break;
case ' ':
e.preventDefault();
lightboxSelectPhoto();
break;
}
}
// Download selected photos
document.getElementById('btn-download-selected')?.addEventListener('click', () => {
if (state.selectedPhotos.length === 0) {
showNotification('Сначала выберите фотографии', 'error');
return;
}
downloadPhotos(state.selectedPhotos);
});
// Download all photos in current album
window.downloadAllPhotos = async function() {
if (!state.currentAlbum) {
showNotification('Сначала откройте альбом', 'error');
return;
}
// Get album name for ZIP filename
const albumName = currentAlbumTitle?.textContent?.replace(/[<>:"/\\|?*]/g, '_') || 'album';
showNotification('Получение списка фото альбома...', 'info');
try {
// Fetch all photos from the album (increase per_page for full album)
const params = new URLSearchParams({
action: 'flickr_photos',
album_id: state.currentAlbum,
page: 1,
per_page: 500 // Get more photos at once
});
const response = await fetch('api.php?' + params);
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
return;
}
if (!data.photos || data.photos.length === 0) {
showNotification('Альбом пуст', 'error');
return;
}
// Download all photos as ZIP with album name
downloadPhotos(data.photos, albumName);
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
}
};
// ============ MULTI-PLATFORM POSTING ============
async function loadTelegramStatus() {
const statusEl = document.getElementById('tg-bot-status');
const statusMini = document.getElementById('tg-status-mini');
const tgChannel = document.getElementById('tg-channel');
try {
const response = await fetch('api.php?action=telegram_status');
const data = await response.json();
const connected = data.connected;
const text = connected ? `@${data.bot_username}` : (data.message || 'Не подключён');
if (statusEl) {
statusEl.className = `status ${connected ? 'connected' : 'disconnected'}`;
statusEl.textContent = connected ? `Подключён: ${text}` : text;
}
if (statusMini) {
statusMini.className = `status-mini ${connected ? 'connected' : ''}`;
statusMini.textContent = connected ? 'Подключён' : 'Не подключён';
}
} catch (error) {
if (statusEl) {
statusEl.className = 'status disconnected';
statusEl.textContent = 'Ошибка проверки';
}
}
// Load channels
try {
const response = await fetch('api.php?action=telegram_channels');
const data = await response.json();
if (data.channels && tgChannel) {
tgChannel.innerHTML = '<option value="">Выберите канал...</option>';
data.channels.forEach(channel => {
const option = document.createElement('option');
option.value = channel.id || channel;
option.textContent = channel.name || channel;
tgChannel.appendChild(option);
});
// Auto-select if only one channel
if (data.channels.length === 1) {
tgChannel.selectedIndex = 1;
}
}
} catch (error) {
console.error('Ошибка загрузки каналов:', error);
}
}
async function loadVKStatus() {
const statusEl = document.getElementById('vk-status');
const statusMini = document.getElementById('vk-status-mini');
const vkGroup = document.getElementById('vk-group');
try {
const response = await fetch('api.php?action=vk_status');
const data = await response.json();
const connected = data.connected;
const text = connected ? (data.user_name || 'VK') : (data.message || 'Не подключён');
if (statusEl) {
statusEl.className = `status ${connected ? 'connected' : 'disconnected'}`;
statusEl.textContent = connected ? `Подключён: ${text}` : text;
}
if (statusMini) {
statusMini.className = `status-mini ${connected ? 'connected' : ''}`;
statusMini.textContent = connected ? 'Подключён' : 'Не подключён';
}
} catch (error) {
if (statusEl) {
statusEl.className = 'status disconnected';
statusEl.textContent = 'Ошибка проверки';
}
}
// Load groups
try {
const response = await fetch('api.php?action=vk_groups');
const data = await response.json();
if (data.groups && vkGroup) {
vkGroup.innerHTML = '<option value="">Выберите группу...</option>';
data.groups.forEach(group => {
const option = document.createElement('option');
option.value = group.id;
option.textContent = group.name;
vkGroup.appendChild(option);
});
// Auto-select if only one group (or community token)
if (data.groups.length === 1) {
vkGroup.selectedIndex = 1;
}
}
} catch (error) {
console.error('Ошибка загрузки групп VK:', error);
}
}
// Send post (immediate or scheduled)
document.getElementById('btn-send-post')?.addEventListener('click', async () => {
const postText = document.getElementById('post-text');
let baseText = postText?.value || '';
const parseMode = document.getElementById('post-parse-mode')?.value || 'HTML';
const isScheduled = document.getElementById('chk-schedule')?.checked;
const scheduledTime = document.getElementById('scheduled-datetime')?.value;
// Check scheduled time
if (isScheduled) {
if (!scheduledTime) {
showNotification('Укажите дату и время публикации', 'error');
return;
}
const scheduledDate = new Date(scheduledTime);
if (scheduledDate <= new Date()) {
showNotification('Время публикации должно быть в будущем', 'error');
return;
}
}
// Add tags to text
const tagsString = getTagsString('post');
const currentTags = tagContexts?.post || [];
if (tagsString) {
baseText = baseText.trim() ? baseText.trim() + '\n\n' + tagsString : tagsString;
}
const platforms = [];
const tgChannel = document.getElementById('tg-channel');
const vkGroup = document.getElementById('vk-group');
const postToTelegram = document.getElementById('chk-telegram')?.checked && tgChannel?.value;
const postToVk = document.getElementById('chk-vk')?.checked && vkGroup?.value;
if (postToTelegram) {
platforms.push({ type: 'telegram', target: tgChannel.value });
}
if (postToVk) {
platforms.push({ type: 'vk', target: vkGroup.value });
}
if (platforms.length === 0) {
showNotification('Выберите платформу и канал/группу', 'error');
return;
}
if (state.selectedPhotos.length === 0 && state.uploadedFiles.length === 0 && !baseText.trim()) {
showNotification('Добавьте фото или текст', 'error');
return;
}
const btnSendPost = document.getElementById('btn-send-post');
const postResult = document.getElementById('post-result');
// Handle scheduled posting
if (isScheduled) {
if (btnSendPost) {
btnSendPost.disabled = true;
btnSendPost.textContent = 'Планирование...';
}
try {
const photoUrls = state.selectedPhotos.map(p => p.urls.large || p.urls.original || p.urls.medium640);
const uploadedFileUrls = state.uploadedFiles
.filter(f => f.url && !f.uploading)
.map(f => ({ url: f.url, type: f.type }));
const formData = new FormData();
const isEditing = scheduledState.editingPostId !== null;
formData.append('action', isEditing ? 'update_scheduled_post' : 'create_scheduled_post');
if (isEditing) {
formData.append('id', scheduledState.editingPostId);
}
formData.append('text', postText?.value || '');
formData.append('tags', JSON.stringify(currentTags));
formData.append('photos', JSON.stringify(photoUrls));
formData.append('uploaded_files', JSON.stringify(uploadedFileUrls));
formData.append('platforms', JSON.stringify(platforms));
formData.append('scheduled_time', scheduledTime.replace('T', ' '));
// Save cross-promo state
const crossPromoEnabled = document.getElementById('chk-cross-promo')?.checked || false;
formData.append('cross_promo', crossPromoEnabled ? '1' : '0');
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
} else {
showNotification(isEditing ? 'Пост обновлён!' : 'Пост запланирован!', 'success');
// Clear form
state.selectedPhotos = [];
state.uploadedFiles = [];
updatePostingPreview();
updateSelectionUI();
syncGallerySelection();
if (postText) postText.value = '';
if (typeof tagContexts !== 'undefined') {
tagContexts.post = [];
const tagsList = document.getElementById('post-tags-list');
if (tagsList) tagsList.innerHTML = '';
}
clearPostDraft();
// Reset schedule UI and editing state
document.getElementById('chk-schedule').checked = false;
scheduledState.editingPostId = null;
updateScheduleUI();
// Refresh scheduled posts list
loadScheduledPosts();
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
} finally {
if (btnSendPost) {
btnSendPost.disabled = false;
btnSendPost.innerHTML = '📅 Запланировать';
}
}
return;
}
if (btnSendPost) {
btnSendPost.disabled = true;
btnSendPost.textContent = 'Публикация...';
}
try {
// Check if there are files still uploading
const stillUploading = state.uploadedFiles.some(f => f.uploading);
if (stillUploading) {
showNotification('Дождитесь завершения загрузки файлов', 'error');
if (btnSendPost) {
btnSendPost.disabled = false;
btnSendPost.textContent = 'Опубликовать';
}
return;
}
const photoUrls = state.selectedPhotos.map(p => p.urls.large || p.urls.original || p.urls.medium640);
// Get uploaded files that finished uploading
const uploadedFileUrls = state.uploadedFiles
.filter(f => f.url && !f.uploading)
.map(f => ({ url: f.url, type: f.type }));
// Check cross-promo settings
const crossPromoEnabled = document.getElementById('chk-cross-promo')?.checked;
// Get settings from localStorage, but also check current input values as fallback
const storedSettings = getCrossPromoSettings();
const crossPromoSettings = {
telegramLink: storedSettings.telegramLink || document.getElementById('cross-promo-telegram')?.value.trim() || '',
vkLink: storedSettings.vkLink || document.getElementById('cross-promo-vk')?.value.trim() || '',
textForTg: storedSettings.textForTg || document.getElementById('cross-promo-text-tg')?.value.trim() || 'Мой канал ВКонтакте',
textForVk: storedSettings.textForVk || document.getElementById('cross-promo-text-vk')?.value.trim() || 'Мой канал в Telegram'
};
// Prepare platform-specific texts
let textForTelegram = baseText;
let textForVk = baseText;
if (crossPromoEnabled) {
console.log('Cross-promo enabled, settings:', crossPromoSettings);
// Add VK link to Telegram post
if (crossPromoSettings.vkLink && postToTelegram) {
const linkText = crossPromoSettings.textForTg || 'Мой канал ВКонтакте';
if (parseMode === 'HTML') {
textForTelegram += `\n\n<a href="${crossPromoSettings.vkLink}">${linkText}</a>`;
} else if (parseMode === 'Markdown') {
textForTelegram += `\n\n[${linkText}](${crossPromoSettings.vkLink})`;
} else {
textForTelegram += `\n\n${linkText}: ${crossPromoSettings.vkLink}`;
}
}
// Add Telegram link to VK post
if (crossPromoSettings.telegramLink && postToVk) {
const linkText = crossPromoSettings.textForVk || 'Мой канал в Telegram';
textForVk += `\n\n${linkText}: ${crossPromoSettings.telegramLink}`;
}
}
// Send to each platform with appropriate text
const results = {};
for (const platform of platforms) {
const formData = new FormData();
formData.append('action', 'multi_post');
formData.append('platforms', JSON.stringify([platform]));
formData.append('text', platform.type === 'telegram' ? textForTelegram : textForVk);
formData.append('photos', JSON.stringify(photoUrls));
formData.append('uploaded_files', JSON.stringify(uploadedFileUrls));
formData.append('parse_mode', parseMode);
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.results) {
Object.assign(results, data.results);
} else if (data.error) {
results[platform.type] = { success: false, error: data.error };
}
}
// Process results
let resultText = 'Результаты:\n';
let hasErrors = false;
Object.entries(results).forEach(([platform, result]) => {
const name = platform === 'telegram' ? 'Telegram' : platform === 'vk' ? 'ВКонтакте' : platform;
if (result.success) {
if (result.warning) {
resultText += `${name}: Опубликовано с предупреждением\n ${result.warning}\n`;
} else {
resultText += `${name}: Успешно\n`;
}
} else {
resultText += `${name}: ${result.error}\n`;
hasErrors = true;
}
});
if (postResult) {
postResult.className = hasErrors ? 'result-message error' : 'result-message success';
postResult.textContent = resultText;
postResult.style.display = 'block';
}
if (!hasErrors) {
state.selectedPhotos = [];
state.uploadedFiles = []; // Clear uploaded files
updatePostingPreview();
updateSelectionUI();
syncGallerySelection();
if (postText) postText.value = '';
// Clear tags
if (typeof tagContexts !== 'undefined') {
tagContexts.post = [];
const tagsList = document.getElementById('post-tags-list');
if (tagsList) tagsList.innerHTML = '';
}
// Clear draft
clearPostDraft();
showNotification('Публикация завершена!', 'success');
}
} catch (error) {
if (postResult) {
postResult.className = 'result-message error';
postResult.textContent = 'Ошибка: ' + error.message;
postResult.style.display = 'block';
}
} finally {
if (btnSendPost) {
btnSendPost.disabled = false;
btnSendPost.textContent = 'Опубликовать';
}
}
});
// ============ SETTINGS ============
document.getElementById('btn-change-password')?.addEventListener('click', async () => {
const currentPassword = document.getElementById('current-password')?.value;
const newPassword = document.getElementById('new-password')?.value;
const confirmPassword = document.getElementById('confirm-password')?.value;
if (!currentPassword || !newPassword) {
showNotification('Заполните все поля', 'error');
return;
}
if (newPassword !== confirmPassword) {
showNotification('Пароли не совпадают', 'error');
return;
}
if (newPassword.length < 8) {
showNotification('Минимум 8 символов', 'error');
return;
}
try {
const formData = new FormData();
formData.append('action', 'change_password');
formData.append('current_password', currentPassword);
formData.append('new_password', newPassword);
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
} else {
showNotification('Пароль изменён', 'success');
document.getElementById('current-password').value = '';
document.getElementById('new-password').value = '';
document.getElementById('confirm-password').value = '';
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
}
});
// ============ VK TOKEN MANAGEMENT ============
// Toggle VK token visibility
document.getElementById('btn-toggle-vk-token')?.addEventListener('click', () => {
const input = document.getElementById('vk-token-input');
if (input) {
input.type = input.type === 'password' ? 'text' : 'password';
}
});
// Save VK token
document.getElementById('btn-save-vk-token')?.addEventListener('click', async () => {
const tokenInput = document.getElementById('vk-token-input');
const token = tokenInput?.value.trim();
const statusSpan = document.getElementById('vk-token-save-status');
if (!token) {
showNotification('Введите токен', 'error');
return;
}
const btn = document.getElementById('btn-save-vk-token');
if (btn) {
btn.disabled = true;
btn.textContent = 'Сохранение...';
}
try {
const formData = new FormData();
formData.append('action', 'save_vk_token');
formData.append('token', token);
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
if (statusSpan) {
statusSpan.textContent = '✗ Ошибка';
statusSpan.className = 'save-status error';
}
} else {
let message = 'Токен сохранён';
if (data.validation?.valid) {
const type = data.validation.type === 'user' ? 'пользовательский' : 'community';
const name = data.validation.user_name || '';
message += ` (${type}${name ? ': ' + name : ''})`;
// Update VK status display
const vkStatus = document.getElementById('vk-status');
if (vkStatus) {
vkStatus.textContent = `Подключён (${type})`;
vkStatus.className = 'status connected';
}
}
showNotification(message, 'success');
if (statusSpan) {
statusSpan.textContent = '✓ Сохранено';
statusSpan.className = 'save-status success';
}
// Reload VK groups
loadVkGroups();
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
if (statusSpan) {
statusSpan.textContent = '✗ Ошибка';
statusSpan.className = 'save-status error';
}
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = 'Сохранить';
}
}
});
// ============ TAGS MANAGEMENT SYSTEM ============
const TAGS_STORAGE_KEY = 'vh_tags_history';
const MAX_TAGS_HISTORY = 100;
// Store for current tags in each context
const tagContexts = {
post: [],
converter: []
};
// Load tags history from localStorage
function getTagsHistory() {
try {
const data = localStorage.getItem(TAGS_STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
return [];
}
}
// Save tag to history
function saveTagToHistory(tag) {
const history = getTagsHistory();
const existingIndex = history.findIndex(t => t.name.toLowerCase() === tag.toLowerCase());
if (existingIndex >= 0) {
// Increment usage count
history[existingIndex].count++;
history[existingIndex].lastUsed = Date.now();
} else {
// Add new tag
history.push({
name: tag,
count: 1,
lastUsed: Date.now()
});
}
// Sort by count and limit
history.sort((a, b) => b.count - a.count);
if (history.length > MAX_TAGS_HISTORY) {
history.splice(MAX_TAGS_HISTORY);
}
localStorage.setItem(TAGS_STORAGE_KEY, JSON.stringify(history));
}
// Get tag suggestions based on input
function getTagSuggestions(input, excludeTags = []) {
const history = getTagsHistory();
const search = input.toLowerCase();
return history
.filter(t => t.name.toLowerCase().includes(search) && !excludeTags.includes(t.name))
.slice(0, 10);
}
// Initialize tag input for a context
function initTagInput(context, inputId, listId, suggestionsId) {
const input = document.getElementById(inputId);
const list = document.getElementById(listId);
const suggestions = document.getElementById(suggestionsId);
if (!input || !list) return;
// Render current tags
function renderTags() {
list.innerHTML = tagContexts[context].map(tag => `
<span class="tag-chip">
#${escapeHtml(tag)}
<button type="button" class="tag-remove" data-tag="${escapeHtml(tag)}" title="Удалить">×</button>
</span>
`).join('');
}
// Add tag
function addTag(tag) {
tag = tag.trim().replace(/^#/, '').replace(/[,\s]+/g, '');
if (!tag || tagContexts[context].includes(tag)) return;
tagContexts[context].push(tag);
saveTagToHistory(tag);
renderTags();
input.value = '';
hideSuggestions();
// Save draft immediately when tags change in post context
if (context === 'post') {
savePostDraftNow();
}
}
// Remove tag
function removeTag(tag) {
tagContexts[context] = tagContexts[context].filter(t => t !== tag);
renderTags();
// Save draft immediately when tags change in post context
if (context === 'post') {
savePostDraftNow();
}
}
// Show suggestions
function showSuggestions(query) {
if (!suggestions) return;
const items = getTagSuggestions(query, tagContexts[context]);
if (items.length === 0 && query.length < 2) {
// Show recent tags if no query
const recent = getTagsHistory().slice(0, 8);
if (recent.length > 0) {
suggestions.innerHTML = recent.map(t => `
<div class="tag-suggestion" data-tag="${escapeHtml(t.name)}">
<span>#${escapeHtml(t.name)}</span>
<span class="tag-count">${t.count}×</span>
</div>
`).join('');
suggestions.classList.add('visible');
}
return;
}
if (items.length === 0) {
hideSuggestions();
return;
}
suggestions.innerHTML = items.map(t => `
<div class="tag-suggestion" data-tag="${escapeHtml(t.name)}">
<span>#${escapeHtml(t.name)}</span>
<span class="tag-count">${t.count}×</span>
</div>
`).join('');
suggestions.classList.add('visible');
}
function hideSuggestions() {
if (suggestions) suggestions.classList.remove('visible');
}
// Event listeners
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
addTag(input.value);
} else if (e.key === 'Backspace' && !input.value && tagContexts[context].length > 0) {
removeTag(tagContexts[context][tagContexts[context].length - 1]);
} else if (e.key === 'Escape') {
hideSuggestions();
}
});
input.addEventListener('input', () => {
showSuggestions(input.value);
});
input.addEventListener('focus', () => {
showSuggestions(input.value);
});
input.addEventListener('blur', () => {
setTimeout(hideSuggestions, 200);
});
// Click on suggestion
if (suggestions) {
suggestions.addEventListener('click', (e) => {
const item = e.target.closest('.tag-suggestion');
if (item) {
addTag(item.dataset.tag);
}
});
}
// Click remove button
list.addEventListener('click', (e) => {
if (e.target.classList.contains('tag-remove')) {
removeTag(e.target.dataset.tag);
}
});
renderTags();
}
// ============ TAG PRESETS MANAGEMENT (Server-side storage) ============
let editingPresetId = null;
let cachedPresets = []; // In-memory cache for sync operations
// Load presets from server
async function loadPresetsFromServer() {
try {
const response = await fetch('api.php?action=get_presets');
const data = await response.json();
if (data.success && data.presets) {
cachedPresets = data.presets;
return data.presets;
}
return [];
} catch (e) {
console.error('Failed to load presets:', e);
return [];
}
}
// Save presets to server
async function savePresetsToServer(presets) {
try {
const response = await fetch('api.php?action=save_presets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ presets })
});
const data = await response.json();
if (data.success) {
cachedPresets = presets;
return true;
}
console.error('Failed to save presets:', data.error);
return false;
} catch (e) {
console.error('Failed to save presets:', e);
return false;
}
}
// Get presets (from cache for sync operations)
function getPresets() {
return cachedPresets;
}
// Generate unique ID
function generatePresetId() {
const maxId = cachedPresets.reduce((max, p) => Math.max(max, p.id || 0), 0);
return maxId + 1;
}
// Render presets in both containers
function renderPresets() {
const presets = getPresets();
const containers = ['post-presets-list', 'converter-presets-list'];
containers.forEach(containerId => {
const container = document.getElementById(containerId);
if (!container) return;
const target = containerId.includes('converter') ? 'converter' : 'post';
container.innerHTML = presets.map(preset => `
<button type="button" class="tag-preset" data-preset-id="${preset.id}" data-tags="${preset.tags.join(',')}" data-target="${target}">
${escapeHtml(preset.name)}
</button>
`).join('');
// Add click handlers
container.querySelectorAll('.tag-preset').forEach(btn => {
btn.addEventListener('click', () => {
applyPreset(btn.dataset.presetId, btn.dataset.target);
});
});
});
}
// Apply preset tags
function applyPreset(presetId, target) {
const presets = getPresets();
const preset = presets.find(p => p.id == presetId);
if (!preset) return;
preset.tags.forEach(tag => {
tag = tag.trim();
if (tag && !tagContexts[target].includes(tag)) {
tagContexts[target].push(tag);
saveTagToHistory(tag);
}
});
// Re-render tags
const listId = target === 'converter' ? 'converter-tags-list' : 'post-tags-list';
const list = document.getElementById(listId);
if (list) {
list.innerHTML = tagContexts[target].map(tag => `
<span class="tag-chip">
#${escapeHtml(tag)}
<button type="button" class="tag-remove" data-tag="${escapeHtml(tag)}" title="Удалить">×</button>
</span>
`).join('');
}
}
// Open preset modal
function openPresetModal(mode = 'list') {
const modal = document.getElementById('preset-modal');
const formSection = document.getElementById('preset-form-section');
const listSection = document.getElementById('preset-list-section');
const titleEl = document.getElementById('preset-modal-title');
if (mode === 'list') {
formSection.style.display = 'none';
listSection.style.display = 'block';
titleEl.textContent = 'Управление пресетами';
renderPresetManager();
} else if (mode === 'add') {
formSection.style.display = 'block';
listSection.style.display = 'none';
titleEl.textContent = 'Добавить пресет';
document.getElementById('preset-name').value = '';
document.getElementById('preset-tags').value = '';
editingPresetId = null;
} else if (mode === 'edit') {
formSection.style.display = 'block';
listSection.style.display = 'none';
titleEl.textContent = 'Редактировать пресет';
}
modal.style.display = 'flex';
}
// Close preset modal
function closePresetModal() {
document.getElementById('preset-modal').style.display = 'none';
editingPresetId = null;
}
// Render preset manager list
function renderPresetManager() {
const presets = getPresets();
const container = document.getElementById('preset-manager-list');
if (presets.length === 0) {
container.innerHTML = '<p class="text-muted">Нет пресетов. Добавьте первый!</p>';
return;
}
container.innerHTML = presets.map(preset => `
<div class="preset-manager-item" data-preset-id="${preset.id}">
<div class="preset-info">
<span class="preset-name">${escapeHtml(preset.name)}</span>
<span class="preset-tags-preview">${preset.tags.map(t => '#' + escapeHtml(t)).join(' ')}</span>
</div>
<div class="preset-actions">
<button type="button" class="btn-icon preset-edit" data-preset-id="${preset.id}" title="Редактировать">✏️</button>
<button type="button" class="btn-icon preset-delete" data-preset-id="${preset.id}" title="Удалить">🗑️</button>
</div>
</div>
`).join('');
// Add event handlers
container.querySelectorAll('.preset-edit').forEach(btn => {
btn.addEventListener('click', () => editPreset(btn.dataset.presetId));
});
container.querySelectorAll('.preset-delete').forEach(btn => {
btn.addEventListener('click', () => deletePreset(btn.dataset.presetId));
});
}
// Edit preset
function editPreset(presetId) {
const presets = getPresets();
const preset = presets.find(p => p.id == presetId);
if (!preset) return;
document.getElementById('preset-name').value = preset.name;
document.getElementById('preset-tags').value = preset.tags.join(', ');
editingPresetId = preset.id;
openPresetModal('edit');
}
// Delete preset
async function deletePreset(presetId) {
if (!confirm('Удалить этот пресет?')) return;
let presets = getPresets();
presets = presets.filter(p => p.id != presetId);
if (await savePresetsToServer(presets)) {
renderPresetManager();
renderPresets();
showNotification('Пресет удалён', 'success');
} else {
showNotification('Ошибка удаления пресета', 'error');
}
}
// Save preset (add or edit)
async function savePreset() {
const name = document.getElementById('preset-name').value.trim();
const tagsStr = document.getElementById('preset-tags').value.trim();
if (!name) {
alert('Введите название пресета');
return;
}
if (!tagsStr) {
alert('Введите хотя бы один тег');
return;
}
const tags = tagsStr.split(',').map(t => t.trim()).filter(t => t);
let presets = [...getPresets()];
if (editingPresetId) {
// Edit existing
const index = presets.findIndex(p => p.id == editingPresetId);
if (index !== -1) {
presets[index].name = name;
presets[index].tags = tags;
}
} else {
// Add new
presets.push({
id: generatePresetId(),
name: name,
tags: tags
});
}
if (await savePresetsToServer(presets)) {
renderPresets();
openPresetModal('list');
showNotification('Пресет сохранён', 'success');
} else {
showNotification('Ошибка сохранения пресета', 'error');
}
}
// Initialize presets system
async function initTagPresets() {
// Load presets from server
await loadPresetsFromServer();
// Initial render
renderPresets();
// Add button handlers
document.getElementById('post-preset-add')?.addEventListener('click', () => openPresetModal('add'));
document.getElementById('converter-preset-add')?.addEventListener('click', () => openPresetModal('add'));
document.getElementById('post-preset-manage')?.addEventListener('click', () => openPresetModal('list'));
document.getElementById('converter-preset-manage')?.addEventListener('click', () => openPresetModal('list'));
// Modal handlers
document.getElementById('preset-modal-close')?.addEventListener('click', closePresetModal);
document.getElementById('preset-add-new')?.addEventListener('click', () => openPresetModal('add'));
document.getElementById('preset-save')?.addEventListener('click', savePreset);
document.getElementById('preset-cancel')?.addEventListener('click', () => openPresetModal('list'));
// Close on overlay click
document.getElementById('preset-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'preset-modal') closePresetModal();
});
}
// Get tags as formatted string
function getTagsString(context) {
return tagContexts[context].map(t => '#' + t).join(' ');
}
// Initialize tag inputs
initTagInput('post', 'post-tags-input', 'post-tags-list', 'post-tags-suggestions');
initTagInput('converter', 'converter-tags-input', 'converter-tags-list', 'converter-tags-suggestions');
initTagPresets();
// Restore draft tags after tagContexts is initialized
restoreDraftTags();
// ============ TEXT FORMATTING TOOLBAR ============
function initTextEditorToolbar() {
document.querySelectorAll('.toolbar-btn[data-format]').forEach(btn => {
btn.addEventListener('click', () => {
const format = btn.dataset.format;
const targetId = btn.dataset.target || 'post-text';
const textarea = document.getElementById(targetId);
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const selected = text.substring(start, end);
let before = '', after = '';
let newText = selected;
// Get format mode from the post-parse-mode selector
const parseMode = document.getElementById('post-parse-mode')?.value || 'HTML';
if (parseMode === 'HTML') {
switch (format) {
case 'bold': before = '<b>'; after = '</b>'; break;
case 'italic': before = '<i>'; after = '</i>'; break;
case 'underline': before = '<u>'; after = '</u>'; break;
case 'strike': before = '<s>'; after = '</s>'; break;
case 'code': before = '<code>'; after = '</code>'; break;
case 'link':
const url = prompt('Введите URL:', 'https://');
if (url) {
before = '<a href="' + url + '">';
after = '</a>';
if (!selected) newText = url;
}
break;
}
} else if (parseMode === 'Markdown') {
switch (format) {
case 'bold': before = '**'; after = '**'; break;
case 'italic': before = '_'; after = '_'; break;
case 'underline': before = '__'; after = '__'; break;
case 'strike': before = '~~'; after = '~~'; break;
case 'code': before = '`'; after = '`'; break;
case 'link':
const url = prompt('Введите URL:', 'https://');
if (url) {
before = '[';
after = '](' + url + ')';
if (!selected) newText = 'ссылка';
}
break;
}
} else {
// Plain text - just insert without formatting
if (format === 'link') {
const url = prompt('Введите URL:', 'https://');
if (url) {
newText = url;
}
}
before = '';
after = '';
}
if (before || after || newText !== selected) {
const replacement = before + newText + after;
textarea.value = text.substring(0, start) + replacement + text.substring(end);
textarea.focus();
textarea.setSelectionRange(start + before.length, start + before.length + newText.length);
}
});
});
}
initTextEditorToolbar();
// ============ SCHEDULED POSTS ============
const scheduledState = {
uploadedFiles: [],
editingPostId: null
};
// Schedule toggle handler
const scheduleCheckbox = document.getElementById('chk-schedule');
const scheduleOptions = document.getElementById('schedule-options');
const btnSendPostMain = document.getElementById('btn-send-post');
function updateScheduleUI() {
const isScheduled = scheduleCheckbox?.checked;
if (scheduleOptions) {
scheduleOptions.classList.toggle('hidden', !isScheduled);
}
if (btnSendPostMain) {
btnSendPostMain.innerHTML = isScheduled ? '📅 Запланировать' : '🚀 Опубликовать';
}
}
scheduleCheckbox?.addEventListener('change', updateScheduleUI);
// Schedule date/time picker
const scheduleDate = document.getElementById('schedule-date');
const scheduleTime = document.getElementById('schedule-time');
const scheduledDatetime = document.getElementById('scheduled-datetime');
// Set default to 1 hour from now
function setDefaultScheduleTime() {
const oneHourLater = new Date();
oneHourLater.setHours(oneHourLater.getHours() + 1);
oneHourLater.setMinutes(0, 0, 0); // Round to nearest hour
setScheduleDateTime(oneHourLater);
}
function setScheduleDateTime(date) {
if (scheduleDate) {
// Use local date components to avoid timezone issues
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
scheduleDate.value = `${year}-${month}-${day}`;
}
if (scheduleTime) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
scheduleTime.value = `${hours}:${minutes}`;
}
syncScheduledDatetime();
}
function syncScheduledDatetime() {
if (scheduleDate && scheduleTime && scheduledDatetime) {
scheduledDatetime.value = `${scheduleDate.value}T${scheduleTime.value}`;
}
}
// Sync hidden field when date/time changes
scheduleDate?.addEventListener('change', syncScheduledDatetime);
scheduleTime?.addEventListener('change', syncScheduledDatetime);
// Set min date to today (local date)
if (scheduleDate) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
scheduleDate.min = `${year}-${month}-${day}`;
}
// Handle preset buttons
document.querySelectorAll('.schedule-presets .preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const preset = btn.dataset.preset;
const now = new Date();
let targetDate = new Date();
switch (preset) {
case '1h':
targetDate.setHours(now.getHours() + 1);
break;
case '3h':
targetDate.setHours(now.getHours() + 3);
break;
case 'tomorrow-10':
targetDate.setDate(now.getDate() + 1);
targetDate.setHours(10, 0, 0, 0);
break;
case 'tomorrow-18':
targetDate.setDate(now.getDate() + 1);
targetDate.setHours(18, 0, 0, 0);
break;
}
setScheduleDateTime(targetDate);
// Highlight active preset
document.querySelectorAll('.schedule-presets .preset-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Initialize with default time
setDefaultScheduleTime();
// Load scheduled and published posts on posting tab switch
document.querySelectorAll('.nav-btn[data-tab="posting"]').forEach(btn => {
btn.addEventListener('click', () => {
loadScheduledPosts();
loadPublishedPosts();
updateScheduledCount();
});
});
async function loadScheduledPosts() {
const list = document.getElementById('scheduled-posts-list');
if (!list) return;
try {
const formData = new FormData();
formData.append('action', 'get_scheduled_posts');
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.posts && data.posts.length > 0) {
const pendingPosts = data.posts.filter(p => p.status === 'pending');
if (pendingPosts.length === 0) {
list.innerHTML = '<p class="placeholder">Нет запланированных постов</p>';
return;
}
list.innerHTML = pendingPosts.map(post => {
const dateStr = new Date(post.scheduled_time).toLocaleString('ru-RU', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
const platforms = (post.platforms || []).map(p => {
const type = p.type || p;
return type === 'telegram' ? 'TG' : type === 'vk' ? 'VK' : type;
}).join(' · ');
// Collect all photo URLs for preview
const allPhotos = [
...(post.photos || []),
...(post.uploaded_files || []).map(f => f.url || f)
];
const photosCount = allPhotos.length;
// Generate photo preview HTML (show up to 4 photos)
let photosPreviewHtml = '';
if (photosCount > 0) {
const previewPhotos = allPhotos.slice(0, 4);
const moreCount = photosCount > 4 ? photosCount - 4 : 0;
photosPreviewHtml = `
<div class="scheduled-photos-preview">
${previewPhotos.map(url => `<img src="${url}" alt="" class="scheduled-thumb" loading="lazy">`).join('')}
${moreCount > 0 ? `<span class="more-photos">+${moreCount}</span>` : ''}
</div>
`;
}
return `
<div class="scheduled-post-card" data-id="${post.id}">
<div class="scheduled-post-header">
<span class="scheduled-time">📅 ${dateStr}</span>
<span class="scheduled-platforms">${platforms}</span>
</div>
${photosPreviewHtml}
<div class="scheduled-post-content">
${post.text ? `<p class="scheduled-text">${escapeHtml(post.text.substring(0, 150))}${post.text.length > 150 ? '...' : ''}</p>` : ''}
${post.tags?.length ? `<span class="scheduled-tags">${post.tags.slice(0, 5).map(t => '#' + t).join(' ')}</span>` : ''}
</div>
<div class="scheduled-post-actions">
<button class="btn btn-small btn-secondary btn-edit-scheduled" data-id="${post.id}">✏️</button>
<button class="btn btn-small btn-danger btn-delete-scheduled" data-id="${post.id}">🗑️</button>
</div>
</div>
`;
}).join('');
// Attach event handlers
list.querySelectorAll('.btn-delete-scheduled').forEach(btn => {
btn.addEventListener('click', () => deleteScheduledPost(btn.dataset.id));
});
list.querySelectorAll('.btn-edit-scheduled').forEach(btn => {
btn.addEventListener('click', () => editScheduledPost(btn.dataset.id, data.posts));
});
// Update count badge
updateScheduledCount(pendingPosts.length);
} else {
list.innerHTML = '<p class="placeholder">Нет запланированных постов</p>';
updateScheduledCount(0);
}
} catch (error) {
list.innerHTML = '<p class="placeholder error">Ошибка загрузки</p>';
}
}
// Update scheduled posts count badge
async function updateScheduledCount(count = null) {
const badge = document.getElementById('scheduled-count');
if (!badge) return;
if (count !== null) {
badge.textContent = count;
badge.style.display = count > 0 ? 'inline-block' : 'none';
return;
}
try {
const formData = new FormData();
formData.append('action', 'get_scheduled_posts');
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.posts) {
const pendingCount = data.posts.filter(p => p.status === 'pending').length;
badge.textContent = pendingCount;
badge.style.display = pendingCount > 0 ? 'inline-block' : 'none';
}
} catch (error) {
console.error('Failed to update scheduled count:', error);
}
}
async function deleteScheduledPost(postId) {
if (!confirm('Удалить этот запланированный пост?')) return;
try {
const formData = new FormData();
formData.append('action', 'delete_scheduled_post');
formData.append('id', postId);
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
} else {
showNotification('Пост удалён', 'success');
loadScheduledPosts();
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
}
}
function editScheduledPost(postId, posts) {
const post = posts.find(p => p.id === postId);
if (!post) return;
const card = document.querySelector(`.scheduled-post-card[data-id="${postId}"]`);
if (!card) return;
// Collect all photo URLs
const allPhotos = [
...(post.photos || []),
...(post.uploaded_files || []).map(f => f.url || f)
];
const hasTg = (post.platforms || []).some(p => (p.type || p) === 'telegram');
const hasVk = (post.platforms || []).some(p => (p.type || p) === 'vk');
const tags = post.tags || [];
// Parse scheduled_time into date and time inputs
let dateVal = '', timeVal = '';
if (post.scheduled_time) {
const dt = new Date(post.scheduled_time.replace(' ', 'T'));
dateVal = `${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`;
timeVal = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}`;
}
// Save original HTML for cancel
const originalHtml = card.innerHTML;
// Render inline editor
card.classList.add('editing');
card.innerHTML = `
<div class="inline-editor">
<div class="inline-editor-photos">
${allPhotos.length > 0 ? allPhotos.map((url, i) => `
<div class="preview-thumb">
<img src="${url}" alt="">
<button class="remove-btn" data-index="${i}">×</button>
</div>
`).join('') : '<p class="placeholder">Нет фото</p>'}
</div>
<textarea class="inline-editor-text" rows="4" placeholder="Текст публикации...">${escapeHtml(post.text || '')}</textarea>
<div class="inline-editor-tags">
<div class="tags-list inline-tags-list">
${tags.map(t => `<span class="tag-chip">#${escapeHtml(t)} <button type="button" class="tag-remove" data-tag="${escapeHtml(t)}">×</button></span>`).join('')}
</div>
<input type="text" class="tags-input inline-tags-input" placeholder="Добавить тег...">
</div>
<div class="inline-editor-row">
<div class="inline-editor-field">
<label>Дата:</label>
<input type="date" class="schedule-input inline-date" value="${dateVal}">
</div>
<div class="inline-editor-field">
<label>Время:</label>
<input type="time" class="schedule-input inline-time" value="${timeVal}">
</div>
</div>
<div class="inline-editor-row">
<label class="checkbox-label compact"><input type="checkbox" class="inline-chk-tg" ${hasTg ? 'checked' : ''}> TG</label>
<label class="checkbox-label compact"><input type="checkbox" class="inline-chk-vk" ${hasVk ? 'checked' : ''}> VK</label>
</div>
<div class="inline-editor-actions">
<button class="btn btn-primary btn-small inline-save">💾 Сохранить</button>
<button class="btn btn-accent btn-small inline-publish-now">🚀 Опубликовать сейчас</button>
<button class="btn btn-secondary btn-small inline-cancel">Отмена</button>
</div>
</div>
`;
// Track editable photos and tags
let editPhotos = [...allPhotos];
let editTags = [...tags];
// Photo remove handlers
card.querySelectorAll('.inline-editor-photos .remove-btn').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.index);
editPhotos.splice(idx, 1);
// Re-render photos
const photosContainer = card.querySelector('.inline-editor-photos');
if (editPhotos.length > 0) {
photosContainer.innerHTML = editPhotos.map((url, i) => `
<div class="preview-thumb">
<img src="${url}" alt="">
<button class="remove-btn" data-index="${i}">×</button>
</div>
`).join('');
// Re-attach handlers
photosContainer.querySelectorAll('.remove-btn').forEach(b => {
b.addEventListener('click', () => {
editPhotos.splice(parseInt(b.dataset.index), 1);
b.parentElement.remove();
if (editPhotos.length === 0) {
photosContainer.innerHTML = '<p class="placeholder">Нет фото</p>';
}
});
});
} else {
photosContainer.innerHTML = '<p class="placeholder">Нет фото</p>';
}
});
});
// Tag remove handlers
card.querySelectorAll('.tag-remove').forEach(btn => {
btn.addEventListener('click', () => {
editTags = editTags.filter(t => t !== btn.dataset.tag);
btn.parentElement.remove();
});
});
// Tag add on Enter
const tagInput = card.querySelector('.inline-tags-input');
tagInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const tag = tagInput.value.trim().replace(/^#/, '').replace(/[,\s]+/g, '');
if (tag && !editTags.includes(tag)) {
editTags.push(tag);
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = `#${escapeHtml(tag)} <button type="button" class="tag-remove" data-tag="${escapeHtml(tag)}">×</button>`;
chip.querySelector('.tag-remove').addEventListener('click', () => {
editTags = editTags.filter(t => t !== tag);
chip.remove();
});
card.querySelector('.inline-tags-list').appendChild(chip);
}
tagInput.value = '';
}
});
// Cancel
card.querySelector('.inline-cancel').addEventListener('click', () => {
card.classList.remove('editing');
card.innerHTML = originalHtml;
// Re-attach original handlers
card.querySelector('.btn-edit-scheduled')?.addEventListener('click', () => editScheduledPost(postId, posts));
card.querySelector('.btn-delete-scheduled')?.addEventListener('click', () => deleteScheduledPost(postId));
});
// Save
card.querySelector('.inline-save').addEventListener('click', async () => {
const text = card.querySelector('.inline-editor-text').value;
const dateInput = card.querySelector('.inline-date').value;
const timeInput = card.querySelector('.inline-time').value;
const tgChecked = card.querySelector('.inline-chk-tg').checked;
const vkChecked = card.querySelector('.inline-chk-vk').checked;
if (!dateInput || !timeInput) {
showNotification('Укажите дату и время', 'error');
return;
}
const scheduledTime = `${dateInput} ${timeInput}:00`;
const platforms = [];
if (tgChecked) platforms.push({ type: 'telegram', target: document.getElementById('tg-channel')?.value || '' });
if (vkChecked) platforms.push({ type: 'vk', target: document.getElementById('vk-group')?.value || '' });
if (platforms.length === 0) {
showNotification('Выберите платформу', 'error');
return;
}
// Separate photos and uploaded files based on original data
const origPhotos = post.photos || [];
const origUploaded = (post.uploaded_files || []).map(f => f.url || f);
const newPhotos = editPhotos.filter(url => origPhotos.includes(url));
const newUploaded = editPhotos.filter(url => origUploaded.includes(url)).map(url => {
const orig = (post.uploaded_files || []).find(f => (f.url || f) === url);
return orig && typeof orig === 'object' ? orig : { url };
});
// Photos not in either original list go to photos
editPhotos.forEach(url => {
if (!newPhotos.includes(url) && !newUploaded.some(f => (f.url || f) === url)) {
newPhotos.push(url);
}
});
const saveBtn = card.querySelector('.inline-save');
saveBtn.disabled = true;
saveBtn.textContent = 'Сохранение...';
try {
const formData = new FormData();
formData.append('action', 'update_scheduled_post');
formData.append('id', postId);
formData.append('text', text);
formData.append('tags', JSON.stringify(editTags));
formData.append('photos', JSON.stringify(newPhotos));
formData.append('uploaded_files', JSON.stringify(newUploaded));
formData.append('platforms', JSON.stringify(platforms));
formData.append('scheduled_time', scheduledTime);
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
saveBtn.disabled = false;
saveBtn.textContent = '💾 Сохранить';
} else {
showNotification('Пост обновлён!', 'success');
loadScheduledPosts();
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
saveBtn.disabled = false;
saveBtn.textContent = '💾 Сохранить';
}
});
// Publish now
card.querySelector('.inline-publish-now').addEventListener('click', async () => {
const text = card.querySelector('.inline-editor-text').value;
const tgChecked = card.querySelector('.inline-chk-tg').checked;
const vkChecked = card.querySelector('.inline-chk-vk').checked;
const platforms = [];
const tgChannel = document.getElementById('tg-channel')?.value;
const vkGroup = document.getElementById('vk-group')?.value;
if (tgChecked && tgChannel) platforms.push({ type: 'telegram', target: tgChannel });
if (vkChecked && vkGroup) platforms.push({ type: 'vk', target: vkGroup });
if (platforms.length === 0) {
showNotification('Выберите платформу и канал/группу', 'error');
return;
}
if (editPhotos.length === 0 && !text.trim()) {
showNotification('Добавьте фото или текст', 'error');
return;
}
if (!confirm('Опубликовать этот пост сейчас?')) return;
const pubBtn = card.querySelector('.inline-publish-now');
pubBtn.disabled = true;
pubBtn.textContent = 'Публикация...';
try {
// Add tags to text
const tagsStr = editTags.map(t => '#' + t).join(' ');
let fullText = text.trim();
if (tagsStr) {
fullText = fullText ? fullText + '\n\n' + tagsStr : tagsStr;
}
// Add cross-promo
const crossPromoEnabled = post.cross_promo;
if (crossPromoEnabled) {
const storedSettings = getCrossPromoSettings();
if (tgChecked && storedSettings.vkLink) {
// Will be handled per-platform below
}
}
// Publish to each platform
const results = {};
for (const platform of platforms) {
let platformText = fullText;
// Cross-promo
if (crossPromoEnabled) {
const s = getCrossPromoSettings();
if (platform.type === 'telegram' && s.vkLink) {
const parseMode = 'HTML';
platformText += `\n\n<a href="${s.vkLink}">${s.textForTg || 'Мой канал ВКонтакте'}</a>`;
}
if (platform.type === 'vk' && s.telegramLink) {
platformText += `\n\n${s.textForVk || 'Мой канал в Telegram'}: ${s.telegramLink}`;
}
}
const formData = new FormData();
formData.append('action', 'multi_post');
formData.append('platforms', JSON.stringify([platform]));
formData.append('text', platformText);
formData.append('photos', JSON.stringify(editPhotos));
formData.append('uploaded_files', JSON.stringify([]));
formData.append('parse_mode', 'HTML');
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.results) Object.assign(results, data.results);
}
// Check results
const allSuccess = Object.values(results).every(r => r.success);
const anySuccess = Object.values(results).some(r => r.success);
if (anySuccess) {
// Delete the scheduled post
const delForm = new FormData();
delForm.append('action', 'delete_scheduled_post');
delForm.append('id', postId);
await fetch('api.php', { method: 'POST', body: delForm });
showNotification(allSuccess ? 'Опубликовано!' : 'Частично опубликовано', allSuccess ? 'success' : 'warning');
loadScheduledPosts();
} else {
const errors = Object.entries(results).map(([p, r]) => `${p}: ${r.error}`).join(', ');
showNotification('Ошибка: ' + errors, 'error');
pubBtn.disabled = false;
pubBtn.textContent = '🚀 Опубликовать сейчас';
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
pubBtn.disabled = false;
pubBtn.textContent = '🚀 Опубликовать сейчас';
}
});
// Scroll to the card
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// ============ PUBLISHED POSTS ARCHIVE ============
async function loadPublishedPosts() {
const list = document.getElementById('published-posts-list');
if (!list) return;
try {
const formData = new FormData();
formData.append('action', 'get_published_posts');
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.posts && data.posts.length > 0) {
list.innerHTML = data.posts.map(post => {
const publishedDate = post.published_at || post.scheduled_time;
const dateStr = new Date(publishedDate).toLocaleString('ru-RU', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
const platforms = (post.platforms || []).map(p => {
const type = p.type || p;
return type === 'telegram' ? 'TG' : type === 'vk' ? 'VK' : type;
}).join(' · ');
// Check results for success/error
const results = post.results || {};
let statusIcons = '';
Object.entries(results).forEach(([platform, result]) => {
const icon = result.success ? '✓' : '✗';
const platformName = platform === 'telegram' ? 'TG' : platform === 'vk' ? 'VK' : platform;
statusIcons += `<span class="result-${result.success ? 'success' : 'error'}" title="${result.error || 'OK'}">${platformName} ${icon}</span> `;
});
// Collect all photo URLs for preview
const allPhotos = [
...(post.photos || []),
...(post.uploaded_files || []).map(f => f.url || f)
];
// Generate photo preview HTML (show up to 3 photos)
let photosPreviewHtml = '';
if (allPhotos.length > 0) {
const previewPhotos = allPhotos.slice(0, 3);
photosPreviewHtml = `
<div class="archive-photos-preview">
${previewPhotos.map(url => `<img src="${url}" alt="" class="archive-thumb" loading="lazy">`).join('')}
${allPhotos.length > 3 ? `<span class="more-photos">+${allPhotos.length - 3}</span>` : ''}
</div>
`;
}
return `
<div class="archive-post-card">
<div class="archive-post-header">
<span class="archive-time">${dateStr}</span>
<span class="archive-results">${statusIcons}</span>
</div>
${photosPreviewHtml}
${post.text ? `<p class="archive-text">${escapeHtml(post.text.substring(0, 100))}${post.text.length > 100 ? '...' : ''}</p>` : ''}
</div>
`;
}).join('');
} else {
list.innerHTML = '<p class="placeholder">Нет опубликованных постов</p>';
}
} catch (error) {
list.innerHTML = '<p class="placeholder error">Ошибка загрузки</p>';
}
}
// Refresh archive button
document.getElementById('btn-refresh-archive')?.addEventListener('click', loadPublishedPosts);
// Load scheduled and published posts on initial page load
loadScheduledPosts();
loadPublishedPosts();
// Create/Update scheduled post
document.getElementById('btn-create-scheduled')?.addEventListener('click', async () => {
const text = document.getElementById('scheduled-text')?.value || '';
const datetime = document.getElementById('scheduled-datetime')?.value || '';
const tgChecked = document.getElementById('scheduled-chk-telegram')?.checked;
const vkChecked = document.getElementById('scheduled-chk-vk')?.checked;
if (!datetime) {
showNotification('Укажите дату и время публикации', 'error');
return;
}
const platforms = [];
if (tgChecked) platforms.push({ type: 'telegram' });
if (vkChecked) platforms.push({ type: 'vk' });
if (platforms.length === 0) {
showNotification('Выберите хотя бы одну платформу', 'error');
return;
}
// Get photos from gallery selection
const photoUrls = state.selectedPhotos.map(p => p.urls.large || p.urls.original || p.urls.medium640);
const formData = new FormData();
formData.append('action', scheduledState.editingPostId ? 'update_scheduled_post' : 'create_scheduled_post');
if (scheduledState.editingPostId) {
formData.append('id', scheduledState.editingPostId);
}
formData.append('text', text);
formData.append('tags', JSON.stringify([])); // TODO: add tags support
formData.append('photos', JSON.stringify(photoUrls));
formData.append('uploaded_files', JSON.stringify(scheduledState.uploadedFiles.filter(f => f.url)));
formData.append('platforms', JSON.stringify(platforms));
formData.append('scheduled_time', datetime.replace('T', ' '));
try {
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification('Ошибка: ' + data.error, 'error');
} else {
showNotification(scheduledState.editingPostId ? 'Пост обновлён' : 'Пост запланирован!', 'success');
// Reset form
document.getElementById('scheduled-text').value = '';
document.getElementById('scheduled-datetime').value = '';
scheduledState.editingPostId = null;
scheduledState.uploadedFiles = [];
document.getElementById('scheduled-uploaded-preview').innerHTML = '';
const btn = document.getElementById('btn-create-scheduled');
if (btn) btn.textContent = '📅 Запланировать публикацию';
loadScheduledPosts();
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
}
});
// File upload for scheduled posts
document.getElementById('btn-scheduled-upload')?.addEventListener('click', () => {
document.getElementById('scheduled-file-upload')?.click();
});
document.getElementById('scheduled-file-upload')?.addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
if (!files.length) return;
for (const file of files) {
if (file.size > 50 * 1024 * 1024) {
showNotification(`Файл ${file.name} слишком большой`, 'error');
continue;
}
const formData = new FormData();
formData.append('action', 'upload_file');
formData.append('file', file);
try {
const response = await fetch('api.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
showNotification(`Ошибка: ${data.error}`, 'error');
} else {
scheduledState.uploadedFiles.push({
url: data.url,
type: data.type,
name: file.name
});
renderScheduledUploads();
}
} catch (error) {
showNotification('Ошибка загрузки', 'error');
}
}
e.target.value = '';
});
function renderScheduledUploads() {
const preview = document.getElementById('scheduled-uploaded-preview');
if (!preview) return;
preview.innerHTML = scheduledState.uploadedFiles.map((file, idx) => `
<div class="preview-item uploaded-item">
<span class="file-name">${escapeHtml(file.name?.substring(0, 15) || 'file')}</span>
<button type="button" class="remove-uploaded" data-idx="${idx}">&times;</button>
</div>
`).join('');
preview.querySelectorAll('.remove-uploaded').forEach(btn => {
btn.addEventListener('click', () => {
scheduledState.uploadedFiles.splice(parseInt(btn.dataset.idx), 1);
renderScheduledUploads();
});
});
}
// Update scheduled photos preview when switching to tab
function updateScheduledPhotosPreview() {
const preview = document.getElementById('scheduled-photos-preview');
if (!preview) return;
if (state.selectedPhotos.length === 0) {
preview.innerHTML = '<p class="placeholder">Сначала выберите фото в Галерее, затем вернитесь сюда</p>';
} else {
preview.innerHTML = state.selectedPhotos.map(photo => `
<div class="preview-item">
<img src="${photo.urls.small || photo.urls.thumbnail}" class="preview-thumb" alt="">
</div>
`).join('');
}
}
// Update preview when tab is switched
document.querySelectorAll('.nav-btn[data-tab="scheduled"]').forEach(btn => {
btn.addEventListener('click', updateScheduledPhotosPreview);
});
// ============ COPY WITH TAGS ============
document.getElementById('btn-copy-with-tags')?.addEventListener('click', () => {
const output = document.getElementById('output-result');
const title = document.getElementById('converter-title')?.value || '';
const text = document.getElementById('converter-text')?.value || '';
const tags = getTagsString('converter');
let fullText = '';
if (title) fullText += title + '\n\n';
if (text) fullText += text + '\n\n';
fullText += output.value;
if (tags) fullText += '\n\n' + tags;
navigator.clipboard.writeText(fullText).then(() => {
const status = document.getElementById('copy-status');
if (status) {
status.textContent = '✓ Скопировано с тегами!';
status.classList.add('visible');
setTimeout(() => status.classList.remove('visible'), 2000);
}
});
});
// Update copy button to show status
document.getElementById('btn-copy')?.addEventListener('click', () => {
const output = document.getElementById('output-result');
navigator.clipboard.writeText(output.value).then(() => {
const status = document.getElementById('copy-status');
if (status) {
status.textContent = '✓ Скопировано!';
status.classList.add('visible');
setTimeout(() => status.classList.remove('visible'), 2000);
}
});
});
// Check Flickr OAuth status
async function loadFlickrOAuthStatus() {
const statusEl = document.getElementById('flickr-oauth-status');
const btnEl = document.getElementById('flickr-oauth-btn');
const bannerEl = document.getElementById('oauth-banner');
try {
const response = await fetch('api.php?action=flickr_oauth_status');
const data = await response.json();
if (data.authorized) {
// Settings page elements
if (statusEl) {
statusEl.className = 'status connected';
statusEl.textContent = 'Авторизован (оригиналы доступны)';
}
if (btnEl) {
btnEl.textContent = 'Переавторизовать';
btnEl.className = 'btn btn-small btn-secondary';
}
// Hide gallery banner when authorized
if (bannerEl) {
bannerEl.classList.add('hidden');
}
} else {
// Settings page elements
if (statusEl) {
statusEl.className = 'status disconnected';
statusEl.textContent = 'Не авторизован';
}
if (btnEl) {
btnEl.textContent = 'Авторизовать';
btnEl.className = 'btn btn-small btn-primary';
}
// Show gallery banner when not authorized
if (bannerEl) {
bannerEl.classList.remove('hidden');
}
}
} catch (error) {
if (statusEl) {
statusEl.className = 'status disconnected';
statusEl.textContent = 'Ошибка проверки';
}
}
}
// ============ WIDGET SETTINGS ============
const widgetApiUrlInput = document.getElementById('widget-api-url');
const widgetApiUrlCode = document.getElementById('widget-api-url-code');
const widgetEnabled = document.getElementById('widget-enabled');
const widgetMaxPhotos = document.getElementById('widget-max-photos');
const widgetCacheTime = document.getElementById('widget-cache-time');
const widgetAlbumsList = document.getElementById('widget-albums-list');
const btnLoadWidgetAlbums = document.getElementById('btn-load-widget-albums');
const btnSaveWidgetSettings = document.getElementById('btn-save-widget-settings');
const widgetSaveStatus = document.getElementById('widget-save-status');
let widgetSelectedAlbums = [];
// Set API URL
if (widgetApiUrlInput) {
const apiUrl = window.location.origin + window.location.pathname.replace('index.php', '') + 'widget_api.php?action=get_photos';
widgetApiUrlInput.value = apiUrl;
if (widgetApiUrlCode) {
widgetApiUrlCode.textContent = apiUrl;
}
}
// Copy widget URL
window.copyWidgetUrl = function() {
if (widgetApiUrlInput) {
widgetApiUrlInput.select();
document.execCommand('copy');
showNotification('URL скопирован', 'success');
}
};
// Load widget settings
async function loadWidgetSettings() {
try {
const response = await fetch('widget_api.php?action=get_settings');
const data = await response.json();
if (data.success && data.settings) {
const settings = data.settings;
if (widgetEnabled) widgetEnabled.checked = settings.enabled !== false;
if (widgetMaxPhotos) widgetMaxPhotos.value = settings.max_photos || 30;
if (widgetCacheTime) widgetCacheTime.value = settings.cache_time || 3600;
widgetSelectedAlbums = settings.albums || [];
}
} catch (error) {
console.error('Failed to load widget settings:', error);
}
}
// Load albums for widget selection
async function loadWidgetAlbums() {
if (!widgetAlbumsList) return;
widgetAlbumsList.innerHTML = '<p class="loading">Загрузка альбомов...</p>';
try {
const response = await fetch('widget_api.php?action=get_albums');
const data = await response.json();
if (data.success && data.albums) {
renderWidgetAlbums(data.albums);
} else {
widgetAlbumsList.innerHTML = '<p class="error">Ошибка загрузки альбомов</p>';
}
} catch (error) {
widgetAlbumsList.innerHTML = '<p class="error">Ошибка: ' + error.message + '</p>';
}
}
// Render widget albums selection
function renderWidgetAlbums(albums) {
if (!widgetAlbumsList) return;
if (albums.length === 0) {
widgetAlbumsList.innerHTML = '<p class="placeholder">Нет доступных альбомов</p>';
return;
}
widgetAlbumsList.innerHTML = albums.map(album => {
const isSelected = widgetSelectedAlbums.includes(album.id);
const title = album.title?._content || album.title || 'Без названия';
const count = album.photos || 0;
const thumb = album.primary_photo_extras?.url_m ||
album.primary_photo_extras?.url_s ||
album.primary_photo_extras?.url_sq || '';
return `
<label class="widget-album-item ${isSelected ? 'selected' : ''}" data-album-id="${album.id}">
<input type="checkbox" ${isSelected ? 'checked' : ''} value="${album.id}">
${thumb ? `<img src="${thumb}" alt="" class="widget-album-thumb">` : '<div class="widget-album-thumb" style="background:#eee;display:flex;align-items:center;justify-content:center;">📁</div>'}
<span class="widget-album-title">${escapeHtml(title)}</span>
<span class="widget-album-count">${count} фото</span>
</label>
`;
}).join('');
// Add event listeners
widgetAlbumsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', function() {
const albumId = this.value;
const label = this.closest('.widget-album-item');
if (this.checked) {
if (!widgetSelectedAlbums.includes(albumId)) {
widgetSelectedAlbums.push(albumId);
}
label.classList.add('selected');
} else {
widgetSelectedAlbums = widgetSelectedAlbums.filter(id => id !== albumId);
label.classList.remove('selected');
}
});
});
}
// Save widget settings
async function saveWidgetSettings() {
const settings = {
enabled: widgetEnabled ? widgetEnabled.checked : true,
albums: widgetSelectedAlbums,
max_photos: widgetMaxPhotos ? parseInt(widgetMaxPhotos.value) : 30,
cache_time: widgetCacheTime ? parseInt(widgetCacheTime.value) : 3600
};
try {
const response = await fetch('widget_api.php?action=save_settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await response.json();
if (data.success) {
if (widgetSaveStatus) {
widgetSaveStatus.textContent = '✓ Сохранено';
widgetSaveStatus.className = 'save-status success';
setTimeout(() => {
widgetSaveStatus.textContent = '';
}, 3000);
}
showNotification('Настройки виджета сохранены', 'success');
} else {
showNotification(data.error || 'Ошибка сохранения', 'error');
}
} catch (error) {
showNotification('Ошибка: ' + error.message, 'error');
}
}
// Widget event listeners
if (btnLoadWidgetAlbums) {
btnLoadWidgetAlbums.addEventListener('click', loadWidgetAlbums);
}
if (btnSaveWidgetSettings) {
btnSaveWidgetSettings.addEventListener('click', saveWidgetSettings);
}
// Load widget settings on tab switch
document.querySelectorAll('.nav-btn[data-tab="widget"]').forEach(btn => {
btn.addEventListener('click', () => {
loadWidgetSettings();
});
});
// Initial load
loadTelegramStatus();
loadVKStatus();
loadFlickrOAuthStatus();
updateScheduledCount(); // Load scheduled posts count badge
// Restore selected photos and uploaded files UI if any were saved
if (state.selectedPhotos.length > 0 || state.uploadedFiles.length > 0) {
updateSelectionUI();
updatePostingPreview();
if (state.selectedPhotos.length > 0) {
console.log('Restored ' + state.selectedPhotos.length + ' Flickr photos from previous session');
}
if (state.uploadedFiles.length > 0) {
console.log('Restored ' + state.uploadedFiles.length + ' uploaded files from previous session');
}
}
// Auto-load albums on page init
if (albumsGrid) {
const cachedAlbums = getAlbumCache();
if (cachedAlbums && cachedAlbums.length > 0) {
// Use cache first for instant display
console.log('Loading cached albums:', cachedAlbums.length);
window._cachedAlbums = cachedAlbums;
renderAlbumsGrid(cachedAlbums);
// Silently refresh in background
setTimeout(() => refreshAlbumsSilently(), 2000);
} else {
// No cache or empty - load from API
console.log('No cache, loading albums from API...');
loadAlbums(false);
}
}
});