|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
|
|
|
<title>GenImage</title> |
|
|
<style> |
|
|
|
|
|
:root { |
|
|
--primary: #667eea; |
|
|
--secondary: #764ba2; |
|
|
--bg-dark: #0f0f23; |
|
|
--bg-medium: #1a1a2e; |
|
|
--bg-light: #2a2a3a; |
|
|
--text-primary: #ffffff; |
|
|
--text-secondary: #aaaaaa; |
|
|
--success: #48bb78; |
|
|
--error: #f56565; |
|
|
--border: rgba(255, 255, 255, 0.1); |
|
|
} |
|
|
|
|
|
* { |
|
|
box-sizing: border-box; |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', Arial, sans-serif; |
|
|
background: var(--bg-dark); |
|
|
color: var(--text-primary); |
|
|
line-height: 1.6; |
|
|
-webkit-font-smoothing: antialiased; |
|
|
} |
|
|
|
|
|
|
|
|
.app-layout { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
min-height: 100vh; |
|
|
max-width: 1400px; |
|
|
margin: 0 auto; |
|
|
padding: 2rem; |
|
|
} |
|
|
|
|
|
.header { |
|
|
text-align: center; |
|
|
margin-bottom: 2rem; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 2.5rem; |
|
|
font-weight: 600; |
|
|
background: linear-gradient(90deg, var(--primary), var(--secondary)); |
|
|
-webkit-background-clip: text; |
|
|
background-clip: text; |
|
|
color: transparent; |
|
|
margin-bottom: 0.5rem; |
|
|
} |
|
|
|
|
|
.tagline { |
|
|
color: var(--text-secondary); |
|
|
font-size: 1.1rem; |
|
|
} |
|
|
|
|
|
|
|
|
.language-switcher { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
right: 0; |
|
|
z-index: 100; |
|
|
} |
|
|
|
|
|
.lang-switcher-toggle { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
background: var(--bg-medium); |
|
|
border: 1px solid var(--border); |
|
|
color: var(--text-primary); |
|
|
font-weight: 600; |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 12px; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.lang-switcher-toggle:hover { |
|
|
border-color: var(--primary); |
|
|
} |
|
|
|
|
|
.lang-switcher-toggle .arrow { |
|
|
font-size: 0.8em; |
|
|
transition: transform 0.2s; |
|
|
} |
|
|
|
|
|
.lang-switcher-toggle.open .arrow { |
|
|
transform: rotate(180deg); |
|
|
} |
|
|
|
|
|
.lang-switcher-menu { |
|
|
position: absolute; |
|
|
top: calc(100% + 5px); |
|
|
right: 0; |
|
|
background: var(--bg-light); |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 12px; |
|
|
padding: 0.5rem; |
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 0.25rem; |
|
|
opacity: 0; |
|
|
transform: translateY(-10px); |
|
|
pointer-events: none; |
|
|
transition: all 0.2s ease-out; |
|
|
} |
|
|
|
|
|
.lang-switcher-menu.open { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
pointer-events: all; |
|
|
} |
|
|
|
|
|
.lang-switcher-option { |
|
|
background: none; |
|
|
border: none; |
|
|
color: var(--text-secondary); |
|
|
font-weight: 600; |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 8px; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
text-align: left; |
|
|
} |
|
|
|
|
|
.lang-switcher-option:hover { |
|
|
background: var(--primary); |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.lang-switcher-option.active { |
|
|
color: var(--primary); |
|
|
} |
|
|
|
|
|
|
|
|
.control-panel { |
|
|
background: var(--bg-medium); |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 16px; |
|
|
padding: 1.5rem; |
|
|
margin-bottom: 1.5rem; |
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
|
|
|
.prompt-form { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.input-group { |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.input-group input { |
|
|
width: 100%; |
|
|
background: var(--bg-light); |
|
|
border: 2px solid var(--border); |
|
|
border-radius: 12px; |
|
|
padding: 1rem 1.5rem; |
|
|
color: var(--text-primary); |
|
|
font-size: 1rem; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.input-group input:focus { |
|
|
outline: none; |
|
|
border-color: var(--primary); |
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3); |
|
|
} |
|
|
|
|
|
|
|
|
.toolbox-bar { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
flex-wrap: wrap; |
|
|
gap: 1.5rem; |
|
|
padding-top: 0.5rem; |
|
|
border-top: 1px solid var(--border); |
|
|
} |
|
|
|
|
|
.toolbox-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.toolbox-item label { |
|
|
font-size: 0.9rem; |
|
|
color: var(--text-secondary); |
|
|
white-space: nowrap; |
|
|
} |
|
|
|
|
|
.toolbox-item input { |
|
|
width: 80px; |
|
|
background: var(--bg-light); |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 8px; |
|
|
padding: 0.5rem; |
|
|
color: var(--text-primary); |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.toolbox-item input:focus { |
|
|
outline: none; |
|
|
border-color: var(--primary); |
|
|
} |
|
|
|
|
|
.toolbox-actions { |
|
|
display: flex; |
|
|
gap: 0.75rem; |
|
|
margin-left: auto; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
.btn { |
|
|
background: linear-gradient(135deg, var(--primary), var(--secondary)); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 0.75rem 1.5rem; |
|
|
font-size: 1rem; |
|
|
font-weight: 600; |
|
|
border-radius: 10px; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.btn:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
|
|
|
.btn-outline { |
|
|
background: transparent; |
|
|
border: 2px solid var(--primary); |
|
|
color: var(--primary); |
|
|
padding: 0.625rem 1.5rem; |
|
|
|
|
|
} |
|
|
|
|
|
.btn-outline:hover { |
|
|
background: var(--primary); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-danger { |
|
|
background: transparent; |
|
|
border: 1px solid var(--error); |
|
|
color: var(--error); |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 12px; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.btn-danger:hover { |
|
|
background: var(--error); |
|
|
color: white; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
|
|
|
|
|
|
.btn-action { |
|
|
background: transparent; |
|
|
border: 1px solid #667EEA; |
|
|
color: #667EEA; |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 12px; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.btn-action:hover { |
|
|
background: #667EEA; |
|
|
color: white; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
|
|
|
|
|
|
.gallery-toolbar { |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
.banner { |
|
|
flex-grow: 1; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
background: linear-gradient(90deg, var(--primary), var(--secondary)); |
|
|
padding: 0.75rem 1.5rem; |
|
|
border-radius: 12px; |
|
|
font-size: 0.95rem; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.banner-content { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.75rem; |
|
|
} |
|
|
|
|
|
.banner-close { |
|
|
background: none; |
|
|
border: none; |
|
|
color: rgba(255, 255, 255, 0.7); |
|
|
font-size: 1.2rem; |
|
|
cursor: pointer; |
|
|
transition: color 0.2s; |
|
|
padding: 0.25rem; |
|
|
line-height: 1; |
|
|
} |
|
|
|
|
|
.banner-close:hover { |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.tabs { |
|
|
display: flex; |
|
|
gap: 0.5rem; |
|
|
margin-bottom: 1.5rem; |
|
|
border-bottom: 1px solid var(--border); |
|
|
} |
|
|
|
|
|
.tab { |
|
|
padding: 0.75rem 1.25rem; |
|
|
border-radius: 8px 8px 0 0; |
|
|
cursor: pointer; |
|
|
border: none; |
|
|
background: none; |
|
|
color: var(--text-secondary); |
|
|
transition: all 0.2s; |
|
|
border-bottom: 2px solid transparent; |
|
|
} |
|
|
|
|
|
.tab:hover { |
|
|
background: var(--bg-light); |
|
|
} |
|
|
|
|
|
.tab.active { |
|
|
font-weight: 600; |
|
|
color: var(--text-primary); |
|
|
border-bottom-color: var(--primary); |
|
|
} |
|
|
|
|
|
|
|
|
.image-gallery { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
|
|
gap: 1.5rem; |
|
|
} |
|
|
|
|
|
.image-card { |
|
|
background: var(--bg-medium); |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 16px; |
|
|
overflow: hidden; |
|
|
transition: transform 0.3s, box-shadow 0.3s; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.image-card:hover { |
|
|
transform: translateY(-5px); |
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.image-card.featured { |
|
|
grid-column: span 2; |
|
|
} |
|
|
|
|
|
.image-display { |
|
|
aspect-ratio: 1/1; |
|
|
position: relative; |
|
|
background: var(--bg-light); |
|
|
cursor: pointer; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.image-display img { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: cover; |
|
|
transition: opacity 0.3s; |
|
|
} |
|
|
|
|
|
.image-info { |
|
|
padding: 1rem; |
|
|
flex-grow: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.image-prompt { |
|
|
font-size: 0.95rem; |
|
|
margin-bottom: 0.75rem; |
|
|
display: -webkit-box; |
|
|
|
|
|
-webkit-box-orient: vertical; |
|
|
overflow: hidden; |
|
|
flex-grow: 1; |
|
|
} |
|
|
|
|
|
.image-meta { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
font-size: 0.8rem; |
|
|
color: var(--text-secondary); |
|
|
} |
|
|
|
|
|
.image-actions { |
|
|
position: absolute; |
|
|
top: 10px; |
|
|
right: 10px; |
|
|
display: flex; |
|
|
gap: 0.5rem; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.action-btn { |
|
|
width: 32px; |
|
|
height: 32px; |
|
|
border-radius: 50%; |
|
|
background: rgba(0, 0, 0, 0.6); |
|
|
color: white; |
|
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.action-btn:hover { |
|
|
transform: scale(1.1); |
|
|
background: rgba(0, 0, 0, 0.8); |
|
|
} |
|
|
|
|
|
.action-btn.delete:hover { |
|
|
background: rgba(255, 0, 0, 0.8); |
|
|
} |
|
|
|
|
|
.spinner { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border: 4px solid rgba(255, 255, 255, 0.1); |
|
|
border-top: 4px solid var(--primary); |
|
|
border-radius: 50%; |
|
|
animation: spin 1s linear infinite; |
|
|
position: relative; |
|
|
|
|
|
} |
|
|
|
|
|
.empty-state { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
text-align: center; |
|
|
gap: 1rem; |
|
|
padding: 3rem; |
|
|
grid-column: 1 / -1; |
|
|
color: var(--text-secondary); |
|
|
border: 2px dashed var(--border); |
|
|
border-radius: 16px; |
|
|
} |
|
|
|
|
|
.empty-state-icon { |
|
|
font-size: 3rem; |
|
|
} |
|
|
|
|
|
|
|
|
.toast-container { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
z-index: 1000; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.toast { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 1rem; |
|
|
padding: 1rem 1.5rem; |
|
|
border-radius: 10px; |
|
|
background: var(--bg-medium); |
|
|
border-left: 4px solid var(--primary); |
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); |
|
|
transform: translateX(calc(100% + 20px)); |
|
|
opacity: 0; |
|
|
animation: toast-in 0.5s forwards cubic-bezier(0.25, 1, 0.5, 1); |
|
|
} |
|
|
|
|
|
.toast.success { |
|
|
border-left-color: var(--success); |
|
|
} |
|
|
|
|
|
.toast.error { |
|
|
border-left-color: var(--error); |
|
|
} |
|
|
|
|
|
.lightbox { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background: rgba(0, 0, 0, 0.9); |
|
|
z-index: 2000; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
opacity: 0; |
|
|
pointer-events: none; |
|
|
transition: opacity 0.3s; |
|
|
} |
|
|
|
|
|
.lightbox.active { |
|
|
opacity: 1; |
|
|
pointer-events: all; |
|
|
} |
|
|
|
|
|
.lightbox-content { |
|
|
position: relative; |
|
|
max-width: 90%; |
|
|
max-height: 90%; |
|
|
} |
|
|
|
|
|
.lightbox-image { |
|
|
max-width: 100%; |
|
|
max-height: 90vh; |
|
|
object-fit: contain; |
|
|
} |
|
|
|
|
|
.lightbox-close { |
|
|
position: absolute; |
|
|
top: -10px; |
|
|
right: -10px; |
|
|
background: var(--bg-dark); |
|
|
border: 1px solid var(--border); |
|
|
color: white; |
|
|
font-size: 1.2rem; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 50%; |
|
|
cursor: pointer; |
|
|
transition: transform 0.2s; |
|
|
} |
|
|
|
|
|
.lightbox-close:hover { |
|
|
transform: scale(1.1); |
|
|
} |
|
|
|
|
|
|
|
|
@keyframes spin { |
|
|
to { |
|
|
transform: rotate(360deg); |
|
|
} |
|
|
} |
|
|
|
|
|
@keyframes toast-in { |
|
|
to { |
|
|
transform: translateX(0); |
|
|
opacity: 1; |
|
|
} |
|
|
} |
|
|
|
|
|
@media (max-width: 860px) { |
|
|
.language-switcher { |
|
|
position: static; |
|
|
justify-content: center; |
|
|
margin-bottom: 1.5rem; |
|
|
} |
|
|
|
|
|
.header { |
|
|
padding-top: 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.toolbox-actions { |
|
|
margin-left: 0; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.toolbox-actions .btn { |
|
|
flex-grow: 1; |
|
|
} |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.app-layout { |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.image-gallery { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
|
|
|
|
|
|
.image-card.featured { |
|
|
grid-column: span 1; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body> |
|
|
<div id="app"></div> |
|
|
<script src="https://unpkg.com/juris@0.88"></script> |
|
|
<script> |
|
|
|
|
|
const translations = { |
|
|
it: { |
|
|
langName: "Italiano", |
|
|
flag: "🇮🇹 ", |
|
|
appTitle: "GenImage", |
|
|
headerTitle: "GenImagini", |
|
|
tagline: "Trasforma la tua immaginazione in immagini straordinarie", |
|
|
bannerText: "⭐ Sblocca le funzionalità Pro per generazioni illimitate e modelli avanzati!", |
|
|
promptLabel: "Prompt AI", |
|
|
|
|
|
selectModelLabel: "Seleziona Modello", |
|
|
promptPlaceholder: "Descrivi cosa vuoi generare...", |
|
|
widthLabel: "Larghezza", |
|
|
heightLabel: "Altezza", |
|
|
seedLabel: "Seme", |
|
|
generateButton: "Genera ✨", |
|
|
randomizeButton: "Casuale", |
|
|
galleryTab: "La tua Galleria", |
|
|
historyTab: "Cronologia Prompt", |
|
|
emptyGalleryTitle: "La tua galleria è vuota", |
|
|
emptyGalleryDesc: "Inserisci un prompt e fai clic su Genera per creare la tua prima immagine.", |
|
|
metaSeed: "Seme: {seed}", |
|
|
metaRandom: "Casuale", |
|
|
failedToLoad: "⚠️ Caricamento fallito", |
|
|
toastHistoryFail: "Impossibile caricare la cronologia", |
|
|
toastImageSuccess: "Immagine generata con successo!", |
|
|
toastImageFail: "Impossibile generare l'immagine", |
|
|
toastEnterPrompt: "Per favore, inserisci un prompt", |
|
|
toastImageRemoved: "Immagine rimossa", |
|
|
toastSettingsLoaded: "Impostazioni del prompt caricate", |
|
|
toastRestoring: "Ripristino di {count} immagini dalla cronologia...", |
|
|
toastNewSeed: "Nuovo seme: {seed}", |
|
|
deleteTooltip: "Elimina Immagine", |
|
|
zoomTooltip: "Visualizza a Schermo Intero", |
|
|
clearAllButton: "Cancella Tutto", |
|
|
clearAllConfirm: "Sei sicuro di voler svuotare l'intera galleria e la cronologia? Questa azione non può essere annullata.", |
|
|
toastGalleryCleared: "Galleria e cronologia svuotate.", |
|
|
clearPrompt: "Cancella Prompt" |
|
|
}, |
|
|
en: { |
|
|
langName: "English", |
|
|
flag:"🇬🇧 ", |
|
|
clearPrompt: "Clear Prompt", |
|
|
appTitle: "GenImage", |
|
|
headerTitle: "GenImages", |
|
|
tagline: "Transform your imagination into stunning visuals", |
|
|
bannerText: "⭐ Unlock Pro features for unlimited generations and advanced models!", |
|
|
promptLabel: "AI Prompt", |
|
|
|
|
|
selectModelLabel: "Select Model", |
|
|
promptPlaceholder: "Describe what you want to generate...", |
|
|
widthLabel: "Width", |
|
|
heightLabel: "Height", |
|
|
seedLabel: "Seed", |
|
|
generateButton: "Generate ✨", |
|
|
randomizeButton: "Randomize", |
|
|
galleryTab: "Your Gallery", |
|
|
historyTab: "Prompt History", |
|
|
emptyGalleryTitle: "Your gallery is empty", |
|
|
emptyGalleryDesc: "Enter a prompt and click Generate to create your first image.", |
|
|
metaSeed: "Seed: {seed}", |
|
|
metaRandom: "Random", |
|
|
failedToLoad: "⚠️ Failed to load", |
|
|
toastHistoryFail: "Failed to load history", |
|
|
toastImageSuccess: "Image generated successfully!", |
|
|
toastImageFail: "Failed to generate image", |
|
|
toastEnterPrompt: "Please enter a prompt", |
|
|
toastImageRemoved: "Image removed", |
|
|
toastSettingsLoaded: "Prompt settings loaded", |
|
|
toastRestoring: "Restoring {count} images from history...", |
|
|
toastNewSeed: "New seed: {seed}", |
|
|
deleteTooltip: "Delete Image", |
|
|
zoomTooltip: "View Fullscreen", |
|
|
clearAllButton: "Clear All", |
|
|
clearAllConfirm: "Are you sure you want to clear the entire gallery and history? This cannot be undone.", |
|
|
toastGalleryCleared: "Gallery and history cleared.", |
|
|
}, |
|
|
fr: { |
|
|
langName: "Français", |
|
|
flag: '🇫🇷 ', |
|
|
appTitle: "GenImage", |
|
|
headerTitle: "GenImages", |
|
|
tagline: "Mettez vos idées en images", |
|
|
bannerText: "⭐ Débloquez les fonctionnalités Pro pour des générations illimitées et des modèles avancés !", |
|
|
promptLabel: "Prompt IA", |
|
|
|
|
|
selectModelLabel: "Sélectionner un modèle", |
|
|
promptPlaceholder: "Décrivez ce que vous voulez générer...", |
|
|
widthLabel: "Largeur", |
|
|
heightLabel: "Hauteur", |
|
|
seedLabel: "Graine", |
|
|
generateButton: "Générer ✨", |
|
|
randomizeButton: "Aléatoire", |
|
|
galleryTab: "Votre Galerie", |
|
|
historyTab: "Historique des Prompts", |
|
|
emptyGalleryTitle: "Votre galerie est vide", |
|
|
emptyGalleryDesc: "Entrez un prompt et cliquez sur Générer pour créer votre première image.", |
|
|
metaSeed: "Graine : {seed}", |
|
|
metaRandom: "Aléatoire", |
|
|
failedToLoad: "⚠️ Échec du chargement", |
|
|
toastHistoryFail: "Échec du chargement de l'historique", |
|
|
toastImageSuccess: "Image générée avec succès !", |
|
|
toastImageFail: "Échec de la génération de l'image", |
|
|
toastEnterPrompt: "Veuillez entrer un prompt", |
|
|
toastImageRemoved: "Image supprimée", |
|
|
toastSettingsLoaded: "Paramètres du prompt chargés", |
|
|
toastRestoring: "Restauration de {count} images de l'historique...", |
|
|
toastNewSeed: "Nouvelle graine : {seed}", |
|
|
deleteTooltip: "Supprimer l'image", |
|
|
zoomTooltip: "Voir en plein écran", |
|
|
clearAllButton: "Tout Effacer", |
|
|
clearAllConfirm: "Êtes-vous sûr de vouloir vider toute la galerie et l'historique ? Cette action est irréversible.", |
|
|
toastGalleryCleared: "Galerie et historique vidés.", |
|
|
clearPrompt: "Effacer le Prompt" |
|
|
}, |
|
|
}; |
|
|
|
|
|
const I18nManager = (props, { getState, setState, subscribe }) => { |
|
|
const t = (key, params = {}) => { |
|
|
const lang = getState('ui.lang', 'en'); |
|
|
let str = translations[lang]?.[key] || translations['en'][key] || key; |
|
|
for (const p in params) { |
|
|
str = str.replace(`{${p}}`, params[p]); |
|
|
} |
|
|
return str; |
|
|
}; |
|
|
|
|
|
return { |
|
|
api: { t }, |
|
|
hooks: { |
|
|
onRegister: () => { |
|
|
subscribe('ui.lang', (lang) => { |
|
|
document.title = t('appTitle'); |
|
|
}); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const uniqueId = () => `${Date.now()}-${Math.floor(Math.random() * 1e9)}`; |
|
|
const debounce = (fn, delay) => { |
|
|
let timeout; |
|
|
return (...args) => { |
|
|
clearTimeout(timeout); |
|
|
timeout = setTimeout(() => fn(...args), delay); |
|
|
}; |
|
|
}; |
|
|
|
|
|
const sanitizeInput = (str) => { |
|
|
const div = document.createElement('div'); |
|
|
div.textContent = str; |
|
|
return div.innerHTML.replace(/[/?#]/g, ' '); |
|
|
}; |
|
|
|
|
|
|
|
|
const ToastSystem = (props, { getState, setState }) => { |
|
|
const showToast = (message, type = 'info', duration = 5000) => { |
|
|
const id = uniqueId(); |
|
|
const toast = { id, message, type }; |
|
|
setState('toasts', [...getState('toasts', []), toast]); |
|
|
|
|
|
setTimeout(() => { |
|
|
const toasts = getState('toasts', []).filter(t => t.id !== id); |
|
|
setState('toasts', toasts); |
|
|
}, duration); |
|
|
}; |
|
|
|
|
|
return { |
|
|
api: { showToast }, |
|
|
hooks: { onRegister: () => setState('toasts', []) } |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const PromptHistoryManager = (props, { getState, setState, headless }) => { |
|
|
const { t } = headless.I18nManager; |
|
|
const save = () => { |
|
|
const history = getState('promptHistory', []).map(item => ({ |
|
|
id: item.id, prompt: item.prompt, seed: item.seed, |
|
|
width: item.width, height: item.height, createdAt: item.createdAt, |
|
|
model: item.model |
|
|
})); |
|
|
localStorage.setItem('juris_history', JSON.stringify(history)); |
|
|
}; |
|
|
|
|
|
const load = () => { |
|
|
try { |
|
|
const stored = localStorage.getItem('juris_history'); |
|
|
if (stored) setState('promptHistory', JSON.parse(stored)); |
|
|
} catch { |
|
|
headless.ToastSystem.showToast(t('toastHistoryFail'), "error"); |
|
|
setState('promptHistory', []); |
|
|
} |
|
|
}; |
|
|
|
|
|
const clearHistory = () => { |
|
|
setState('promptHistory', []); |
|
|
localStorage.removeItem('juris_history'); |
|
|
}; |
|
|
|
|
|
return { |
|
|
hooks: { onRegister: load }, |
|
|
api: { |
|
|
add: (item) => { |
|
|
|
|
|
let history = [item, ...getState('promptHistory', [])].slice(0, 50); |
|
|
setState('promptHistory', history); |
|
|
save(); |
|
|
}, |
|
|
remove: (id) => { |
|
|
const newHistory = getState('promptHistory', []).filter(i => i.id !== id); |
|
|
setState('promptHistory', newHistory); |
|
|
save(); |
|
|
}, |
|
|
clearAll: () => { |
|
|
if (confirm(t('clearAllConfirm'))) { |
|
|
clearHistory(); |
|
|
setState('ui.imagesList', []); |
|
|
setState('ui.imagesById', {}); |
|
|
headless.ToastSystem.showToast(t('toastGalleryCleared'), 'info'); |
|
|
} |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const ImageGenerator = (props, { getState, setState, headless }) => { |
|
|
const { t } = headless.I18nManager; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const generateSingleImage = async (imageId, prompt, seed, width, height, modelUsed) => { |
|
|
try { |
|
|
const encodedPrompt = encodeURIComponent(sanitizeInput(prompt)); |
|
|
|
|
|
let url = `https://image.pollinations.ai/prompt/${encodedPrompt}?nologo=true&enhance=true&model=${modelUsed}&width=${width}&height=${height}`; |
|
|
if (seed) url += `&seed=${seed}`; |
|
|
|
|
|
await new Promise((resolve, reject) => { |
|
|
const img = new Image(); |
|
|
img.onload = () => resolve(url); |
|
|
img.onerror = () => reject(new Error("Image generation failed")); |
|
|
img.src = url; |
|
|
}); |
|
|
|
|
|
setState(`ui.imagesById.${imageId}`, { |
|
|
...getState(`ui.imagesById.${imageId}`), |
|
|
status: "loaded", |
|
|
imageUrl: url, |
|
|
}); |
|
|
|
|
|
headless.ToastSystem.showToast(t('toastImageSuccess'), "success"); |
|
|
} catch (error) { |
|
|
console.error("Image generation error:", error); |
|
|
setState(`ui.imagesById.${imageId}.status`, "error"); |
|
|
headless.ToastSystem.showToast(t('toastImageFail'), "error"); |
|
|
} |
|
|
}; |
|
|
|
|
|
const generateQueued = debounce(async () => { |
|
|
const queue = getState('ui.generationQueue', []); |
|
|
if (queue.length === 0) return; |
|
|
|
|
|
const [task, ...remaining] = queue; |
|
|
setState('ui.generationQueue', remaining); |
|
|
await generateSingleImage(...task); |
|
|
generateQueued(); |
|
|
}, 300); |
|
|
|
|
|
return { |
|
|
api: { |
|
|
generate: async () => { |
|
|
const prompt = sanitizeInput(getState("ui.prompt", "").trim()); |
|
|
const seed = getState("ui.seed", ""); |
|
|
const width = Math.min(2048, Math.max(64, getState("ui.width", "512"))); |
|
|
const height = Math.min(2048, Math.max(64, getState("ui.height", "512"))); |
|
|
const currentModel = getState("model", "flux"); |
|
|
|
|
|
if (!prompt) { |
|
|
headless.ToastSystem.showToast(t('toastEnterPrompt'), "error"); |
|
|
return; |
|
|
} |
|
|
|
|
|
const imageId = uniqueId(); |
|
|
const newImage = { |
|
|
id: imageId, status: "loading", prompt, seed, width, height, imageUrl: null, |
|
|
historyId: uniqueId(), createdAt: new Date().toISOString(), |
|
|
model: currentModel |
|
|
}; |
|
|
|
|
|
setState(`ui.imagesById.${imageId}`, newImage); |
|
|
setState("ui.imagesList", [imageId, ...getState("ui.imagesList", [])]); |
|
|
headless.PromptHistoryManager.add(newImage); |
|
|
|
|
|
|
|
|
setState('ui.generationQueue', [...getState('ui.generationQueue', []), [imageId, prompt, seed, width, height, currentModel]]); |
|
|
generateQueued(); |
|
|
}, |
|
|
|
|
|
regenerateFromHistory: () => { |
|
|
const history = getState('promptHistory', []); |
|
|
if (!history || history.length === 0) return; |
|
|
const count = Math.min(50, history.length); |
|
|
|
|
|
history.slice(0, count).forEach(item => { |
|
|
const id = uniqueId(); |
|
|
|
|
|
const modelToUse = item.model || getState("model", "flux"); |
|
|
|
|
|
const newImage = { |
|
|
id, status: "loading", prompt: item.prompt, seed: item.seed, width: item.width, |
|
|
height: item.height, imageUrl: null, historyId: item.id, |
|
|
createdAt: item.createdAt || new Date().toISOString(), |
|
|
model: modelToUse |
|
|
}; |
|
|
|
|
|
setState(`ui.imagesById.${id}`, newImage); |
|
|
setState("ui.imagesList", [...getState("ui.imagesList", []), id]); |
|
|
|
|
|
setState('ui.generationQueue', [...getState('ui.generationQueue', []), [id, item.prompt, item.seed, item.width, item.height, modelToUse]]); |
|
|
}); |
|
|
|
|
|
generateQueued(); |
|
|
headless.ToastSystem.showToast(t('toastRestoring', { count }), "info"); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const ImageDisplay = (props, { getState, setState, headless }) => ({ |
|
|
render: () => { |
|
|
const { t } = headless.I18nManager; |
|
|
const img = getState(`ui.imagesById.${props.id}`); |
|
|
if (!img) return "ignore"; |
|
|
|
|
|
return { |
|
|
div: { |
|
|
className: { "image-card": true, "featured": props.featured }, |
|
|
children: [ |
|
|
{ |
|
|
div: { |
|
|
className: "image-display", |
|
|
children: [ |
|
|
{ |
|
|
div: { |
|
|
className: "image-actions", |
|
|
children: [ |
|
|
{ |
|
|
button: { |
|
|
className: "action-btn delete", text: "✕", title: () => t('deleteTooltip'), |
|
|
onclick: () => { |
|
|
headless.PromptHistoryManager.remove(img.historyId); |
|
|
const ids = getState("ui.imagesList", []); |
|
|
setState("ui.imagesList", ids.filter((id) => id !== props.id)); |
|
|
const newMap = { ...getState("ui.imagesById") }; |
|
|
delete newMap[props.id]; |
|
|
setState("ui.imagesById", newMap); |
|
|
headless.ToastSystem.showToast(t('toastImageRemoved'), "info"); |
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
] |
|
|
} |
|
|
}, |
|
|
{ |
|
|
div: { |
|
|
style: { width: '100%', height: '100%' }, |
|
|
onclick: () => { |
|
|
setState("ui.prompt", img.prompt); |
|
|
setState("ui.seed", img.seed); |
|
|
setState("ui.width", img.width); |
|
|
setState("ui.height", img.height); |
|
|
setState("model", img.model || "flux"); |
|
|
headless.ToastSystem.showToast(t('toastSettingsLoaded'), "info"); |
|
|
}, |
|
|
children: () => { |
|
|
if (img.status === "loading") return [ |
|
|
{ |
|
|
div: |
|
|
{ |
|
|
style: { display: 'grid', height: '100%', placeItems: 'center' }, |
|
|
children: [{ div: { className: "spinner" } }] |
|
|
} |
|
|
} |
|
|
]; |
|
|
if (img.status === "error") return [{ div: { text: t('failedToLoad') } }]; |
|
|
return [{ img: { src: img.imageUrl, alt: img.prompt, loading: "lazy" } }]; |
|
|
} |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
}, |
|
|
{ |
|
|
div: { |
|
|
className: "image-info", |
|
|
children: [ |
|
|
{ div: { className: "image-prompt", text: img.prompt } }, |
|
|
{ |
|
|
div: { |
|
|
className: "image-meta", |
|
|
children: [ |
|
|
{ span: { text: `${img.width}x${img.height}` } }, |
|
|
{ span: { text: () => img.seed ? t('metaSeed', { seed: img.seed }) : t('metaRandom') } }, |
|
|
{ span: { text: `Model: ${img.model || "N/A"}` } } |
|
|
] |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
}; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const ToastDisplay = (props, { getState }) => ({ |
|
|
render: () => ({ |
|
|
div: { |
|
|
className: "toast-container", |
|
|
children: () => getState('toasts', []).map(toast => ({ |
|
|
div: { |
|
|
key: toast.id, className: `toast ${toast.type}`, |
|
|
children: [ |
|
|
{ div: { text: toast.type === 'success' ? '✓' : '⚠' } }, |
|
|
{ div: { text: toast.message } } |
|
|
] |
|
|
} |
|
|
})) |
|
|
} |
|
|
}) |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const GalleryControls = (props, { getState, setState, headless }) => ({ |
|
|
render: () => { |
|
|
const { t } = headless.I18nManager; |
|
|
return { |
|
|
div: { |
|
|
style: {display: "none"}, |
|
|
className: "tabs", |
|
|
children: [ |
|
|
{ |
|
|
button: { |
|
|
className: { "tab": true, "active": getState('ui.activeTab', 'images') === 'images' }, |
|
|
text: () => t('galleryTab'), |
|
|
onclick: () => setState('ui.activeTab', 'images') |
|
|
} |
|
|
}, |
|
|
{ |
|
|
button: { |
|
|
className: { "tab": true, "active": getState('ui.activeTab') === 'history' }, |
|
|
text: () => t('historyTab'), |
|
|
onclick: () => setState('ui.activeTab', 'history') |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const SelectModels = (props, { getState, setState, headless }) => { |
|
|
const { t } = headless.I18nManager; |
|
|
|
|
|
return { |
|
|
div: { |
|
|
className: "input-group", |
|
|
children: [ |
|
|
{ |
|
|
label: { |
|
|
style: { |
|
|
color: "var(--primary)", |
|
|
display: "block", |
|
|
marginBottom: "0.5rem", |
|
|
}, |
|
|
text: () => t('selectModelLabel') |
|
|
} |
|
|
}, |
|
|
{ |
|
|
select: { |
|
|
|
|
|
style: { |
|
|
background: "#1A1A2E", |
|
|
color: "var(--text-primary)", |
|
|
fontSize: "1rem", |
|
|
border: "1px solid rgba(255,255,255,0.5)", |
|
|
borderRadius: "5px", |
|
|
padding: "0.5rem 1rem", |
|
|
appearance: "none", |
|
|
cursor: "pointer", |
|
|
outline: "none", |
|
|
backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%20197.4l-14.3-14.3c-2.1-2.1-4.7-3.3-7.7-3.3s-5.6%201.2-7.7%203.3L146.2%20275.9%2034.7%20164.4c-2.1-2.1-4.7-3.3-7.7-3.3s-5.6%201.2-7.7%203.3L5%20178.7c-2.1%202.1-3.3%204.7-3.3%207.7s1.2%205.6%203.3%207.7l136.2%20136.2c2.1%202.1%204.7%203.3%207.7%203.3s5.6-1.2%207.7-3.3L287%20212.8c2.1-2.1%203.3-4.7%203.3-7.7s-1.2-5.6-3.3-7.7z%22%2F%3E%3C%2Fsvg%3E")', |
|
|
backgroundRepeat: 'no-repeat', |
|
|
backgroundPosition: 'right 0.7em top 50%', |
|
|
backgroundSize: '0.65em auto', |
|
|
paddingRight: '2.5em', |
|
|
minWidth: '100px', |
|
|
width: '100%' |
|
|
}, |
|
|
onchange: (e) => setState("model", e.target.value), |
|
|
value: () => getState("model", "flux"), |
|
|
children: [ |
|
|
|
|
|
{ |
|
|
option: { |
|
|
text: "Flux", |
|
|
value: "flux" |
|
|
} |
|
|
}, |
|
|
{ |
|
|
option: { |
|
|
text: "Turbo", |
|
|
value: "turbo" |
|
|
} |
|
|
}, |
|
|
] |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const PromptInput = (props, { getState, setState, headless }) => { |
|
|
const { t } = headless.I18nManager; |
|
|
return { |
|
|
div: { |
|
|
className: "input-group", |
|
|
children: [ |
|
|
|
|
|
{ |
|
|
div: { |
|
|
style: { display: "flex", gap: "2rem", flexDirection: "column"}, |
|
|
} |
|
|
}, |
|
|
{ SelectModels: {} }, |
|
|
{ |
|
|
div: { |
|
|
style: { |
|
|
position: "relative", |
|
|
top: "0.5rem", |
|
|
color: "var(--primary)", |
|
|
}, |
|
|
text: t("promptLabel") |
|
|
} |
|
|
}, |
|
|
{ |
|
|
textarea: { |
|
|
style: { width: "100%", |
|
|
maxWidth: '100%', |
|
|
minWidth: '100%', |
|
|
display: "flex", |
|
|
"flex-direction": "column", |
|
|
"background": "#1A1A2E", |
|
|
"color": "var(--text-primary)", |
|
|
"font-size": "1rem", |
|
|
"border": "1px solid rgba(255,255,255,0.5)", |
|
|
"border-radius": "5px", |
|
|
"padding": "1rem", |
|
|
spellcheck: "false" , |
|
|
autocorrect:"off", |
|
|
marginTop: "1rem" |
|
|
}, |
|
|
rows: "5", |
|
|
value: () => getState("ui.prompt", ""), |
|
|
oninput: e => setState("ui.prompt", e.target.value) |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
const Toolbox = (props, { getState, setState, headless }) => { |
|
|
|
|
|
const { t } = headless.I18nManager; |
|
|
return { |
|
|
div: { |
|
|
className: "toolbox-bar", |
|
|
children: [ |
|
|
{ div: { |
|
|
className: "toolbox-item", |
|
|
style: () => ({ display: 'flex', 'flex-direction': getState("ui.isMobile") ? 'column' : 'row' }), |
|
|
children: [ |
|
|
{ label: { text: () => t('widthLabel') } }, |
|
|
{ input: { |
|
|
type: "number", min: "64", max: "2048", step: "16", |
|
|
value: () => getState("ui.width", "512"), |
|
|
oninput: e => setState("ui.width", e.target.value) } }] } |
|
|
}, |
|
|
{ div: { |
|
|
className: "toolbox-item", |
|
|
style: () => ({ display: 'flex', 'flex-direction': getState("ui.isMobile") ? 'column' : 'row' }), |
|
|
children: [ |
|
|
{ label: { text: () => t('heightLabel') } }, |
|
|
{ input: { |
|
|
type: "number", min: "64", max: "2048", step: "16", |
|
|
value: () => getState("ui.height", "512"), |
|
|
oninput: e => setState("ui.height", e.target.value) } }] } }, |
|
|
{ div: { |
|
|
className: "toolbox-item", |
|
|
style: () => ({ display: 'flex', 'flex-direction': getState("ui.isMobile") ? 'column' : 'row' }), |
|
|
children: [ |
|
|
{ label: { text: () => t('seedLabel') } }, |
|
|
{ input: { |
|
|
type: "number", |
|
|
value: () => getState("ui.seed", ""), |
|
|
oninput: e => setState("ui.seed", e.target.value) } }] |
|
|
} }, |
|
|
{ |
|
|
div: { |
|
|
className: 'toolbox-actions', |
|
|
children: [ |
|
|
{ button: { |
|
|
type: "button", |
|
|
className: "btn btn-outline", |
|
|
text: () => t('randomizeButton'), |
|
|
onclick: () => { |
|
|
const newSeed = Math.floor(Math.random() * 1000000); |
|
|
setState("ui.seed", newSeed); |
|
|
headless.ToastSystem.showToast(t('toastNewSeed', { seed: newSeed }), "info"); } |
|
|
} }, |
|
|
{ button: { type: "submit", className: "btn", text: () => t('generateButton') } } |
|
|
] |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const ControlPanel = (props, { headless }) => { |
|
|
return { |
|
|
div: { |
|
|
className: "control-panel", |
|
|
children: [{ |
|
|
form: { |
|
|
className: "prompt-form", |
|
|
onsubmit: (e) => { e.preventDefault(); headless.ImageGenerator?.generate(); }, |
|
|
children: [ |
|
|
{ PromptInput: {} }, |
|
|
{ Toolbox: {} } |
|
|
] |
|
|
} |
|
|
}] |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const Gallery = (props, { getState, headless }) => ({ |
|
|
render: () => { |
|
|
const { t } = headless.I18nManager; |
|
|
const images = getState("ui.imagesList", []); |
|
|
if (images.length === 0) { |
|
|
return { |
|
|
div: { |
|
|
className: "empty-state", |
|
|
children: [ |
|
|
{ div: { className: "empty-state-icon", text: "🖼️" } }, |
|
|
{ h3: { text: () => t('emptyGalleryTitle') } }, |
|
|
{ p: { text: () => t('emptyGalleryDesc') } } |
|
|
] |
|
|
} |
|
|
}; |
|
|
} |
|
|
return { |
|
|
div: { |
|
|
className: "image-gallery", |
|
|
children: images.map((id, index) => ({ ImageDisplay: { key: id, id, featured: index === 0 } })) |
|
|
} |
|
|
}; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const LanguageSwitcher = (props, { getState, setState }) => { |
|
|
const currentLang = getState('ui.lang', 'en'); |
|
|
return { |
|
|
div: { |
|
|
children: [ |
|
|
{ |
|
|
div: { |
|
|
style: { |
|
|
display: "flex", |
|
|
gap: "1rem", |
|
|
justifyContent: "center", |
|
|
position: "absolute", |
|
|
right: 0, |
|
|
top: "-0.5rem" |
|
|
}, |
|
|
children: Object.keys(translations).map(langCode => ({ |
|
|
button: { |
|
|
style: { |
|
|
border:"none", |
|
|
cursor: "pointer", |
|
|
|
|
|
background: "rgba(255,255,255,0.121)", |
|
|
border: "1px solid rgba(255, 255, 255, 0.4)", |
|
|
fontSize: "2rem", |
|
|
width: "3rem" |
|
|
}, |
|
|
key: langCode, |
|
|
text: translations[langCode].flag , |
|
|
onclick: () => { |
|
|
setState('ui.lang', langCode); |
|
|
|
|
|
} |
|
|
} |
|
|
})) |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const Banner = (props, { getState, setState, headless }) => ({ |
|
|
render: () => { |
|
|
const { t } = headless.I18nManager; |
|
|
if (!getState('ui.isBannerVisible', true)) return "ignore"; |
|
|
|
|
|
return { |
|
|
div: { |
|
|
className: 'banner', |
|
|
children: [ |
|
|
{ |
|
|
div: { |
|
|
className: 'banner-content', |
|
|
children: [{ span: { text: () => t('bannerText') } }] |
|
|
} |
|
|
}, |
|
|
{ |
|
|
button: { |
|
|
className: 'banner-close', |
|
|
text: '✕', |
|
|
onclick: () => setState('ui.isBannerVisible', false) |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const Toolbar = (props, { setState, headless }) => ({ |
|
|
render: () => { |
|
|
const { t } = headless.I18nManager; |
|
|
return { |
|
|
div: { |
|
|
className: 'gallery-toolbar', |
|
|
style: { |
|
|
display: "flex", |
|
|
gap: "1rem", |
|
|
"justify-content": "center", |
|
|
"align-items": "center", padding: "0 0.5rem", |
|
|
"margin-bottom": "1.5rem" |
|
|
}, |
|
|
children: [ |
|
|
{ |
|
|
button: { |
|
|
className: 'btn btn-action', |
|
|
text: t("clearPrompt"), |
|
|
onclick: () => setState("ui.prompt", "") |
|
|
} |
|
|
}, |
|
|
{ |
|
|
button: { |
|
|
className: 'btn btn-danger', |
|
|
text: () => t('clearAllButton'), |
|
|
onclick: () => headless.PromptHistoryManager?.clearAll() |
|
|
} |
|
|
} |
|
|
] |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const App = (props, { headless }) => ({ |
|
|
render: () => { |
|
|
const { t } = headless.I18nManager; |
|
|
return { |
|
|
div: { |
|
|
className: "app-layout", |
|
|
children: [ |
|
|
{ |
|
|
div: { |
|
|
className: "header", |
|
|
children: [ |
|
|
{ LanguageSwitcher: {} }, |
|
|
{ h1: |
|
|
{ |
|
|
style: { 'margin-top': '2rem'}, |
|
|
text: () => t('headerTitle') |
|
|
} }, |
|
|
{ p: { className: "tagline", text: () => t('tagline') } } |
|
|
] |
|
|
} |
|
|
}, |
|
|
{ ControlPanel: {} }, |
|
|
{ Toolbar: {} }, |
|
|
|
|
|
{ Gallery: {} }, |
|
|
|
|
|
|
|
|
] |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const app = new Juris({ |
|
|
states: { |
|
|
ui: { |
|
|
lang: 'fr', |
|
|
isMobile: 'false', |
|
|
langSwitcherOpen: false, |
|
|
isBannerVisible: true, |
|
|
prompt: "Oil thick past paint of Paris in winter", |
|
|
seed: "57811", |
|
|
width: "1024", |
|
|
height: "1024", |
|
|
imagesList: [], |
|
|
imagesById: {}, |
|
|
generationQueue: [], |
|
|
activeTab: "images", |
|
|
lightbox: { activeImage: null }, |
|
|
}, |
|
|
model: "flux", |
|
|
promptHistory: [], |
|
|
toasts: [], |
|
|
}, |
|
|
components: { |
|
|
App, ControlPanel, PromptInput, Toolbox, |
|
|
GalleryControls, Gallery, ImageDisplay, ToastDisplay, |
|
|
LanguageSwitcher, Toolbar, Banner, SelectModels |
|
|
}, |
|
|
headlessComponents: { |
|
|
I18nManager: { fn: I18nManager, options: { autoInit: true } }, |
|
|
ImageGenerator: { fn: ImageGenerator, options: { autoInit: true } }, |
|
|
PromptHistoryManager: { fn: PromptHistoryManager, options: { autoInit: true } }, |
|
|
ToastSystem: { fn: ToastSystem, options: { autoInit: true } }, |
|
|
}, |
|
|
layout: { App: {} }, |
|
|
}); |
|
|
|
|
|
app.render(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (app.headlessAPIs.ImageGenerator) { |
|
|
app.headlessAPIs.ImageGenerator.regenerateFromHistory(); |
|
|
} |
|
|
}, 500); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; |
|
|
if (e.key === 'Escape') app.setState('ui.lightbox.activeImage', null); |
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { |
|
|
e.preventDefault(); |
|
|
app.headlessAPIs.ImageGenerator?.generate(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (window.matchMedia("(max-width: 640px)").matches) { |
|
|
app.setState("ui.isMobile", true); |
|
|
} |
|
|
else { |
|
|
app.setState("ui.isMobile", false); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
</script> |
|
|
</body> |
|
|
|
|
|
</html> |
|
|
|