Compare commits
6 Commits
apk
...
2c6416bfe0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c6416bfe0 | |||
| 9a808e566d | |||
| 6c6ac7fc29 | |||
| bbe0862241 | |||
| 26d647dce7 | |||
| a72bc1844e |
251
README.md
251
README.md
@@ -1,8 +1,249 @@
|
||||
# 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.
|
||||
Applicazione web **Progressive Web App (PWA)** per tracciare i punteggi di partite di pallavolo in tempo reale.
|
||||
|
||||
## Recommended IDE Setup
|
||||
---
|
||||
|
||||
## Panoramica
|
||||
|
||||
**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.
|
||||
|
||||
### Funzionalità Principali
|
||||
|
||||
- **Gestione Completa Partite**
|
||||
- Tracciamento punti in tempo reale per entrambe le squadre
|
||||
- Conteggio automatico dei set (fino a 5 set)
|
||||
- Indicatore visivo del servizio
|
||||
- Cronologia punti con striscia visiva
|
||||
|
||||
- **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, 5° set a 15)
|
||||
|
||||
- **Controlli Multimodali**
|
||||
- Scorciatoie da tastiera complete (vedi sezione [Shortcuts](#shortcuts))
|
||||
- Sintesi vocale per annunci punteggio in italiano (Web Speech API)
|
||||
|
||||
- **Personalizzazione**
|
||||
- Configurazione dinamica nomi squadre
|
||||
- Toggle layout orizzontale (inverti home/guest)
|
||||
- Modalità visualizzazione: punteggio semplice o formazioni complete
|
||||
- Nascondi/mostra controlli e cronologia
|
||||
|
||||
---
|
||||
|
||||
## Requisiti
|
||||
|
||||
### Requisiti di Sistema
|
||||
|
||||
#### 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
|
||||
|
||||
#### 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)
|
||||
|
||||
### Requisiti Browser (Utente Finale)
|
||||
|
||||
| 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 |
|
||||
|
||||
### Browser Testati e Supportati
|
||||
|
||||
| Browser | Versione Minima | Supporto | Note |
|
||||
|---------|-----------------|----------|------|
|
||||
| Chrome/Chromium | 90+ | ✅ Completo | Consigliato per tutte le features |
|
||||
| Firefox | 88+ | ✅ Completo | Supporto completo PWA e Speech API |
|
||||
|
||||
---
|
||||
|
||||
## Installazione e Setup
|
||||
|
||||
### Prerequisiti
|
||||
|
||||
- **Node.js** v20.2.0 (consigliato)
|
||||
- **npm** o **yarn**
|
||||
|
||||
### Installazione con NVM (consigliato)
|
||||
|
||||
```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 per Sviluppo
|
||||
|
||||
### Dev Server
|
||||
|
||||
Avvia il server di sviluppo con hot-reload:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
L'applicazione sarà disponibile su [http://localhost:5173](http://localhost:5173)
|
||||
|
||||
### Modalità Sviluppo
|
||||
- Hot Module Replacement (HMR) attivo
|
||||
- Source maps per debugging
|
||||
- Vue DevTools supportato
|
||||
- Errori e warnings in console
|
||||
|
||||
---
|
||||
|
||||
## Comandi per Build
|
||||
|
||||
### Build Produzione
|
||||
|
||||
Genera i file ottimizzati per il deployment:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
**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`)
|
||||
|
||||
### Preview Build
|
||||
|
||||
Anteprima locale della build di produzione:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
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):
|
||||
|
||||
```javascript
|
||||
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' }
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 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 (5°)**: Primo 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
|
||||
└─────┴─────┴─────┘
|
||||
```
|
||||
|
||||
La rotazione avviene in senso orario: 1→2→3→4→5→6→1
|
||||
|
||||
- [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).
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
<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-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>
|
||||
@@ -106,7 +149,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">
|
||||
|
||||
@@ -24,6 +24,7 @@ export default {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
servizioPrecedente: [], // Stack per tracciare i cambi palla
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -37,6 +38,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 +72,14 @@ export default {
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
this.sp.striscia = { home: [0], guest: [0] }
|
||||
this.sp.servizioPrecedente = []
|
||||
},
|
||||
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 +89,12 @@ export default {
|
||||
}
|
||||
},
|
||||
incPunt(team) {
|
||||
// Controlla se c'è già una condizione di vittoria
|
||||
if (this.checkVittoria()) {
|
||||
this.$waveui.notify("Il set è terminato!", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sp.punt[team]++;
|
||||
if (team == 'home') {
|
||||
this.sp.striscia.home.push(this.sp.punt.home)
|
||||
@@ -83,19 +103,56 @@ export default {
|
||||
this.sp.striscia.guest.push(this.sp.punt.guest)
|
||||
this.sp.striscia.home.push(' ')
|
||||
}
|
||||
|
||||
// Ruota la formazione solo se c'è cambio palla (conquista del servizio)
|
||||
const cambioPalla = (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome);
|
||||
this.sp.servizioPrecedente.push(cambioPalla); // Salva se c'è stato cambio palla
|
||||
|
||||
if (cambioPalla) {
|
||||
this.sp.form[team].push(this.sp.form[team].shift());
|
||||
}
|
||||
|
||||
this.sp.servHome = (team == "home");
|
||||
this.sp.form[team].push(this.sp.form[team].shift());
|
||||
},
|
||||
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 (5° set)
|
||||
const 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) {
|
||||
var tmpHome = this.sp.striscia.home.pop()
|
||||
var tmpGuest = this.sp.striscia.guest.pop()
|
||||
var cambioPalla = this.sp.servizioPrecedente.pop() // Recupera se c'era stato cambio palla
|
||||
|
||||
if (tmpHome == ' ') {
|
||||
this.sp.punt.guest--
|
||||
this.sp.form.guest.unshift(this.sp.form.guest.pop());
|
||||
// Ruota indietro solo se c'era stato un cambio palla
|
||||
if (cambioPalla) {
|
||||
this.sp.form.guest.unshift(this.sp.form.guest.pop());
|
||||
}
|
||||
} else {
|
||||
this.sp.punt.home--
|
||||
this.sp.form.home.unshift(this.sp.form.home.pop());
|
||||
// Ruota indietro solo se c'era stato un cambio palla
|
||||
if (cambioPalla) {
|
||||
this.sp.form.home.unshift(this.sp.form.home.pop());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -171,7 +228,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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,3 +122,43 @@ button:focus-visible {
|
||||
color: blue;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
Reference in New Issue
Block a user