/**
* 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 => `
#${escapeHtml(tag)}
`).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 = '
Нажмите кнопку выше чтобы добавить фото
';
return;
}
postPhotosPreview.innerHTML = '';
// Render Flickr photos
state.selectedPhotos.forEach((photo, index) => {
const div = document.createElement('div');
div.className = 'preview-thumb';
div.innerHTML = `
flickr
`;
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
? `▶`
: `
`
}
${file.uploading ? '' : ''}
файл
`;
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 = 'Альбомы не найдены
';
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
? `
`
: `📁
`
}
${escapeHtml(title)}
${count} фото
⋮⋮
`;
// 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 = `
Загрузка альбомов с Flickr...
`;
if (dragHint) dragHint.classList.add('hidden');
// Also update button state
if (btnLoadAlbums) {
btnLoadAlbums.disabled = true;
btnLoadAlbums.innerHTML = '⏳ Загрузка...';
}
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 = 'Альбомы не найдены. Проверьте настройки Flickr API.
';
showNotification('Альбомы не найдены', 'error');
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
albumsGrid.innerHTML = `
⏱️
Превышено время ожидания. Flickr API не отвечает.
`;
showNotification('Таймаут: сервер не отвечает', 'error');
return;
}
console.error('Ошибка загрузки альбомов:', error);
albumsGrid.innerHTML = `
⚠️
Ошибка загрузки: ${escapeHtml(error.message)}
`;
showNotification('Ошибка: ' + error.message, 'error');
} finally {
state.isLoadingAlbums = false;
if (btnLoadAlbums) {
btnLoadAlbums.disabled = false;
btnLoadAlbums.innerHTML = '↻ Загрузить альбомы';
}
}
}
// 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 = `
Загрузка альбомов...
`;
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 = `
`;
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 = `Ошибка: ${data.error}
`;
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 = 'Фотографии не найдены
';
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 = `Ошибка: ${error.message}
`;
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.innerHTML = `
${videoBadge}
${escapeHtml(photo.title)}
`;
// 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 = `
Загрузка фотографий...
`;
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 = `
Скачать ${photos.length} фото
Выберите формат загрузки:
`;
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 = `
`;
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 = `
▶
Смотреть на Flickr
`;
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
? '✓ Выбрано'
: '+ Выбрать';
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
? '✓ Выбрано'
: '+ Выбрать';
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 = '';
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 = '';
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${linkText}`;
} 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 => `
#${escapeHtml(tag)}
`).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 => `
#${escapeHtml(t.name)}
${t.count}×
`).join('');
suggestions.classList.add('visible');
}
return;
}
if (items.length === 0) {
hideSuggestions();
return;
}
suggestions.innerHTML = items.map(t => `
#${escapeHtml(t.name)}
${t.count}×
`).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 => `
`).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 => `
#${escapeHtml(tag)}
`).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 = 'Нет пресетов. Добавьте первый!
';
return;
}
container.innerHTML = presets.map(preset => `
${escapeHtml(preset.name)}
${preset.tags.map(t => '#' + escapeHtml(t)).join(' ')}
`).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 = ''; 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 = '';
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 = 'Нет запланированных постов
';
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 = `
${previewPhotos.map(url => `

`).join('')}
${moreCount > 0 ? `
+${moreCount}` : ''}
`;
}
return `
${photosPreviewHtml}
${post.text ? `
${escapeHtml(post.text.substring(0, 150))}${post.text.length > 150 ? '...' : ''}
` : ''}
${post.tags?.length ? `
${post.tags.slice(0, 5).map(t => '#' + t).join(' ')}` : ''}
`;
}).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 = 'Нет запланированных постов
';
updateScheduledCount(0);
}
} catch (error) {
list.innerHTML = 'Ошибка загрузки
';
}
}
// 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 = `
${allPhotos.length > 0 ? allPhotos.map((url, i) => `
`).join('') : '
Нет фото
'}
`;
// 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) => `
`).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 = 'Нет фото
';
}
});
});
} else {
photosContainer.innerHTML = 'Нет фото
';
}
});
});
// 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)} `;
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${s.textForTg || 'Мой канал ВКонтакте'}`;
}
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 += `${platformName} ${icon} `;
});
// 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 = `
${previewPhotos.map(url => `

`).join('')}
${allPhotos.length > 3 ? `
+${allPhotos.length - 3}` : ''}
`;
}
return `
${photosPreviewHtml}
${post.text ? `
${escapeHtml(post.text.substring(0, 100))}${post.text.length > 100 ? '...' : ''}
` : ''}
`;
}).join('');
} else {
list.innerHTML = 'Нет опубликованных постов
';
}
} catch (error) {
list.innerHTML = 'Ошибка загрузки
';
}
}
// 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) => `
${escapeHtml(file.name?.substring(0, 15) || 'file')}
`).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 = 'Сначала выберите фото в Галерее, затем вернитесь сюда
';
} else {
preview.innerHTML = state.selectedPhotos.map(photo => `
`).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 = 'Загрузка альбомов...
';
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 = 'Ошибка загрузки альбомов
';
}
} catch (error) {
widgetAlbumsList.innerHTML = 'Ошибка: ' + error.message + '
';
}
}
// Render widget albums selection
function renderWidgetAlbums(albums) {
if (!widgetAlbumsList) return;
if (albums.length === 0) {
widgetAlbumsList.innerHTML = 'Нет доступных альбомов
';
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 `
`;
}).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);
}
}
});