push iniziale
This commit is contained in:
297
README.md
297
README.md
@@ -1,3 +1,296 @@
|
|||||||
# Traduttore
|
# Traduttore AI 🌍
|
||||||
|
|
||||||
Applicazione desktop che sfrutta l'AI per tradurre testi dall'italiano all'inglese e viceversa. Configurata per modelli ollama in locale.
|
Un'applicazione desktop elegante e moderna per tradurre testi dall'inglese all'italiano e viceversa utilizzando modelli AI locali tramite Ollama. L'app rileva automaticamente la lingua del testo inserito e fornisce la traduzione istantanea.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## ✨ Caratteristiche
|
||||||
|
|
||||||
|
- 🎨 **Design moderno** con gradienti colorati e interfaccia intuitiva
|
||||||
|
- 🔄 **Rilevamento automatico** della lingua (inglese/italiano)
|
||||||
|
- 🤖 **Integrazione con Ollama** per traduzioni locali e private
|
||||||
|
- ⚡ **Traduzione istantanea** con un solo click
|
||||||
|
- 📋 **Menu contestuale** con Copia/Incolla (tasto destro)
|
||||||
|
- ⚙️ **Configurazione flessibile** di modello e URL Ollama
|
||||||
|
- 🖥️ **Multi-piattaforma** (Linux e Windows)
|
||||||
|
- 🎯 **Rilevamento automatico** direzione traduzione (EN→IT o IT→EN)
|
||||||
|
|
||||||
|
## 📋 Requisiti
|
||||||
|
|
||||||
|
- **Node.js** v18 o superiore
|
||||||
|
- **npm** o **yarn**
|
||||||
|
- **Ollama** installato e in esecuzione
|
||||||
|
- Almeno un modello LLM scaricato in Ollama (consigliati: llama3, mistral, qwen3:8b)
|
||||||
|
|
||||||
|
## 🚀 Installazione
|
||||||
|
|
||||||
|
### 1. Clona il repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/tuousername/traduttore-ai.git
|
||||||
|
cd Traduttore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Installa le dipendenze
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Installa Ollama
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installazione automatica
|
||||||
|
curl -fsSL https://ollama.com/install.sh | sh
|
||||||
|
|
||||||
|
# Oppure scarica manualmente da:
|
||||||
|
# https://ollama.com/download
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
|
||||||
|
1. Scarica l'installer da: https://ollama.com/download/windows
|
||||||
|
2. Esegui il file `.exe` e segui la procedura guidata
|
||||||
|
3. Ollama si avvierà automaticamente
|
||||||
|
|
||||||
|
### 4. Scarica un modello
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Modello consigliato (buon bilanciamento qualità/velocità)
|
||||||
|
ollama pull llama3
|
||||||
|
|
||||||
|
# Oppure per traduzioni più accurate
|
||||||
|
ollama pull qwen3:8b
|
||||||
|
|
||||||
|
# Per computer meno potenti
|
||||||
|
ollama pull mistral
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Verifica che Ollama sia in esecuzione
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux - controlla se il servizio è attivo
|
||||||
|
systemctl status ollama
|
||||||
|
|
||||||
|
# Oppure prova a fare una richiesta
|
||||||
|
curl http://localhost:11434/api/tags
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 Utilizzo
|
||||||
|
|
||||||
|
### Modalità Sviluppo
|
||||||
|
|
||||||
|
Per avviare l'applicazione in modalità sviluppo con hot-reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build per Distribuzione
|
||||||
|
|
||||||
|
#### Linux (AppImage)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:linux
|
||||||
|
```
|
||||||
|
|
||||||
|
Il file eseguibile verrà creato in:
|
||||||
|
- `dist/Traduttore AI-x.x.x.AppImage`
|
||||||
|
|
||||||
|
Per eseguire:
|
||||||
|
```bash
|
||||||
|
chmod +x "dist/Traduttore AI-x.x.x.AppImage"
|
||||||
|
./"dist/Traduttore AI-x.x.x.AppImage"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows (Installer .exe)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
Il file installer verrà creato in:
|
||||||
|
- `dist/Traduttore AI Setup x.x.x.exe`
|
||||||
|
|
||||||
|
Installa l'applicazione eseguendo il file `.exe`.
|
||||||
|
|
||||||
|
#### Build completa (tutte le piattaforme)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Come Usare
|
||||||
|
|
||||||
|
### Primo Avvio
|
||||||
|
|
||||||
|
1. **Avvia l'app** con `npm start` (dev) o esegui il file buildato
|
||||||
|
2. **Configura Ollama**: clicca sull'icona ⚙️ in alto a destra
|
||||||
|
3. **Inserisci l'URL**: di default è `http://localhost:11434`
|
||||||
|
4. **Clicca "Aggiorna"** per caricare i modelli disponibili
|
||||||
|
5. **Seleziona un modello** dalla lista
|
||||||
|
6. **Clicca "Salva"**
|
||||||
|
|
||||||
|
### Tradurre un Testo
|
||||||
|
|
||||||
|
1. **Scrivi o incolla** il testo nell'area a sinistra
|
||||||
|
2. L'app rileverà **automaticamente** la lingua (🇮🇹 Italiano o 🇬🇧 Inglese)
|
||||||
|
3. Clicca il pulsante **"Traduci"** o premi `Ctrl+Enter`
|
||||||
|
4. La traduzione apparirà nell'area a destra
|
||||||
|
|
||||||
|
### Scorciatoie da Tastiera
|
||||||
|
|
||||||
|
| Tasto | Azione |
|
||||||
|
|-------|--------|
|
||||||
|
| `Ctrl+Enter` | Traduci il testo |
|
||||||
|
| `Ctrl+,` | Apri impostazioni |
|
||||||
|
| `Ctrl+Q` | Esci dall'applicazione |
|
||||||
|
| `Esc` | Chiudi modale impostazioni |
|
||||||
|
| `Ctrl+C` | Copia testo selezionato |
|
||||||
|
| `Ctrl+V` | Incolla testo |
|
||||||
|
| `Ctrl+X` | Taglia testo selezionato |
|
||||||
|
| `Ctrl+A` | Seleziona tutto il testo |
|
||||||
|
|
||||||
|
### Menu Contestuale (Tasto Destro)
|
||||||
|
|
||||||
|
Clicca con il tasto destro su qualsiasi textarea per:
|
||||||
|
- Taglia
|
||||||
|
- Copia
|
||||||
|
- Incolla
|
||||||
|
- Seleziona tutto
|
||||||
|
- Elimina
|
||||||
|
|
||||||
|
## 🛠️ Risoluzione Problemi
|
||||||
|
|
||||||
|
### "Errore: fetch failed" o "ECONNREFUSED"
|
||||||
|
|
||||||
|
**Problema**: L'app non riesce a connettersi a Ollama
|
||||||
|
|
||||||
|
**Soluzioni**:
|
||||||
|
1. Verifica che Ollama sia in esecuzione:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:11434/api/tags
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Se non risponde, avvia Ollama:
|
||||||
|
```bash
|
||||||
|
ollama serve
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Verifica che la porta non sia occupata:
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
netstat -tulpn | grep 11434
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr 11434
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Nessun modello trovato"
|
||||||
|
|
||||||
|
**Problema**: Ollama non ha modelli installati
|
||||||
|
|
||||||
|
**Soluzione**:
|
||||||
|
```bash
|
||||||
|
ollama pull llama3
|
||||||
|
# oppure
|
||||||
|
ollama pull qwen3:8b
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traduzione lenta o non risponde
|
||||||
|
|
||||||
|
**Possibili cause**:
|
||||||
|
- Modello troppo pesante per il tuo hardware
|
||||||
|
- Memoria RAM insufficiente
|
||||||
|
- GPU non utilizzata (se disponibile)
|
||||||
|
|
||||||
|
**Soluzioni**:
|
||||||
|
- Usa un modello più leggero: `ollama pull mistral` o `ollama pull llama3:8b`
|
||||||
|
- Chiudi altre applicazioni per liberare RAM
|
||||||
|
- Verifica che Ollama stia usando la GPU: `ollama ps`
|
||||||
|
|
||||||
|
### Errore su Windows: "Ollama not found"
|
||||||
|
|
||||||
|
**Soluzione**:
|
||||||
|
1. Verifica che Ollama sia installato correttamente
|
||||||
|
2. Aggiungi Ollama al PATH di sistema:
|
||||||
|
- Pannello di Controllo → Sistema → Impostazioni Avanzate → Variabili d'ambiente
|
||||||
|
- Aggiungi il percorso di installazione di Ollama (es: `C:\Users\<username>\AppData\Local\Programs\Ollama`)
|
||||||
|
|
||||||
|
### Problemi con IPv6 (Linux)
|
||||||
|
|
||||||
|
Se vedi errori come `connect ECONNREFUSED ::1:11434`, l'app ora gestisce automaticamente questo problema sostituendo `localhost` con `127.0.0.1`.
|
||||||
|
|
||||||
|
## 📁 Struttura del Progetto
|
||||||
|
|
||||||
|
```
|
||||||
|
Traduttore/
|
||||||
|
├── main.js # Processo principale Electron
|
||||||
|
├── preload.js # Bridge sicuro tra processi
|
||||||
|
├── index.html # Interfaccia utente
|
||||||
|
├── renderer.js # Logica applicazione
|
||||||
|
├── styles.css # Stili e design
|
||||||
|
├── package.json # Configurazione npm
|
||||||
|
└── README.md # Questo file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configurazione Avanzata
|
||||||
|
|
||||||
|
### Cambiare la porta di Ollama
|
||||||
|
|
||||||
|
Se vuoi usare una porta diversa per Ollama:
|
||||||
|
|
||||||
|
**Linux**:
|
||||||
|
```bash
|
||||||
|
OLLAMA_HOST=0.0.0.0:8080 ollama serve
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows**:
|
||||||
|
```cmd
|
||||||
|
set OLLAMA_HOST=0.0.0.0:8080
|
||||||
|
ollama serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Poi nell'app, imposta l'URL: `http://localhost:8080`
|
||||||
|
|
||||||
|
### Modelli Consigliati
|
||||||
|
|
||||||
|
| Modello | Dimensione | Qualità | Velocità | Uso |
|
||||||
|
|---------|-----------|---------|----------|-----|
|
||||||
|
| llama3 | 4.7 GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Uso generale |
|
||||||
|
| qwen3:8b | 4.9 GB | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Traduzioni accurate |
|
||||||
|
| mistral | 4.1 GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Buon compromesso |
|
||||||
|
| llama3:8b | 4.7 GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Versione ridotta |
|
||||||
|
|
||||||
|
## 🤝 Contribuire
|
||||||
|
|
||||||
|
Se vuoi contribuire al progetto:
|
||||||
|
|
||||||
|
1. Fork il repository
|
||||||
|
2. Crea un branch: `git checkout -b feature/nuova-funzionalita`
|
||||||
|
3. Committa le modifiche: `git commit -am 'Aggiunta nuova funzionalità'`
|
||||||
|
4. Push al branch: `git push origin feature/nuova-funzionalita`
|
||||||
|
5. Apri una Pull Request
|
||||||
|
|
||||||
|
## 📝 Changelog
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- ✨ Rilascio iniziale
|
||||||
|
- 🎨 Design moderno con gradienti
|
||||||
|
- 🤖 Integrazione Ollama
|
||||||
|
- 🔄 Rilevamento automatico lingua
|
||||||
|
- 📋 Menu contestuale tasto destro
|
||||||
|
- 🖥️ Supporto Linux e Windows
|
||||||
|
|
||||||
|
## 📄 Licenza
|
||||||
|
|
||||||
|
MIT License - vedi file [LICENSE](LICENSE) per i dettagli.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Fatto con ❤️ e ☕ per la comunità open source
|
||||||
|
|
||||||
|
**Autore**: Tu
|
||||||
|
**Versione**: 1.0.0
|
||||||
|
|||||||
133
index.html
Normal file
133
index.html
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Traduttore AI</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<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=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<path d="M2 12h20"/>
|
||||||
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Traduttore AI</h1>
|
||||||
|
</div>
|
||||||
|
<button class="settings-btn" id="settingsBtn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l-.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="language-indicator">
|
||||||
|
<span id="detectedLang">Rilevamento automatico...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="translation-area">
|
||||||
|
<div class="input-section">
|
||||||
|
<div class="textarea-container">
|
||||||
|
<textarea
|
||||||
|
id="inputText"
|
||||||
|
placeholder="Inserisci il testo da tradurre..."
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
<div class="textarea-actions">
|
||||||
|
<button class="action-btn" id="clearBtn" title="Cancella">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M3 6h18"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
|
||||||
|
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="char-count" id="charCount">0 caratteri</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="output-section">
|
||||||
|
<div class="textarea-container">
|
||||||
|
<textarea
|
||||||
|
id="outputText"
|
||||||
|
placeholder="La traduzione apparirà qui..."
|
||||||
|
readonly
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
<div class="textarea-actions">
|
||||||
|
<button class="action-btn" id="copyBtn" title="Copia">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="status-indicator" id="statusIndicator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="translate-section">
|
||||||
|
<button class="translate-btn" id="translateBtn">
|
||||||
|
<span class="btn-text">Traduci</span>
|
||||||
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M5 12h14"/>
|
||||||
|
<path d="M12 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Impostazioni -->
|
||||||
|
<div class="modal" id="settingsModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Impostazioni</h2>
|
||||||
|
<button class="close-btn" id="closeModal">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="baseUrl">Ollama Base URL</label>
|
||||||
|
<input type="text" id="baseUrl" value="http://localhost:11434" placeholder="http://localhost:11434">
|
||||||
|
<span class="help-text">Lascia vuoto per usare localhost:11434</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="modelSelect">Modello</label>
|
||||||
|
<select id="modelSelect">
|
||||||
|
<option value="">Carica modelli...</option>
|
||||||
|
</select>
|
||||||
|
<button class="refresh-btn" id="refreshModels">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="23 4 23 10 17 10"/>
|
||||||
|
<polyline points="1 20 1 14 7 14"/>
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||||
|
</svg>
|
||||||
|
Aggiorna
|
||||||
|
</button>
|
||||||
|
<span class="help-text">Seleziona il modello per la traduzione</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="save-btn" id="saveSettings">Salva</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notification -->
|
||||||
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
|
<script src="renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
193
main.js
Normal file
193
main.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 900,
|
||||||
|
minHeight: 600,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
},
|
||||||
|
titleBarStyle: 'hiddenInset',
|
||||||
|
show: false
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadFile('index.html');
|
||||||
|
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Menu contestuale (tasto destro)
|
||||||
|
mainWindow.webContents.on('context-menu', (event, params) => {
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'Taglia',
|
||||||
|
accelerator: 'CmdOrCtrl+X',
|
||||||
|
role: 'cut',
|
||||||
|
enabled: params.editFlags.canCut
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Copia',
|
||||||
|
accelerator: 'CmdOrCtrl+C',
|
||||||
|
role: 'copy',
|
||||||
|
enabled: params.editFlags.canCopy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Incolla',
|
||||||
|
accelerator: 'CmdOrCtrl+V',
|
||||||
|
role: 'paste',
|
||||||
|
enabled: params.editFlags.canPaste
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Seleziona tutto',
|
||||||
|
accelerator: 'CmdOrCtrl+A',
|
||||||
|
role: 'selectAll',
|
||||||
|
enabled: params.editFlags.canSelectAll
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Elimina',
|
||||||
|
role: 'delete',
|
||||||
|
enabled: params.editFlags.canDelete
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
contextMenu.popup(mainWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
createMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
const template = [
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Esci',
|
||||||
|
accelerator: 'CmdOrCtrl+Q',
|
||||||
|
click: () => {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Impostazioni',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Configura Modello e URL',
|
||||||
|
accelerator: 'CmdOrCtrl+,',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.webContents.send('open-settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Aiuto',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Informazioni',
|
||||||
|
click: () => {
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Informazioni',
|
||||||
|
message: 'Traduttore AI',
|
||||||
|
detail: 'Applicazione desktop per traduzioni con modelli AI locali tramite Ollama.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('translate-text', async (event, { text, direction, model, baseUrl }) => {
|
||||||
|
try {
|
||||||
|
const sourceLang = direction === 'en-it' ? 'inglese' : 'italiano';
|
||||||
|
const targetLang = direction === 'en-it' ? 'italiano' : 'inglese';
|
||||||
|
|
||||||
|
// Usa 127.0.0.1 invece di localhost per evitare problemi IPv6
|
||||||
|
const cleanBaseUrl = baseUrl.replace('localhost', '127.0.0.1');
|
||||||
|
|
||||||
|
const response = await fetch(`${cleanBaseUrl}/api/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
prompt: `Traduci il seguente testo da ${sourceLang} a ${targetLang}. Rispondi SOLO con la traduzione, senza spiegazioni o commenti aggiuntivi:\n\n${text}`,
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Errore HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { success: true, translation: data.response };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-models', async (event, baseUrl) => {
|
||||||
|
try {
|
||||||
|
// Assicurati che l'URL sia pulito e sostituisci localhost con 127.0.0.1 per evitare problemi IPv6
|
||||||
|
let cleanUrl = baseUrl.trim().replace(/\/$/, '');
|
||||||
|
cleanUrl = cleanUrl.replace('localhost', '127.0.0.1');
|
||||||
|
const fullUrl = `${cleanUrl}/api/tags`;
|
||||||
|
|
||||||
|
console.log('Richiesta modelli da:', fullUrl);
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Risposta status:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Errore HTTP: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Modelli trovati:', data.models?.length || 0);
|
||||||
|
return { success: true, models: data.models || [] };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore in get-models:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
4103
package-lock.json
generated
Normal file
4103
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "traduttore-ai",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Applicazione desktop per traduzioni con AI locale",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron .",
|
||||||
|
"build": "electron-builder",
|
||||||
|
"build:win": "electron-builder --win",
|
||||||
|
"build:linux": "electron-builder --linux"
|
||||||
|
},
|
||||||
|
"author": "Tu",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^28.0.0",
|
||||||
|
"electron-builder": "^24.9.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.tuo.traduttore",
|
||||||
|
"productName": "Traduttore AI",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"main.js",
|
||||||
|
"preload.js",
|
||||||
|
"index.html",
|
||||||
|
"renderer.js",
|
||||||
|
"styles.css",
|
||||||
|
"node_modules/**/*"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "icon.ico"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": "AppImage",
|
||||||
|
"icon": "icon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
preload.js
Normal file
7
preload.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
translateText: (data) => ipcRenderer.invoke('translate-text', data),
|
||||||
|
getModels: (baseUrl) => ipcRenderer.invoke('get-models', baseUrl),
|
||||||
|
onOpenSettings: (callback) => ipcRenderer.on('open-settings', callback)
|
||||||
|
});
|
||||||
335
renderer.js
Normal file
335
renderer.js
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
// Elementi DOM
|
||||||
|
const inputText = document.getElementById('inputText');
|
||||||
|
const outputText = document.getElementById('outputText');
|
||||||
|
const translateBtn = document.getElementById('translateBtn');
|
||||||
|
const detectedLang = document.getElementById('detectedLang');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const copyBtn = document.getElementById('copyBtn');
|
||||||
|
const charCount = document.getElementById('charCount');
|
||||||
|
const statusIndicator = document.getElementById('statusIndicator');
|
||||||
|
const settingsBtn = document.getElementById('settingsBtn');
|
||||||
|
const settingsModal = document.getElementById('settingsModal');
|
||||||
|
const closeModal = document.getElementById('closeModal');
|
||||||
|
const baseUrlInput = document.getElementById('baseUrl');
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
const refreshModels = document.getElementById('refreshModels');
|
||||||
|
const saveSettings = document.getElementById('saveSettings');
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
|
||||||
|
// Stato applicazione
|
||||||
|
let isTranslating = false;
|
||||||
|
let currentDetectedLang = null;
|
||||||
|
|
||||||
|
// Carica impostazioni salvate
|
||||||
|
function loadSettings() {
|
||||||
|
const savedBaseUrl = localStorage.getItem('ollamaBaseUrl') || 'http://localhost:11434';
|
||||||
|
const savedModel = localStorage.getItem('ollamaModel') || '';
|
||||||
|
|
||||||
|
baseUrlInput.value = savedBaseUrl;
|
||||||
|
|
||||||
|
if (savedModel) {
|
||||||
|
const existingOption = Array.from(modelSelect.options).find(opt => opt.value === savedModel);
|
||||||
|
if (!existingOption) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = savedModel;
|
||||||
|
option.textContent = savedModel;
|
||||||
|
modelSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
modelSelect.value = savedModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salva impostazioni
|
||||||
|
function saveSettingsData() {
|
||||||
|
const baseUrl = baseUrlInput.value.trim() || 'http://localhost:11434';
|
||||||
|
const model = modelSelect.value;
|
||||||
|
|
||||||
|
localStorage.setItem('ollamaBaseUrl', baseUrl);
|
||||||
|
localStorage.setItem('ollamaModel', model);
|
||||||
|
|
||||||
|
closeSettingsModal();
|
||||||
|
showToast('Impostazioni salvate!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carica modelli disponibili
|
||||||
|
async function loadAvailableModels() {
|
||||||
|
const baseUrl = baseUrlInput.value.trim() || 'http://localhost:11434';
|
||||||
|
|
||||||
|
console.log('DEBUG - Tentativo di caricare modelli da:', baseUrl);
|
||||||
|
console.log('DEBUG - electronAPI disponibile?', !!window.electronAPI);
|
||||||
|
console.log('DEBUG - getModels funzione?', typeof window.electronAPI?.getModels);
|
||||||
|
|
||||||
|
refreshModels.disabled = true;
|
||||||
|
refreshModels.innerHTML = `
|
||||||
|
<svg class="spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="23 4 23 10 17 10"/>
|
||||||
|
<polyline points="1 20 1 14 7 14"/>
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||||
|
</svg>
|
||||||
|
Caricamento...
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cleanBaseUrl = baseUrl.trim();
|
||||||
|
console.log('Tentativo connessione a:', cleanBaseUrl);
|
||||||
|
const result = await window.electronAPI.getModels(cleanBaseUrl);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
modelSelect.innerHTML = '<option value="">Seleziona un modello...</option>';
|
||||||
|
|
||||||
|
result.models.forEach(model => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = model.name;
|
||||||
|
option.textContent = model.name;
|
||||||
|
modelSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedModel = localStorage.getItem('ollamaModel');
|
||||||
|
if (savedModel && result.models.find(m => m.name === savedModel)) {
|
||||||
|
modelSelect.value = savedModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`${result.models.length} modelli trovati!`);
|
||||||
|
} else {
|
||||||
|
showToast('Errore: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Errore completo:', error);
|
||||||
|
showToast('Errore di connessione: ' + (error.message || 'Verifica che Ollama sia in esecuzione'), 'error');
|
||||||
|
} finally {
|
||||||
|
refreshModels.disabled = false;
|
||||||
|
refreshModels.innerHTML = `
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="23 4 23 10 17 10"/>
|
||||||
|
<polyline points="1 20 1 14 7 14"/>
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||||
|
</svg>
|
||||||
|
Aggiorna
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rileva la lingua del testo
|
||||||
|
function detectLanguage(text) {
|
||||||
|
if (!text.trim()) {
|
||||||
|
currentDetectedLang = null;
|
||||||
|
detectedLang.textContent = 'Rilevamento automatico...';
|
||||||
|
detectedLang.className = '';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caratteri italiani comuni
|
||||||
|
const italianChars = /[àèéìòùÀÈÉÌÒÙ]/;
|
||||||
|
const italianWords = /\b(ciao|grazie|buongiorno|come|sono|per|con|su|tra|fra|nel|del|degli|della)\b/gi;
|
||||||
|
|
||||||
|
// Caratteri inglesi comuni (rari in italiano)
|
||||||
|
const englishChars = /[wkyjWQX]/;
|
||||||
|
const englishWords = /\b(the|and|for|are|with|you|that|have|this|from|they|we|say|her|she|or|an|will|my|one|all|would|there|their|what|so|up|out|if|about|who|get|which|go|me|when|make|can|like|time|no|just|him|know|take|people|into|year|your|good|some|could|them|see|other|than|then|now|look|only|come|its|over|think|also|back|after|use|two|how|our|work|first|well|way|even|new|want|because|any|these|give|day|most|us)\b/gi;
|
||||||
|
|
||||||
|
const hasItalianChars = italianChars.test(text);
|
||||||
|
const hasItalianWords = (text.match(italianWords) || []).length;
|
||||||
|
const hasEnglishChars = englishChars.test(text);
|
||||||
|
const hasEnglishWords = (text.match(englishWords) || []).length;
|
||||||
|
|
||||||
|
// Logica di rilevamento
|
||||||
|
let detected = null;
|
||||||
|
|
||||||
|
if (hasItalianChars) {
|
||||||
|
detected = 'it';
|
||||||
|
} else if (hasEnglishChars && !hasItalianWords) {
|
||||||
|
detected = 'en';
|
||||||
|
} else if (hasItalianWords > hasEnglishWords) {
|
||||||
|
detected = 'it';
|
||||||
|
} else if (hasEnglishWords > italianWords) {
|
||||||
|
detected = 'en';
|
||||||
|
} else {
|
||||||
|
// Fallback: verifica la distribuzione delle vocali
|
||||||
|
const vowels = text.toLowerCase().match(/[aeiou]/g) || [];
|
||||||
|
const yCount = (text.match(/y/gi) || []).length;
|
||||||
|
if (yCount > 0) {
|
||||||
|
detected = 'en';
|
||||||
|
} else {
|
||||||
|
detected = 'it'; // default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDetectedLang = detected;
|
||||||
|
updateLanguageIndicator();
|
||||||
|
return detected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiorna l'indicatore di lingua
|
||||||
|
function updateLanguageIndicator() {
|
||||||
|
if (currentDetectedLang === 'it') {
|
||||||
|
detectedLang.innerHTML = '🇮🇹 Italiano → 🇬🇧 Inglese';
|
||||||
|
detectedLang.className = 'detected-it';
|
||||||
|
} else if (currentDetectedLang === 'en') {
|
||||||
|
detectedLang.innerHTML = '🇬🇧 Inglese → 🇮🇹 Italiano';
|
||||||
|
detectedLang.className = 'detected-en';
|
||||||
|
} else {
|
||||||
|
detectedLang.textContent = 'Rilevamento automatico...';
|
||||||
|
detectedLang.className = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traduci testo
|
||||||
|
async function translateText() {
|
||||||
|
const text = inputText.value.trim();
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
showToast('Inserisci del testo da tradurre', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = localStorage.getItem('ollamaBaseUrl') || 'http://localhost:11434';
|
||||||
|
const model = localStorage.getItem('ollamaModel');
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
showToast('Configura un modello nelle impostazioni', 'warning');
|
||||||
|
openSettingsModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rileva la lingua se non già rilevata
|
||||||
|
const detected = currentDetectedLang || detectLanguage(text);
|
||||||
|
if (!detected) {
|
||||||
|
showToast('Impossibile rilevare la lingua', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = detected === 'en' ? 'en-it' : 'it-en';
|
||||||
|
|
||||||
|
isTranslating = true;
|
||||||
|
translateBtn.disabled = true;
|
||||||
|
translateBtn.innerHTML = `
|
||||||
|
<span class="spinner"></span>
|
||||||
|
<span class="btn-text">Traduzione...</span>
|
||||||
|
`;
|
||||||
|
statusIndicator.className = 'status-indicator translating';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.translateText({
|
||||||
|
text,
|
||||||
|
direction,
|
||||||
|
model,
|
||||||
|
baseUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
outputText.value = result.translation.trim();
|
||||||
|
statusIndicator.className = 'status-indicator success';
|
||||||
|
showToast('Traduzione completata!', 'success');
|
||||||
|
} else {
|
||||||
|
outputText.value = '';
|
||||||
|
statusIndicator.className = 'status-indicator error';
|
||||||
|
showToast('Errore: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
outputText.value = '';
|
||||||
|
statusIndicator.className = 'status-indicator error';
|
||||||
|
showToast('Errore: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
isTranslating = false;
|
||||||
|
translateBtn.disabled = false;
|
||||||
|
translateBtn.innerHTML = `
|
||||||
|
<span class="btn-text">Traduci</span>
|
||||||
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M5 12h14"/>
|
||||||
|
<path d="M12 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiorna conteggio caratteri
|
||||||
|
function updateCharCount() {
|
||||||
|
const count = inputText.value.length;
|
||||||
|
charCount.textContent = `${count} caratter${count === 1 ? 'e' : 'i'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copia negli appunti
|
||||||
|
async function copyToClipboard() {
|
||||||
|
const text = outputText.value;
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
showToast('Testo copiato!', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
outputText.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
showToast('Testo copiato!', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulisci input
|
||||||
|
function clearInput() {
|
||||||
|
inputText.value = '';
|
||||||
|
outputText.value = '';
|
||||||
|
updateCharCount();
|
||||||
|
statusIndicator.className = 'status-indicator';
|
||||||
|
currentDetectedLang = null;
|
||||||
|
detectedLang.textContent = 'Rilevamento automatico...';
|
||||||
|
detectedLang.className = '';
|
||||||
|
inputText.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostra toast
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.className = `toast show ${type}`;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestione modale
|
||||||
|
function openSettingsModal() {
|
||||||
|
settingsModal.classList.add('show');
|
||||||
|
loadAvailableModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettingsModal() {
|
||||||
|
settingsModal.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
translateBtn.addEventListener('click', translateText);
|
||||||
|
inputText.addEventListener('input', () => {
|
||||||
|
updateCharCount();
|
||||||
|
detectLanguage(inputText.value);
|
||||||
|
});
|
||||||
|
clearBtn.addEventListener('click', clearInput);
|
||||||
|
copyBtn.addEventListener('click', copyToClipboard);
|
||||||
|
settingsBtn.addEventListener('click', openSettingsModal);
|
||||||
|
closeModal.addEventListener('click', closeSettingsModal);
|
||||||
|
refreshModels.addEventListener('click', loadAvailableModels);
|
||||||
|
saveSettings.addEventListener('click', saveSettingsData);
|
||||||
|
|
||||||
|
// Chiudi modale con ESC
|
||||||
|
settingsModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === settingsModal) {
|
||||||
|
closeSettingsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeSettingsModal();
|
||||||
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
translateText();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ascolta richiesta apertura impostazioni dal menu
|
||||||
|
window.electronAPI.onOpenSettings((event) => {
|
||||||
|
openSettingsModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inizializzazione
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadSettings();
|
||||||
|
updateCharCount();
|
||||||
|
});
|
||||||
599
styles.css
Normal file
599
styles.css
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
/* Variabili colori - Design allegro e moderno */
|
||||||
|
:root {
|
||||||
|
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
--success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
--warning-gradient: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
|
||||||
|
--bg-primary: #f8fafc;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-tertiary: #f1f5f9;
|
||||||
|
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--text-tertiary: #94a3b8;
|
||||||
|
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 12px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-xl: 24px;
|
||||||
|
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-normal: 250ms ease;
|
||||||
|
--transition-slow: 350ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset e base */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container principale */
|
||||||
|
.app-container {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
color: #667eea;
|
||||||
|
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: none;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn:hover {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: white;
|
||||||
|
transform: rotate(30deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicatore lingua automatica */
|
||||||
|
.language-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-indicator span {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-indicator span.detected-it {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-indicator span.detected-en {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Area traduzione */
|
||||||
|
.translation-area {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section,
|
||||||
|
.output-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-container:focus-within {
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-primary);
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#outputText {
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: #667eea;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status indicator */
|
||||||
|
.status-indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-tertiary);
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.translating {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.success {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.error {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulsante traduci */
|
||||||
|
.translate-section {
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-btn {
|
||||||
|
padding: 16px 48px;
|
||||||
|
border: none;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
color: white;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-3px) scale(1.02);
|
||||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transition: transform var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-btn:hover:not(:disabled) .btn-icon {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
transform: scale(0.95);
|
||||||
|
transition: transform var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show .modal-content {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input,
|
||||||
|
.setting-item select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input:focus,
|
||||||
|
.setting-item select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover:not(:disabled) {
|
||||||
|
border-color: #667eea;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn svg.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
padding: 12px 32px;
|
||||||
|
border: none;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
color: white;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Notification */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(100px);
|
||||||
|
padding: 16px 32px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 2000;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background: var(--success-gradient);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background: var(--secondary-gradient);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.translation-area {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-group {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swap-btn {
|
||||||
|
order: 1;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar personalizzata */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user