12 Commits

Author SHA1 Message Date
2e66a6cf2a style(ui): simmetria header in modalità Formazione
- Allinea l’ordine degli elementi a destra (punteggio, servizio, nome)
- Mantiene coerenza visiva tra lato sinistro e destro
2026-01-28 16:15:00 +01:00
c923bdbf64 fix(ui): stabilizza header in formazione e cambio servizio
- Riserva spazio fisso per l’icona del servizio per evitare scatti
- Stabilizza la larghezza del punteggio inline in modalità Formazione
- Migliora la coerenza visiva nelle testate home/guest
2026-01-28 16:09:36 +01:00
139dcc9c5b Fix: blocca incrementi a set concluso senza notifiche
- Rimuove il messaggio "set terminato" mantenendo il blocco incrementi
- Semplifica il controllo: stop su checkVittoria() senza flag persistente
- Evita ripetizioni dovute a shortcut (Ctrl/Shift + ↑)
2026-01-28 16:05:11 +01:00
24dda41b0d Aggiunge selezione modalità partita (2/3 o 3/5)
- UI per scegliere la modalità partita nella Home
- Logica set decisivo adattata al best-of selezionato
- README aggiornato con nuove regole e descrizione feature
2026-01-28 14:57:14 +01:00
4cbb5fb48d Corregge ripristino servizio quando si annulla un punto
Risolve il bug dove l'indicatore del servizio (palla) non veniva
ripristinato correttamente quando si tornava indietro nel punteggio.

Implementa uno storico completo che salva lo stato del servizio prima
di ogni punto, permettendo di ripristinare esattamente la situazione
precedente quando si annulla un punto (incluso servizio e rotazioni)
2026-01-28 14:35:40 +01:00
eae5cbf964 Fissa dimensioni riquadri punteggio per evitare spostamenti 2026-01-28 14:30:17 +01:00
2c6416bfe0 Aggiorna README.md 2026-01-28 13:31:42 +01:00
9a808e566d Merge pull request 'wip-formazione' (#3) from wip-formazione into master
Reviewed-on: #3
2026-01-28 12:00:45 +01:00
6c6ac7fc29 Limita il cambio palla solo a inizio set (0-0)
- Aggiunge computed property isPunteggioZeroZero per verificare lo stato del punteggio
- Crea metodo cambiaPalla() con validazione che blocca il cambio se il punteggio non è 0-0
- Disabilita il pulsante cambio palla quando il punteggio non è 0-0
- Mostra notifica di avviso se si tenta il cambio palla durante il set
- Aggiorna scorciatoia tastiera Ctrl+ArrowLeft per usare la stessa validazione
2026-01-26 13:45:22 +01:00
bbe0862241 Implementa rotazione regolamentare con cambio palla
- La formazione ruota solo quando si conquista il servizio (cambio palla)
- Aggiunge array servizioPrecedente per tracciare i cambi palla
- Fix decPunt per annullare correttamente la rotazione
- Fix resetta per pulire lo stack dei servizi precedenti
2026-01-25 17:57:22 +01:00
26d647dce7 Aggiunge configurazione manuale numeri di maglia
- Aggiunge campi input nel dialog configurazione per modificare manualmente i numeri di maglia dei giocatori
- Disegna campi da pallavolo stilizzati (220x220px) con linea dei 3 metri posizionata a 1/3 dall'alto
- Layout corrisponde alla visualizzazione sul campo (ordine rotazione [3,2,1,4,5,0])
- Proporzioni realistiche: zona anteriore 33%, zona posteriore 67%
- Sfondo marrone chiaro e bordi grigi per migliore leggibilità
2026-01-25 17:46:31 +01:00
a72bc1844e Blocca assegnazione punti al raggiungimento della vittoria
Aggiunge controllo che impedisce di assegnare ulteriori punti quando
viene raggiunta la condizione di vittoria (25 punti con 2 di vantaggio
nei set 1-4, 15 punti con 2 di vantaggio nel set decisivo)
2026-01-24 19:00:07 +01:00
15 changed files with 2599 additions and 4764 deletions

14
.gitignore vendored
View File

@@ -11,8 +11,7 @@ lerna-debug.log*
currentCommit.txt
node_modules
dist/*
!dist/android/
dist
dist-ssr
*.local
@@ -26,14 +25,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Capacitor
android/
ios/
.capacitor/
# Build output
output/
*.apk
!dist/android/*.apk
*.aab

View File

@@ -1,83 +0,0 @@
# 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'\
"]

566
README.md
View File

@@ -1,437 +1,251 @@
# Segnapunti - Anto
# Segnapunti Anto
Un'applicazione web segnapunti per pallavolo con funzionalità di sintesi vocale, modalità formazione e supporto PWA (Progressive Web App).
Applicazione web **Progressive Web App (PWA)** per tracciare i punteggi di partite di pallavolo in tempo reale.
## Cos'è
---
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.
## Panoramica
### Caratteristiche principali
**Segnapunti Anto** è un'applicazione digitale per il tracciamento dei punteggi durante partite di pallavolo, ottimizzata per l'uso su tablet e smartphone. Sviluppata per il team Antoniana, l'app fornisce un'interfaccia fullscreen touch-friendly con supporto offline e controlli da tastiera.
- **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
### Funzionalità Principali
## Come funziona
- **Gestione Completa Partite**
- Tracciamento punti in tempo reale per entrambe le squadre
- Conteggio automatico dei set (modalità 2/3 o 3/5)
- Indicatore visivo del servizio
- Cronologia punti con striscia visiva
### Interfaccia
- **Formazioni Squadra**
- Visualizzazione interattiva dei 6 giocatori in campo
- Rotazione automatica regolamentare al cambio palla
- Configurazione manuale dei numeri di maglia
- Supporto logica pallavolo ufficiale (25 punti + 2 di vantaggio, tie-break a 15 nel set decisivo)
L'applicazione divide lo schermo in due metà:
- **Controlli Multimodali**
- Scorciatoie da tastiera complete (vedi sezione [Shortcuts](#shortcuts))
- Sintesi vocale per annunci punteggio in italiano (Web Speech API)
- **Metà sinistra (gialla su sfondo nero)**: Squadra Home - Antoniana
- **Metà destra (bianca su sfondo blu)**: Squadra Guest
- **Personalizzazione**
- Configurazione dinamica nomi squadre
- Selettore modalità partita: al meglio di 3 o al meglio di 5
- Toggle layout orizzontale (inverti home/guest)
- Modalità visualizzazione: punteggio semplice o formazioni complete
- Nascondi/mostra controlli e cronologia
#### 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
## Requisiti
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)
### Requisiti di Sistema
#### Funzionalità
#### Per Sviluppo
- **Sistema Operativo**: Linux, macOS, Windows (WSL2 consigliato)
- **Node.js**: v20.2.0 o superiore (LTS consigliato)
- **npm**: v9.0.0 o superiore (incluso con Node.js)
- **RAM**: Minimo 2GB, consigliato 4GB
- **Spazio Disco**: ~500MB per dipendenze e build
- **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)
#### Per Deployment
- **Server Web**: Qualsiasi server statico (nginx, Apache, Vercel, Netlify)
- **HTTPS**: Obbligatorio per Service Worker e PWA (eccetto localhost)
- **Connessione Internet**: Solo per primo caricamento (poi funziona offline)
### Controlli da tastiera
### Requisiti Browser (Utente Finale)
- **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
| Requisito | Dettaglio | Necessità |
|-----------|-----------|-----------|
| **JavaScript ES6+** | Supporto moduli, arrow functions, async/await | Obbligatorio |
| **Service Worker API** | Per funzionalità offline PWA | Obbligatorio |
| **Fullscreen API** | Per modalità schermo intero | Consigliato |
| **Web Speech API** | Per sintesi vocale punteggi | Opzionale |
| **Local Storage** | Per persistenza configurazioni | Consigliato |
### Comportamento su mobile
### Browser Testati e Supportati
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
| Browser | Versione Minima | Supporto | Note |
|---------|-----------------|----------|------|
| Chrome/Chromium | 90+ | ✅ Completo | Consigliato per tutte le features |
| Firefox | 88+ | ✅ Completo | Supporto completo PWA e Speech API |
## 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
## Installazione e Setup
### Prerequisiti
- **Node.js** (versione 14 o superiore, raccomandata v20.2.0)
- **Node.js** v20.2.0 (consigliato)
- **npm** o **yarn**
### Installazione
### Installazione con NVM (consigliato)
1. Clona il repository:
2. Installa le dipendenze:
```bash
# Installa la versione corretta di Node.js
nvm install v20.2.0
nvm use v20.2.0
# Clona il repository
git clone <repository-url>
cd segnapunti
# Installa le dipendenze
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).
## Comandi per Sviluppo
#### 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
### Dev Server
#### Anteprima build di produzione
```bash
npm run preview
```
Avvia un server locale per testare la build di produzione.
Avvia il server di sviluppo con hot-reload:
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 "
L'applicazione sarà disponibile su [http://localhost:5173](http://localhost:5173)
# Su Windows
ipconfig
```
### Modalità Sviluppo
- Hot Module Replacement (HMR) attivo
- Source maps per debugging
- Vue DevTools supportato
- Errori e warnings in console
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)
## Comandi per Build
### Build e Deploy
### Build Produzione
#### Build per produzione
Genera i file ottimizzati per il deployment:
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
**Output:**
- Cartella `/dist` con file statici ottimizzati
- Service Worker generato automaticamente
- PWA manifest configurato
- Assets minificati e con hash per cache busting
- Base path: `/segnap` (modificabile in `vite.config.js`)
#### Anteprima della build
### Preview Build
Anteprima locale della build di produzione:
Prima del deploy, puoi testare la build in locale:
```bash
npm run preview
```
#### Deploy su GitHub Pages
Serve i file dalla cartella `/dist` per testare la build prima del deploy.
---
## Shortcuts
### Controlli Tastiera Squadra Home
| Scorciatoia | Azione |
|-------------|--------|
| `Ctrl + ↑` | Incrementa punti |
| `Ctrl + ↓` | Decrementa punti |
| `Ctrl + →` | Incrementa set |
### Controlli Tastiera Squadra Guest
| Scorciatoia | Azione |
|-------------|--------|
| `Shift + ↑` | Incrementa punti |
| `Shift + ↓` | Decrementa punti |
| `Shift + →` | Incrementa set |
### Comandi Globali
| Scorciatoia | Azione |
|-------------|--------|
| `Ctrl + ←` | Cambio palla (servizio) - **solo a 0-0** |
| `Ctrl + M` | Apri configurazione nomi squadre e formazioni |
| `Ctrl + B` | Toggle visibilità barra pulsanti |
| `Ctrl + F` | Attiva/disattiva fullscreen |
| `Ctrl + S` | Annuncio vocale punteggio corrente |
| `Ctrl + Z` | Switch tra visualizzazione formazioni e punteggio |
---
## Configurazione PWA
L'applicazione è configurata come **Progressive Web App** nel file [vite.config.js](vite.config.js):
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;
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: "app_segnap",
short_name: "segnap",
description: "Segnapunti standalone.",
background_color: "#eee",
theme_color: '#ffffff',
display: "fullscreen",
orientation: "landscape",
icons: [
{ src: 'segnap-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'segnap-512x512.png', sizes: '512x512', type: 'image/png' }
]
}
}
})
```
**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>
### Caratteristiche PWA
- **Display**: Fullscreen per massimizzare lo spazio visivo
- **Orientamento**: Landscape (orizzontale) ottimizzato per tablet
- **Auto-update**: Service Worker con aggiornamento automatico
- **Offline**: Funzionamento completo senza connessione internet
- **Installabile**: Aggiungibile alla home screen come app nativa
### Installazione PWA
**Android/Desktop (Chrome):**
- Menu → "Installa app" o icona (⊕) nella barra degli indirizzi
**iOS (Safari):**
- Share (□↑) → "Aggiungi a Home"
---
## Logica Regolamentare Pallavolo
### Vittoria Set
- **Set regolari (1-4)**: Primo a 25 punti con almeno 2 di vantaggio
- **Set decisivo**:
- Modalità 2/3: 3° set a 15 punti con almeno 2 di vantaggio
- Modalità 3/5: 5° set a 15 punti con almeno 2 di vantaggio
- **Blocco automatico**: Non consente assegnare punti oltre la vittoria
### Rotazione Formazione
La rotazione avviene **automaticamente** quando:
1. La squadra **conquista il servizio** (cambio palla)
2. Il punteggio è diverso da 0-0
**Limitazione cambio palla manuale:**
- Il cambio manuale del servizio (`Ctrl + ←`) è consentito **solo a 0-0**
- Questa limitazione previene errori nella rotazione delle formazioni
### Formazione in Campo
Visualizzazione a 6 posizioni standard:
```
Rete
┌─────┬─────┬─────┐
│ 4 │ 3 │ 2 │ ← Fila anteriore
├─────┼─────┼─────┤
│ 5 │ 6 │ 1 │ ← Fila posteriore
└─────┴─────┴─────┘
```
#### 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.
La rotazione avviene in senso orario: 1→2→3→4→5→6→1

View File

@@ -1,41 +0,0 @@
#!/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 ""

View File

@@ -1,21 +0,0 @@
{
"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"
}
}

View File

@@ -1,42 +0,0 @@
#!/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"

View File

@@ -3,10 +3,7 @@
<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, 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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Segnapunti - Anto</title>
</head>
<body>

6109
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,25 +6,16 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"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"
"preview": "vite preview"
},
"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.4.38",
"wave-ui": "^3.17.0"
"vue": "^3.2.47",
"wave-ui": "^3.3.0"
},
"devDependencies": {
"@capacitor/cli": "^6.2.0",
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.10",
"vite-plugin-pwa": "^0.20.5"
"@vitejs/plugin-vue": "^4.1.0",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.16.0"
}
}

View File

@@ -1,40 +0,0 @@
#!/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!"

View File

@@ -1,9 +1,5 @@
<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: {},
@@ -30,17 +26,9 @@ 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();
@@ -95,64 +83,32 @@ export default {
this.sp.form[team].unshift(this.sp.form[team].pop());
}
},
async speak() {
// Prepara il testo da pronunciare
let text = "";
speak() {
const msg = new SpeechSynthesisUtterance();
if (this.sp.punt.home + this.sp.punt.guest == 0) {
text = "zero a zero";
msg.text = "zero a zero";
} else if (this.sp.punt.home == this.sp.punt.guest) {
text = this.sp.punt.home + " pari";
msg.text = this.sp.punt.home + " pari";
} else {
if (this.sp.servHome) {
text = this.sp.punt.home + " a " + this.sp.punt.guest;
msg.text = this.sp.punt.home + " a " + this.sp.punt.guest;
} else {
text = this.sp.punt.guest + " a " + this.sp.punt.home;
msg.text = this.sp.punt.guest + " a " + this.sp.punt.home;
}
}
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';
// 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();
const italianVoice = voices.find(voice =>
voice.lang.startsWith('it') || voice.name.includes('Italian') || voice.name.includes('italiano')
);
if (italianVoice) {
msg.voice = italianVoice;
}
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);
}
},
apriDialogConfig() {
this.disabilitaTastiSpeciali();

View File

@@ -1,9 +1,70 @@
<section class="homepage">
<w-dialog v-model="diaNomi.show" :width="500" @close="abilitaTastiSpeciali()">
<w-input v-model="sp.nomi.home" type="text" class="pa3">Home</w-input>
<w-input v-model="sp.nomi.guest" type="text" class="pa3">Guest</w-input>
<w-button @click="order = !order">Inverti ordine</w-button>
<w-button bg-color="success" @click="diaNomi.show = false">
<w-dialog v-model="diaNomi.show" :width="600" @close="abilitaTastiSpeciali()">
<w-input v-model="sp.nomi.home" type="text" class="pa3">Nome Home</w-input>
<w-input v-model="sp.nomi.guest" type="text" class="pa3">Nome Guest</w-input>
<w-flex justify-center align-center class="pa3">
<span class="mr3">Modalità partita:</span>
<w-button
@click="modalitaPartita = '2/3'"
:bg-color="modalitaPartita === '2/3' ? 'success' : 'grey-light4'"
:dark="modalitaPartita === '2/3'"
class="ma1">
2/3
</w-button>
<w-button
@click="modalitaPartita = '3/5'"
:bg-color="modalitaPartita === '3/5' ? 'success' : 'grey-light4'"
:dark="modalitaPartita === '3/5'"
class="ma1">
3/5
</w-button>
</w-flex>
<w-flex justify-space-around class="pa3">
<div class="campo-config">
<div class="text-bold mb3 text-center">Formazione Home</div>
<div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore">
<w-input v-model="sp.form.home[3]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[2]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[1]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
<!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore">
<w-input v-model="sp.form.home[4]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[5]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[0]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
</div>
</div>
<div class="campo-config">
<div class="text-bold mb3 text-center">Formazione Guest</div>
<div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore">
<w-input v-model="sp.form.guest[3]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[2]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[1]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
<!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore">
<w-input v-model="sp.form.guest[4]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[5]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[0]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
</div>
</div>
</w-flex>
<w-button @click="order = !order" class="ma2">Inverti ordine</w-button>
<w-button bg-color="success" @click="diaNomi.show = false" class="ma2">
Ok
</w-button>
</w-dialog>
@@ -13,16 +74,22 @@
<!-- home guest -->
<div class="hea home">
<span @click="decPunt('home')" :style="{ 'float': 'left' }">
{{ sp.nomi.home }} <img v-if="sp.servHome" src="/serv.png" width="25" />
<span v-if="visuForm">{{ sp.punt.home }}</span>
{{ sp.nomi.home }}
<span class="serv-slot">
<img v-show="sp.servHome" src="/serv.png" width="25" />
</span>
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
</span>
<span @click="incSet('home')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.home }}</span>
</div>
<div class="hea guest">
<span @click="decPunt('guest')" :style="{ 'float': 'right' }">
<img v-if="!sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.guest }}
<span v-if="visuForm">{{ sp.punt.guest }}</span>
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
<span class="serv-slot">
<img v-show="!sp.servHome" src="/serv.png" width="25" />
</span>
{{ sp.nomi.guest }}
</span>
<span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span>
</div>
@@ -40,8 +107,10 @@
</div>
</span>
<span v-else>
<div class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</div>
<div class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div>
<w-flex class="punteggio-container">
<w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
<w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
</w-flex>
</span>
</span>
@@ -50,16 +119,22 @@
<div class="hea guest">
<span @click="decPunt('guest')" :style="{ 'float': 'left' }">
{{ sp.nomi.guest }} <img v-if="!sp.servHome" src="/serv.png" width="25" />
<span v-if="visuForm">{{ sp.punt.guest }}</span>
{{ sp.nomi.guest }}
<span class="serv-slot">
<img v-show="!sp.servHome" src="/serv.png" width="25" />
</span>
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
</span>
<span @click="incSet('guest')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.guest }}</span>
</div>
<div class="hea home">
<span @click="decPunt('home')" :style="{ 'float': 'right' }">
<img v-if="sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.home }}
<span v-if="visuForm">{{ sp.punt.home }}</span>
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
<span class="serv-slot">
<img v-show="sp.servHome" src="/serv.png" width="25" />
</span>
{{ sp.nomi.home }}
</span>
<span @click="incSet('home')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.home }}</span>
</div>
@@ -77,8 +152,10 @@
</div>
</span>
<span v-else>
<div class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div>
<div class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</div>
<w-flex class="punteggio-container">
<w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
<w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
</w-flex>
</span>
</span>
@@ -106,7 +183,7 @@
<w-button @click="apriDialogConfig()">
<img src="/gear.png" width="25" />
</w-button>
<w-button @click="sp.servHome = !sp.servHome">
<w-button @click="cambiaPalla" :disabled="!isPunteggioZeroZero">
<img src="/serv.png" width="25" />
</w-button>
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">

View File

@@ -14,6 +14,7 @@ export default {
visuForm: false,
visuButt: true,
visuStriscia: true,
modalitaPartita: "3/5", // "2/3" o "3/5"
sp: {
striscia: { home: [0], guest: [0] },
servHome: true,
@@ -24,6 +25,7 @@ export default {
home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"],
},
storicoServizio: [], // Stack per tracciare lo stato del servizio prima di ogni punto
},
}
},
@@ -37,6 +39,11 @@ export default {
}
this.abilitaTastiSpeciali();
},
computed: {
isPunteggioZeroZero() {
return this.sp.punt.home === 0 && this.sp.punt.guest === 0;
}
},
methods: {
closeApp() {
var win = window.open("", "_self");
@@ -66,6 +73,14 @@ export default {
guest: ["1", "2", "3", "4", "5", "6"],
}
this.sp.striscia = { home: [0], guest: [0] }
this.sp.storicoServizio = []
},
cambiaPalla() {
if (!this.isPunteggioZeroZero) {
this.$waveui.notify("Cambio palla consentito solo a inizio set (0-0)", "warning");
return;
}
this.sp.servHome = !this.sp.servHome;
},
incSet(team) {
if (this.sp.set[team] == 2) {
@@ -75,6 +90,17 @@ export default {
}
},
incPunt(team) {
// Se il set è già terminato, evita ulteriori incrementi
if (this.checkVittoria()) {
return;
}
// Salva lo stato del servizio PRIMA di modificarlo
this.sp.storicoServizio.push({
servHome: this.sp.servHome,
cambioPalla: (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome)
});
this.sp.punt[team]++;
if (team == 'home') {
this.sp.striscia.home.push(this.sp.punt.home)
@@ -83,21 +109,69 @@ export default {
this.sp.striscia.guest.push(this.sp.punt.guest)
this.sp.striscia.home.push(' ')
}
this.sp.servHome = (team == "home");
// Ruota la formazione solo se c'è cambio palla (conquista del servizio)
const cambioPalla = (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome);
if (cambioPalla) {
this.sp.form[team].push(this.sp.form[team].shift());
}
this.sp.servHome = (team == "home");
},
checkVittoria() {
const puntHome = this.sp.punt.home;
const puntGuest = this.sp.punt.guest;
const setHome = this.sp.set.home;
const setGuest = this.sp.set.guest;
const totSet = setHome + setGuest;
// Determina se siamo nel set decisivo in base alla modalità partita
let isSetDecisivo = false;
if (this.modalitaPartita === "2/3") {
// Tie-break al 3° set (quando totSet >= 2)
isSetDecisivo = totSet >= 2;
} else {
// Tie-break al 5° set (quando totSet >= 4)
isSetDecisivo = totSet >= 4;
}
const punteggioVittoria = isSetDecisivo ? 15 : 25;
// Vittoria con punteggio >= 25 (o 15 per set decisivo) e almeno 2 punti di vantaggio
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) {
return true; // Home ha vinto
}
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) {
return true; // Guest ha vinto
}
return false;
},
decPunt() {
if (this.sp.striscia.home.length > 1) {
if (this.sp.striscia.home.length > 1 && this.sp.storicoServizio.length > 0) {
var tmpHome = this.sp.striscia.home.pop()
var tmpGuest = this.sp.striscia.guest.pop()
var statoServizio = this.sp.storicoServizio.pop() // Recupera lo stato completo del servizio
if (tmpHome == ' ') {
this.sp.punt.guest--
// Ruota indietro solo se c'era stato un cambio palla
if (statoServizio.cambioPalla) {
this.sp.form.guest.unshift(this.sp.form.guest.pop());
}
} else {
this.sp.punt.home--
// Ruota indietro solo se c'era stato un cambio palla
if (statoServizio.cambioPalla) {
this.sp.form.home.unshift(this.sp.form.home.pop());
}
}
// Ripristina il servizio allo stato precedente
this.sp.servHome = statoServizio.servHome;
}
},
// decPunt(team) {
// // decrementa il punteggio se è > 0.
@@ -171,7 +245,7 @@ export default {
} else if (e.shiftKey && e.key == "ArrowRight") {
this.incSet("guest")
} else if (e.ctrlKey && e.key == "ArrowLeft") {
this.sp.servHome = !this.sp.servHome
this.cambiaPalla()
} else { return false }
}
}

View File

@@ -1,33 +1,17 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
touch-action: none;
height: 100%;
width: 100%;
overflow: hidden;
}
html {
height: 100%;
width: 100%;
overflow: hidden;
position: fixed;
touch-action: pan-x pan-y;
height: 100%
}
body {
overscroll-behavior: none;
overscroll-behavior-y: contain;
margin: 0;
padding: 0;
place-items: center;
min-width: 320px;
width: 100%;
height: 100vh;
height: 100dvh; /* Dynamic viewport height per mobile */
min-height: 100vh;
background-color: #000;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
button {
@@ -54,13 +38,8 @@ button:focus-visible {
}
#app {
margin: 0;
padding: 0;
margin: 0 auto;
text-align: center;
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.campo {
user-select: none;
@@ -79,6 +58,19 @@ button:focus-visible {
padding-right: 10px;
border-radius: 5px;
}
.score-inline {
display: inline-block;
min-width: 3ch;
text-align: center;
}
.serv-slot {
display: inline-flex;
width: 25px;
height: 25px;
align-items: center;
justify-content: center;
vertical-align: middle;
}
.tal {
text-align: left;
}
@@ -101,8 +93,23 @@ button:focus-visible {
float: left;
width: 50%;
}
.punteggio-container {
width: 100%;
height: 100%;
display: flex;
}
.punt {
font-size: 60vh;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
min-height: 50vh;
min-width: 50vw;
max-width: 50vw;
overflow: hidden;
box-sizing: border-box;
}
.form {
font-size: 5vh;
@@ -144,43 +151,42 @@ button:focus-visible {
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;
}
.campo-config {
display: flex;
flex-direction: column;
align-items: center;
}
.campo-pallavolo {
border: 3px solid #999;
background-color: rgba(205, 133, 63, 0.25);
position: relative;
width: 220px;
height: 220px;
display: flex;
flex-direction: column;
padding: 0;
}
.fila-anteriore {
height: 33.33%;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.fila-posteriore {
height: 66.67%;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.linea-tre-metri {
border-top: 2px solid #666;
width: 100%;
height: 0;
margin: 0;
}

View File

@@ -4,7 +4,7 @@ import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({
base: '/',
base: process.env.NODE_ENV === 'production' ? '/segnap' : '/',
plugins: [
vue(),
VitePWA({