// Main application state and logic function app() { return { state: { token: null, password: '', loading: false, isProcessing: false, statusMessage: '', mediaQueue: [], currentMedia: null }, init() { // Check for existing token on component mount this.checkExistingToken(); // If we have a token, start filling the media queue if (this.state.token) { // Use setTimeout to ensure Alpine is fully initialized setTimeout(() => { this.fillMediaQueue(); }, 0); } }, checkExistingToken() { const savedToken = localStorage.getItem('adminToken'); if (savedToken) { try { // Parse JWT payload to check for expiration const payload = JSON.parse(atob(savedToken.split('.')[1])); const currentTime = Math.floor(Date.now() / 1000); if (payload.exp && payload.exp > currentTime) { this.state.token = savedToken; } else { // Token expired, remove it localStorage.removeItem('adminToken'); } } catch (error) { // Invalid token format, remove it localStorage.removeItem('adminToken'); } } }, async handleLogin() { if (!this.state.password.trim()) { this.showToast("Error", "Please enter a password", "error"); return; } this.state.loading = true; try { const response = await fetch('/admin/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ Password: this.state.password }) }); const data = await response.json(); if (response.ok && data.token) { this.state.token = data.token; localStorage.setItem('adminToken', data.token); this.showToast("Success", data.message || "Login successful"); this.state.password = ''; } else { this.showToast("Error", data.message || "Login failed", "error"); } } catch (error) { this.showToast("Error", "Network error. Please try again.", "error"); } finally { this.state.loading = false; } }, handleGiteaLogin() { // Redirect to the Gitea OAuth endpoint window.location.href = '/admin/api/login/oauth/gitea'; }, handleLogout() { this.state.token = null; localStorage.removeItem('adminToken'); }, async fetchMedia() { try { const response = await fetch('/admin/api/getmedia', { headers: { 'Authorization': `Bearer ${this.state.token}`, }, }); if (response.status === 401) { this.showToast("Session Expired", "Please login again", "error"); this.handleLogout(); // Redirect to login page after logout setTimeout(() => { // Trigger a page reload to update the UI based on state location.reload(); }, 1500); return null; } if (response.ok) { return await response.json(); } else { const errorData = await response.json(); this.showToast("Error", errorData.message || "Failed to fetch media", "error"); return null; } } catch (error) { this.showToast("Error", "Network error. Please try again.", "error"); return null; } }, async fetchNextMedia() { const media = await this.fetchMedia(); if (media) { this.state.mediaQueue.push(media); // Update current media if it's the first item if (this.state.mediaQueue.length === 1) { this.state.currentMedia = media; } // Preload next items (up to 5 in queue total) if (this.state.mediaQueue.length < 5) { this.preloadMedia(); } } }, async fillMediaQueue() { // Don't fetch if already loading or queue is full if (this.state.loading || this.state.mediaQueue.length >= 5) return; // Determine how many more items we need const itemsNeeded = 5 - this.state.mediaQueue.length; // Fetch items one by one for (let i = 0; i < itemsNeeded; i++) { await this.fetchNextMedia(); } }, preloadMedia() { // Preload all items after the first one in the queue this.state.mediaQueue.slice(1, 5).forEach(media => { if (!media.preview_url) return; const img = new Image(); img.onload = () => { // Successful preload - nothing to do }; img.onerror = () => { // This media is broken even before we show it: remove it now this.state.mediaQueue = this.state.mediaQueue.filter(item => item.id !== media.id); // Update current media if needed if (this.state.currentMedia && this.state.currentMedia.id === media.id) { this.state.currentMedia = this.state.mediaQueue.length > 0 ? this.state.mediaQueue[0] : null; } }; img.src = media.preview_url; }); }, async handleAction(action) { if (!this.state.currentMedia) return; this.state.isProcessing = true; this.state.statusMessage = `${action === 'approve' ? 'Approving' : 'Rejecting'}...`; try { const response = await fetch(`/admin/api/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.state.token}`, }, body: JSON.stringify({ mediaId: this.state.currentMedia.id }), }); if (response.status === 401) { this.showToast("Session Expired", "Please login again", "error"); this.handleLogout(); // Redirect to login page after logout setTimeout(() => { // Trigger a page reload to update the UI based on state location.reload(); }, 1500); return; } const data = await response.json(); if (response.ok) { this.state.statusMessage = `${action === 'approve' ? 'Approved' : 'Rejected'} successfully!`; // Show success message briefly, then move to the next item setTimeout(() => { // Remove the current item from the queue this.state.mediaQueue.shift(); // Update current media if (this.state.mediaQueue.length > 0) { this.state.currentMedia = this.state.mediaQueue[0]; // Fetch next item to keep queue full this.fetchNextMedia(); } else { this.state.currentMedia = null; } this.state.statusMessage = ''; this.state.isProcessing = false; }, 1000); } else { this.showToast("Error", data.message || `Failed to ${action} media`, "error"); this.state.isProcessing = false; this.state.statusMessage = ''; } } catch (error) { this.showToast("Error", "Network error. Please try again.", "error"); this.state.isProcessing = false; this.state.statusMessage = ''; } }, handleImageError(e) { const imgEl = e.target; if (!imgEl.dataset.fallbackTried) { imgEl.dataset.fallbackTried = 'true'; imgEl.src = this.state.currentMedia.remote_url; } else { // Both preview AND remote have failed - drop it this.state.mediaQueue = this.state.mediaQueue.filter( item => item.id !== this.state.currentMedia.id ); // Update current media if (this.state.mediaQueue.length > 0) { this.state.currentMedia = this.state.mediaQueue[0]; } else { this.state.currentMedia = null; } } }, showToast(title, message, variant = 'default') { // Simple toast notification const toast = document.createElement('div'); toast.className = `fixed top-4 right-4 p-4 rounded-md shadow-lg z-50 max-w-md ${ variant === 'error' ? 'bg-red-100 border border-red-300 text-red-700' : 'bg-green-100 border border-green-300 text-green-700' }`; toast.innerHTML = `