Compare commits
7 Commits
wip-databa
...
apk
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c0bcdb833 | |||
| 5e9377c681 | |||
| b174e66c8b | |||
| 3394113ea0 | |||
| 1f71846e23 | |||
| 9346117603 | |||
| 2e956aef75 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -11,7 +11,8 @@ lerna-debug.log*
|
||||
currentCommit.txt
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist/*
|
||||
!dist/android/
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
@@ -25,3 +26,14 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Capacitor
|
||||
android/
|
||||
ios/
|
||||
.capacitor/
|
||||
|
||||
# Build output
|
||||
output/
|
||||
*.apk
|
||||
!dist/android/*.apk
|
||||
*.aab
|
||||
83
Dockerfile
Normal file
83
Dockerfile
Normal file
@@ -0,0 +1,83 @@
|
||||
# Dockerfile per build APK Android con Capacitor
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# Evita prompt interattivi
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Installa dipendenze base
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
unzip \
|
||||
wget \
|
||||
openjdk-21-jdk \
|
||||
build-essential \
|
||||
imagemagick \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Installa Node.js 20.x (LTS)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Installa Gradle 8.5
|
||||
ENV GRADLE_VERSION=8.5
|
||||
RUN wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip -P /tmp \
|
||||
&& unzip -d /opt/gradle /tmp/gradle-${GRADLE_VERSION}-bin.zip \
|
||||
&& ln -s /opt/gradle/gradle-${GRADLE_VERSION}/bin/gradle /usr/bin/gradle \
|
||||
&& rm /tmp/gradle-${GRADLE_VERSION}-bin.zip
|
||||
|
||||
# Installa Android SDK
|
||||
ENV ANDROID_SDK_ROOT=/opt/android-sdk
|
||||
ENV ANDROID_HOME=${ANDROID_SDK_ROOT}
|
||||
ENV PATH=${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools
|
||||
|
||||
RUN mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools \
|
||||
&& wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -P /tmp \
|
||||
&& unzip -d ${ANDROID_SDK_ROOT}/cmdline-tools /tmp/commandlinetools-linux-9477386_latest.zip \
|
||||
&& mv ${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools ${ANDROID_SDK_ROOT}/cmdline-tools/latest \
|
||||
&& rm /tmp/commandlinetools-linux-9477386_latest.zip
|
||||
|
||||
# Accetta licenze Android SDK
|
||||
RUN yes | sdkmanager --licenses || true
|
||||
|
||||
# Installa componenti Android necessari
|
||||
RUN sdkmanager "platform-tools" \
|
||||
"platforms;android-34" \
|
||||
"build-tools;34.0.0" \
|
||||
"extras;google;google_play_services" \
|
||||
"extras;android;m2repository" \
|
||||
"extras;google;m2repository"
|
||||
|
||||
# Imposta JAVA_HOME in base all'architettura
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
echo "export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-${ARCH}" >> /etc/profile && \
|
||||
ln -sf /usr/lib/jvm/java-21-openjdk-${ARCH} /usr/lib/jvm/default-java
|
||||
|
||||
ENV JAVA_HOME=/usr/lib/jvm/default-java
|
||||
|
||||
# Directory di lavoro
|
||||
WORKDIR /app
|
||||
|
||||
# Copia package files per cache layer
|
||||
COPY package*.json ./
|
||||
|
||||
# Installa dipendenze npm
|
||||
RUN npm ci --legacy-peer-deps || npm install --legacy-peer-deps
|
||||
|
||||
# Copia il resto del progetto
|
||||
COPY . .
|
||||
|
||||
# Build script
|
||||
CMD ["bash", "-c", "\
|
||||
npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
npx cap add android && \
|
||||
npx cap sync && \
|
||||
sed -i 's/android:configChanges=\"\\([^\"]*\\)\"/android:configChanges=\"\\1\" android:screenOrientation=\"sensorLandscape\"/g' android/app/src/main/AndroidManifest.xml && \
|
||||
bash setup-android-icons.sh && \
|
||||
cd android && ./gradlew assembleDebug --no-daemon && \
|
||||
mkdir -p /app/dist/android && \
|
||||
cp app/build/outputs/apk/debug/app-debug.apk /app/dist/android/segnapunti-debug.apk && \
|
||||
echo '' && \
|
||||
echo 'APK generato: /app/dist/android/segnapunti-debug.apk'\
|
||||
"]
|
||||
439
README.md
439
README.md
@@ -1,8 +1,437 @@
|
||||
# Vue 3 + Vite
|
||||
# nvm use v20.2.0
|
||||
# Segnapunti - Anto
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
Un'applicazione web segnapunti per pallavolo con funzionalità di sintesi vocale, modalità formazione e supporto PWA (Progressive Web App).
|
||||
|
||||
## Recommended IDE Setup
|
||||
## Cos'è
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
Segnapunti è un'applicazione standalone per tenere il punteggio durante le partite di pallavolo. L'applicazione è ottimizzata per dispositivi mobili in modalità landscape (orizzontale) e può essere installata come app nativa grazie al supporto PWA.
|
||||
|
||||
### Caratteristiche principali
|
||||
|
||||
- **Segnapunti digitale**: Visualizzazione chiara del punteggio per le due squadre (Home e Guest)
|
||||
- **Contatore set**: Traccia i set vinti da ciascuna squadra (fino a 3 set)
|
||||
- **Indicatore battuta**: Mostra quale squadra ha il servizio
|
||||
- **Modalità formazione**: Visualizza la rotazione dei giocatori in campo (posizioni 1-6)
|
||||
- **Sintesi vocale**: Annuncia il punteggio in italiano
|
||||
- **PWA**: Installabile come app standalone con funzionamento offline
|
||||
- **Controllo da tastiera**: Scorciatoie per gestire rapidamente il punteggio
|
||||
- **No Sleep**: Impedisce allo schermo di spegnersi durante l'uso
|
||||
- **Fullscreen automatico**: Su dispositivi mobili si avvia a schermo intero
|
||||
|
||||
## Come funziona
|
||||
|
||||
### Interfaccia
|
||||
|
||||
L'applicazione divide lo schermo in due metà:
|
||||
|
||||
- **Metà sinistra (gialla su sfondo nero)**: Squadra Home - Antoniana
|
||||
- **Metà destra (bianca su sfondo blu)**: Squadra Guest
|
||||
|
||||
#### Visualizzazioni
|
||||
|
||||
1. **Modalità Punteggio** (default):
|
||||
- Mostra il punteggio corrente in caratteri molto grandi
|
||||
- Cliccando sul punteggio si incrementa di 1
|
||||
- Cliccando sul nome della squadra si decrementa di 1
|
||||
|
||||
2. **Modalità Formazione**:
|
||||
- Mostra le posizioni dei giocatori in campo (1-6)
|
||||
- La formazione ruota automaticamente quando si incrementa il punteggio
|
||||
- L'ordine visualizzato è: [3, 2, 1, 4, 5, 0] (ordine inverso rispetto alla rotazione standard)
|
||||
|
||||
#### Funzionalità
|
||||
|
||||
- **Reset**: Azzera punteggio e formazioni, mantiene i nomi delle squadre
|
||||
- **Cambio servizio**: Toggle manuale del servizio tra le squadre
|
||||
- **Configurazione nomi**: Dialog per modificare i nomi delle squadre
|
||||
- **Sintesi vocale**:
|
||||
- Pronuncia il punteggio in italiano
|
||||
- Se 0-0 dice "zero a zero"
|
||||
- Se pari dice "[punteggio] pari"
|
||||
- Altrimenti annuncia prima il punteggio della squadra al servizio
|
||||
- **Incremento set**: Click sul contatore set per aumentarlo (si azzera dopo 2)
|
||||
|
||||
### Controlli da tastiera
|
||||
|
||||
- **Ctrl + M**: Apri dialog configurazione nomi
|
||||
- **Ctrl + B**: Mostra/nascondi barra pulsanti in basso
|
||||
- **Ctrl + F**: Attiva modalità fullscreen
|
||||
- **Ctrl + S**: Pronuncia il punteggio
|
||||
- **Ctrl + Z**: Alterna tra modalità punteggio e formazione
|
||||
- **Ctrl + ↑**: Incrementa punteggio Home
|
||||
- **Ctrl + ↓**: Decrementa punteggio Home
|
||||
- **Ctrl + →**: Incrementa set Home
|
||||
- **Shift + ↑**: Incrementa punteggio Guest
|
||||
- **Shift + ↓**: Decrementa punteggio Guest
|
||||
- **Shift + →**: Incrementa set Guest
|
||||
- **Ctrl + ←**: Cambia servizio
|
||||
|
||||
### Comportamento su mobile
|
||||
|
||||
Su dispositivi mobili l'applicazione:
|
||||
1. Attiva automaticamente NoSleep per impedire lo spegnimento dello schermo
|
||||
2. Si avvia in modalità fullscreen
|
||||
3. Esegue un test di sintesi vocale al caricamento
|
||||
4. Mostra il pulsante "Esci" per chiudere l'app
|
||||
|
||||
## Stack tecnologico
|
||||
|
||||
### Framework e librerie
|
||||
|
||||
- **Vue 3** (^3.2.47): Framework JavaScript progressivo per l'interfaccia utente
|
||||
- **Vite** (^4.3.9): Build tool moderno e veloce
|
||||
- **Wave UI** (^3.3.0): Libreria di componenti UI per Vue
|
||||
- **NoSleep.js** (^0.12.0): Previene lo spegnimento automatico dello schermo
|
||||
- **vite-plugin-pwa** (^0.16.0): Plugin per generare la PWA
|
||||
|
||||
### API Web utilizzate
|
||||
|
||||
- **Web Speech API**: Per la sintesi vocale (Text-to-Speech)
|
||||
- **Fullscreen API**: Per la modalità schermo intero
|
||||
- **Service Worker**: Per il funzionamento offline (PWA)
|
||||
|
||||
## Come svilupparlo
|
||||
|
||||
### Prerequisiti
|
||||
|
||||
- **Node.js** (versione 14 o superiore, raccomandata v20.2.0)
|
||||
- **npm** o **yarn**
|
||||
|
||||
### Installazione
|
||||
|
||||
1. Clona il repository:
|
||||
|
||||
2. Installa le dipendenze:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Comandi di sviluppo
|
||||
|
||||
#### Avviare il server di sviluppo
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Questo comando avvia Vite in modalità development. L'applicazione sarà disponibile su `http://localhost:5173` (o altra porta se già occupata).
|
||||
|
||||
#### Build per produzione
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
Genera i file ottimizzati per la produzione nella cartella `dist/`. Include:
|
||||
- Minificazione del codice
|
||||
- Tree-shaking per rimuovere codice non utilizzato
|
||||
- Generazione del Service Worker per la PWA
|
||||
- Generazione del manifest per l'installazione
|
||||
|
||||
#### Anteprima build di produzione
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
Avvia un server locale per testare la build di produzione.
|
||||
|
||||
La configurazione PWA si trova in [vite.config.js](vite.config.js:10-32):
|
||||
|
||||
- **Display**: fullscreen
|
||||
- **Orientamento**: landscape (orizzontale)
|
||||
- **Colore tema**: bianco
|
||||
- **Base path**: `/segnap` in produzione, `/` in sviluppo
|
||||
|
||||
### Modifiche comuni
|
||||
|
||||
#### Cambiare i colori delle squadre
|
||||
|
||||
Modifica il file [src/style.css](src/style.css:101-108):
|
||||
```css
|
||||
.home {
|
||||
background-color: black;
|
||||
color: yellow;
|
||||
}
|
||||
.guest {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
#### Modificare i nomi predefiniti
|
||||
|
||||
Nel file [src/components/HomePage.vue](src/components/HomePage.vue:20), modifica:
|
||||
```javascript
|
||||
nomi: { home: "Antoniana", guest: "Guest" }
|
||||
```
|
||||
|
||||
#### Cambiare la voce della sintesi vocale
|
||||
|
||||
Nel file [src/components/HomePage.vue](src/components/HomePage.vue:104), modifica:
|
||||
```javascript
|
||||
msg.voice = voices.find(voice => voice.name === 'Google italiano');
|
||||
```
|
||||
|
||||
#### Modificare l'ordine di visualizzazione delle formazioni
|
||||
|
||||
Nel template del componente [src/components/HomePage.vue](src/components/HomePage.vue:181), l'ordine di visualizzazione delle posizioni è controllato dall'array nell'attributo `v-for`. La sequenza `[3, 2, 1, 4, 5, 0]` rappresenta l'ordine in cui vengono mostrate le posizioni dei giocatori sulla griglia 2x3 del campo.
|
||||
|
||||
Per modificare l'ordine di visualizzazione, modifica questo array:
|
||||
```vue
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
|
||||
{{ sp.form.home[x] }}
|
||||
</div>
|
||||
```
|
||||
|
||||
L'ordine attuale mostra le posizioni in questo schema:
|
||||
```
|
||||
3 2 1
|
||||
4 5 0
|
||||
```
|
||||
|
||||
Dove 0 corrisponde alla posizione 1 nel pallavolo, 1 alla posizione 2, e così via.
|
||||
|
||||
## Testing e Deploy
|
||||
|
||||
### Testing su dispositivi mobili
|
||||
|
||||
Per testare l'applicazione su dispositivi mobili nella stessa rete locale:
|
||||
|
||||
1. Avvia il server di sviluppo:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. Vite mostrerà l'indirizzo locale e di rete. Se non viene mostrato, trova l'indirizzo IP del tuo computer:
|
||||
```bash
|
||||
# Su Linux/Mac
|
||||
hostname -I
|
||||
# oppure
|
||||
ifconfig | grep "inet "
|
||||
|
||||
# Su Windows
|
||||
ipconfig
|
||||
```
|
||||
|
||||
3. Dal dispositivo mobile, connesso alla stessa rete Wi-Fi, naviga su:
|
||||
```
|
||||
http://[tuo-ip]:5173
|
||||
```
|
||||
|
||||
4. Accetta eventuali permessi richiesti dal browser per:
|
||||
- Modalità fullscreen
|
||||
- Sintesi vocale
|
||||
- Wake Lock (prevenzione spegnimento schermo)
|
||||
|
||||
### Build e Deploy
|
||||
|
||||
#### Build per produzione
|
||||
|
||||
Genera i file ottimizzati per la produzione:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Questo comando crea una cartella `dist/` contenente:
|
||||
- HTML, CSS e JavaScript minificati
|
||||
- Service Worker per il funzionamento offline
|
||||
- Manifest PWA per l'installazione come app
|
||||
- Asset ottimizzati
|
||||
|
||||
#### Anteprima della build
|
||||
|
||||
Prima del deploy, puoi testare la build in locale:
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
#### Deploy su GitHub Pages
|
||||
|
||||
1. Nel file [vite.config.js](vite.config.js:7), verifica che il `base` path corrisponda al nome del repository:
|
||||
```javascript
|
||||
base: process.env.NODE_ENV === 'production' ? '/nome-repository' : '/',
|
||||
```
|
||||
|
||||
2. Esegui la build:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. Publica il contenuto della cartella `dist/` su GitHub Pages usando uno di questi metodi:
|
||||
- Manualmente tramite l'interfaccia GitHub
|
||||
- Usando `gh-pages`:
|
||||
```bash
|
||||
npm install -D gh-pages
|
||||
npx gh-pages -d dist
|
||||
```
|
||||
- Tramite GitHub Actions (configurando un workflow)
|
||||
|
||||
#### Deploy su server web
|
||||
|
||||
Il contenuto della cartella `dist/` può essere servito da qualsiasi server web statico:
|
||||
|
||||
**Nginx:**
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name tuo-dominio.com;
|
||||
root /percorso/a/dist;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Apache (.htaccess):**
|
||||
```apache
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.html$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.html [L]
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
#### Installazione come PWA
|
||||
|
||||
Dopo il deploy, gli utenti possono installare l'app:
|
||||
|
||||
**Su Android/Chrome:**
|
||||
1. Visita il sito
|
||||
2. Tocca il menu (⋮) > "Aggiungi a schermata Home"
|
||||
3. L'app si avvierà in modalità fullscreen standalone
|
||||
|
||||
**Su iOS/Safari:**
|
||||
1. Visita il sito
|
||||
2. Tocca il pulsante Condividi
|
||||
3. Seleziona "Aggiungi a Home"
|
||||
|
||||
## Dettagli tecnici
|
||||
|
||||
### Architettura del componente
|
||||
|
||||
L'applicazione è strutturata come Single Page Application (SPA) con un unico componente principale `HomePage`. Questo design semplice è ideale per un'app focalizzata su un'unica funzionalità.
|
||||
|
||||
### Gestione dello stato
|
||||
|
||||
Lo stato dell'applicazione è gestito localmente nel componente tramite `data()`:
|
||||
- `punt`: punteggio corrente delle due squadre
|
||||
- `set`: numero di set vinti
|
||||
- `servHome`: boolean che indica quale squadra ha il servizio
|
||||
- `form`: array con le posizioni dei giocatori (1-6)
|
||||
- `nomi`: nomi personalizzabili delle squadre
|
||||
- `visuForm`: toggle tra visualizzazione punteggio/formazione
|
||||
- `visuButt`: visibilità della barra dei pulsanti
|
||||
|
||||
### Gestione della rotazione
|
||||
|
||||
La rotazione dei giocatori segue le regole del pallavolo:
|
||||
|
||||
**Incremento punteggio** ([HomePage.vue:74-78](src/components/HomePage.vue:74-78)):
|
||||
```javascript
|
||||
incPunt(team) {
|
||||
this.sp.punt[team]++;
|
||||
this.sp.servHome = (team == "home");
|
||||
this.sp.form[team].push(this.sp.form[team].shift());
|
||||
}
|
||||
```
|
||||
- Il servizio passa alla squadra che ha segnato
|
||||
- La rotazione avviene con `push(shift())`: il primo elemento va in fondo
|
||||
- Simula la rotazione oraria dei giocatori
|
||||
|
||||
**Decremento punteggio** ([HomePage.vue:79-85](src/components/HomePage.vue:79-85)):
|
||||
```javascript
|
||||
decPunt(team) {
|
||||
if (this.sp.punt[team] > 0) {
|
||||
this.sp.punt[team]--;
|
||||
this.sp.form[team].unshift(this.sp.form[team].pop());
|
||||
}
|
||||
}
|
||||
```
|
||||
- La rotazione inversa avviene con `unshift(pop())`: l'ultimo elemento va all'inizio
|
||||
- Permette di annullare errori di punteggio
|
||||
|
||||
### Sintesi vocale
|
||||
|
||||
La funzione `speak()` ([HomePage.vue:86-112](src/components/HomePage.vue:86-112)) utilizza la Web Speech API:
|
||||
|
||||
```javascript
|
||||
const msg = new SpeechSynthesisUtterance();
|
||||
msg.lang = 'it_IT';
|
||||
msg.voice = voices.find(voice => voice.name === 'Google italiano');
|
||||
```
|
||||
|
||||
Logica di annuncio:
|
||||
- "zero a zero" se il punteggio è 0-0
|
||||
- "[numero] pari" se il punteggio è uguale
|
||||
- "[servizio] a [ricezione]" annunciando prima chi ha il servizio
|
||||
|
||||
### Prevenzione spegnimento schermo
|
||||
|
||||
NoSleep.js ([HomePage.vue:32-33](src/components/HomePage.vue:32-33)) viene attivato al mount su dispositivi mobili:
|
||||
```javascript
|
||||
var noSleep = new NoSleep();
|
||||
noSleep.enable();
|
||||
```
|
||||
|
||||
Questo impedisce allo schermo di spegnersi durante le partite, funzionalità essenziale per un segnapunti.
|
||||
|
||||
### Controlli da tastiera
|
||||
|
||||
Gli eventi tastiera sono gestiti tramite listener globale ([HomePage.vue:121-150](src/components/HomePage.vue:121-150)):
|
||||
|
||||
```javascript
|
||||
window.addEventListener("keydown", this.funzioneTastiSpeciali);
|
||||
```
|
||||
|
||||
I listener vengono temporaneamente disabilitati quando si aprono dialog per permettere l'inserimento di testo.
|
||||
|
||||
### Progressive Web App
|
||||
|
||||
La configurazione PWA in [vite.config.js](vite.config.js:10-33) definisce:
|
||||
|
||||
- **Manifest**: metadati dell'app (nome, icone, orientamento)
|
||||
- **Service Worker**: strategia di cache per funzionamento offline
|
||||
- **Display mode**: fullscreen per esperienza app-like
|
||||
- **Orientamento forzato**: landscape per massimizzare spazio visibile
|
||||
|
||||
## Setup IDE raccomandato
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) - Editor consigliato
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Supporto Vue 3 (disabilita Vetur se installato)
|
||||
- [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) - IntelliSense migliorato
|
||||
|
||||
### Estensioni VS Code utili
|
||||
|
||||
File [.vscode/extensions.json](.vscode/extensions.json) suggerisce:
|
||||
- Vue Language Features (Volar)
|
||||
- TypeScript Vue Plugin
|
||||
|
||||
## Risoluzione problemi comuni
|
||||
|
||||
### La sintesi vocale non funziona
|
||||
|
||||
- Verifica che il browser supporti la Web Speech API (Chrome/Edge consigliati)
|
||||
- Su alcuni browser è necessaria l'interazione utente prima di utilizzare la sintesi vocale
|
||||
- Controlla che la voce "Google italiano" sia disponibile nel sistema
|
||||
|
||||
### L'app non si installa come PWA
|
||||
|
||||
- Verifica che il sito sia servito tramite HTTPS (richiesto per Service Workers)
|
||||
- Controlla che le icone in `public/` siano presenti e accessibili
|
||||
- Ispeziona la console del browser per errori del Service Worker
|
||||
|
||||
### Lo schermo si spegne comunque
|
||||
|
||||
- NoSleep.js richiede un'interazione utente per attivarsi
|
||||
- Su iOS, la prevenzione dello spegnimento ha limitazioni del sistema operativo
|
||||
- Verifica che non ci siano impostazioni di risparmio energetico aggressive
|
||||
|
||||
### La rotazione non funziona correttamente
|
||||
|
||||
- Le formazioni si azzerano con il RESET
|
||||
- Verifica che gli array `form` abbiano sempre 6 elementi
|
||||
- La visualizzazione può essere invertita ma la logica di rotazione rimane corretta
|
||||
|
||||
## Licenza
|
||||
|
||||
Progetto privato. Tutti i diritti riservati.
|
||||
|
||||
41
build-apk.sh
Executable file
41
build-apk.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo " Building APK for Segnapunti Android "
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Rileva architettura
|
||||
ARCH=$(uname -m)
|
||||
echo "Architettura rilevata: $ARCH"
|
||||
|
||||
# Crea directory dist/android se non esiste
|
||||
mkdir -p dist/android
|
||||
|
||||
echo "Building Docker image..."
|
||||
docker build --platform linux/$(uname -m) -t segnapunti-android-builder .
|
||||
|
||||
echo ""
|
||||
echo "Building APK inside Docker container..."
|
||||
docker run --rm \
|
||||
-v "$(pwd)/dist:/app/dist" \
|
||||
segnapunti-android-builder
|
||||
|
||||
echo ""
|
||||
echo "Fixing permissions on dist folder..."
|
||||
sudo chown -R $USER:$USER dist
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Build completed successfully!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "APK location: $(pwd)/dist/android/segnapunti-debug.apk"
|
||||
echo ""
|
||||
echo "Per installare su dispositivo Android:"
|
||||
echo " adb install dist/android/segnapunti-debug.apk"
|
||||
echo ""
|
||||
echo "Oppure trasferisci il file sul dispositivo"
|
||||
echo "e installalo manualmente."
|
||||
echo ""
|
||||
21
capacitor.config.json
Normal file
21
capacitor.config.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"appId": "com.antoniana.segnapunti",
|
||||
"appName": "Segnapunti",
|
||||
"webDir": "dist",
|
||||
"bundledWebRuntime": false,
|
||||
"server": {
|
||||
"androidScheme": "https"
|
||||
},
|
||||
"android": {
|
||||
"allowMixedContent": true,
|
||||
"webContentsDebuggingEnabled": true
|
||||
},
|
||||
"plugins": {
|
||||
"SplashScreen": {
|
||||
"launchShowDuration": 0
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"orientation": "landscape"
|
||||
}
|
||||
}
|
||||
42
generate-icons.sh
Executable file
42
generate-icons.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Script per generare automaticamente le icone Android da segnap-512x512.png
|
||||
|
||||
SOURCE="./public/segnap-512x512.png"
|
||||
BASE_DIR="./android/app/src/main/res"
|
||||
|
||||
# Verifica che ImageMagick sia installato
|
||||
if ! command -v convert &> /dev/null; then
|
||||
echo "Errore: ImageMagick non è installato."
|
||||
echo "Installa con: sudo apt-get install imagemagick"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verifica che il file sorgente esista
|
||||
if [ ! -f "$SOURCE" ]; then
|
||||
echo "Errore: File sorgente $SOURCE non trovato"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verifica che la directory android esista
|
||||
if [ ! -d "./android" ]; then
|
||||
echo "Errore: Directory android/ non trovata."
|
||||
echo "Esegui prima: npx cap add android"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generazione icone Android da $SOURCE..."
|
||||
|
||||
# Crea le directory se non esistono e genera le icone
|
||||
for size in "mdpi:48" "hdpi:72" "xhdpi:96" "xxhdpi:144" "xxxhdpi:192"; do
|
||||
density="${size%:*}"
|
||||
pixels="${size#*:}"
|
||||
dir="$BASE_DIR/mipmap-$density"
|
||||
|
||||
mkdir -p "$dir"
|
||||
convert "$SOURCE" -resize ${pixels}x${pixels} "$dir/ic_launcher.png"
|
||||
echo " ✓ Creata icona $density (${pixels}x${pixels})"
|
||||
done
|
||||
|
||||
echo "Icone generate con successo!"
|
||||
echo ""
|
||||
echo "Prossimo passo: npx cap sync"
|
||||
@@ -3,7 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<title>Segnapunti - Anto</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
6205
package-lock.json
generated
6205
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -6,16 +6,25 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"cap:sync": "npm run build && cap sync",
|
||||
"cap:open": "cap open android",
|
||||
"android:build:debug": "cd android && ./gradlew assembleDebug",
|
||||
"docker:build": "./build-apk.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/text-to-speech": "^6.0.0",
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.1",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"vue": "^3.2.47",
|
||||
"wave-ui": "^3.3.0"
|
||||
"vue": "^3.4.38",
|
||||
"wave-ui": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-pwa": "^0.16.0"
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-pwa": "^0.20.5"
|
||||
}
|
||||
}
|
||||
|
||||
40
setup-android-icons.sh
Normal file
40
setup-android-icons.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Configurazione icone Android..."
|
||||
|
||||
# Rimuove icone default di Capacitor
|
||||
rm -rf android/app/src/main/res/mipmap-*
|
||||
|
||||
# Crea directory per tutte le densità
|
||||
for density in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
|
||||
mkdir -p android/app/src/main/res/mipmap-$density
|
||||
done
|
||||
|
||||
# Mappa densità -> dimensioni
|
||||
declare -A sizes=(
|
||||
["mdpi"]="48"
|
||||
["hdpi"]="72"
|
||||
["xhdpi"]="96"
|
||||
["xxhdpi"]="144"
|
||||
["xxxhdpi"]="192"
|
||||
)
|
||||
|
||||
# Genera icone per ogni densità
|
||||
for density in "${!sizes[@]}"; do
|
||||
size="${sizes[$density]}"
|
||||
echo " Generando icone ${size}x${size} per $density..."
|
||||
|
||||
convert public/segnap-512x512.png -resize ${size}x${size} \
|
||||
android/app/src/main/res/mipmap-$density/ic_launcher.png
|
||||
|
||||
convert public/segnap-512x512.png -resize ${size}x${size} \
|
||||
android/app/src/main/res/mipmap-$density/ic_launcher_round.png
|
||||
done
|
||||
|
||||
# Icona per il file APK (visibile da PC)
|
||||
mkdir -p android/app/src/main/res/drawable
|
||||
convert public/segnap-512x512.png -resize 512x512 \
|
||||
android/app/src/main/res/drawable/icon.png
|
||||
|
||||
echo "Icone configurate con successo!"
|
||||
@@ -1,5 +1,9 @@
|
||||
<script>
|
||||
import NoSleep from "nosleep.js";
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { App } from '@capacitor/app';
|
||||
import { TextToSpeech } from '@capacitor-community/text-to-speech';
|
||||
|
||||
export default {
|
||||
name: "HomePage",
|
||||
components: {},
|
||||
@@ -26,9 +30,17 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Carica le voci (necessario su alcuni browser)
|
||||
this.voices = window.speechSynthesis.getVoices();
|
||||
|
||||
// Ascolta l'evento voiceschanged per Android/Chrome
|
||||
if (speechSynthesis.onvoiceschanged !== undefined) {
|
||||
speechSynthesis.onvoiceschanged = () => {
|
||||
this.voices = window.speechSynthesis.getVoices();
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isMobile()) {
|
||||
this.speak();
|
||||
var noSleep = new NoSleep();
|
||||
noSleep.enable();
|
||||
document.documentElement.requestFullscreen();
|
||||
@@ -83,32 +95,64 @@ export default {
|
||||
this.sp.form[team].unshift(this.sp.form[team].pop());
|
||||
}
|
||||
},
|
||||
speak() {
|
||||
const msg = new SpeechSynthesisUtterance();
|
||||
async speak() {
|
||||
// Prepara il testo da pronunciare
|
||||
let text = "";
|
||||
if (this.sp.punt.home + this.sp.punt.guest == 0) {
|
||||
msg.text = "zero a zero";
|
||||
text = "zero a zero";
|
||||
} else if (this.sp.punt.home == this.sp.punt.guest) {
|
||||
msg.text = this.sp.punt.home + " pari";
|
||||
text = this.sp.punt.home + " pari";
|
||||
} else {
|
||||
if (this.sp.servHome) {
|
||||
msg.text = this.sp.punt.home + " a " + this.sp.punt.guest;
|
||||
text = this.sp.punt.home + " a " + this.sp.punt.guest;
|
||||
} else {
|
||||
msg.text = this.sp.punt.guest + " a " + this.sp.punt.home;
|
||||
text = this.sp.punt.guest + " a " + this.sp.punt.home;
|
||||
}
|
||||
}
|
||||
// msg.volume = 1.0; // speech volume (default: 1.0)
|
||||
// msg.pitch = 1.0; // speech pitch (default: 1.0)
|
||||
// msg.rate = 1.0; // speech rate (default: 1.0)
|
||||
msg.lang = 'it_IT'; // speech language (default: 'en-US')
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
msg.voice = voices.find(voice => voice.name === 'Google italiano'); // voice URI (default: platform-dependent)
|
||||
// msg.onboundary = function (event) {
|
||||
// console.log('Speech reached a boundary:', event.name);
|
||||
// };
|
||||
// msg.onpause = function (event) {
|
||||
// console.log('Speech paused:', event.utterance.text.substring(event.charIndex));
|
||||
// };
|
||||
window.speechSynthesis.speak(msg);
|
||||
|
||||
console.log('Speak called, text:', text);
|
||||
console.log('Is native platform:', Capacitor.isNativePlatform());
|
||||
|
||||
// Usa plugin nativo su mobile, Web API su desktop
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
try {
|
||||
console.log('Using native TTS');
|
||||
await TextToSpeech.speak({
|
||||
text: text,
|
||||
lang: 'it-IT',
|
||||
rate: 0.9,
|
||||
pitch: 1.0,
|
||||
volume: 1.0,
|
||||
category: 'ambient'
|
||||
});
|
||||
console.log('TTS completed successfully');
|
||||
} catch (error) {
|
||||
console.error('TTS error:', error);
|
||||
this.$waveui.notify('Errore TTS: ' + error.message, 'error', 3000);
|
||||
}
|
||||
} else {
|
||||
// Fallback Web API per desktop/browser
|
||||
console.log('Using web API speechSynthesis');
|
||||
window.speechSynthesis.cancel();
|
||||
|
||||
const msg = new SpeechSynthesisUtterance();
|
||||
msg.text = text;
|
||||
msg.volume = 1.0;
|
||||
msg.rate = 0.9;
|
||||
msg.pitch = 1.0;
|
||||
msg.lang = 'it-IT';
|
||||
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
const italianVoice = voices.find(voice =>
|
||||
voice.lang.startsWith('it') || voice.name.includes('Italian') || voice.name.includes('italiano')
|
||||
);
|
||||
|
||||
if (italianVoice) {
|
||||
msg.voice = italianVoice;
|
||||
}
|
||||
|
||||
window.speechSynthesis.speak(msg);
|
||||
}
|
||||
},
|
||||
apriDialogConfig() {
|
||||
this.disabilitaTastiSpeciali();
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
touch-action: pan-x pan-y;
|
||||
height: 100%
|
||||
touch-action: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior-y: contain;
|
||||
overscroll-behavior: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
height: 100dvh; /* Dynamic viewport height per mobile */
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -38,8 +54,13 @@ button:focus-visible {
|
||||
}
|
||||
|
||||
#app {
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.campo {
|
||||
user-select: none;
|
||||
@@ -121,4 +142,45 @@ button:focus-visible {
|
||||
background-color: rgb(206, 247, 3);
|
||||
color: blue;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Responsive mobile */
|
||||
@media (max-width: 768px) {
|
||||
.hea {
|
||||
font-size: 4vw;
|
||||
}
|
||||
|
||||
.hea span {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.hea img {
|
||||
width: 18px !important;
|
||||
}
|
||||
|
||||
.punt {
|
||||
font-size: 45vh;
|
||||
}
|
||||
|
||||
.form {
|
||||
font-size: 4vh;
|
||||
border-top: #fff dashed 15px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.formdiv {
|
||||
font-size: 15vh;
|
||||
}
|
||||
|
||||
.bot button {
|
||||
font-size: 0.7em;
|
||||
padding: 0.4em 0.8em;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.bot img {
|
||||
width: 20px !important;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: process.env.NODE_ENV === 'production' ? '/segnap' : '/',
|
||||
base: '/',
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
|
||||
Reference in New Issue
Block a user