Initial commit of Doormile CRM Mobile with UI modernizations
This commit is contained in:
709
lib/screens/dashboard_screen.dart
Normal file
709
lib/screens/dashboard_screen.dart
Normal file
@@ -0,0 +1,709 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/client.dart';
|
||||
import '../models/activity.dart';
|
||||
import '../models/app_stats.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'surveys_list_screen.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
final AppStats stats;
|
||||
final List<Activity> activities;
|
||||
final List<Client> clients;
|
||||
final List<dynamic> surveys;
|
||||
final List<dynamic> pricing;
|
||||
final void Function(int) onNavigate;
|
||||
final VoidCallback onDataChanged;
|
||||
final bool backendReady;
|
||||
final VoidCallback onLogout;
|
||||
final bool isActive;
|
||||
|
||||
const DashboardScreen({
|
||||
super.key,
|
||||
required this.stats,
|
||||
required this.activities,
|
||||
required this.clients,
|
||||
this.surveys = const [],
|
||||
this.pricing = const [],
|
||||
required this.onNavigate,
|
||||
required this.onDataChanged,
|
||||
required this.backendReady,
|
||||
required this.onLogout,
|
||||
this.isActive = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DashboardScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.isActive && !oldWidget.isActive) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 100),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
WideTopBanner(
|
||||
backendReady: widget.backendReady,
|
||||
subtitle: 'DASHBOARD',
|
||||
onLogout: widget.onLogout,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_WelcomeHeader(),
|
||||
const SizedBox(height: 20),
|
||||
_BentoGrid(stats: widget.stats),
|
||||
const SizedBox(height: 20),
|
||||
_QuickActions(onNavigate: widget.onNavigate),
|
||||
const SizedBox(height: 20),
|
||||
_MarketInsights(surveys: widget.surveys, pricing: widget.pricing, onNavigate: widget.onNavigate, onDataChanged: widget.onDataChanged),
|
||||
const SizedBox(height: 20),
|
||||
_RecentActivity(
|
||||
activities: widget.activities,
|
||||
clients: widget.clients,
|
||||
onNavigate: widget.onNavigate,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_InsightCard(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WelcomeHeader extends StatelessWidget {
|
||||
static String get _greeting {
|
||||
final h = DateTime.now().hour;
|
||||
if (h < 12) return 'Good Morning';
|
||||
if (h < 17) return 'Good Afternoon';
|
||||
if (h < 21) return 'Good Evening';
|
||||
return 'Good Night';
|
||||
}
|
||||
|
||||
static String get _subtext {
|
||||
final h = DateTime.now().hour;
|
||||
if (h < 12) return 'Start your day with a fresh logistics survey.';
|
||||
if (h < 17) return 'Keep the momentum log your field visits.';
|
||||
if (h < 21) return 'Wrap up the day any pending surveys to log?';
|
||||
return 'Late session active your data is synced.';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_greeting,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.darkBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_subtext,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BentoGrid extends StatelessWidget {
|
||||
final AppStats stats;
|
||||
const _BentoGrid({required this.stats});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Large card spanning full width
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD3E4FE),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFA6C8FF).withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'TOTAL CLIENTS',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
stats.totalClients.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(m) => '${m[1]},',
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.darkBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_upward,
|
||||
size: 14, color: AppColors.greenDark),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'+12% from last month',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.greenDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: -20,
|
||||
bottom: -20,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.primary.withValues(alpha: 0.06),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _SmallStatCard(
|
||||
icon: Icons.calendar_today,
|
||||
iconColor: AppColors.primary,
|
||||
value: stats.todayEntries.toString(),
|
||||
label: "Today's Entries",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _SmallStatCard(
|
||||
icon: Icons.access_time,
|
||||
iconColor: const Color(0xFF004D47),
|
||||
value: stats.pendingFollowups.toString(),
|
||||
label: 'Pending Follow-ups',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SmallStatCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String value;
|
||||
final String label;
|
||||
|
||||
const _SmallStatCard({
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.value,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border.withValues(alpha: 0.35)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 24, color: iconColor),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.darkBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickActions extends StatelessWidget {
|
||||
final void Function(int) onNavigate;
|
||||
const _QuickActions({required this.onNavigate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'QUICK ACTIONS',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => onNavigate(4),
|
||||
icon: const Icon(Icons.add, size: 18, color: Colors.white),
|
||||
label: const Text(
|
||||
'Quick Add Client',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
elevation: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => onNavigate(1),
|
||||
icon: Icon(Icons.search, size: 18, color: AppColors.darkBlue),
|
||||
label: Text(
|
||||
'Search Client Database',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.darkBlue,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFD3E4FE),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: const BorderSide(color: Color(0xFFA6C8FF)),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MarketInsights extends StatelessWidget {
|
||||
final List<dynamic> surveys;
|
||||
final List<dynamic> pricing;
|
||||
final void Function(int) onNavigate;
|
||||
final VoidCallback onDataChanged;
|
||||
|
||||
const _MarketInsights({required this.surveys, required this.pricing, required this.onNavigate, required this.onDataChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'MARKET DATA',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (ctx) => SurveysListScreen(
|
||||
surveys: surveys,
|
||||
onDataChanged: onDataChanged,
|
||||
),
|
||||
));
|
||||
},
|
||||
child: _SmallStatCard(
|
||||
icon: Icons.storefront_outlined,
|
||||
iconColor: const Color(0xFFE87C00),
|
||||
value: surveys.length.toString(),
|
||||
label: 'Competitor Surveys',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onNavigate(3),
|
||||
child: _SmallStatCard(
|
||||
icon: Icons.price_change_outlined,
|
||||
iconColor: const Color(0xFF15803D),
|
||||
value: pricing.length.toString(),
|
||||
label: 'Carrier Pricing',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentActivity extends StatelessWidget {
|
||||
final List<Activity> activities;
|
||||
final List<Client> clients;
|
||||
final void Function(int) onNavigate;
|
||||
|
||||
const _RecentActivity({
|
||||
required this.activities,
|
||||
required this.clients,
|
||||
required this.onNavigate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'RECENT ACTIVITY',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => onNavigate(1),
|
||||
child: Text(
|
||||
'View all',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Column(
|
||||
children: activities
|
||||
.take(5)
|
||||
.map((activity) => _ActivityTile(
|
||||
activity: activity,
|
||||
onTap: () => onNavigate(1),
|
||||
isLast: activity == activities.last,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityTile extends StatelessWidget {
|
||||
final Activity activity;
|
||||
final VoidCallback onTap;
|
||||
final bool isLast;
|
||||
|
||||
const _ActivityTile({
|
||||
required this.activity,
|
||||
required this.onTap,
|
||||
required this.isLast,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color iconBg;
|
||||
Color iconColor;
|
||||
IconData iconData;
|
||||
|
||||
switch (activity.icon) {
|
||||
case ActivityIcon.shipping:
|
||||
iconBg = const Color(0xFFD5E0F8);
|
||||
iconColor = const Color(0xFF1E3A8A);
|
||||
iconData = Icons.local_shipping;
|
||||
break;
|
||||
case ActivityIcon.call:
|
||||
iconBg = const Color(0xFFFFE4E6);
|
||||
iconColor = AppColors.primary;
|
||||
iconData = Icons.phone;
|
||||
break;
|
||||
case ActivityIcon.edit:
|
||||
iconBg = const Color(0xFFDCFCE7);
|
||||
iconColor = const Color(0xFF15803D);
|
||||
iconData = Icons.edit;
|
||||
break;
|
||||
}
|
||||
|
||||
Widget? badge;
|
||||
if (activity.status == ActivityStatus.newStatus) {
|
||||
badge = _Badge(label: 'NEW', bg: const Color(0xFFFFDAD7), fg: const Color(0xFF930013));
|
||||
} else if (activity.status == ActivityStatus.pending) {
|
||||
badge = _Badge(label: 'PENDING', bg: const Color(0xFFD8E3FB), fg: const Color(0xFF3C475A));
|
||||
} else if (activity.status == ActivityStatus.completed) {
|
||||
badge = _Badge(label: 'DONE', bg: const Color(0xFFD8F9EE), fg: const Color(0xFF115E59));
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: isLast
|
||||
? const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
)
|
||||
: null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: isLast
|
||||
? null
|
||||
: Border(
|
||||
bottom: BorderSide(
|
||||
color: AppColors.border.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBg,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(iconData, size: 18, color: iconColor),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.clientName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.darkBlue,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
activity.action,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
?badge,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Badge extends StatelessWidget {
|
||||
final String label;
|
||||
final Color bg;
|
||||
final Color fg;
|
||||
|
||||
const _Badge({required this.label, required this.bg, required this.fg});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: fg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InsightCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 160,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF0B1C30), Color(0xFF8D0012)],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: CustomPaint(painter: _GridPainter()),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.auto_awesome,
|
||||
size: 12, color: const Color(0xFFFFDAD7)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'INSIGHT OF THE WEEK',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
color: const Color(0xFFFFDAD7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'Weekly Performance Report',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Analyze your team's survey conversion rates.",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.04)
|
||||
..strokeWidth = 1;
|
||||
const spacing = 30.0;
|
||||
for (double x = 0; x < size.width; x += spacing) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
|
||||
}
|
||||
for (double y = 0; y < size.height; y += spacing) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_GridPainter oldDelegate) => false;
|
||||
}
|
||||
Reference in New Issue
Block a user