2026-03-30 16:00:01 +02:00
// ===========================================================================
// FILE: lib/ui/profile/profile_screen.dart
// ===========================================================================
import ' dart:ui ' ;
import ' dart:math ' ;
import ' package:flutter/material.dart ' ;
import ' package:provider/provider.dart ' ;
import ' package:cloud_firestore/cloud_firestore.dart ' ;
import ' package:firebase_auth/firebase_auth.dart ' ;
import ' package:shared_preferences/shared_preferences.dart ' ;
import ' ../../core/theme_manager.dart ' ;
import ' ../../core/app_colors.dart ' ;
import ' ../../services/storage_service.dart ' ;
import ' ../../widgets/painters.dart ' ;
import ' ../../widgets/cyber_border.dart ' ;
class ProfileScreen extends StatefulWidget {
const ProfileScreen ( { super . key } ) ;
@ override
State < ProfileScreen > createState ( ) = > _ProfileScreenState ( ) ;
}
class _ProfileScreenState extends State < ProfileScreen > {
final TextEditingController _nameController = TextEditingController ( ) ;
final TextEditingController _passController = TextEditingController ( ) ;
bool _isLoading = false ;
bool _obscurePassword = true ;
String _errorMessage = " " ;
List < String > _nameSuggestions = [ ] ;
bool _isGhostMode = false ;
late User _currentUser ;
@ override
void initState ( ) {
super . initState ( ) ;
_currentUser = FirebaseAuth . instance . currentUser ! ;
_loadGhostMode ( ) ;
}
@ override
void dispose ( ) {
_nameController . dispose ( ) ;
_passController . dispose ( ) ;
super . dispose ( ) ;
}
Future < void > _loadGhostMode ( ) async {
try {
var doc = await FirebaseFirestore . instance . collection ( ' leaderboard ' ) . doc ( _currentUser . uid ) . get ( ) ;
if ( doc . exists & & doc . data ( ) ! . containsKey ( ' isGhost ' ) ) {
setState ( ( ) {
_isGhostMode = doc . data ( ) ! [ ' isGhost ' ] ;
} ) ;
}
} catch ( e ) {
debugPrint ( " Errore caricamento Ghost Mode: $ e " ) ;
}
}
Future < void > _toggleGhostMode ( bool value ) async {
setState ( ( ) = > _isGhostMode = value ) ;
try {
await FirebaseFirestore . instance . collection ( ' leaderboard ' ) . doc ( _currentUser . uid ) . set (
{ ' isGhost ' : value } , SetOptions ( merge: true )
) ;
} catch ( e ) {
debugPrint ( " Errore salvataggio Ghost Mode: $ e " ) ;
}
}
String _getPlayerTitle ( int level ) {
2026-03-30 17:00:01 +02:00
if ( level < 10 ) return " Principiante " ;
if ( level < 20 ) return " Apprendista " ;
if ( level < 40 ) return " Sfidante " ;
if ( level < 60 ) return " Tattico dell'Arena " ;
if ( level < 80 ) return " Maestro dei Quadrati " ;
if ( level < 100 ) return " Gran Maestro " ;
if ( level < 130 ) return " Campione della Griglia " ;
if ( level < 160 ) return " Entità Digitale " ;
if ( level < 200 ) return " Oracolo del Codice " ;
return " Leggenda Suprema " ;
2026-03-30 16:00:01 +02:00
}
Future < void > _handleRegistration ( ) async {
final name = _nameController . text . trim ( ) . toUpperCase ( ) ;
final password = _passController . text . trim ( ) ;
setState ( ( ) { _errorMessage = " " ; _nameSuggestions . clear ( ) ; _isLoading = true ; } ) ;
if ( name . isEmpty | | password . isEmpty ) {
setState ( ( ) { _errorMessage = " Compila tutti i campi! " ; _isLoading = false ; } ) ;
return ;
}
if ( password . length < 6 ) {
setState ( ( ) { _errorMessage = " Password troppo corta (min. 6 caratteri) " ; _isLoading = false ; } ) ;
return ;
}
try {
// 1. Controllo univocità del nome
var existingUser = await FirebaseFirestore . instance . collection ( ' leaderboard ' ) . where ( ' name ' , isEqualTo: name ) . get ( ) ;
if ( existingUser . docs . isNotEmpty & & existingUser . docs . first . id ! = _currentUser . uid ) {
// Nome già preso, generiamo suggerimenti
List < String > suggestions = [ ] ;
int attempts = 0 ;
final rand = Random ( ) ;
while ( suggestions . length < 3 & & attempts < 15 ) {
String candidate = " $ name ${ rand . nextInt ( 99 ) + 1 } " ;
var check = await FirebaseFirestore . instance . collection ( ' leaderboard ' ) . where ( ' name ' , isEqualTo: candidate ) . get ( ) ;
if ( check . docs . isEmpty & & ! suggestions . contains ( candidate ) ) {
suggestions . add ( candidate ) ;
}
attempts + + ;
}
setState ( ( ) {
_errorMessage = " Nome già in uso! Scegline un altro: " ;
_nameSuggestions = suggestions ;
_isLoading = false ;
} ) ;
return ;
}
// 2. Registrazione sicura
final fakeEmail = " ${ name . replaceAll ( ' ' , ' ' ) } @tetraq.game " . toLowerCase ( ) ;
if ( _currentUser . isAnonymous ) {
final credential = EmailAuthProvider . credential ( email: fakeEmail , password: password ) ;
await _currentUser . linkWithCredential ( credential ) ;
}
await StorageService . instance . savePlayerName ( name ) ;
await StorageService . instance . syncLeaderboard ( ) ;
setState ( ( ) { _isLoading = false ; } ) ;
if ( mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " Account Protetto con Successo! " ) , backgroundColor: Colors . green ) ) ;
} on FirebaseAuthException catch ( e ) {
String msg = " Errore di connessione. " ;
if ( e . code = = ' email-already-in-use ' | | e . code = = ' credential-already-in-use ' ) msg = " Utente già registrato. Se sei tu, fai il login. " ;
setState ( ( ) { _errorMessage = msg ; _isLoading = false ; } ) ;
} catch ( e ) {
setState ( ( ) { _errorMessage = " Errore: $ e " ; _isLoading = false ; } ) ;
}
}
Future < void > _deleteAccount ( ) async {
bool confirm = await showDialog (
context: context ,
builder: ( ctx ) = > AlertDialog (
backgroundColor: Colors . black87 ,
title: const Text ( " ATTENZIONE " , style: TextStyle ( color: Colors . redAccent , fontWeight: FontWeight . bold ) ) ,
content: const Text ( " Stai per eliminare definitivamente il tuo profilo, i tuoi XP e le statistiche. \n L'operazione è irreversibile. \n \n Vuoi procedere? " , style: TextStyle ( color: Colors . white ) ) ,
actions: [
TextButton ( onPressed: ( ) = > Navigator . pop ( ctx , false ) , child: const Text ( " ANNULLA " , style: TextStyle ( color: Colors . grey ) ) ) ,
ElevatedButton (
style: ElevatedButton . styleFrom ( backgroundColor: Colors . redAccent ) ,
onPressed: ( ) = > Navigator . pop ( ctx , true ) ,
child: const Text ( " SÌ, ELIMINA " , style: TextStyle ( color: Colors . white , fontWeight: FontWeight . bold ) ) ,
) ,
] ,
)
) ? ? false ;
if ( ! confirm ) return ;
setState ( ( ) = > _isLoading = true ) ;
try {
// 1. Elimina record da Firestore
await FirebaseFirestore . instance . collection ( ' leaderboard ' ) . doc ( _currentUser . uid ) . delete ( ) ;
// 2. Elimina l'utente Auth
await _currentUser . delete ( ) ;
// 3. Pulisci i dati locali sensibili
final prefs = await SharedPreferences . getInstance ( ) ;
await prefs . remove ( ' totalXP ' ) ;
await prefs . remove ( ' wins ' ) ;
await prefs . remove ( ' losses ' ) ;
await prefs . remove ( ' cpuLevel ' ) ;
await prefs . remove ( ' playerName ' ) ;
await prefs . remove ( ' favorites ' ) ;
// 4. Ricrea un anonimo pulito e torna alla Home
await FirebaseAuth . instance . signInAnonymously ( ) ;
await StorageService . instance . init ( ) ;
if ( mounted ) {
Navigator . of ( context ) . popUntil ( ( route ) = > route . isFirst ) ;
}
} on FirebaseAuthException catch ( e ) {
setState ( ( ) = > _isLoading = false ) ;
if ( e . code = = ' requires-recent-login ' ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " Per sicurezza, riavvia l'app prima di eliminare l'account. " ) , backgroundColor: Colors . redAccent ) ) ;
} else {
ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( " Errore: ${ e . message } " ) , backgroundColor: Colors . redAccent ) ) ;
}
}
}
@ override
Widget build ( BuildContext context ) {
final themeManager = context . watch < ThemeManager > ( ) ;
final theme = themeManager . currentColors ;
final themeType = themeManager . currentThemeType ;
Color inkColor = const Color ( 0xFF111122 ) ;
int wins = StorageService . instance . wins ;
int losses = StorageService . instance . losses ;
int totalGames = wins + losses ;
double winRate = totalGames > 0 ? ( wins / totalGames ) * 100 : 0.0 ;
int level = StorageService . instance . playerLevel ;
String title = _getPlayerTitle ( level ) ;
String playerName = StorageService . instance . playerName ;
if ( playerName . isEmpty ) playerName = " GUEST " ;
bool isAnon = _currentUser . isAnonymous ;
return Scaffold (
backgroundColor: theme . background ,
appBar: AppBar (
title: Text ( " PROFILO GIOCATORE " , style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : theme . text , fontWeight: FontWeight . w900 , letterSpacing: 1.5 ) ) ) ,
backgroundColor: Colors . transparent ,
elevation: 0 ,
iconTheme: IconThemeData ( color: themeType = = AppThemeType . doodle ? inkColor : theme . text ) ,
) ,
body: Stack (
children: [
if ( themeType = = AppThemeType . doodle )
Positioned . fill ( child: CustomPaint ( painter: FullScreenGridPainter ( Colors . blue . withOpacity ( 0.15 ) ) ) ) ,
SingleChildScrollView (
physics: const BouncingScrollPhysics ( ) ,
padding: const EdgeInsets . all ( 20 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
// --- SEZIONE 1: IDENTITÀ ---
Container (
padding: const EdgeInsets . all ( 20 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white : theme . text . withOpacity ( 0.05 ) ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all ( color: themeType = = AppThemeType . doodle ? inkColor : theme . playerBlue . withOpacity ( 0.3 ) , width: 2 ) ,
boxShadow: themeType = = AppThemeType . doodle ? [ BoxShadow ( color: inkColor . withOpacity ( 0.8 ) , offset: const Offset ( 4 , 4 ) ) ] : [ ] ,
) ,
child: Column (
children: [
CircleAvatar ( radius: 40 , backgroundColor: theme . playerBlue . withOpacity ( 0.2 ) , child: Icon ( Icons . person , size: 45 , color: theme . playerBlue ) ) ,
const SizedBox ( height: 15 ) ,
Text ( playerName , style: getSharedTextStyle ( themeType , TextStyle ( fontSize: 28 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? inkColor : theme . text ) ) ) ,
const SizedBox ( height: 5 ) ,
Text ( title , style: getSharedTextStyle ( themeType , TextStyle ( fontSize: 14 , fontWeight: FontWeight . bold , color: theme . playerRed ) ) ) ,
const SizedBox ( height: 15 ) ,
Row (
mainAxisAlignment: MainAxisAlignment . center ,
children: [
Icon ( isAnon ? Icons . warning_amber_rounded : Icons . verified_user , color: isAnon ? Colors . orange : Colors . green , size: 18 ) ,
const SizedBox ( width: 5 ) ,
Text ( isAnon ? " Account non protetto " : " Account protetto sul Cloud " , style: getSharedTextStyle ( themeType , TextStyle ( color: isAnon ? Colors . orange : Colors . green , fontWeight: FontWeight . bold , fontSize: 12 ) ) ) ,
] ,
)
] ,
) ,
) ,
const SizedBox ( height: 20 ) ,
// --- SEZIONE 2: STATISTICHE AVANZATE ---
Row (
children: [
Expanded ( child: _buildStatCard ( " Vittorie " , " $ wins " , Icons . emoji_events , Colors . amber , theme , themeType ) ) ,
const SizedBox ( width: 15 ) ,
Expanded ( child: _buildStatCard ( " Sconfitte " , " $ losses " , Icons . sentiment_very_dissatisfied , theme . playerRed , theme , themeType ) ) ,
] ,
) ,
const SizedBox ( height: 15 ) ,
_buildStatCard ( " Win Rate Globale " , " ${ winRate . toStringAsFixed ( 1 ) } % " , Icons . pie_chart , theme . playerBlue , theme , themeType , isWide: true ) ,
const SizedBox ( height: 25 ) ,
// --- SEZIONE 3: REGISTRAZIONE (Solo se anonimo) ---
if ( isAnon ) . . . [
Container (
padding: const EdgeInsets . all ( 20 ) ,
decoration: BoxDecoration (
color: Colors . orange . withOpacity ( 0.1 ) ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all ( color: Colors . orange , width: 2 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
Text ( " Metti al sicuro i tuoi progressi! " , textAlign: TextAlign . center , style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : Colors . white , fontWeight: FontWeight . bold , fontSize: 16 ) ) ) ,
const SizedBox ( height: 15 ) ,
TextField (
controller: _nameController , textCapitalization: TextCapitalization . characters , textAlign: TextAlign . center , maxLength: 8 ,
style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : Colors . white , fontSize: 20 , fontWeight: FontWeight . bold ) ) ,
decoration: InputDecoration ( hintText: " Scegli un Nome " , hintStyle: TextStyle ( color: Colors . grey . withOpacity ( 0.6 ) ) , filled: true , fillColor: Colors . black12 , counterText: " " , border: OutlineInputBorder ( borderRadius: BorderRadius . circular ( 15 ) , borderSide: BorderSide . none ) ) ,
) ,
const SizedBox ( height: 10 ) ,
TextField (
controller: _passController , obscureText: _obscurePassword , textAlign: TextAlign . center , maxLength: 20 ,
style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : Colors . white , fontSize: 20 , fontWeight: FontWeight . bold ) ) ,
decoration: InputDecoration (
hintText: " Scegli Password " , hintStyle: TextStyle ( color: Colors . grey . withOpacity ( 0.6 ) ) , filled: true , fillColor: Colors . black12 , counterText: " " , border: OutlineInputBorder ( borderRadius: BorderRadius . circular ( 15 ) , borderSide: BorderSide . none ) ,
suffixIcon: IconButton ( icon: Icon ( _obscurePassword ? Icons . visibility : Icons . visibility_off , color: Colors . grey ) , onPressed: ( ) = > setState ( ( ) = > _obscurePassword = ! _obscurePassword ) ) ,
) ,
) ,
if ( _errorMessage . isNotEmpty ) . . . [
const SizedBox ( height: 10 ) ,
Text ( _errorMessage , textAlign: TextAlign . center , style: const TextStyle ( color: Colors . redAccent , fontWeight: FontWeight . bold ) ) ,
] ,
if ( _nameSuggestions . isNotEmpty ) . . . [
const SizedBox ( height: 10 ) ,
Wrap (
spacing: 8 , alignment: WrapAlignment . center ,
children: _nameSuggestions . map ( ( s ) = > ActionChip (
label: Text ( s , style: const TextStyle ( fontWeight: FontWeight . bold ) ) ,
backgroundColor: theme . playerBlue . withOpacity ( 0.2 ) ,
side: BorderSide ( color: theme . playerBlue ) ,
onPressed: ( ) { _nameController . text = s ; _handleRegistration ( ) ; } ,
) ) . toList ( ) ,
)
] ,
const SizedBox ( height: 15 ) ,
_isLoading
? const Center ( child: CircularProgressIndicator ( color: Colors . orange ) )
: ElevatedButton (
style: ElevatedButton . styleFrom ( backgroundColor: Colors . orange , foregroundColor: Colors . white , padding: const EdgeInsets . symmetric ( vertical: 15 ) , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) ) ,
onPressed: _handleRegistration ,
child: Text ( " SALVA PROFILO " , style: getSharedTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . w900 , letterSpacing: 1.5 ) ) ) ,
)
] ,
) ,
) ,
const SizedBox ( height: 25 ) ,
] ,
// --- SEZIONE 4: IMPOSTAZIONI PRIVACY ---
Text ( " PRIVACY " , style: getSharedTextStyle ( themeType , TextStyle ( fontSize: 14 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.6 ) : theme . text . withOpacity ( 0.5 ) , letterSpacing: 1.5 ) ) ) ,
const SizedBox ( height: 10 ) ,
SwitchListTile (
contentPadding: EdgeInsets . zero ,
activeColor: theme . playerBlue ,
title: Text ( " Modalità Fantasma " , style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : theme . text , fontWeight: FontWeight . bold ) ) ) ,
subtitle: Text ( " Nessuno ti vedrà online o potrà invitarti. " , style: TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.6 ) : theme . text . withOpacity ( 0.5 ) , fontSize: 12 ) ) ,
value: _isGhostMode ,
onChanged: _toggleGhostMode ,
) ,
const Divider ( ) ,
// --- SEZIONE 5: GESTIONE PREFERITI ---
const SizedBox ( height: 15 ) ,
Text ( " AMICI PREFERITI " , style: getSharedTextStyle ( themeType , TextStyle ( fontSize: 14 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.6 ) : theme . text . withOpacity ( 0.5 ) , letterSpacing: 1.5 ) ) ) ,
const SizedBox ( height: 10 ) ,
_buildFavoritesList ( theme , themeType , inkColor ) ,
const SizedBox ( height: 40 ) ,
// --- SEZIONE 6: DANGER ZONE ---
OutlinedButton . icon (
style: OutlinedButton . styleFrom (
foregroundColor: Colors . redAccent ,
side: const BorderSide ( color: Colors . redAccent , width: 2 ) ,
padding: const EdgeInsets . symmetric ( vertical: 15 ) ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) ,
) ,
icon: const Icon ( Icons . delete_forever ) ,
label: Text ( " ELIMINA PROFILO " , style: getSharedTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . w900 , letterSpacing: 1.5 ) ) ) ,
onPressed: _deleteAccount ,
) ,
const SizedBox ( height: 30 ) ,
] ,
) ,
) ,
if ( _isLoading & & ! isAnon )
Positioned . fill ( child: Container ( color: Colors . black54 , child: const Center ( child: CircularProgressIndicator ( color: Colors . redAccent ) ) ) ) ,
] ,
) ,
) ;
}
Widget _buildStatCard ( String title , String value , IconData icon , Color color , ThemeColors theme , AppThemeType themeType , { bool isWide = false } ) {
Color inkColor = const Color ( 0xFF111122 ) ;
return Container (
padding: const EdgeInsets . all ( 15 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white : theme . text . withOpacity ( 0.05 ) ,
borderRadius: BorderRadius . circular ( 15 ) ,
border: Border . all ( color: themeType = = AppThemeType . doodle ? inkColor : color . withOpacity ( 0.3 ) , width: 1.5 ) ,
boxShadow: themeType = = AppThemeType . doodle ? [ BoxShadow ( color: inkColor . withOpacity ( 0.8 ) , offset: const Offset ( 3 , 3 ) ) ] : [ ] ,
) ,
child: Column (
crossAxisAlignment: isWide ? CrossAxisAlignment . center : CrossAxisAlignment . start ,
children: [
Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( icon , color: color , size: 20 ) ,
const SizedBox ( width: 8 ) ,
Text ( title , style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.6 ) : theme . text . withOpacity ( 0.6 ) , fontSize: 11 , fontWeight: FontWeight . bold ) ) ) ,
] ,
) ,
const SizedBox ( height: 10 ) ,
Center (
child: Text ( value , style: getSharedTextStyle ( themeType , TextStyle ( fontSize: 24 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? inkColor : theme . text ) ) ) ,
) ,
] ,
) ,
) ;
}
Widget _buildFavoritesList ( ThemeColors theme , AppThemeType themeType , Color inkColor ) {
final favs = StorageService . instance . favorites ;
if ( favs . isEmpty ) {
return Container (
padding: const EdgeInsets . all ( 20 ) ,
decoration: BoxDecoration ( borderRadius: BorderRadius . circular ( 15 ) , border: Border . all ( color: Colors . grey . withOpacity ( 0.3 ) , style: BorderStyle . solid ) ) ,
child: Center ( child: Text ( " Nessun amico salvato. " , style: TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.5 ) : theme . text . withOpacity ( 0.5 ) ) ) ) ,
) ;
}
return ListView . builder (
shrinkWrap: true ,
physics: const NeverScrollableScrollPhysics ( ) ,
itemCount: favs . length ,
itemBuilder: ( ctx , i ) {
return Container (
margin: const EdgeInsets . only ( bottom: 8 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white : theme . text . withOpacity ( 0.02 ) ,
borderRadius: BorderRadius . circular ( 12 ) ,
border: Border . all ( color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.2 ) : theme . text . withOpacity ( 0.1 ) ) ,
) ,
child: ListTile (
leading: Icon ( Icons . star , color: Colors . amber . shade600 ) ,
title: Text ( favs [ i ] [ ' name ' ] ! , style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : theme . text , fontWeight: FontWeight . bold ) ) ) ,
trailing: IconButton (
icon: const Icon ( Icons . close , color: Colors . redAccent ) ,
onPressed: ( ) async {
await StorageService . instance . toggleFavorite ( favs [ i ] [ ' uid ' ] ! , favs [ i ] [ ' name ' ] ! ) ;
setState ( ( ) { } ) ;
} ,
) ,
) ,
) ;
} ,
) ;
}
}
class FullScreenGridPainter extends CustomPainter {
final Color gridColor ;
FullScreenGridPainter ( this . gridColor ) ;
@ override
void paint ( Canvas canvas , Size size ) {
final Paint paperGridPaint = Paint ( ) . . color = gridColor . . strokeWidth = 1.0 . . style = PaintingStyle . stroke ;
double paperStep = 20.0 ;
for ( double i = 0 ; i < = size . width ; i + = paperStep ) canvas . drawLine ( Offset ( i , 0 ) , Offset ( i , size . height ) , paperGridPaint ) ;
for ( double i = 0 ; i < = size . height ; i + = paperStep ) canvas . drawLine ( Offset ( 0 , i ) , Offset ( size . width , i ) , paperGridPaint ) ;
}
@ override bool shouldRepaint ( covariant CustomPainter oldDelegate ) = > false ;
}