467 lines
No EOL
22 KiB
Dart
467 lines
No EOL
22 KiB
Dart
// ===========================================================================
|
|
// 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) {
|
|
if (level < 5) return "Principiante";
|
|
if (level < 10) return "Sfidante";
|
|
if (level < 15) return "Maestro dei Quadrati";
|
|
return "Leggenda del Neon";
|
|
}
|
|
|
|
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.\nL'operazione è irreversibile.\n\nVuoi 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;
|
|
} |