17 Commits

Author SHA1 Message Date
2c0bcdb833 Aggiorna dipendenze e Node.js 20 con fix permessi dist
- Aggiorna Vite: 4.3.9 → 5.4.10
- Aggiorna Vue: 3.2.47 → 3.4.38
- Aggiorna @vitejs/plugin-vue: 4.1.0 → 5.1.4
- Aggiorna vite-plugin-pwa: 0.16.0 → 0.20.5
- Aggiorna Capacitor: 6.0.0 → 6.2.0
- Aggiorna wave-ui: 3.3.0 → 3.17.0
- Aggiorna Node.js nel Dockerfile: 18.x → 20.x (LTS)
- Aggiunge fix automatico permessi dist dopo build Docker
2026-01-24 18:43:21 +01:00
5e9377c681 Unifica output APK nella cartella /dist/android invece di /output 2026-01-24 18:41:11 +01:00
b174e66c8b Corregge encoding README.md (caratteri accentati) 2026-01-24 18:02:57 +01:00
3394113ea0 Aggiunge sensor landscape e tenta fix audio mobile (WIP)
- Cambia orientamento da landscape a sensorLandscape (rotazione 180°)
- Aggiorna Java 17 -> 21 per plugin TTS
- Aggiunge @capacitor-community/text-to-speech per audio nativo
- Implementa fallback Web API su desktop, plugin nativo su mobile
- Corregge lang 'it_IT' -> 'it-IT', aggiunge log debug

Nota: audio mobile ancora non funziona, richiede debug
2026-01-24 16:29:11 +01:00
1f71846e23 Ottimizza layout mobile e setup icone APK Android
- Forza orientamento landscape e blocca scroll su mobile
- Aggiunge media queries responsive per schermi <768px
- Usa 100dvh e position:fixed per layout fullscreen
- Crea setup-android-icons.sh per generazione automatica icone Android
- Configura Capacitor per landscape, disabilita splash screen
- Refactoring Dockerfile con ImageMagick per icone multi-densità
2026-01-24 16:29:11 +01:00
9346117603 Aggiunge integrazione Capacitor per build APK Android 2026-01-24 16:29:11 +01:00
2e956aef75 Aggiorna README.md 2026-01-24 10:10:00 +01:00
Attilio Grilli
d57204f4c1 Permette di invertire l'ordine del segnapunti
Lo scambio puo' essere effettuato cliccando sul primo pulsante in basso
quello che apre il modal dove è possibiile modificare i nomi
2025-04-03 09:58:00 +02:00
Attilio Grilli
40b5751440 porta homepage in un componente a parte dividendo il file
aggiunge la striscia del punteggio in basso.
aggiorna il file style.css
2025-03-26 15:08:42 +01:00
Attilio Grilli
6824fb3539 Modifica il colore nel file style.css 2025-02-11 14:12:14 +01:00
Attilio Grilli
81e93c8108 Varie modifiche su HomePage.vue:
- Variabile servHome true|false per cui cambio servizio = !servHome
e quando incrementa this.sp.servHome = (team == "home");
- Se cellulare speak()
- I tasti speciali sono in una funzione
- la funzione speak verifica il pari e il servHome
- Dialog per cambiare nomi
- ridisegnato il bot
2025-02-11 14:00:01 +01:00
Attilio Grilli
ef3886b9f3 aggiunge le icone png (ne toglie una)
elimina text-align: right dalla classe .bot
2025-02-11 13:31:19 +01:00
Attilio Grilli
8c59b3b115 Aggiunge la versione nvm in README 2025-02-11 13:29:30 +01:00
Attilio Grilli
44c0825a0a bordi controlli tastiera e punteggio vocale.
Eliminati i bordi sui titoli e sui set.
Eliminato HelloWorld.vue.
Inseriti alcuni controlli via tastiera tra cui:
ctrl+B = toggle visualizzazione bottoni.
ctrl+H = passaggio a fullscreen (per PC).
ctrl o shift + freccia su o giu per i punteggi.
ctrl o shifth + freccia dx incrementa i set.
inserito il servizio - serve per il punteggio vocale.
Quindi inserito il punteggio vocale.
Si deve usare una iconcina per indicare il servizio,
adesso è visualizzata una x.
2023-07-18 15:18:03 +02:00
Attilio Grilli
01b9a0748f Aggiunto vawe-ui per gli alert.
piccoli aggiustamenti sulla visualizzazione.
adesso il punteggio inc e dec anche con le formazioni.
2023-06-12 10:31:27 +02:00
Attilio Grilli
8a4dc19542 Modifiche per adattare il piu' possibile alla tipologia APP.
Aggiunto un bordo trasparente alle 2 icone.
In manifest forzata la modalità fullscreen landscape, aiutandosi
anche con documentElement.requestFullscreen().
Le formazioni vengono visualizzate cambiando posto con il punteggio.
inseriti opportuni cambiamenti su style.css.
inserito in css-body overscroll-behavior-y: contain;
per prevenire la ricarica della pagina con swipe-down.
modificato il title direttamente in index.html.
2023-06-07 14:46:09 +02:00
Attilio Grilli
6d58ed18c8 Aggiunta una configurazione per PWA in vite.config.js
adesso l'app si installa e parte landscape.
manca la partenza in fullscreen e qualcosa per gestire un modal.
il reset usa l'alert che scombina un po' tutto.
2023-06-06 16:03:26 +02:00
31 changed files with 9022 additions and 2294 deletions

17
.gitignore vendored
View File

@@ -7,8 +7,12 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# DEscrizione ultimo commit (memoria corta...)
currentCommit.txt
node_modules
dist
dist/*
!dist/android/
dist-ssr
*.local
@@ -22,3 +26,14 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Capacitor
android/
ios/
.capacitor/
# Build output
output/
*.apk
!dist/android/*.apk
*.aab

83
Dockerfile Normal file
View 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'\
"]

438
README.md
View File

@@ -1,7 +1,437 @@
# Vue 3 + Vite
# 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
View 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
View 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"
}
}

1
dev-dist/registerSW.js Normal file
View File

@@ -0,0 +1 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })

92
dev-dist/sw.js Normal file
View File

@@ -0,0 +1,92 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-5357ef54'], (function (workbox) { 'use strict';
self.skipWaiting();
workbox.clientsClaim();
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "registerSW.js",
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"revision": null,
"url": "index.html"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
}));

3394
dev-dist/workbox-5357ef54.js Normal file

File diff suppressed because it is too large Load Diff

42
generate-icons.sh Executable file
View 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"

View File

@@ -3,8 +3,11 @@
<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" />
<title>Vite + Vue</title>
<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>
<div id="app"></div>

6269
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +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"
"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"
}
}

BIN
public/antoniana.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
public/exit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/gear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/segnap-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
public/segnap-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
public/serv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/speaker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

40
setup-android-icons.sh Normal file
View 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!"

View File

@@ -1,5 +1,5 @@
<script setup>
import HomePage from './components/HomePage.vue'
import HomePage from './components/HomePage/index.vue'
</script>
<template>

BIN
src/assets/serve.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,40 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -1,13 +1,27 @@
<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: {},
data() {
return {
voices: null,
diaNomi: {
show: false,
home: "",
guest: "",
},
visuForm: false,
visuButt: true,
sp: {
servHome: true,
punt: { home: 0, guest: 0 },
set: { home: 0, guest: 0 },
nomi: { home: "Antoniana", guest: "Guest" },
form: {
home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"],
@@ -15,13 +29,29 @@ export default {
},
}
},
mounted () {
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()) {
var noSleep = new NoSleep();
noSleep.enable();
document.documentElement.requestFullscreen();
}
this.abilitaTastiSpeciali();
},
methods: {
closeApp() {
var win = window.open("", "_self");
win.close();
},
fullScreen() {
document.documentElement.requestFullscreen();
},
@@ -37,40 +67,198 @@ export default {
}
},
resetta() {
if (confirm("Confermi il reset del punteggio ?")) {
this.sp.punt.home = 0;
this.sp.punt.guest = 0;
this.sp.form = {
home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"],
}
this.$waveui.notify("Punteggio<br />RESETTATO", "success");
this.visuForm = false;
this.sp.punt.home = 0;
this.sp.punt.guest = 0;
this.sp.form = {
home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"],
}
},
incSet(team) {
if (this.sp.set[team] == 2) {
this.sp.set[team] = 0;
} else {
this.sp.set[team]++;
}
},
incPunt(team) {
this.sp.punt[team]++;
this.sp.servHome = (team == "home");
this.sp.form[team].push(this.sp.form[team].shift());
},
decPunt(team) {
// decrementa il punteggio se è > 0.
if (this.sp.punt[team] > 0) {
this.sp.punt[team]--;
this.sp.form[team].unshift(this.sp.form[team].pop());
}
},
async speak() {
// Prepara il testo da pronunciare
let text = "";
if (this.sp.punt.home + this.sp.punt.guest == 0) {
text = "zero a zero";
} else if (this.sp.punt.home == this.sp.punt.guest) {
text = this.sp.punt.home + " pari";
} else {
if (this.sp.servHome) {
text = this.sp.punt.home + " a " + this.sp.punt.guest;
} else {
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';
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();
this.diaNomi.show = true;
},
disabilitaTastiSpeciali() {
window.removeEventListener("keydown", this.funzioneTastiSpeciali);
},
abilitaTastiSpeciali() {
window.addEventListener("keydown", this.funzioneTastiSpeciali);
},
funzioneTastiSpeciali(e) {
e.preventDefault();
if (e.ctrlKey && e.key == "m") {
this.diaNomi.show = true
} else if (e.ctrlKey && e.key == "b") {
this.visuButt = !this.visuButt
} else if (e.ctrlKey && e.key == "f") {
document.documentElement.requestFullscreen();
} else if (e.ctrlKey && e.key == "s") {
this.speak();
} else if (e.ctrlKey && e.key == "z") {
this.visuForm = !this.visuForm
} else if (e.ctrlKey && e.key == "ArrowUp") {
this.incPunt("home")
} else if (e.ctrlKey && e.key == "ArrowDown") {
this.decPunt("home")
} else if (e.ctrlKey && e.key == "ArrowRight") {
this.incSet("home")
} else if (e.shiftKey && e.key == "ArrowUp") {
this.incPunt("guest")
} else if (e.shiftKey && e.key == "ArrowDown") {
this.decPunt("guest")
} else if (e.shiftKey && e.key == "ArrowRight") {
this.incSet("guest")
} else if (e.ctrlKey && e.key == "ArrowLeft") {
this.sp.servHome = !this.sp.servHome
} else { return false }
}
}
};
</script>
<template>
<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 bg-color="success" @click="diaNomi.show = false">
Ok
</w-button>
</w-dialog>
<div class="campo">
<div class="hea home" @click="decPunt('home')">HOME</div>
<div class="hea guest" @click="decPunt('guest')">GUEST</div>
<div class="col home" @click="incPunt('home')">{{ sp.punt.home }}</div>
<div class="col guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div>
<div class="bot tal">
<button @click="fullScreen">Fullscreen</button>
<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>
</span>
<span @click="incSet('home')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.home }}</span>
</div>
<div class="bot tar">
<button @click="resetta">RESET</button>
<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>
<span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span>
</div>
<span v-if="visuForm">
<div class="col form home" @click="incPunt('home')">
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
{{ sp.form.home[x] }}
</div>
</div>
<div class="col form guest" @click="incPunt('guest')">
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
{{ sp.form.guest[x] }}
</div>
</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>
</span>
<div class="bot" v-if="visuButt">
<w-flex justify-space-between class="pa2">
<w-confirm right align-bottom v-if="isMobile()" question="CHIUDO ?" cancel="NO" confirm="SI" @confirm="closeApp">
<img src="/exit.png" width="25" />
</w-confirm>
<w-button @click="apriDialogConfig()">
<img src="/gear.png" width="25" />
</w-button>
<w-button @click="sp.servHome = !sp.servHome">
<img src="/serv.png" width="25" />
</w-button>
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">
RESET
</w-confirm>
<w-button @click="visuForm = !visuForm">
<span v-if="visuForm">PUNTEGGIO</span>
<span v-if="!visuForm">FORMAZIONI</span>
</w-button>
<w-button @click="speak">
<img src="/speaker.png" width="25" />
</w-button>
</w-flex>
</div>
</div>
</template>

View File

@@ -0,0 +1,129 @@
<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">
Ok
</w-button>
</w-dialog>
<div class="campo">
<span v-if="order">
<!-- 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>
</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>
<span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span>
</div>
<span v-if="visuForm">
<div class="col form home" @click="incPunt('home')">
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
{{ sp.form.home[x] }}
</div>
</div>
<div class="col form guest" @click="incPunt('guest')">
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
{{ sp.form.guest[x] }}
</div>
</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>
</span>
</span>
<span v-else>
<!-- guest home -->
<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>
</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>
<span @click="incSet('home')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.home }}</span>
</div>
<span v-if="visuForm">
<div class="col form guest" @click="incPunt('guest')">
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
{{ sp.form.guest[x] }}
</div>
</div>
<div class="col form home" @click="incPunt('home')">
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
{{ sp.form.home[x] }}
</div>
</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>
</span>
</span>
<div class="striscia" v-if="visuStriscia">
<div>
<span class="text-bold mr1">{{ sp.nomi.home }}</span>
<div v-for="h in sp.striscia.home" class="item">
{{String(h)}}
</div>
</div>
<div class="guest">
<span class="text-bold mr1">{{ sp.nomi.guest }}</span>
<div v-for="h in sp.striscia.guest" class="item">
{{String(h)}}
</div>
</div>
</div>
<div class="bot" v-if="visuButt">
<w-flex justify-space-between class="pa2">
<w-confirm right align-bottom v-if="isMobile()" question="CHIUDO ?" cancel="NO" confirm="SI" @confirm="closeApp">
<img src="/exit.png" width="25" />
</w-confirm>
<w-button @click="apriDialogConfig()">
<img src="/gear.png" width="25" />
</w-button>
<w-button @click="sp.servHome = !sp.servHome">
<img src="/serv.png" width="25" />
</w-button>
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">
RESET
</w-confirm>
<w-button @click="visuForm = !visuForm">
<span v-if="visuForm">PUNTEGGIO</span>
<span v-if="!visuForm">FORMAZIONI</span>
</w-button>
<w-button @click="visuStriscia = !visuStriscia">
STRISCIA
</w-button>
<w-button @click="speak">
<img src="/speaker.png" width="25" />
</w-button>
</w-flex>
</div>
</div>
</section>

View File

@@ -0,0 +1,178 @@
import NoSleep from "nosleep.js";
export default {
name: "HomePage",
components: {},
data() {
return {
order: true,
voices: null,
diaNomi: {
show: false,
home: "",
guest: "",
},
visuForm: false,
visuButt: true,
visuStriscia: true,
sp: {
striscia: { home: [0], guest: [0] },
servHome: true,
punt: { home: 0, guest: 0 },
set: { home: 0, guest: 0 },
nomi: { home: "Antoniana", guest: "Guest" },
form: {
home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"],
},
},
}
},
mounted() {
this.voices = window.speechSynthesis.getVoices();
if (this.isMobile()) {
this.speak();
var noSleep = new NoSleep();
noSleep.enable();
document.documentElement.requestFullscreen();
}
this.abilitaTastiSpeciali();
},
methods: {
closeApp() {
var win = window.open("", "_self");
win.close();
},
fullScreen() {
document.documentElement.requestFullscreen();
},
isMobile() {
if (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
) {
return true;
} else {
return false;
}
},
resetta() {
this.$waveui.notify("Punteggio<br />RESETTATO", "success");
this.visuForm = false;
this.sp.punt.home = 0;
this.sp.punt.guest = 0;
this.sp.form = {
home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"],
}
this.sp.striscia = { home: [0], guest: [0] }
},
incSet(team) {
if (this.sp.set[team] == 2) {
this.sp.set[team] = 0;
} else {
this.sp.set[team]++;
}
},
incPunt(team) {
this.sp.punt[team]++;
if (team == 'home') {
this.sp.striscia.home.push(this.sp.punt.home)
this.sp.striscia.guest.push(' ')
} else {
this.sp.striscia.guest.push(this.sp.punt.guest)
this.sp.striscia.home.push(' ')
}
this.sp.servHome = (team == "home");
this.sp.form[team].push(this.sp.form[team].shift());
},
decPunt() {
if (this.sp.striscia.home.length > 1) {
var tmpHome = this.sp.striscia.home.pop()
var tmpGuest = this.sp.striscia.guest.pop()
if (tmpHome == ' ') {
this.sp.punt.guest--
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());
}
}
},
// decPunt(team) {
// // decrementa il punteggio se è > 0.
// if (this.sp.punt[team] > 0) {
// this.sp.punt[team]--;
// this.sp.striscia.home.pop()
// this.sp.striscia.guest.pop()
// this.sp.form[team].unshift(this.sp.form[team].pop());
// }
// },
speak() {
const msg = new SpeechSynthesisUtterance();
if (this.sp.punt.home + this.sp.punt.guest == 0) {
msg.text = "zero a zero";
} else if (this.sp.punt.home == this.sp.punt.guest) {
msg.text = this.sp.punt.home + " pari";
} else {
if (this.sp.servHome) {
msg.text = this.sp.punt.home + " a " + this.sp.punt.guest;
} else {
msg.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);
},
apriDialogConfig() {
this.disabilitaTastiSpeciali();
this.diaNomi.show = true;
},
disabilitaTastiSpeciali() {
window.removeEventListener("keydown", this.funzioneTastiSpeciali);
},
abilitaTastiSpeciali() {
window.addEventListener("keydown", this.funzioneTastiSpeciali);
},
funzioneTastiSpeciali(e) {
e.preventDefault();
if (e.ctrlKey && e.key == "m") {
this.diaNomi.show = true
} else if (e.ctrlKey && e.key == "b") {
this.visuButt = !this.visuButt
} else if (e.ctrlKey && e.key == "f") {
document.documentElement.requestFullscreen();
} else if (e.ctrlKey && e.key == "s") {
this.speak();
} else if (e.ctrlKey && e.key == "z") {
this.visuForm = !this.visuForm
} else if (e.ctrlKey && e.key == "ArrowUp") {
this.incPunt("home")
} else if (e.ctrlKey && e.key == "ArrowDown") {
this.decPunt("home")
} else if (e.ctrlKey && e.key == "ArrowRight") {
this.incSet("home")
} else if (e.shiftKey && e.key == "ArrowUp") {
this.incPunt("guest")
} else if (e.shiftKey && e.key == "ArrowDown") {
this.decPunt("guest")
} else if (e.shiftKey && e.key == "ArrowRight") {
this.incSet("guest")
} else if (e.ctrlKey && e.key == "ArrowLeft") {
this.sp.servHome = !this.sp.servHome
} else { return false }
}
}
}

View File

@@ -0,0 +1,112 @@
.homepage {
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
touch-action: pan-x pan-y;
height: 100%
}
body {
overscroll-behavior-y: contain;
margin: 0;
place-items: center;
min-width: 320px;
width: 100%;
min-height: 100vh;
background-color: #000;
}
button {
margin-left: 10px;
margin-right: 10px;
border-radius: 8px;
border: 1px solid #fff;
padding: 0.6em 1.2em;
font-size: 0.8em;
font-weight: 500;
font-family: inherit;
color: #fff;
background-color: #000;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
background-color: #333;
}
button:focus, button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
#app {
margin: 0 auto;
text-align: center;
}
.campo {
user-select: none;
width: 100%;
display: table;
color: #fff;
}
.hea {
float: left;
width: 50%;
font-size: xx-large;
}
.hea span {
/* border: 1px solid #f3fb00; */
padding-left: 10px;
padding-right: 10px;
border-radius: 5px;
}
.tal {
text-align: left;
}
.tar {
text-align: right;
}
.bot {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
margin-top: 10px;
margin-bottom: 1px;
background-color: #111;
}
.col {
margin-left: auto;
margin-right: auto;
text-align: center;
float: left;
width: 50%;
}
.punt {
font-size: 60vh;
}
.form {
font-size: 5vh;
border-top: #fff dashed 25px;
padding-top: 50px;
}
.formtit {
font-size: 5vh;
margin-top: 30px;
margin-bottom: 20px;
}
.formdiv {
font-size: 20vh;
float: left;
width: 32%;
}
.home {
background-color: black;
color: yellow;
}
.guest {
background-color: blue;
color: white
}
.item-stri {
background-color: #fff;
}
}

View File

@@ -0,0 +1,3 @@
<template src="./HomePage.html"></template>
<script src="./HomePage.js"></script>

View File

@@ -1,5 +1,10 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import WaveUI from 'wave-ui'
import 'wave-ui/dist/wave-ui.css'
createApp(App).mount('#app')
const app = createApp(App)
app.use(WaveUI)
app.mount('#app')

View File

@@ -1,16 +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: 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 {
@@ -19,7 +36,7 @@ button {
border-radius: 8px;
border: 1px solid #fff;
padding: 0.6em 1.2em;
font-size: 1em;
font-size: 0.8em;
font-weight: 500;
font-family: inherit;
color: #fff;
@@ -37,10 +54,14 @@ 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;
width: 100%;
@@ -52,6 +73,12 @@ button:focus-visible {
width: 50%;
font-size: xx-large;
}
.hea span {
/* border: 1px solid #f3fb00; */
padding-left: 10px;
padding-right: 10px;
border-radius: 5px;
}
.tal {
text-align: left;
}
@@ -59,19 +86,101 @@ button:focus-visible {
text-align: right;
}
.bot {
float: left;
width: 50%;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
margin-top: 10px;
background-color: #000;
margin-bottom: 1px;
background-color: #111;
}
.col {
margin-left: auto;
margin-right: auto;
text-align: center;
float: left;
font-size: 60vh;
width: 50%;
}
.punt {
font-size: 60vh;
}
.form {
font-size: 5vh;
border-top: #fff dashed 25px;
padding-top: 50px;
}
.formtit {
font-size: 5vh;
margin-top: 30px;
margin-bottom: 20px;
}
.formdiv {
font-size: 20vh;
float: left;
width: 32%;
}
.home {
background-color: #00f;
background-color: black;
color: yellow;
}
.guest {
background-color: #f00;
background-color: blue;
color: white
}
.striscia {
position:fixed;
text-align: right;
bottom: 50px;
right: 10px;
margin-left: -10000px;
}
.striscia .item {
width: 25px;
text-align: center;
font-weight: bold;
display: inline-block;
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;
}
}

View File

@@ -4,11 +4,32 @@ import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({
base: process.env.NODE_ENV === 'production' ? '/segnap' : '/',
base: '/',
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate'
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'
}
]
}
})
],
})