xcrop's picture
Rename index.html to app.py
0a72b62 verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>AR PIXELS – AI Image Generator</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,1,0" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Noto Sans', 'sans-serif'],
display: ['Space Grotesk', 'sans-serif'],
},
colors: {
primary: '#8B5CF6',
secondary: '#A78BFA',
dark: '#0F0F13',
surface: '#18181B',
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
'slide-up': 'slideUp 0.4s ease-out',
'pulse-slow': 'pulse 3s infinite',
}
}
}
}
</script>
<style>
/* Custom Styles & Utilities */
:root {
--glass-border: rgba(255, 255, 255, 0.08);
--glass-bg: rgba(24, 24, 27, 0.65);
}
body {
-webkit-tap-highlight-color: transparent;
overscroll-behavior-y: none;
}
.glass {
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
}
.glass-panel {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* Range Slider Styling */
input[type=range] {
-webkit-appearance: none;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: #8B5CF6;
margin-top: -8px;
box-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: rgba(255,255,255,0.1);
border-radius: 2px;
}
/* Toast Notification */
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-100px);
z-index: 9999;
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.toast.show {
transform: translateX(-50%) translateY(0);
}
/* Masonry Grid */
.masonry-grid {
column-count: 2;
column-gap: 12px;
}
.masonry-item {
break-inside: avoid;
margin-bottom: 12px;
}
.loader {
border: 3px solid rgba(255,255,255,0.1);
border-left-color: #8B5CF6;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body class="bg-gray-100 dark:bg-dark text-gray-800 dark:text-white h-screen w-screen overflow-hidden transition-colors duration-300">
<!-- Toast Container -->
<div id="toast-container" class="toast glass px-6 py-3 rounded-full flex items-center gap-3 shadow-2xl min-w-[300px]">
<span id="toast-icon" class="material-symbols-rounded text-primary">info</span>
<p id="toast-message" class="text-sm font-medium">Notification</p>
</div>
<!-- MAIN APP CONTAINER -->
<div id="app" class="h-full w-full relative">
<!-- SCREEN 1: ONBOARDING -->
<section id="screen-onboarding" class="h-full w-full flex flex-col items-center justify-center relative z-50 bg-dark">
<!-- Animated Background Tiles -->
<div class="absolute inset-0 grid grid-cols-3 gap-2 opacity-20 overflow-hidden pointer-events-none">
<div class="bg-gradient-to-br from-purple-500 to-blue-500 rounded-xl h-40 w-full animate-pulse-slow"></div>
<div class="bg-gradient-to-br from-pink-500 to-rose-500 rounded-xl h-64 w-full mt-10"></div>
<div class="bg-gradient-to-br from-indigo-500 to-cyan-500 rounded-xl h-48 w-full"></div>
<div class="bg-gradient-to-br from-amber-500 to-orange-500 rounded-xl h-56 w-full -mt-12"></div>
<div class="bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl h-40 w-full"></div>
<div class="bg-gradient-to-br from-fuchsia-500 to-purple-600 rounded-xl h-60 w-full mt-8"></div>
</div>
<div class="absolute inset-0 bg-gradient-to-t from-dark via-dark/80 to-transparent"></div>
<div class="z-10 flex flex-col items-center text-center px-6 animate-slide-up">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-tr from-primary to-blue-500 flex items-center justify-center shadow-lg shadow-purple-500/30 mb-6">
<span class="material-symbols-rounded text-4xl text-white">auto_awesome</span>
</div>
<h1 class="font-display text-3xl font-bold tracking-tight mb-2">AR PIXELS</h1>
<h2 class="text-4xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-blue-400 mb-6 leading-tight">
Imagine.<br>Type.<br>Create.
</h2>
<p class="text-gray-400 mb-10 max-w-xs">Turn your wildest dreams into breathtaking reality with AI-powered pixel art.</p>
<button onclick="router.navigate('create')" class="group relative w-full max-w-xs overflow-hidden rounded-full bg-primary p-4 transition-all hover:bg-purple-600 active:scale-95">
<div class="absolute inset-0 -translate-x-full group-hover:animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/20 to-transparent"></div>
<span class="relative font-bold text-white tracking-wide">Get Started</span>
</button>
</div>
</section>
<!-- SCREEN 2: CREATE (Main) -->
<section id="screen-create" class="h-full w-full flex flex-col hidden">
<!-- Header -->
<header class="flex justify-between items-center px-6 pt-12 pb-4 glass sticky top-0 z-20">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-primary to-blue-500 flex items-center justify-center">
<span class="material-symbols-rounded text-sm text-white">auto_awesome</span>
</div>
<span class="font-display font-bold text-lg">AR PIXELS</span>
</div>
<button onclick="router.navigate('settings')" class="p-2 rounded-full hover:bg-white/10 transition-colors">
<span class="material-symbols-rounded">settings</span>
</button>
</header>
<!-- Scrollable Content -->
<div class="flex-1 overflow-y-auto hide-scrollbar px-6 pb-32 pt-4">
<!-- Prompt Area -->
<div class="mb-6 relative">
<label class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2 block">Prompt</label>
<textarea id="prompt-input" rows="4" class="w-full bg-gray-200 dark:bg-white/5 border border-transparent dark:border-white/10 rounded-2xl p-4 text-base focus:outline-none focus:border-primary transition-colors resize-none placeholder-gray-500" placeholder="A futuristic city with flying cars in cyberpunk style, neon lights..."></textarea>
<button onclick="app.surpriseMe()" class="absolute bottom-3 right-3 flex items-center gap-1 bg-white/10 hover:bg-white/20 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors border border-white/5 backdrop-blur-sm">
<span class="material-symbols-rounded text-sm text-primary">shuffle</span>
Surprise Me
</button>
</div>
<!-- Aspect Ratio -->
<div class="mb-8">
<label class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3 block">Aspect Ratio</label>
<div class="grid grid-cols-3 gap-3">
<button onclick="app.setRatio('9:16')" id="ratio-9-16" class="ratio-btn active flex flex-col items-center justify-center gap-2 p-3 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 transition-all">
<div class="w-3 h-5 border-2 border-current rounded-sm opacity-50"></div>
<span class="text-xs font-medium">9:16</span>
</button>
<button onclick="app.setRatio('1:1')" id="ratio-1-1" class="ratio-btn flex flex-col items-center justify-center gap-2 p-3 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 transition-all">
<div class="w-4 h-4 border-2 border-current rounded-sm opacity-50"></div>
<span class="text-xs font-medium">1:1</span>
</button>
<button onclick="app.setRatio('16:9')" id="ratio-16-9" class="ratio-btn flex flex-col items-center justify-center gap-2 p-3 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 transition-all">
<div class="w-5 h-3 border-2 border-current rounded-sm opacity-50"></div>
<span class="text-xs font-medium">16:9</span>
</button>
</div>
</div>
<!-- Guidance Scale -->
<div class="mb-6">
<div class="flex justify-between items-end mb-3">
<label class="text-xs font-bold text-gray-500 uppercase tracking-wider">Guidance Scale</label>
<span id="guidance-value" class="text-primary font-display font-bold text-lg">7.5</span>
</div>
<input type="range" id="guidance-slider" min="1" max="20" step="0.5" value="7.5" class="w-full" oninput="app.updateSlider(this.value)">
<div class="flex justify-between mt-2 text-[10px] text-gray-500">
<span>Creative</span>
<span>Balanced</span>
<span>Strict</span>
</div>
</div>
</div>
<!-- Generate Button Area -->
<div class="absolute bottom-[80px] left-0 w-full px-6 pb-4 pt-6 bg-gradient-to-t from-gray-100 dark:from-dark via-gray-100 dark:via-dark to-transparent z-10">
<button id="btn-generate" onclick="app.generateImage()" class="w-full bg-gradient-to-r from-primary to-blue-600 p-4 rounded-xl font-bold text-white shadow-lg shadow-primary/25 active:scale-95 transition-all flex items-center justify-center gap-2">
<span class="material-symbols-rounded">draw</span>
<span>Generate Art</span>
</button>
</div>
</section>
<!-- SCREEN 3: RESULT DETAIL -->
<section id="screen-result" class="fixed inset-0 z-[60] bg-dark hidden">
<!-- Fullscreen Image -->
<div class="absolute inset-0 flex items-center justify-center bg-black">
<img id="result-image" src="" alt="Generated" class="max-w-full max-h-full object-contain opacity-0 transition-opacity duration-500">
</div>
<!-- Top Controls -->
<div class="absolute top-0 left-0 right-0 p-6 flex justify-between items-center bg-gradient-to-b from-black/80 to-transparent">
<button onclick="router.back()" class="w-10 h-10 rounded-full glass flex items-center justify-center text-white active:scale-90 transition-transform">
<span class="material-symbols-rounded">arrow_back</span>
</button>
<button onclick="app.downloadImage()" class="w-10 h-10 rounded-full glass flex items-center justify-center text-white active:scale-90 transition-transform">
<span class="material-symbols-rounded">download</span>
</button>
</div>
<!-- Bottom Sheet -->
<div class="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black via-black/90 to-transparent pt-20">
<div class="glass-panel p-4 rounded-2xl mb-4 border border-white/10">
<p class="text-[10px] text-gray-400 uppercase tracking-widest mb-1">Prompt Used</p>
<p id="result-prompt" class="text-sm text-gray-200 leading-relaxed line-clamp-3">A cyberpunk city...</p>
</div>
<button onclick="router.back()" class="w-full bg-white text-black p-4 rounded-xl font-bold active:scale-95 transition-transform flex items-center justify-center gap-2">
<span class="material-symbols-rounded">refresh</span>
Generate Another
</button>
</div>
</section>
<!-- SCREEN 4: GALLERY -->
<section id="screen-gallery" class="h-full w-full hidden flex flex-col">
<header class="px-6 pt-12 pb-4 glass sticky top-0 z-20">
<h2 class="font-display font-bold text-2xl">Community</h2>
</header>
<div class="flex-1 overflow-y-auto hide-scrollbar p-4 pb-24">
<div id="gallery-grid" class="masonry-grid">
<!-- Gallery items injected via JS -->
</div>
</div>
</section>
<!-- SCREEN 5: HISTORY -->
<section id="screen-history" class="h-full w-full hidden flex flex-col">
<header class="flex justify-between items-end px-6 pt-12 pb-4 glass sticky top-0 z-20">
<h2 class="font-display font-bold text-2xl">My Creations</h2>
<button onclick="app.clearHistory()" class="text-xs text-red-400 font-medium hover:text-red-300">Clear All</button>
</header>
<div class="flex-1 overflow-y-auto hide-scrollbar p-4 pb-24">
<div id="history-list" class="space-y-3">
<!-- History items injected via JS -->
<div class="text-center mt-20 text-gray-500 text-sm hidden" id="empty-history">
No creations yet.<br>Start imagining!
</div>
</div>
</div>
</section>
<!-- SCREEN 6: SETTINGS -->
<section id="screen-settings" class="h-full w-full hidden flex flex-col bg-white dark:bg-dark">
<header class="flex items-center gap-4 px-6 pt-12 pb-4 glass sticky top-0 z-20">
<button onclick="router.back()" class="active:scale-90 transition-transform">
<span class="material-symbols-rounded text-2xl">arrow_back</span>
</button>
<h2 class="font-display font-bold text-2xl">Settings</h2>
</header>
<div class="p-6 space-y-8">
<!-- Preferences -->
<div>
<h3 class="text-xs font-bold text-primary uppercase tracking-wider mb-4">Preferences</h3>
<div class="glass-panel rounded-xl overflow-hidden">
<div class="flex items-center justify-between p-4 border-b border-white/5">
<div class="flex items-center gap-3">
<span class="material-symbols-rounded text-gray-400">dark_mode</span>
<span class="font-medium">Dark Mode</span>
</div>
<button id="toggle-theme" onclick="app.toggleTheme()" class="w-12 h-6 bg-primary rounded-full relative transition-colors">
<div class="w-4 h-4 bg-white rounded-full absolute top-1 right-1 transition-all"></div>
</button>
</div>
<div class="flex items-center justify-between p-4">
<div class="flex items-center gap-3">
<span class="material-symbols-rounded text-gray-400">notifications</span>
<span class="font-medium">Notifications</span>
</div>
<button onclick="app.toggleNotif()" class="w-12 h-6 bg-primary rounded-full relative transition-colors" id="toggle-notif">
<div class="w-4 h-4 bg-white rounded-full absolute top-1 right-1 transition-all"></div>
</button>
</div>
</div>
</div>
<!-- About -->
<div>
<h3 class="text-xs font-bold text-primary uppercase tracking-wider mb-4">Support & About</h3>
<div class="glass-panel rounded-xl overflow-hidden">
<button class="w-full flex items-center justify-between p-4 border-b border-white/5 hover:bg-white/5 text-left">
<span class="font-medium">Help Center</span>
<span class="material-symbols-rounded text-gray-500 text-sm">chevron_right</span>
</button>
<button class="w-full flex items-center justify-between p-4 border-b border-white/5 hover:bg-white/5 text-left">
<span class="font-medium">Privacy Policy</span>
<span class="material-symbols-rounded text-gray-500 text-sm">chevron_right</span>
</button>
<button class="w-full flex items-center justify-between p-4 hover:bg-white/5 text-left">
<span class="font-medium">Report a Bug</span>
<span class="material-symbols-rounded text-gray-500 text-sm">chevron_right</span>
</button>
</div>
</div>
<div class="text-center pt-8 opacity-50">
<p class="font-display font-bold text-sm tracking-widest">AR PIXELS</p>
<p class="text-xs mt-1">Version 1.0.0</p>
<p class="text-xs mt-4 text-primary">Founder: AR BRAINCODE</p>
</div>
</div>
</section>
<!-- BOTTOM NAVIGATION -->
<nav id="bottom-nav" class="fixed bottom-0 left-0 w-full glass z-40 hidden pb-safe">
<div class="grid grid-cols-3 h-16 items-center">
<button onclick="router.navigate('create')" id="nav-create" class="flex flex-col items-center gap-1 text-primary transition-colors">
<span class="material-symbols-rounded text-2xl filled-icon">add_circle</span>
<span class="text-[10px] font-medium">Create</span>
</button>
<button onclick="router.navigate('gallery')" id="nav-gallery" class="flex flex-col items-center gap-1 text-gray-500 hover:text-gray-300 transition-colors">
<span class="material-symbols-rounded text-2xl">explore</span>
<span class="text-[10px] font-medium">Gallery</span>
</button>
<button onclick="router.navigate('history')" id="nav-history" class="flex flex-col items-center gap-1 text-gray-500 hover:text-gray-300 transition-colors">
<span class="material-symbols-rounded text-2xl">history</span>
<span class="text-[10px] font-medium">History</span>
</button>
</div>
<!-- Safe area spacer for iPhone X+ -->
<div class="h-4 w-full"></div>
</nav>
</div>
<!-- JAVASCRIPT LOGIC -->
<script>
// --- DATA & STATE ---
const config = {
ratios: {
'9:16': { w: 720, h: 1280 },
'1:1': { w: 1024, h: 1024 },
'16:9': { w: 1280, h: 720 }
},
surprisePrompts: [
"A futuristic neon city with flying cars in cyberpunk style, detailed, 8k",
"A cute robot gardener watering plants in a solarpunk greenhouse, soft lighting",
"An astronaut meditating on Mars, galaxy background, surreal art",
"Detailed portrait of a bioluminescent forest creature, fantasy style",
"Vintage polaroid of a 1980s arcade, vaporwave aesthetic",
"A majestic lion made of clouds in a sunset sky, cinematic lighting"
],
galleryImages: [
{ url: "https://image.pollinations.ai/prompt/cyberpunk%20girl?width=400&height=600&model=flux", h: "h-64" },
{ url: "https://image.pollinations.ai/prompt/abstract%20fluid%20art?width=400&height=400&model=flux", h: "h-40" },
{ url: "https://image.pollinations.ai/prompt/retro%20car%20sunset?width=400&height=300&model=flux", h: "h-32" },
{ url: "https://image.pollinations.ai/prompt/space%20station?width=400&height=500&model=flux", h: "h-48" },
{ url: "https://image.pollinations.ai/prompt/fantasy%20castle?width=400&height=600&model=flux", h: "h-56" },
{ url: "https://image.pollinations.ai/prompt/underwater%20city?width=400&height=400&model=flux", h: "h-40" }
]
};
const state = {
currentScreen: 'onboarding',
ratio: '9:16',
guidance: 7.5,
notifications: true,
isGenerating: false
};
// --- ROUTER ---
const router = {
historyStack: [],
navigate(screenId) {
// Haptic feedback
if(navigator.vibrate) navigator.vibrate(10);
// Add to history if moving forward normally
if(screenId !== this.historyStack[this.historyStack.length - 1]) {
this.historyStack.push(screenId);
}
// Hide all screens
document.querySelectorAll('section').forEach(el => el.classList.add('hidden'));
// Show new screen
const screen = document.getElementById(`screen-${screenId}`);
screen.classList.remove('hidden');
// Animation reset
screen.classList.remove('animate-fade-in');
void screen.offsetWidth; // trigger reflow
screen.classList.add('animate-fade-in');
// Update State
state.currentScreen = screenId;
// Handle Navbar
const nav = document.getElementById('bottom-nav');
if(['create', 'gallery', 'history'].includes(screenId)) {
nav.classList.remove('hidden');
this.updateNavHighlight(screenId);
} else {
nav.classList.add('hidden');
}
// Load Data triggers
if(screenId === 'history') app.loadHistory();
if(screenId === 'gallery') app.loadGallery();
},
back() {
if(this.historyStack.length > 1) {
this.historyStack.pop();
const prevScreen = this.historyStack[this.historyStack.length - 1];
this.navigate(prevScreen);
// Remove duplicate push from navigate
this.historyStack.pop();
} else {
this.navigate('create');
}
},
updateNavHighlight(screenId) {
document.querySelectorAll('#bottom-nav button').forEach(btn => {
btn.classList.remove('text-primary');
btn.classList.add('text-gray-500');
btn.querySelector('span.material-symbols-rounded').classList.remove('filled-icon');
});
const activeBtn = document.getElementById(`nav-${screenId}`);
if(activeBtn) {
activeBtn.classList.remove('text-gray-500');
activeBtn.classList.add('text-primary');
activeBtn.querySelector('span.material-symbols-rounded').classList.add('filled-icon');
}
}
};
// --- CORE LOGIC ---
const app = {
init() {
// Check local storage for theme
if (localStorage.getItem('theme') === 'light') {
document.documentElement.classList.remove('dark');
}
// Initial load
this.loadGallery();
},
showToast(msg, type = 'info') {
const toast = document.getElementById('toast-container');
const text = document.getElementById('toast-message');
const icon = document.getElementById('toast-icon');
text.innerText = msg;
if(type === 'success') { icon.innerText = 'check_circle'; icon.className = 'material-symbols-rounded text-green-400'; }
else if(type === 'error') { icon.innerText = 'error'; icon.className = 'material-symbols-rounded text-red-400'; }
else { icon.innerText = 'info'; icon.className = 'material-symbols-rounded text-primary'; }
toast.classList.add('show');
if(navigator.vibrate) navigator.vibrate(20);
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
},
setRatio(ratio) {
state.ratio = ratio;
document.querySelectorAll('.ratio-btn').forEach(btn => {
btn.classList.remove('border-primary', 'text-primary', 'bg-primary/10');
btn.classList.add('border-white/10', 'bg-white/5');
});
const activeBtn = document.getElementById(`ratio-${ratio.replace(':','-')}`);
activeBtn.classList.remove('border-white/10', 'bg-white/5');
activeBtn.classList.add('border-primary', 'text-primary', 'bg-primary/10');
if(navigator.vibrate) navigator.vibrate(5);
},
updateSlider(val) {
state.guidance = val;
document.getElementById('guidance-value').innerText = val;
},
surpriseMe() {
const prompt = config.surprisePrompts[Math.floor(Math.random() * config.surprisePrompts.length)];
document.getElementById('prompt-input').value = prompt;
if(navigator.vibrate) navigator.vibrate(10);
},
async generateImage() {
const promptInput = document.getElementById('prompt-input');
const prompt = promptInput.value.trim();
const btn = document.getElementById('btn-generate');
const btnText = btn.querySelector('span:last-child');
const btnIcon = btn.querySelector('span:first-child');
if(!prompt) {
this.showToast('Please enter a prompt first', 'error');
return;
}
// UI Loading State
state.isGenerating = true;
btn.disabled = true;
btnText.innerText = "Dreaming...";
btnIcon.innerText = "hourglass_empty";
btnIcon.classList.add('animate-spin');
const { w, h } = config.ratios[state.ratio];
const seed = Math.floor(Math.random() * 100000);
const encodedPrompt = encodeURIComponent(prompt);
// Pollinations API URL
const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=${w}&height=${h}&seed=${seed}&nologo=true&model=flux`;
try {
// Preload image
await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Generation failed"));
img.src = imageUrl;
});
// Success
const resultImg = document.getElementById('result-image');
resultImg.src = imageUrl;
resultImg.style.opacity = '0';
document.getElementById('result-prompt').innerText = prompt;
// Save to history
this.saveToHistory({
url: imageUrl,
prompt: prompt,
timestamp: new Date().getTime()
});
// Navigate
router.navigate('result');
// Fade in image
setTimeout(() => { resultImg.style.opacity = '1'; }, 100);
this.showToast('Image generated successfully!', 'success');
} catch (e) {
this.showToast('Failed to generate. Try again.', 'error');
} finally {
// Reset UI
state.isGenerating = false;
btn.disabled = false;
btnText.innerText = "Generate Art";
btnIcon.innerText = "draw";
btnIcon.classList.remove('animate-spin');
}
},
saveToHistory(item) {
let history = JSON.parse(localStorage.getItem('ar_pixels_history') || '[]');
history.unshift(item); // Add to beginning
if(history.length > 20) history.pop(); // Limit to 20
localStorage.setItem('ar_pixels_history', JSON.stringify(history));
},
loadHistory() {
const history = JSON.parse(localStorage.getItem('ar_pixels_history') || '[]');
const container = document.getElementById('history-list');
const emptyMsg = document.getElementById('empty-history');
// Keep the empty message element, remove others
Array.from(container.children).forEach(child => {
if(child.id !== 'empty-history') child.remove();
});
if(history.length === 0) {
emptyMsg.classList.remove('hidden');
return;
}
emptyMsg.classList.add('hidden');
history.forEach(item => {
const el = document.createElement('div');
el.className = "flex items-center gap-4 bg-white/5 p-3 rounded-xl border border-white/5 active:scale-98 transition-transform cursor-pointer";
el.onclick = () => {
document.getElementById('result-image').src = item.url;
document.getElementById('result-prompt').innerText = item.prompt;
router.navigate('result');
setTimeout(() => { document.getElementById('result-image').style.opacity = '1'; }, 100);
};
el.innerHTML = `
<img src="${item.url}" class="w-16 h-16 rounded-lg object-cover bg-gray-800">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white truncate">${item.prompt}</p>
<p class="text-xs text-gray-500 mt-1">${new Date(item.timestamp).toLocaleDateString()}</p>
</div>
<span class="material-symbols-rounded text-gray-500">chevron_right</span>
`;
container.appendChild(el);
});
},
clearHistory() {
if(confirm("Delete all history?")) {
localStorage.removeItem('ar_pixels_history');
this.loadHistory();
this.showToast('History cleared', 'info');
}
},
loadGallery() {
const container = document.getElementById('gallery-grid');
if(container.children.length > 0) return; // Prevent duplicates
config.galleryImages.forEach(item => {
const div = document.createElement('div');
div.className = "masonry-item rounded-xl overflow-hidden relative group";
div.innerHTML = `
<img src="${item.url}" loading="lazy" class="w-full h-auto object-cover bg-gray-800 transition-transform duration-700 group-hover:scale-110">
<div class="absolute inset-0 bg-black/20 group-hover:bg-black/0 transition-colors"></div>
`;
container.appendChild(div);
});
},
async downloadImage() {
const img = document.getElementById('result-image');
try {
const response = await fetch(img.src);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `AR-PIXELS-${Date.now()}.jpg`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
this.showToast('Image saved to device', 'success');
} catch (e) {
this.showToast('Download failed', 'error');
}
},
toggleTheme() {
const html = document.documentElement;
const toggle = document.getElementById('toggle-theme').querySelector('div');
if (html.classList.contains('dark')) {
html.classList.remove('dark');
localStorage.setItem('theme', 'light');
toggle.style.right = 'auto';
toggle.style.left = '4px';
} else {
html.classList.add('dark');
localStorage.setItem('theme', 'dark');
toggle.style.left = 'auto';
toggle.style.right = '4px';
}
if(navigator.vibrate) navigator.vibrate(10);
},
toggleNotif() {
state.notifications = !state.notifications;
const toggle = document.getElementById('toggle-notif').querySelector('div');
if (state.notifications) {
toggle.style.left = 'auto';
toggle.style.right = '4px';
this.showToast('Notifications On', 'success');
} else {
toggle.style.right = 'auto';
toggle.style.left = '4px';
this.showToast('Notifications Off', 'info');
}
if(navigator.vibrate) navigator.vibrate(10);
}
};
// Initialize App
window.addEventListener('load', () => {
app.init();
// Highlight default aspect ratio
app.setRatio('9:16');
// Setup Theme Toggle visual state
if(!document.documentElement.classList.contains('dark')) {
const t = document.getElementById('toggle-theme').querySelector('div');
t.style.right = 'auto'; t.style.left = '4px';
}
});
</script>
</body>
</html>