POLISHING STUFF and Smarter AI, and alot i mean.. ALOT OF BUG FIX AND OPTIMIZE

This commit is contained in:
Wowieee4
2026-05-29 15:50:51 +07:00
parent 1110e5a42d
commit d2b3534dde
65 changed files with 4150 additions and 2974 deletions
@@ -0,0 +1,3 @@
export 'bounce_tap.dart';
export 'fade_slide_wrapper.dart';
export 'stagger_wrapper.dart';
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
class BounceTap extends StatefulWidget {
final Widget child;
final VoidCallback? onTap;
final HitTestBehavior behavior;
final Duration pressDuration;
final Duration releaseDuration;
const BounceTap({
super.key,
required this.child,
this.onTap,
this.behavior = HitTestBehavior.opaque,
this.pressDuration = const Duration(milliseconds: 80),
this.releaseDuration = const Duration(milliseconds: 120),
});
@override
State<BounceTap> createState() => _BounceTapState();
}
class _BounceTapState extends State<BounceTap>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _scale;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
value: 1,
lowerBound: 0.94,
upperBound: 1,
duration: widget.pressDuration,
reverseDuration: widget.releaseDuration,
);
_scale = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
reverseCurve: Curves.elasticOut,
);
}
void _press() {
if (widget.onTap == null) return;
_controller.animateTo(
0.94,
duration: widget.pressDuration,
curve: Curves.easeOut,
);
}
void _release({bool triggerTap = false}) {
if (widget.onTap == null) return;
_controller.animateTo(
1,
duration: widget.releaseDuration,
curve: Curves.elasticOut,
);
if (triggerTap) {
widget.onTap?.call();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Listener(
behavior: widget.behavior,
onPointerDown: (_) => _press(),
onPointerCancel: (_) => _release(),
onPointerUp: (_) => _release(triggerTap: true),
child: AnimatedBuilder(
animation: _scale,
child: widget.child,
builder: (context, child) => Transform.scale(
scale: _scale.value,
child: child,
),
),
);
}
}
@@ -0,0 +1,75 @@
import 'dart:async';
import 'package:flutter/material.dart';
class FadeSlideWrapper extends StatefulWidget {
final Widget child;
final Duration delay;
final Duration duration;
const FadeSlideWrapper({
super.key,
required this.child,
this.delay = Duration.zero,
this.duration = const Duration(milliseconds: 400),
});
@override
State<FadeSlideWrapper> createState() => _FadeSlideWrapperState();
}
class _FadeSlideWrapperState extends State<FadeSlideWrapper>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _opacity;
late final Animation<Offset> _offset;
Timer? _delayTimer;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
final curved = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
);
_opacity = Tween<double>(begin: 0, end: 1).animate(curved);
_offset = Tween<Offset>(
begin: const Offset(0, 30),
end: Offset.zero,
).animate(curved);
if (widget.delay == Duration.zero) {
_controller.forward();
} else {
_delayTimer = Timer(widget.delay, () {
if (mounted) _controller.forward();
});
}
}
@override
void dispose() {
_delayTimer?.cancel();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: widget.child,
builder: (context, child) => Opacity(
opacity: _opacity.value,
child: Transform.translate(
offset: _offset.value,
child: child,
),
),
);
}
}
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
class StaggerWrapper extends StatefulWidget {
final List<Widget> children;
final Axis direction;
final Duration duration;
final Duration stagger;
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
final MainAxisSize mainAxisSize;
final TextDirection? textDirection;
final VerticalDirection verticalDirection;
final TextBaseline? textBaseline;
const StaggerWrapper({
super.key,
required this.children,
this.direction = Axis.vertical,
this.duration = const Duration(milliseconds: 400),
this.stagger = const Duration(milliseconds: 60),
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.center,
this.mainAxisSize = MainAxisSize.max,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
this.textBaseline,
});
@override
State<StaggerWrapper> createState() => _StaggerWrapperState();
}
class _StaggerWrapperState extends State<StaggerWrapper>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = _createController()..forward();
}
@override
void didUpdateWidget(covariant StaggerWrapper oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.children.length != widget.children.length ||
oldWidget.duration != widget.duration ||
oldWidget.stagger != widget.stagger) {
_controller.dispose();
_controller = _createController()..forward();
}
}
AnimationController _createController() {
final totalDuration = widget.duration +
widget.stagger *
(widget.children.isEmpty ? 0 : widget.children.length - 1);
return AnimationController(vsync: this, duration: totalDuration);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final animatedChildren = List<Widget>.generate(
widget.children.length,
(index) => _StaggeredChild(
controller: _controller,
duration: widget.duration,
stagger: widget.stagger,
index: index,
child: widget.children[index],
),
);
if (widget.direction == Axis.horizontal) {
return Row(
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: widget.crossAxisAlignment,
mainAxisSize: widget.mainAxisSize,
textDirection: widget.textDirection,
verticalDirection: widget.verticalDirection,
textBaseline: widget.textBaseline,
children: animatedChildren,
);
}
return Column(
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: widget.crossAxisAlignment,
mainAxisSize: widget.mainAxisSize,
textDirection: widget.textDirection,
verticalDirection: widget.verticalDirection,
textBaseline: widget.textBaseline,
children: animatedChildren,
);
}
}
class _StaggeredChild extends StatelessWidget {
final AnimationController controller;
final Duration duration;
final Duration stagger;
final int index;
final Widget child;
const _StaggeredChild({
required this.controller,
required this.duration,
required this.stagger,
required this.index,
required this.child,
});
@override
Widget build(BuildContext context) {
final totalMs =
controller.duration?.inMilliseconds ?? duration.inMilliseconds;
final beginMs = stagger.inMilliseconds * index;
final endMs = beginMs + duration.inMilliseconds;
final begin = totalMs <= 0 ? 0.0 : (beginMs / totalMs).clamp(0.0, 1.0);
final end = totalMs <= 0 ? 1.0 : (endMs / totalMs).clamp(begin, 1.0);
final curve = CurvedAnimation(
parent: controller,
curve: Interval(begin, end, curve: Curves.easeOutCubic),
);
final opacity = Tween<double>(begin: 0, end: 1).animate(curve);
final offset = Tween<Offset>(
begin: const Offset(0, 30),
end: Offset.zero,
).animate(curve);
return AnimatedBuilder(
animation: controller,
child: child,
builder: (context, child) => Opacity(
opacity: opacity.value,
child: Transform.translate(
offset: offset.value,
child: child,
),
),
);
}
}
@@ -1,5 +1,7 @@
// ignore_for_file: prefer_const_constructors, sort_child_properties_last
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -11,6 +13,8 @@ import '../../core/services/stt_service.dart';
import '../../core/services/tts_service.dart';
import '../../core/services/voice_command_handler.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import 'animations/animations.dart';
class UserShell extends StatefulWidget {
final Widget child;
@@ -26,9 +30,6 @@ class _UserShellState extends State<UserShell> {
super.initState();
_loadVoiceCommands();
_startHardwareShortcuts();
WidgetsBinding.instance.addPostFrameCallback((_) {
_startVoiceListening();
});
sl<VoiceCommandHandler>().onCommand = (key) {
if (!mounted) return;
switch (key) {
@@ -66,17 +67,6 @@ class _UserShellState extends State<UserShell> {
};
}
Future<void> _startVoiceListening() async {
await runFriendlyAction(
() async {
await sl<SttService>().init();
await sl<SttService>().startListening();
},
onError: (_) {},
fallback: 'Voice listener belum bisa dimuat.',
);
}
Future<void> _loadVoiceCommands() async {
await runFriendlyAction(
() async {
@@ -139,6 +129,7 @@ class _UserShellState extends State<UserShell> {
@override
void dispose() {
sl<HardwareShortcutListener>().stopListening();
unawaited(sl<SttService>().stopListening());
super.dispose();
}
@@ -198,15 +189,7 @@ class _AppShell extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
final useRail = constraints.maxWidth >= 760;
final content = AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
child: KeyedSubtree(
key: ValueKey(location),
child: child,
),
);
final content = child;
return Scaffold(
backgroundColor: AppColors.surface,
@@ -257,71 +240,40 @@ class _RailNavigation extends StatelessWidget {
final itemHeight = compact ? 58.0 : 70.0;
return DecoratedBox(
decoration: const BoxDecoration(color: Colors.white),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Color(0x14000000),
blurRadius: 18,
offset: Offset(6, 0),
),
],
),
child: SafeArea(
right: false,
child: SizedBox(
width: width,
child: ListView.separated(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: compact ? 6 : 12,
),
itemCount: items.length,
separatorBuilder: (_, __) => SizedBox(height: compact ? 2 : 6),
itemBuilder: (context, index) {
final item = items[index];
final selected = index == selectedIndex;
return Semantics(
button: true,
selected: selected,
label: item.label,
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () => context.go(items[index].route),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
height: itemHeight,
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: selected
? const Color(0xFFEFF6FF)
: Colors.transparent,
borderRadius: BorderRadius.circular(18),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
selected ? item.selectedIcon : item.icon,
size: compact ? 23 : 25,
color: selected
? AppColors.primary
: const Color(0xFF334155),
),
SizedBox(height: compact ? 2 : 5),
Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: compact ? 10 : 12,
height: 1,
fontWeight: selected
? FontWeight.w800
: FontWeight.w600,
color: selected
? const Color(0xFF1D4ED8)
: const Color(0xFF334155),
),
),
],
child: Column(
children: [
for (var index = 0; index < items.length; index++)
Expanded(
child: Center(
child: _RailNavItem(
item: items[index],
selected: index == selectedIndex,
compact: compact,
height: itemHeight,
),
),
),
),
);
},
],
),
),
),
),
@@ -344,82 +296,154 @@ class _BottomScrollNavigation extends StatelessWidget {
Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).padding.bottom;
final extraBottom = bottom > 12 ? 12.0 : bottom;
return DecoratedBox(
return Container(
margin: EdgeInsets.zero,
decoration: BoxDecoration(
color: Colors.white,
border: const Border(top: BorderSide(color: AppColors.border)),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 18,
offset: const Offset(0, -8),
),
AppDecorations.cardShadow.first,
],
),
child: SafeArea(
top: false,
child: SizedBox(
height: 68 + extraBottom,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 6),
itemBuilder: (context, index) {
final item = items[index];
final selected = index == selectedIndex;
return Semantics(
button: true,
selected: selected,
label: item.label,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => context.go(item.route),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
width: 72,
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: selected
? const Color(0xFFEFF6FF)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: selected
? const Color(0xFFBFDBFE)
: Colors.transparent,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
selected ? item.selectedIcon : item.icon,
color: selected
? AppColors.primary
: const Color(0xFF64748B),
size: 22,
),
const SizedBox(height: 4),
Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight:
selected ? FontWeight.w800 : FontWeight.w600,
color: selected
? AppColors.primary
: const Color(0xFF64748B),
),
),
],
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: SafeArea(
top: false,
child: SizedBox(
height: 68 + extraBottom,
child: Row(
children: [
for (var index = 0; index < items.length; index++)
Expanded(
child: _BottomNavItem(
item: items[index],
selected: index == selectedIndex,
),
),
],
),
),
),
),
);
}
}
class _RailNavItem extends StatelessWidget {
final _ShellItem item;
final bool selected;
final bool compact;
final double height;
const _RailNavItem({
required this.item,
required this.selected,
required this.compact,
required this.height,
});
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
selected: selected,
label: item.label,
child: BounceTap(
onTap: () => context.go(item.route),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOutCubic,
height: height,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: selected ? AppColors.softBlueBg : Colors.transparent,
borderRadius: BorderRadius.circular(24),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
selected ? item.selectedIcon : item.icon,
size: compact ? 21 : 24,
color: selected ? AppColors.primary : AppColors.muted,
),
SizedBox(height: compact ? 1 : 4),
Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: compact ? 9.5 : 11.5,
height: 1,
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
color: selected ? AppColors.primary : AppColors.muted,
),
);
},
),
],
),
),
),
);
}
}
class _BottomNavItem extends StatelessWidget {
final _ShellItem item;
final bool selected;
const _BottomNavItem({
required this.item,
required this.selected,
});
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
selected: selected,
label: item.label,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
child: BounceTap(
onTap: () => context.go(item.route),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
gradient: selected ? AppDecorations.blueGradient : null,
borderRadius: BorderRadius.circular(50),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedScale(
scale: selected ? 1.08 : 1.0,
duration: const Duration(milliseconds: 160),
curve: Curves.easeOutCubic,
child: Icon(
selected ? item.selectedIcon : item.icon,
color: selected ? Colors.white : AppColors.muted,
size: 21,
),
),
const SizedBox(height: 3),
Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10,
height: 1,
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
color: selected ? Colors.white : AppColors.muted,
),
),
],
),
),
),
),
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import 'animations/animations.dart';
class FeaturePage extends StatelessWidget {
final String title;
@@ -18,80 +21,79 @@ class FeaturePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final short = constraints.maxHeight < 520;
final compact = constraints.maxWidth < 420 || short;
final wide = constraints.maxWidth >= 900;
final horizontal = compact ? 12.0 : 20.0;
return Padding(
padding: EdgeInsets.fromLTRB(
horizontal,
short ? 8 : 12,
horizontal,
short ? 10 : 14,
),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: wide ? 1160 : double.infinity,
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final short = constraints.maxHeight < 520;
final compact = constraints.maxWidth < 420 || short;
final wide = constraints.maxWidth >= 900;
final horizontal = compact ? 12.0 : 20.0;
return FadeSlideWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(
horizontal,
short ? 8 : 12,
horizontal,
short ? 10 : 14,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 12, end: 0),
duration: const Duration(milliseconds: 360),
curve: Curves.easeOutCubic,
builder: (_, offset, child) => Opacity(
opacity: (1 - offset / 12).clamp(0.0, 1.0),
child: Transform.translate(
offset: Offset(0, offset),
child: child,
),
),
child: compact
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_FeatureHeading(
title: title,
subtitle: subtitle,
compact: compact,
),
if (trailing != null) ...[
const SizedBox(height: 10),
Align(
alignment: Alignment.centerLeft,
child: trailing!,
),
],
],
)
: Row(
children: [
Expanded(
child: _FeatureHeading(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: wide ? 1160 : double.infinity,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
compact
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_FeatureHeading(
title: title,
subtitle: subtitle,
compact: compact,
),
),
if (trailing != null) trailing!,
],
),
if (trailing != null) ...[
const SizedBox(height: 10),
Align(
alignment: Alignment.centerLeft,
child: trailing!,
),
],
],
)
: Row(
children: [
Expanded(
child: _FeatureHeading(
title: title,
subtitle: subtitle,
compact: compact,
),
),
if (trailing != null) trailing!,
],
),
SizedBox(height: short ? 8 : (compact ? 12 : 16)),
Expanded(
child: child,
),
],
),
SizedBox(height: short ? 8 : (compact ? 12 : 16)),
Expanded(
child: child,
),
],
),
),
),
),
);
},
);
},
),
),
);
}
@@ -118,21 +120,18 @@ class _FeatureHeading extends StatelessWidget {
title,
maxLines: short ? 1 : 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontSize: compact ? 22 : null,
fontWeight: FontWeight.w900,
color: AppColors.text,
),
style: AppTextStyles.heading.copyWith(
fontSize: compact ? 22 : null,
),
),
const SizedBox(height: 2),
Text(
subtitle,
maxLines: short ? 1 : (compact ? 2 : 3),
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.muted,
style: AppTextStyles.body.copyWith(
color: AppColors.textMuted,
fontWeight: FontWeight.w500,
height: 1.25,
),
),
],
@@ -157,8 +156,10 @@ class FeatureEmptyPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Container(
constraints: const BoxConstraints(maxWidth: 380),
padding: const EdgeInsets.all(24),
decoration: AppDecorations.card,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -167,7 +168,7 @@ class FeatureEmptyPanel extends StatelessWidget {
height: 72,
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(50),
border: Border.all(color: AppColors.border),
),
child: Icon(icon, size: 36, color: AppColors.primary),
@@ -214,8 +215,9 @@ class FeatureErrorPanel extends StatelessWidget {
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(8),
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFFECACA)),
boxShadow: AppDecorations.cardShadow,
),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -0,0 +1,186 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
class WalkGuideLoadingScreen extends StatefulWidget {
final String subtitle;
const WalkGuideLoadingScreen({
super.key,
required this.subtitle,
});
@override
State<WalkGuideLoadingScreen> createState() => _WalkGuideLoadingScreenState();
}
class _WalkGuideLoadingScreenState extends State<WalkGuideLoadingScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _logoScale;
bool _showName = false;
Timer? _nameTimer;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
_logoScale = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(begin: 1.0, end: 1.08).chain(
CurveTween(curve: Curves.easeInOut),
),
weight: 50,
),
TweenSequenceItem(
tween: Tween(begin: 1.08, end: 1.0).chain(
CurveTween(curve: Curves.easeInOut),
),
weight: 50,
),
]).animate(_controller);
_nameTimer = Timer(const Duration(milliseconds: 300), () {
if (mounted) setState(() => _showName = true);
});
}
@override
void dispose() {
_nameTimer?.cancel();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.softBlueBg,
body: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.softBlueBg, Colors.white],
),
),
child: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 340),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
animation: _logoScale,
child: Container(
width: 92,
height: 92,
decoration: BoxDecoration(
gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Color(0x334A90D9),
blurRadius: 28,
offset: Offset(0, 14),
),
],
),
child: const Icon(
Icons.navigation_rounded,
color: Colors.white,
size: 54,
),
),
builder: (context, child) => Transform.scale(
scale: _logoScale.value,
child: child,
),
),
const SizedBox(height: 24),
AnimatedOpacity(
opacity: _showName ? 1 : 0,
duration: const Duration(milliseconds: 420),
curve: Curves.easeOutCubic,
child: Column(
children: [
Text(
'WalkGuide',
textAlign: TextAlign.center,
style: AppTextStyles.heading.copyWith(
fontSize: 32,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 8),
Text(
widget.subtitle,
textAlign: TextAlign.center,
style: AppTextStyles.body.copyWith(
color: AppColors.textMuted,
fontWeight: FontWeight.w500,
),
),
],
),
),
const SizedBox(height: 34),
_DotWave(controller: _controller),
],
),
),
),
),
),
),
);
}
}
class _DotWave extends StatelessWidget {
final AnimationController controller;
const _DotWave({required this.controller});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
3,
(index) {
final phase = (controller.value + index * 0.18) % 1.0;
final y = math.sin(phase * math.pi * 2) * -5;
final opacity = 0.45 + (math.sin(phase * math.pi * 2) + 1) * 0.25;
return Transform.translate(
offset: Offset(0, y),
child: Container(
width: 9,
height: 9,
margin: const EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: AppColors.primaryBlue.withValues(alpha: opacity),
shape: BoxShape.circle,
),
),
);
},
),
);
},
);
}
}