917 lines
32 KiB
JavaScript
917 lines
32 KiB
JavaScript
/**
|
||
* Enhanced Main Application with Physics-Based Swipe Animations
|
||
* Integrates SwipePhysics engine and EnhancedSwipeCard component
|
||
*/
|
||
|
||
import { showToast, updateImageInfo } from './utils.js';
|
||
import { swipeIntegration, createSwipeSettingsPanel } from './swipe-integration.js';
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Enhanced application state
|
||
const state = {
|
||
currentImageInfo: null,
|
||
currentOrientation: ['all'],
|
||
currentActions: ['Unactioned'],
|
||
previousOrientation: ['all'],
|
||
allowNsfw: false,
|
||
searchKeywords: [],
|
||
sortOrder: 'random',
|
||
isLoading: false,
|
||
|
||
// Physics and animation state
|
||
animationQuality: getAnimationQuality(),
|
||
performanceMode: getPerformanceMode(),
|
||
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||
|
||
// Statistics for performance monitoring
|
||
stats: {
|
||
totalSwipes: 0,
|
||
averageSwipeTime: 0,
|
||
lastSwipeTime: 0,
|
||
performanceMetrics: []
|
||
}
|
||
};
|
||
|
||
// DOM elements
|
||
const lastActionText = document.getElementById('last-action');
|
||
const orientationFilters = document.querySelector('.orientation-filters');
|
||
const actionFilters = document.querySelector('.action-filters');
|
||
const modal = document.getElementById('fullscreen-modal');
|
||
const fullscreenImage = document.getElementById('fullscreen-image');
|
||
const closeModal = document.querySelector('.close-modal');
|
||
const searchInput = document.getElementById('search-input');
|
||
const nsfwToggleBtn = document.getElementById('toggle-nsfw');
|
||
const searchButton = document.getElementById('search-button');
|
||
const keywordPillsContainer = document.getElementById('keyword-pills-container');
|
||
const fullscreenToggle = document.getElementById('fullscreen-toggle');
|
||
|
||
// Initialize swipe system using integration manager
|
||
let enhancedSwipeCard = null;
|
||
|
||
// Initialize the swipe integration system
|
||
swipeIntegration.initialize(
|
||
document.querySelector('.swipe-container'),
|
||
performEnhancedSwipe,
|
||
{
|
||
threshold: 120,
|
||
velocityThreshold: state.reducedMotion ? 1 : 3,
|
||
springTension: state.animationQuality === 'high' ? 320 : 280,
|
||
springFriction: state.animationQuality === 'high' ? 28 : 35,
|
||
mass: 1.1,
|
||
momentumDecay: 0.91,
|
||
enablePhysics: true,
|
||
enableVisualEffects: true,
|
||
enableHapticFeedback: true,
|
||
animationQuality: state.animationQuality,
|
||
performanceMode: state.performanceMode
|
||
}
|
||
).then(() => {
|
||
enhancedSwipeCard = swipeIntegration.getSwipeCard();
|
||
console.log('Swipe system initialized successfully');
|
||
|
||
// Setup modal interactions after swipe card is initialized
|
||
if (enhancedSwipeCard && enhancedSwipeCard.card) {
|
||
enhancedSwipeCard.card.addEventListener('click', () => {
|
||
if (!enhancedSwipeCard.state.hasMoved && state.currentImageInfo) {
|
||
openEnhancedModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Load initial image after initialization
|
||
loadNewImageWithAnimation();
|
||
}).catch(error => {
|
||
console.error('Failed to initialize swipe system:', error);
|
||
// Fallback to basic functionality
|
||
loadNewImageWithAnimation();
|
||
});
|
||
|
||
// Performance monitoring
|
||
let performanceObserver;
|
||
if ('PerformanceObserver' in window) {
|
||
performanceObserver = new PerformanceObserver((list) => {
|
||
const entries = list.getEntries();
|
||
entries.forEach(entry => {
|
||
if (entry.entryType === 'measure' && entry.name.includes('swipe')) {
|
||
state.stats.performanceMetrics.push({
|
||
name: entry.name,
|
||
duration: entry.duration,
|
||
timestamp: entry.startTime
|
||
});
|
||
|
||
// Keep only last 50 measurements
|
||
if (state.stats.performanceMetrics.length > 50) {
|
||
state.stats.performanceMetrics.shift();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
performanceObserver.observe({ entryTypes: ['measure'] });
|
||
}
|
||
|
||
/**
|
||
* Enhanced swipe handler with physics feedback
|
||
*/
|
||
function performEnhancedSwipe(direction) {
|
||
if (!state.currentImageInfo) return;
|
||
|
||
// Prevent duplicate calls within a short time window
|
||
const now = performance.now();
|
||
if (state.stats.lastSwipeTime && (now - state.stats.lastSwipeTime) < 500) {
|
||
console.log('Duplicate swipe prevented:', direction);
|
||
return;
|
||
}
|
||
|
||
const swipeStartTime = now;
|
||
performance.mark('swipe-start');
|
||
|
||
// Update statistics
|
||
state.stats.totalSwipes++;
|
||
state.stats.lastSwipeTime = swipeStartTime;
|
||
|
||
// Enhanced action mapping with more descriptive feedback
|
||
const actionMap = {
|
||
left: 'Discarded',
|
||
right: 'Kept',
|
||
up: 'Favorited',
|
||
down: 'Marked for review'
|
||
};
|
||
|
||
// Backend action mapping (single letters for simplicity and backwards compatibility)
|
||
const backendActionMap = {
|
||
left: 'D', // Discard
|
||
right: 'K', // Keep
|
||
up: 'F', // Favourite
|
||
down: 'R' // Review
|
||
};
|
||
|
||
const actionName = actionMap[direction] || direction;
|
||
const backendAction = backendActionMap[direction] || direction;
|
||
|
||
lastActionText.textContent = `Last action: ${actionName}`;
|
||
|
||
// Enhanced toast with physics-based animation
|
||
showEnhancedToast(actionName, direction);
|
||
|
||
// Record selection with performance tracking - use backend action name
|
||
recordEnhancedSelection(state.currentImageInfo, backendAction);
|
||
|
||
// Add haptic feedback for supported devices
|
||
if ('vibrate' in navigator && !state.reducedMotion) {
|
||
const vibrationPattern = getVibrationPattern(direction);
|
||
navigator.vibrate(vibrationPattern);
|
||
}
|
||
|
||
// Performance measurement
|
||
performance.mark('swipe-end');
|
||
performance.measure('swipe-complete', 'swipe-start', 'swipe-end');
|
||
|
||
// Update average swipe time
|
||
const swipeTime = performance.now() - swipeStartTime;
|
||
state.stats.averageSwipeTime = (
|
||
(state.stats.averageSwipeTime * (state.stats.totalSwipes - 1) + swipeTime) /
|
||
state.stats.totalSwipes
|
||
);
|
||
|
||
// Load new image with staggered animation
|
||
setTimeout(() => {
|
||
loadNewImageWithAnimation();
|
||
}, state.animationQuality === 'high' ? 600 : 400);
|
||
}
|
||
|
||
/**
|
||
* Enhanced toast notification with direction-based styling
|
||
*/
|
||
function showEnhancedToast(message, direction) {
|
||
const toastEl = document.getElementById('toast');
|
||
if (!toastEl) return;
|
||
|
||
// Add direction-specific class for styling
|
||
toastEl.className = `toast enhanced-toast toast-${direction}`;
|
||
|
||
// Enhanced message with icon
|
||
const icons = {
|
||
left: '🗑️',
|
||
right: '✅',
|
||
up: '⭐',
|
||
down: '⏰'
|
||
};
|
||
|
||
toastEl.innerHTML = `
|
||
<span class="toast-icon">${icons[direction] || '📱'}</span>
|
||
<span class="toast-message">${message}</span>
|
||
`;
|
||
|
||
toastEl.classList.add('show');
|
||
|
||
// Auto-hide with enhanced timing
|
||
setTimeout(() => {
|
||
toastEl.classList.remove('show');
|
||
setTimeout(() => {
|
||
toastEl.className = 'toast';
|
||
}, 300);
|
||
}, state.reducedMotion ? 2000 : 3000);
|
||
}
|
||
|
||
/**
|
||
* Get vibration pattern based on swipe direction
|
||
*/
|
||
function getVibrationPattern(direction) {
|
||
const patterns = {
|
||
left: [50, 30, 50], // Double tap for discard
|
||
right: [100], // Single strong for keep
|
||
up: [30, 20, 30, 20, 30], // Triple tap for favorite
|
||
down: [80, 40, 80] // Double long for review
|
||
};
|
||
return patterns[direction] || [50];
|
||
}
|
||
|
||
/**
|
||
* Enhanced selection recording with retry logic
|
||
*/
|
||
async function recordEnhancedSelection(imageInfo, action) {
|
||
const maxRetries = 3;
|
||
let retryCount = 0;
|
||
|
||
const attemptRecord = async () => {
|
||
try {
|
||
const response = await fetch('/selection', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Request-ID': generateRequestId()
|
||
},
|
||
body: JSON.stringify({
|
||
image_path: imageInfo.path,
|
||
action,
|
||
timestamp: Date.now(),
|
||
swipe_stats: {
|
||
total_swipes: state.stats.totalSwipes,
|
||
average_time: state.stats.averageSwipeTime
|
||
}
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('Enhanced selection recorded:', data);
|
||
|
||
return data;
|
||
} catch (error) {
|
||
console.error(`Selection recording attempt ${retryCount + 1} failed:`, error);
|
||
|
||
if (retryCount < maxRetries) {
|
||
retryCount++;
|
||
// Exponential backoff
|
||
const delay = Math.pow(2, retryCount) * 1000;
|
||
await new Promise(resolve => setTimeout(resolve, delay));
|
||
return attemptRecord();
|
||
} else {
|
||
// Show user-friendly error
|
||
showEnhancedToast('Failed to save selection', 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
return attemptRecord();
|
||
}
|
||
|
||
/**
|
||
* Load new image with enhanced animations
|
||
*/
|
||
function loadNewImageWithAnimation() {
|
||
if (state.isLoading) return;
|
||
|
||
state.isLoading = true;
|
||
performance.mark('image-load-start');
|
||
|
||
// Show enhanced loading state
|
||
if (enhancedSwipeCard && enhancedSwipeCard.showLoading) {
|
||
enhancedSwipeCard.showLoading();
|
||
}
|
||
|
||
const params = new URLSearchParams({
|
||
orientation: state.currentOrientation.join(','),
|
||
t: new Date().getTime(),
|
||
allow_nsfw: state.allowNsfw ? '1' : '0',
|
||
sort: state.sortOrder
|
||
});
|
||
|
||
if (state.searchKeywords.length > 0) {
|
||
params.append('search', state.searchKeywords.join(','));
|
||
}
|
||
|
||
if (state.currentActions.length > 0) {
|
||
params.append('actions', state.currentActions.join(','));
|
||
}
|
||
|
||
fetch(`/random-image?${params.toString()}`)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
state.isLoading = false;
|
||
if (enhancedSwipeCard && enhancedSwipeCard.hideLoading) {
|
||
enhancedSwipeCard.hideLoading();
|
||
}
|
||
|
||
performance.mark('image-load-end');
|
||
performance.measure('image-load-complete', 'image-load-start', 'image-load-end');
|
||
|
||
if (data && data.path) {
|
||
state.currentImageInfo = data;
|
||
|
||
// Set image with enhanced loading animation
|
||
if (enhancedSwipeCard && enhancedSwipeCard.setImage) {
|
||
enhancedSwipeCard.setImage(data);
|
||
}
|
||
updateImageInfo(data);
|
||
adjustContainerToImageWithAnimation(data.orientation);
|
||
|
||
// Preload next image for smoother experience
|
||
if (state.performanceMode !== 'low') {
|
||
preloadNextImage();
|
||
}
|
||
} else {
|
||
handleNoImagesState(data);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Enhanced image loading error:', error);
|
||
state.isLoading = false;
|
||
if (enhancedSwipeCard && enhancedSwipeCard.hideLoading) {
|
||
enhancedSwipeCard.hideLoading();
|
||
}
|
||
|
||
// Show enhanced error state
|
||
showEnhancedError('Failed to load image. Please try again.');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Adjust container with smooth animation
|
||
*/
|
||
function adjustContainerToImageWithAnimation(orientation) {
|
||
const container = document.querySelector('.swipe-container');
|
||
if (window.innerWidth < 992) return; // Skip on mobile
|
||
|
||
container.style.transition = 'all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
|
||
|
||
if (orientation === 'landscape') {
|
||
container.style.flex = '4';
|
||
container.style.transform = 'scale(1.02)';
|
||
} else {
|
||
container.style.flex = '2';
|
||
container.style.transform = 'scale(1)';
|
||
}
|
||
|
||
// Reset transform after animation
|
||
setTimeout(() => {
|
||
container.style.transform = '';
|
||
}, 600);
|
||
}
|
||
|
||
/**
|
||
* Preload next image for smoother experience
|
||
*/
|
||
function preloadNextImage() {
|
||
const params = new URLSearchParams({
|
||
orientation: state.currentOrientation.join(','),
|
||
allow_nsfw: state.allowNsfw ? '1' : '0',
|
||
sort: state.sortOrder,
|
||
preload: '1'
|
||
});
|
||
|
||
fetch(`/random-image?${params.toString()}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data && data.path) {
|
||
// Preload the image
|
||
const img = new Image();
|
||
img.src = data.path;
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// Silently fail preloading
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Handle no images state with enhanced feedback
|
||
*/
|
||
function handleNoImagesState(data) {
|
||
const message = data?.message || 'No more images available.';
|
||
if (enhancedSwipeCard && enhancedSwipeCard.card) {
|
||
enhancedSwipeCard.card.innerHTML = `
|
||
<div class="no-images-message enhanced-message">
|
||
<div class="no-images-icon">📷</div>
|
||
<div class="no-images-text">${message}</div>
|
||
<button class="retry-btn" onclick="location.reload()">Refresh</button>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// Fallback for when swipe card isn't initialized
|
||
const container = document.querySelector('.swipe-container .image-card');
|
||
if (container) {
|
||
container.innerHTML = `
|
||
<div class="no-images-message enhanced-message">
|
||
<div class="no-images-icon">📷</div>
|
||
<div class="no-images-text">${message}</div>
|
||
<button class="retry-btn" onclick="location.reload()">Refresh</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
updateImageInfo({
|
||
filename: 'No image',
|
||
creation_date: '',
|
||
resolution: '',
|
||
prompt_data: ''
|
||
});
|
||
state.currentImageInfo = null;
|
||
}
|
||
|
||
/**
|
||
* Show enhanced error message
|
||
*/
|
||
function showEnhancedError(message) {
|
||
if (enhancedSwipeCard && enhancedSwipeCard.card) {
|
||
enhancedSwipeCard.card.innerHTML = `
|
||
<div class="error-message enhanced-error">
|
||
<div class="error-icon">⚠️</div>
|
||
<div class="error-text">${message}</div>
|
||
<button class="retry-btn" onclick="location.reload()">Try Again</button>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// Fallback for when swipe card isn't initialized
|
||
const container = document.querySelector('.swipe-container .image-card');
|
||
if (container) {
|
||
container.innerHTML = `
|
||
<div class="error-message enhanced-error">
|
||
<div class="error-icon">⚠️</div>
|
||
<div class="error-text">${message}</div>
|
||
<button class="retry-btn" onclick="location.reload()">Try Again</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Enhanced event listeners with improved performance
|
||
setupEnhancedEventListeners();
|
||
setupKeyboardShortcuts();
|
||
setupPerformanceMonitoring();
|
||
|
||
/**
|
||
* Setup enhanced event listeners
|
||
*/
|
||
function setupEnhancedEventListeners() {
|
||
// Orientation filters with enhanced feedback
|
||
orientationFilters?.addEventListener('click', (e) => {
|
||
const button = e.target.closest('button');
|
||
if (!button) return;
|
||
|
||
// Add ripple effect
|
||
createRippleEffect(button, e);
|
||
|
||
const clickedOrientation = button.dataset.orientation;
|
||
|
||
if (clickedOrientation === 'all') {
|
||
state.currentOrientation = ['all'];
|
||
} else {
|
||
if (state.currentOrientation.length === 1 && state.currentOrientation[0] === 'all') {
|
||
state.currentOrientation = [];
|
||
}
|
||
|
||
const index = state.currentOrientation.indexOf(clickedOrientation);
|
||
if (index > -1) {
|
||
state.currentOrientation.splice(index, 1);
|
||
} else {
|
||
state.currentOrientation.push(clickedOrientation);
|
||
}
|
||
}
|
||
|
||
if (state.currentOrientation.length === 0) {
|
||
state.currentOrientation = ['all'];
|
||
}
|
||
|
||
updateFilterButtonStates(orientationFilters, state.currentOrientation);
|
||
loadNewImageWithAnimation();
|
||
});
|
||
|
||
// Action filters with enhanced feedback
|
||
actionFilters?.addEventListener('click', (e) => {
|
||
const button = e.target.closest('button');
|
||
if (!button) return;
|
||
|
||
createRippleEffect(button, e);
|
||
|
||
const clickedAction = button.dataset.action;
|
||
|
||
if (state.currentActions.length === 1 && state.currentActions[0] === 'Unactioned' && clickedAction !== 'Unactioned') {
|
||
state.currentActions = [];
|
||
}
|
||
|
||
const index = state.currentActions.indexOf(clickedAction);
|
||
if (index > -1) {
|
||
state.currentActions.splice(index, 1);
|
||
} else {
|
||
state.currentActions.push(clickedAction);
|
||
}
|
||
|
||
if (state.currentActions.length === 0) {
|
||
state.currentActions = ['Unactioned'];
|
||
}
|
||
|
||
updateFilterButtonStates(actionFilters, state.currentActions);
|
||
loadNewImageWithAnimation();
|
||
});
|
||
|
||
// Enhanced search functionality
|
||
searchButton?.addEventListener('click', addSearchKeywordWithAnimation);
|
||
searchInput?.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
addSearchKeywordWithAnimation();
|
||
}
|
||
});
|
||
|
||
closeModal?.addEventListener('click', closeEnhancedModal);
|
||
modal?.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
closeEnhancedModal();
|
||
}
|
||
});
|
||
|
||
// NSFW toggle with enhanced feedback
|
||
nsfwToggleBtn?.addEventListener('click', () => {
|
||
state.allowNsfw = !state.allowNsfw;
|
||
nsfwToggleBtn.dataset.allow = state.allowNsfw ? '1' : '0';
|
||
nsfwToggleBtn.classList.toggle('active', state.allowNsfw);
|
||
|
||
// Enhanced visual feedback
|
||
createButtonPulse(nsfwToggleBtn);
|
||
loadNewImageWithAnimation();
|
||
});
|
||
|
||
// Sort order change with enhanced feedback
|
||
document.getElementById('sort-order')?.addEventListener('change', (e) => {
|
||
state.sortOrder = e.target.value;
|
||
showEnhancedToast(`Sorted by: ${e.target.selectedOptions[0].text}`, 'info');
|
||
loadNewImageWithAnimation();
|
||
});
|
||
|
||
// Button click handlers with enhanced feedback
|
||
document.getElementById('btn-left')?.addEventListener('click', () => {
|
||
createButtonPulse(document.getElementById('btn-left'));
|
||
performEnhancedSwipe('left');
|
||
});
|
||
|
||
document.getElementById('btn-right')?.addEventListener('click', () => {
|
||
createButtonPulse(document.getElementById('btn-right'));
|
||
performEnhancedSwipe('right');
|
||
});
|
||
|
||
document.getElementById('btn-up')?.addEventListener('click', () => {
|
||
createButtonPulse(document.getElementById('btn-up'));
|
||
performEnhancedSwipe('up');
|
||
});
|
||
|
||
document.getElementById('btn-down')?.addEventListener('click', () => {
|
||
createButtonPulse(document.getElementById('btn-down'));
|
||
performEnhancedSwipe('down');
|
||
});
|
||
|
||
// Fullscreen toggle functionality
|
||
fullscreenToggle?.addEventListener('click', () => {
|
||
toggleFullscreenMode();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Setup keyboard shortcuts with enhanced feedback
|
||
*/
|
||
function setupKeyboardShortcuts() {
|
||
document.addEventListener('keydown', (e) => {
|
||
if (state.isLoading || document.activeElement === searchInput) return;
|
||
if (modal?.style.display === 'flex') return;
|
||
|
||
const keyMap = {
|
||
ArrowLeft: 'left',
|
||
ArrowRight: 'right',
|
||
ArrowUp: 'up',
|
||
ArrowDown: 'down',
|
||
KeyA: 'left',
|
||
KeyD: 'right',
|
||
KeyW: 'up',
|
||
KeyS: 'down'
|
||
};
|
||
|
||
if (keyMap[e.code]) {
|
||
e.preventDefault();
|
||
|
||
// Visual feedback for keyboard shortcuts
|
||
const button = document.getElementById(`btn-${keyMap[e.code]}`);
|
||
if (button) {
|
||
createButtonPulse(button);
|
||
}
|
||
|
||
performEnhancedSwipe(keyMap[e.code]);
|
||
}
|
||
|
||
// Modal controls
|
||
if (modal?.style.display === 'flex' && e.key === 'Escape') {
|
||
closeEnhancedModal();
|
||
}
|
||
|
||
// Fullscreen controls
|
||
if (e.key === 'Escape' && document.body.classList.contains('fullscreen-mode')) {
|
||
e.preventDefault();
|
||
toggleFullscreenMode();
|
||
}
|
||
|
||
// F11 alternative for fullscreen (F key)
|
||
if (e.key === 'f' || e.key === 'F') {
|
||
e.preventDefault();
|
||
toggleFullscreenMode();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Setup performance monitoring
|
||
*/
|
||
function setupPerformanceMonitoring() {
|
||
// Monitor frame rate
|
||
let frameCount = 0;
|
||
let lastTime = performance.now();
|
||
|
||
function monitorFPS() {
|
||
frameCount++;
|
||
const currentTime = performance.now();
|
||
|
||
if (currentTime - lastTime >= 1000) {
|
||
const fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
|
||
|
||
// Adjust animation quality based on performance
|
||
if (fps < 30 && state.animationQuality === 'high') {
|
||
state.animationQuality = 'medium';
|
||
console.log('Reduced animation quality due to low FPS:', fps);
|
||
} else if (fps > 50 && state.animationQuality === 'medium') {
|
||
state.animationQuality = 'high';
|
||
console.log('Increased animation quality due to good FPS:', fps);
|
||
}
|
||
|
||
frameCount = 0;
|
||
lastTime = currentTime;
|
||
}
|
||
|
||
requestAnimationFrame(monitorFPS);
|
||
}
|
||
|
||
if (state.performanceMode !== 'low') {
|
||
requestAnimationFrame(monitorFPS);
|
||
}
|
||
}
|
||
|
||
// Utility functions
|
||
function getAnimationQuality() {
|
||
const userAgent = navigator.userAgent.toLowerCase();
|
||
const isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(userAgent);
|
||
const isLowEnd = /android.*chrome\/[1-6][0-9]/.test(userAgent);
|
||
|
||
if (isLowEnd) return 'low';
|
||
if (isMobile) return 'medium';
|
||
return 'high';
|
||
}
|
||
|
||
function getPerformanceMode() {
|
||
if ('connection' in navigator) {
|
||
const connection = navigator.connection;
|
||
if (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g') {
|
||
return 'low';
|
||
}
|
||
}
|
||
return 'normal';
|
||
}
|
||
|
||
function generateRequestId() {
|
||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||
}
|
||
|
||
function createRippleEffect(button, event) {
|
||
if (state.reducedMotion) return;
|
||
|
||
const ripple = document.createElement('span');
|
||
ripple.classList.add('ripple-effect');
|
||
|
||
const rect = button.getBoundingClientRect();
|
||
const size = Math.max(rect.width, rect.height);
|
||
const x = event.clientX - rect.left - size / 2;
|
||
const y = event.clientY - rect.top - size / 2;
|
||
|
||
ripple.style.width = ripple.style.height = `${size}px`;
|
||
ripple.style.left = `${x}px`;
|
||
ripple.style.top = `${y}px`;
|
||
|
||
button.appendChild(ripple);
|
||
|
||
setTimeout(() => {
|
||
ripple.remove();
|
||
}, 600);
|
||
}
|
||
|
||
function createButtonPulse(button) {
|
||
if (state.reducedMotion) return;
|
||
|
||
button.style.transform = 'scale(0.95)';
|
||
setTimeout(() => {
|
||
button.style.transform = '';
|
||
}, 150);
|
||
}
|
||
|
||
function updateFilterButtonStates(container, activeStates) {
|
||
container.querySelectorAll('button').forEach(btn => {
|
||
const isActive = activeStates.includes(btn.dataset.orientation || btn.dataset.action);
|
||
btn.classList.toggle('active', isActive);
|
||
});
|
||
}
|
||
|
||
function addSearchKeywordWithAnimation() {
|
||
const newKeyword = searchInput.value.trim();
|
||
if (newKeyword && !state.searchKeywords.includes(newKeyword)) {
|
||
state.searchKeywords.push(newKeyword);
|
||
renderKeywordPillsWithAnimation();
|
||
loadNewImageWithAnimation();
|
||
}
|
||
searchInput.value = '';
|
||
searchInput.focus();
|
||
}
|
||
|
||
function renderKeywordPillsWithAnimation() {
|
||
keywordPillsContainer.innerHTML = '';
|
||
|
||
state.searchKeywords.forEach((keyword, index) => {
|
||
const pill = document.createElement('div');
|
||
pill.className = 'keyword-pill enhanced-pill';
|
||
pill.style.animationDelay = `${index * 100}ms`;
|
||
pill.innerHTML = `
|
||
<span class="pill-text">${keyword}</span>
|
||
<button class="remove-keyword" data-keyword="${keyword}">×</button>
|
||
`;
|
||
|
||
keywordPillsContainer.appendChild(pill);
|
||
});
|
||
|
||
// Add event listeners for removal
|
||
keywordPillsContainer.addEventListener('click', (e) => {
|
||
if (e.target.classList.contains('remove-keyword')) {
|
||
const keywordToRemove = e.target.dataset.keyword;
|
||
state.searchKeywords = state.searchKeywords.filter(k => k !== keywordToRemove);
|
||
renderKeywordPillsWithAnimation();
|
||
loadNewImageWithAnimation();
|
||
}
|
||
});
|
||
}
|
||
|
||
function openEnhancedModal() {
|
||
if (!state.currentImageInfo) return;
|
||
|
||
fullscreenImage.src = state.currentImageInfo.path;
|
||
document.getElementById('modal-resolution').textContent = `Resolution: ${state.currentImageInfo.resolution}`;
|
||
document.getElementById('modal-filename').textContent = `Filename: ${state.currentImageInfo.filename || 'N/A'}`;
|
||
document.getElementById('modal-creation-date').textContent = `Creation Date: ${state.currentImageInfo.creation_date || 'N/A'}`;
|
||
document.getElementById('modal-prompt-data').textContent = `Prompt: ${state.currentImageInfo.prompt_data || 'N/A'}`;
|
||
|
||
modal.style.display = 'flex';
|
||
|
||
if (!state.reducedMotion) {
|
||
setTimeout(() => {
|
||
modal.classList.add('show');
|
||
}, 10);
|
||
} else {
|
||
modal.classList.add('show');
|
||
}
|
||
}
|
||
|
||
function closeEnhancedModal() {
|
||
modal.classList.remove('show');
|
||
|
||
setTimeout(() => {
|
||
modal.style.display = 'none';
|
||
}, state.reducedMotion ? 0 : 400);
|
||
}
|
||
|
||
// Setup settings panel
|
||
setupSettingsPanel();
|
||
|
||
// Expose state for debugging
|
||
window.enhancedSwipeState = state;
|
||
window.enhancedSwipeCard = enhancedSwipeCard;
|
||
window.swipeIntegration = swipeIntegration;
|
||
|
||
/**
|
||
* Setup settings panel for swipe mode selection
|
||
*/
|
||
function setupSettingsPanel() {
|
||
const settingsToggle = document.getElementById('swipe-settings-toggle');
|
||
const settingsPanelContainer = document.getElementById('swipe-settings-panel');
|
||
let settingsPanel = null;
|
||
let isPanelOpen = false;
|
||
|
||
if (!settingsToggle || !settingsPanelContainer) {
|
||
console.warn('Settings panel elements not found');
|
||
return;
|
||
}
|
||
|
||
settingsToggle.addEventListener('click', () => {
|
||
if (!isPanelOpen) {
|
||
// Create and show settings panel
|
||
settingsPanel = createSwipeSettingsPanel(swipeIntegration);
|
||
settingsPanelContainer.appendChild(settingsPanel);
|
||
settingsPanelContainer.style.display = 'block';
|
||
|
||
// Add show class after a brief delay for animation
|
||
setTimeout(() => {
|
||
settingsPanel.classList.add('show');
|
||
}, 10);
|
||
|
||
isPanelOpen = true;
|
||
|
||
// Add click outside to close
|
||
document.addEventListener('click', handleOutsideClick);
|
||
} else {
|
||
closeSettingsPanel();
|
||
}
|
||
});
|
||
|
||
function handleOutsideClick(e) {
|
||
if (settingsPanel && !settingsPanel.contains(e.target) && !settingsToggle.contains(e.target)) {
|
||
closeSettingsPanel();
|
||
}
|
||
}
|
||
|
||
function closeSettingsPanel() {
|
||
if (settingsPanel) {
|
||
settingsPanel.classList.remove('show');
|
||
setTimeout(() => {
|
||
settingsPanelContainer.removeChild(settingsPanel);
|
||
settingsPanelContainer.style.display = 'none';
|
||
settingsPanel = null;
|
||
}, 300);
|
||
}
|
||
isPanelOpen = false;
|
||
document.removeEventListener('click', handleOutsideClick);
|
||
}
|
||
|
||
// Add keyboard shortcut for settings (Ctrl/Cmd + ,)
|
||
document.addEventListener('keydown', (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === ',') {
|
||
e.preventDefault();
|
||
settingsToggle.click();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Toggle fullscreen mode for immersive image viewing
|
||
*/
|
||
function toggleFullscreenMode() {
|
||
const body = document.body;
|
||
const isFullscreen = body.classList.contains('fullscreen-mode');
|
||
|
||
if (isFullscreen) {
|
||
// Exit fullscreen mode
|
||
body.classList.remove('fullscreen-mode');
|
||
fullscreenToggle.setAttribute('aria-label', 'Enter fullscreen mode');
|
||
fullscreenToggle.title = 'Enter fullscreen mode';
|
||
|
||
// Enhanced feedback
|
||
showEnhancedToast('Exited fullscreen mode', 'info');
|
||
} else {
|
||
// Enter fullscreen mode
|
||
body.classList.add('fullscreen-mode');
|
||
fullscreenToggle.setAttribute('aria-label', 'Exit fullscreen mode');
|
||
fullscreenToggle.title = 'Exit fullscreen mode';
|
||
|
||
// Enhanced feedback
|
||
showEnhancedToast('Entered fullscreen mode', 'info');
|
||
}
|
||
|
||
// Add visual feedback to the button
|
||
createButtonPulse(fullscreenToggle);
|
||
|
||
// Trigger a resize event to help any responsive components adjust
|
||
setTimeout(() => {
|
||
window.dispatchEvent(new Event('resize'));
|
||
}, 300);
|
||
}
|
||
});
|