Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ L'interfaccia nuova (spec in `docs/gui-v2-spec.md`) sopra l'engine v2, più gli
- **Gestore LoRA**: pila con ordine e pesi. **Sei ricette** (Foglio, Mondi, Mix, Set, Formati, Rinnova) al posto del catalogo lungo.
- **Modelli uncensored**: interruttore spento di serie, avviso da accettare, etichetta sulle voci.
- **Asset** in `resources/gui` (111 WebP, con manifest: prompt, seed, modello). 60 sono scontornati per colore, da rifare con BiRefNet.
- **Asset nelle tessere**: le 111 immagini ora si vedono. Anteprime degli stili (28), generi, categorie di Modifica, luoghi, meteo e stagioni, costumi, animali, espressioni, locandine delle ricette, copertine dei LoRA senza anteprima, modi dello Studio, mascotte e sfondi (nebulosa, pianeta, aurora). Passano da Vite (`src/renderer/src/asset.ts`) e finiscono nell'installer dentro `app.asar`. Se un file manca o non si carica la tessera resta con l'icona. Prove: `test/asset.mjs` (in `npm test`) e `test/asset-ui.mjs`.
- Non ancora provati con la GPU: qualità di SAM 2 e BiRefNet, riquadri dal vivo, LoRA su Klein.

## 1.1.0 — 2026-10-21
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build": "electron-vite build",
"start": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
"test": "node test/modelli.mjs && node test/catalogo.mjs && node test/spazio.mjs && node test/lora.mjs && node test/regioni.mjs && node test/adatta.mjs && node test/grafi_v2.mjs && node test/confronta_grafi.mjs",
"test": "node test/modelli.mjs && node test/catalogo.mjs && node test/spazio.mjs && node test/lora.mjs && node test/regioni.mjs && node test/adatta.mjs && node test/grafi_v2.mjs && node test/confronta_grafi.mjs && node test/asset.mjs",
"prendi-uv": "node scripts/prendi-uv.mjs",
"dist": "npm run prendi-uv && npm run build && electron-builder --win --x64",
"prova": "node test/prove.mjs",
Expand Down
53 changes: 53 additions & 0 deletions src/renderer/src/anteprime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Da quale id (stile, controllo, genere, modo, ricetta…) all'immagine di resources/gui: SOLO le chiavi, nessun import di file,
// così si prova in Node (test/asset.mjs). Una chiave è «gruppo/id» e corrisponde a resources/gui/<gruppo>/<id>.webp.
// Gli URL veri li dà asset.ts (Vite li mette nell'installer). Se una chiave non ha il file la tessera resta con l'icona di prima.

export type ChiaveAsset = string

/** i generi che hanno la tessera in Crea (nell'ordine della spec) */
export const GENERI_TESSERA = ['foto', 'ritratto', 'prodotto', 'poster', 'infografica', 'logo', 'social', 'miniatura', 'fumetto', 'invito', 'menu']

export const chiaveStile = (id: string): ChiaveAsset => `stili/${id}`
/** «libero» è il genere senza genere: ha la sua tessera */
export const chiaveGenere = (id: string): ChiaveAsset => `generi/${id || 'libero'}`

/** i controlli di Modifica che hanno la griglia con le anteprime → la cartella degli asset (le opzioni hanno lo stesso id del file) */
export const GRUPPO_CONTROLLO: Record<string, string> = {
animale: 'animali', costume: 'costumi', luogo: 'luoghi', meteo: 'meteo', stagione: 'meteo', espressione: 'facce'
}
/** gli id delle opzioni che nel file si chiamano in un altro modo */
const NOME_FILE: Record<string, Record<string, string>> = { espressione: { ride: 'ridere' } }

export function chiaveOpzione(controllo: string, opzione: string): ChiaveAsset | undefined {
const g = GRUPPO_CONTROLLO[controllo]
return g ? `${g}/${NOME_FILE[controllo]?.[opzione] ?? opzione}` : undefined
}

/** le categorie di Modifica (rapide.ts) → tessera/<file> */
const CATEGORIA_FILE: Record<string, string> = {
persone: 'persone', vestiti: 'vestiti', situazioni: 'situazioni', prospettiva: 'prospettiva', luce: 'luce-colore', stili: 'stili', testo: 'testi', ritocco: 'ritocco'
}
export const chiaveCategoria = (id: string): ChiaveAsset | undefined => (CATEGORIA_FILE[id] ? `tessera/${CATEGORIA_FILE[id]}` : undefined)
export const CATEGORIE_CON_ASSET = Object.keys(CATEGORIA_FILE)

/** i modi dello Studio (Crea, Modifica, Box, Ricette) */
export const chiaveModo = (id: string): ChiaveAsset => `modi/${id}`
/** i modi di Modifica: Espandi e Estrai hanno la tessera, «Scrivi» usa quella di Modifica */
export const chiaveModoModifica = (id: string): ChiaveAsset => (id === 'espandi' || id === 'estrai' ? `tessera/${id}` : 'modi/modifica')

/** le sei ricette: l'id del workflow è «r-foglio»; Unisci e Come l'esempio sono le due metà di Mix */
export const RICETTE_CON_ASSET = ['foglio', 'mondi', 'mix', 'set', 'formati', 'rinnova']
export function chiaveRicetta(idWorkflow: string): ChiaveAsset | undefined {
const id = idWorkflow === 'unisci' || idWorkflow === 'esempio' ? 'mix' : idWorkflow.replace(/^r-/, '')
return RICETTE_CON_ASSET.includes(id) ? `ricette/${id}` : undefined
}

/** la copertina generica di un LoRA senza anteprima: «Qwen» o «Klein» dalla famiglia; se serve a più famiglie o non si capisce → niente (resta la sigla colorata) */
export function chiaveLora(l: { famiglie?: string[]; compatibile?: string }): ChiaveAsset | undefined {
const f = new Set((l.famiglie || []).map((x) => (x === 'qwen' ? 'qwen' : 'klein')))
if (f.size === 1) return `lora/${[...f][0]}`
return !f.size && l.compatibile === 'si' ? 'lora/qwen' : undefined
}

export const SFONDI = { nebulosa: 'sfondi/nebulosa', calda: 'sfondi/nebulosa-calda', stelle: 'sfondi/stelle', aurora: 'sfondi/aurora', eroe: 'sfondi/hero-pianeta', setup: 'sfondi/setup-pianeta' } as const
export const MASCOTTE = { vuoto: 'mascotte/fluttua', errore: 'mascotte/cartello', setup: 'mascotte/saluta' } as const
19 changes: 19 additions & 0 deletions src/renderer/src/asset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Gli URL degli asset della GUI v2 (resources/gui/**/*.webp). Vite li importa come URL: finiscono in out/renderer/assets e quindi
// nell'installer (electron-builder impacchetta out/**) senza extraResources né protocolli. I più piccoli di 4 kB diventano data: URI (CSP: img-src data:).
// La chiave è «gruppo/id» (anteprime.ts). Chi non c'è → undefined: la tessera ripiega sull'icona di prima, senza errori.
// Fuori da Vite (i test che compilano i componenti con esbuild per il rendering lato server) `import.meta.glob` non esiste: l'elenco resta vuoto.
let FILE: Record<string, string> = {}
try {
FILE = import.meta.glob('../../../resources/gui/**/*.webp', { eager: true, query: '?url', import: 'default' }) as Record<string, string>
} catch {
/* senza Vite: nessun asset, le tessere restano con le icone */
}

const URLS: Record<string, string> = {}
for (const [percorso, url] of Object.entries(FILE)) {
const m = /resources\/gui\/(.+)\.webp$/.exec(percorso)
if (m) URLS[m[1]] = url
}

export const urlAsset = (chiave: string | undefined): string | undefined => (chiave ? URLS[chiave] : undefined)
export const elencoAsset = (): string[] => Object.keys(URLS)
3 changes: 2 additions & 1 deletion src/renderer/src/componenti/ControlliCategoria.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// campioni colore, il quadrante 3×3 e le righe di testo. Il valore sta nell'id dell'idea attiva: qui non c'è stato proprio.
import { useEffect, useState, type JSX } from 'react'
import { Tessera, Tessere } from './Tessere'
import { chiaveOpzione } from '../anteprime'
import { conControllo, controlliDi, leggiMulti, scriviMulti, valoreDa, type Controllo, type Parte } from '../controlli'
import type { Rapida } from '../rapide'

Expand Down Expand Up @@ -70,7 +71,7 @@ function Multi(props: { c: Extract<Controllo, { tipo: 'multi' }>; valore: Record
}
return (
<Tessere key={x.id} colonne={x.opzioni.length <= 2 ? 2 : x.opzioni.length <= 4 ? 4 : 5}>
{x.opzioni.map((o) => <Tessera key={o.id} id={`ctl-${p.c.id}-${o.id}`} icona={<span aria-hidden>{o.icona}</span>} nome={o.nome} su={p.valore[x.id] === o.id} titolo={`${o.nome} · un clic la accende, un altro la spegne`} onClick={() => imposta(x.id, o.id)} />)}
{x.opzioni.map((o) => <Tessera key={o.id} id={`ctl-${p.c.id}-${o.id}`} icona={<span aria-hidden>{o.icona}</span>} asset={chiaveOpzione(p.c.id, o.id)} nome={o.nome} su={p.valore[x.id] === o.id} titolo={`${o.nome} · un clic la accende, un altro la spegne`} onClick={() => imposta(x.id, o.id)} />)}
</Tessere>
)
})}
Expand Down
7 changes: 4 additions & 3 deletions src/renderer/src/componenti/Generi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { usaStato } from '../stato'
import { I } from './Icone'
import { Casella } from './Casella'
import { Tessera, Tessere } from './Tessere'
import { chiaveGenere, GENERI_TESSERA } from '../anteprime'
import { PRESET, campiCon, componiPreset, presetDa } from '../preset'

const icona = (nome: string): JSX.Element => {
Expand All @@ -16,7 +17,7 @@ const icona = (nome: string): JSX.Element => {
}

/** i generi che hanno la tessera, nell'ordine della spec */
const GENERI = ['foto', 'ritratto', 'prodotto', 'poster', 'infografica', 'logo', 'social', 'miniatura', 'fumetto', 'invito', 'menu']
const GENERI = GENERI_TESSERA

/** sceglie un genere (o «Libero» con '') */
export function scegliGenere(id: string, prima: (prompt: string) => void): void {
Expand All @@ -43,9 +44,9 @@ export function GrigliaGeneri({ prima }: { prima: (prompt: string) => void }): J
<Tessere colonne={4}>
{GENERI.map((id) => {
const p = presetDa(id)!
return <Tessera key={id} id={`genere-${id}`} icona={icona(p.icona)} nome={p.nome === 'Post social' ? 'Social' : p.nome} titolo={`${p.nome}: ${p.descrizione}`} su={crea.preset === id} onClick={() => scegliGenere(id, prima)} />
return <Tessera key={id} id={`genere-${id}`} icona={icona(p.icona)} asset={chiaveGenere(id)} nome={p.nome === 'Post social' ? 'Social' : p.nome} titolo={`${p.nome}: ${p.descrizione}`} su={crea.preset === id} onClick={() => scegliGenere(id, prima)} />
})}
<Tessera id="genere-libero" icona={icona('libero')} nome="Libero" titolo="Scrivi tu il prompt, senza genere" su={!crea.preset || nascosto} onClick={() => scegliGenere('', prima)} />
<Tessera id="genere-libero" icona={icona('libero')} asset={chiaveGenere('')} nome="Libero" titolo="Scrivi tu il prompt, senza genere" su={!crea.preset || nascosto} onClick={() => scegliGenere('', prima)} />
</Tessere>
)
}
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/src/componenti/LabVista.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useEffect, type JSX } from 'react'
import { usaStato } from '../stato'
import { api, urlFile } from '../api'
import { I } from './Icone'
import { ImgAsset } from './Tessere'
import { chiaveRicetta } from '../anteprime'
import { PrimaDopo } from './PrimaDopo'
import { VistaZoom } from './VistaZoom'
import { percentuale } from './Coda'
Expand All @@ -29,6 +31,7 @@ export function Catalogo(p: { cerca: string; scelto: string | null; scegli: (id:
<div className="carte-wf">
{voci.map((w) => (
<button key={w.id} className={`carta-wf ${p.scelto === w.id || (w.id === 'r-mix' && (p.scelto === 'unisci' || p.scelto === 'esempio')) ? 'su' : ''}`} onClick={() => p.scegli(w.id)}>
<ImgAsset chiave={chiaveRicetta(w.id)} className="locandina-wf" />
<div className="riga spazia">
<span className="ic-wf grande">{icona(w.icona)}</span>
<span className="riga" style={{ gap: 6 }}>{w.utente && <span className="distintivo">mia</span>}<span className={`distintivo ${w.modo === 'catena' ? 'forse' : 'si'}`}>{w.modo === 'catena' ? <><I.catena /> catena</> : w.modo === 'speciale' ? <><I.crea /> foto nuova</> : <><I.serie /> serie</>}</span></span>
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/componenti/ScaffaliStili.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { JSX } from 'react'
import { INTENSITA, SCAFFALI, STILI, type Intensita } from '../stili'
import { Pianeti, Tessera } from './Tessere'
import { chiaveStile } from '../anteprime'

export function ScaffaliStili(p: { valore: string; scegli: (id: string) => void }): JSX.Element {
return (
Expand All @@ -11,7 +12,7 @@ export function ScaffaliStili(p: { valore: string; scegli: (id: string) => void
<div className="titolo-scaffale">{sc.nome}</div>
<div className="scaffale">
{STILI.filter((s) => s.scaffale === sc.id).map((s) => (
<Tessera key={s.id} id={`stile-${s.id}`} icona={<span aria-hidden>{s.icona}</span>} nome={s.nome} fondo={s.fondo} titolo={`${s.nome}: ${s.frammento}`} su={p.valore === s.id} onClick={() => p.scegli(p.valore === s.id ? '' : s.id)} />
<Tessera key={s.id} id={`stile-${s.id}`} icona={<span aria-hidden>{s.icona}</span>} nome={s.nome} fondo={s.fondo} asset={chiaveStile(s.id)} pieno titolo={`${s.nome}: ${s.frammento}`} su={p.valore === s.id} onClick={() => p.scegli(p.valore === s.id ? '' : s.id)} />
))}
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/renderer/src/componenti/SceltaLora.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useEffect, useRef, useState, type JSX } from 'react'
import { usaStato } from '../stato'
import { urlFile } from '../api'
import { I } from './Icone'
import { ImgAsset } from './Tessere'
import { chiaveLora } from '../anteprime'
import type { InfoLora } from '@shared/tipi'

export function coloreDa(nome: string): string {
Expand All @@ -19,11 +21,14 @@ export const sigla = (nome: string): string => {
}

export function Copertina({ l, grande }: { l: InfoLora; grande?: boolean }): JSX.Element {
// senza anteprima: la copertina generica della famiglia (asset), e se non c'è la sigla colorata di prima
const generica = l.anteprima ? undefined : chiaveLora(l)
return l.anteprima ? (
<img src={urlFile(l.anteprima)} alt="" />
) : (
<div style={{ background: coloreDa(l.nome), width: '100%', height: '100%', display: 'grid', placeItems: 'center' }}>
<span className={grande ? 'sigla' : ''}>{sigla(l.nome)}</span>
<div style={{ background: coloreDa(l.nome), width: '100%', height: '100%', display: 'grid', placeItems: 'center', position: 'relative' }}>
<ImgAsset chiave={generica} stile={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
<span className={grande ? 'sigla' : ''} style={generica ? { position: 'relative', textShadow: '0 2px 10px rgba(0,0,0,.7)' } : undefined}>{sigla(l.nome)}</span>
</div>
)
}
Expand Down
25 changes: 22 additions & 3 deletions src/renderer/src/componenti/Tessere.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// I pezzi base della GUI v2 «Orbita»: la tessera (icona grande + una parola), la griglia, la fascetta Uncensored e i pulsanti di misura.
// Regola d'oro: una tessera ha un'icona e una parola; il nome lungo sta nel tooltip (`titolo`). Niente chip di testo sotto le tessere.
import type { CSSProperties, JSX, ReactNode } from 'react'
import { useState, type CSSProperties, type JSX, type ReactNode } from 'react'
import { urlAsset } from '../asset'
import { I } from './Icone'

export interface PropsTessera {
Expand All @@ -22,15 +23,33 @@ export interface PropsTessera {
fondo?: string
uncensored?: boolean
id?: string
/** l'immagine di resources/gui («gruppo/id», vedi anteprime.ts): al posto dell'icona, o a tutta tessera con `pieno`. Se il file manca o non si carica resta l'icona di prima */
asset?: string
/** l'immagine riempie la tessera (anteprime degli stili) invece di fare da icona */
pieno?: boolean
}

/** l'immagine di un asset, o niente se il file non c'è o non si carica (il chiamante ripiega sull'icona) */
export function ImgAsset(p: { chiave: string | undefined; className?: string; stile?: CSSProperties; ripiego?: ReactNode }): JSX.Element | null {
const url = urlAsset(p.chiave)
const [rotto, setRotto] = useState<string | null>(null)
if (!url || rotto === url) return <>{p.ripiego ?? null}</>
return <img src={url} alt="" aria-hidden draggable={false} className={p.className} style={p.stile} onError={() => setRotto(url)} />
}

export function Tessera(p: PropsTessera): JSX.Element {
const larga = p.sotto !== undefined
const classe = ['tessera-o', larga ? 'larga' : '', p.fondo ? 'anteprima' : '', p.su ? 'su' : '', p.daScaricare ? 'da-scaricare' : '', p.uncensored ? 'fiamma' : '', p.disabilitata ? 'spenta' : ''].filter(Boolean).join(' ')
const url = urlAsset(p.asset)
const [rotto, setRotto] = useState<string | null>(null)
const img = url && rotto !== url ? url : undefined
const pieno = !!img && !!p.pieno && !larga
const classe = ['tessera-o', larga ? 'larga' : '', p.fondo || pieno ? 'anteprima' : '', pieno ? 'foto' : '', img && !pieno ? 'con-img' : '', p.su ? 'su' : '', p.daScaricare ? 'da-scaricare' : '', p.uncensored ? 'fiamma' : '', p.disabilitata ? 'spenta' : ''].filter(Boolean).join(' ')
const immagine = img ? <img src={img} alt="" aria-hidden draggable={false} className={pieno ? 'pieno' : 'ic-img'} onError={() => setRotto(img)} /> : null
return (
<button type="button" className={classe} disabled={p.disabilitata} onClick={p.onClick} title={p.titolo || p.nome} aria-pressed={p.su} aria-label={p.nome} data-id={p.id}>
{p.fondo && <span className="fondo" style={{ background: p.fondo }} />}
<span className="ic">{p.icona}</span>
{pieno && immagine}
{!pieno && <span className="ic">{immagine || p.icona}</span>}
{larga ? (
<span className="testi">
<span className="nome">{p.nome} {p.uncensored && <Fascetta />}</span>
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import './orbita.css'
import { App } from './App'
import { Component, type ReactNode } from 'react'
import { usaStato } from './stato'
import { ImgAsset } from './componenti/Tessere'
import { MASCOTTE } from './anteprime'

/** se una pagina va in errore non resta la finestra bianca: si vede cosa è successo e si può ricaricare */
class Salvagente extends Component<{ children: ReactNode }, { errore: string | null }> {
Expand All @@ -15,6 +17,7 @@ class Salvagente extends Component<{ children: ReactNode }, { errore: string | n
if (!this.state.errore) return this.props.children
return (
<div style={{ padding: 32, fontFamily: 'sans-serif' }}>
<ImgAsset chiave={MASCOTTE.errore} stile={{ width: 120, height: 120, objectFit: 'contain' }} />
<h2>Qualcosa è andato storto</h2>
<p style={{ color: '#666' }}>{this.state.errore}</p>
<p>Le tue foto e la galleria sono al sicuro. Ricarica l'interfaccia per ripartire.</p>
Expand Down
Loading
Loading