Update lot features

This commit is contained in:
2026-05-28 16:29:23 +07:00
parent 6272ece15d
commit 1110e5a42d
81 changed files with 4306 additions and 29813 deletions
@@ -10,24 +10,37 @@ import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart';
class ServerConnectScreen extends StatefulWidget {
const ServerConnectScreen({super.key});
final bool editMode;
const ServerConnectScreen({super.key, this.editMode = false});
@override
State<ServerConnectScreen> createState() => _ServerConnectScreenState();
}
class _ServerConnectScreenState extends State<ServerConnectScreen> {
final _url = TextEditingController(text: 'http://127.0.0.1:8080');
final _url = TextEditingController(text: AppConstants.defaultServerUrl);
bool _loading = false;
bool _ok = false;
String? _message;
@override
void initState() {
super.initState();
_loadCurrentUrl();
}
@override
void dispose() {
_url.dispose();
super.dispose();
}
Future<void> _loadCurrentUrl() async {
final current = await AppConstants.getServerUrl();
if (!mounted || current == null || current.isEmpty) return;
setState(() => _url.text = current);
}
Future<void> _test() async {
setState(() {
_loading = true;
@@ -38,10 +51,12 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
() async {
final clean = AppConstants.normalizeServerUrl(_url.text);
await sl<ApiClient>().init(clean);
final res = await Dio(BaseOptions(
connectTimeout: AppConstants.pingTimeout,
receiveTimeout: AppConstants.pingTimeout,
)).get('$clean/api/v1/auth/ping');
final res = await Dio(
BaseOptions(
connectTimeout: AppConstants.pingTimeout,
receiveTimeout: AppConstants.pingTimeout,
),
).get('$clean/api/v1/auth/ping');
_ok = res.statusCode == 200 && res.data['success'] == true;
_message = _ok
? 'Server aktif. WalkGuide siap tersambung.'
@@ -49,7 +64,7 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
},
onError: (message) => _message = message,
fallback:
'Tidak bisa terhubung. Untuk HP via USB, jalankan adb reverse tcp:8080 tcp:8080 lalu pakai http://127.0.0.1:8080.',
'Tidak bisa terhubung ke server publik. Pastikan backend aktif dan jaringan HP stabil.',
);
if (mounted) setState(() => _loading = false);
}
@@ -58,17 +73,16 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
final clean = AppConstants.normalizeServerUrl(_url.text);
await AppConstants.setServerUrl(clean);
await sl<ApiClient>().init(clean);
if (mounted) context.go('/splash');
if (mounted) context.go(widget.editMode ? '/login' : '/splash');
}
void _useUsbUrl() => setState(() => _url.text = 'http://127.0.0.1:8080');
void _useAndroidEmulatorUrl() =>
setState(() => _url.text = 'http://10.0.2.2:8080');
void _usePublicUrl() =>
setState(() => _url.text = AppConstants.defaultServerUrl);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F8FC),
backgroundColor: const Color(0xFF061421),
body: Stack(
children: [
const Positioned.fill(
@@ -79,24 +93,13 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
end: Alignment.bottomRight,
colors: [
Color(0xFF071226),
Color(0xFF123D6B),
Color(0xFFF7FAFC)
Color(0xFF0F3B57),
Color(0xFF0B6B6C),
],
stops: [0, 0.42, 1],
),
),
),
),
const Positioned(
top: -80,
right: -70,
child: _GlowBlob(size: 250, color: Color(0xFF38BDF8)),
),
const Positioned(
bottom: -90,
left: -80,
child: _GlowBlob(size: 260, color: Color(0xFF22C55E)),
),
SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
@@ -109,190 +112,220 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.fromLTRB(
horizontalPadding,
compact ? 10 : 32,
compact ? 12 : 28,
horizontalPadding,
20 + MediaQuery.of(context).viewInsets.bottom,
12 + MediaQuery.of(context).viewInsets.bottom,
),
child: Center(
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: compact ? 380 : 520),
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 18, end: 0),
duration: const Duration(milliseconds: 520),
curve: Curves.easeOutCubic,
builder: (_, y, child) => Opacity(
opacity: (1 - y / 18).clamp(0, 1),
child: Transform.translate(
offset: Offset(0, y), child: child),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight -
MediaQuery.of(context).padding.vertical -
24,
),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: compact ? 420 : 520,
),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96),
borderRadius:
BorderRadius.circular(compact ? 22 : 28),
border: Border.all(
color: Colors.white.withValues(alpha: 0.7)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 34,
offset: const Offset(0, 22),
),
],
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 18, end: 0),
duration: const Duration(milliseconds: 520),
curve: Curves.easeOutCubic,
builder: (_, y, child) => Opacity(
opacity: (1 - y / 18).clamp(0, 1),
child: Transform.translate(
offset: Offset(0, y),
child: child,
),
),
child: ClipRRect(
borderRadius:
BorderRadius.circular(compact ? 22 : 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: EdgeInsets.fromLTRB(
compact ? 14 : 22,
compact ? 14 : 22,
compact ? 14 : 22,
compact ? 14 : 20,
),
decoration: const BoxDecoration(
color: Color(0xFF071226),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: compact ? 38 : 48,
height: compact ? 38 : 48,
decoration: BoxDecoration(
color: const Color(0xFF2563EB),
borderRadius:
BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96),
borderRadius: BorderRadius.circular(
compact ? 22 : 28,
),
border: Border.all(
color: Colors.white.withValues(alpha: 0.7),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 34,
offset: const Offset(0, 22),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
compact ? 22 : 28,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: EdgeInsets.fromLTRB(
compact ? 14 : 22,
compact ? 14 : 22,
compact ? 14 : 22,
compact ? 14 : 20,
),
decoration: const BoxDecoration(
color: Color(0xFF071226),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: compact ? 38 : 48,
height: compact ? 38 : 48,
decoration: BoxDecoration(
color: const Color(0xFF2563EB),
borderRadius:
BorderRadius.circular(16),
),
child: Icon(
Icons.navigation_rounded,
color: Colors.white,
size: compact ? 24 : 28,
),
),
child: Icon(
Icons.navigation_rounded,
color: Colors.white,
size: compact ? 24 : 28,
const SizedBox(width: 12),
Expanded(
child: Text(
'WalkGuide',
style: TextStyle(
color: Colors.white,
fontSize: compact ? 16 : 20,
fontWeight: FontWeight.w900,
),
),
),
],
),
SizedBox(height: compact ? 14 : 18),
Text(
'Connect to Server',
style: TextStyle(
color: Colors.white,
fontSize: compact ? 22 : 30,
fontWeight: FontWeight.w900,
height: 1,
),
),
const SizedBox(height: 8),
Text(
widget.editMode
? 'Ubah alamat backend Spring Boot WalkGuide yang aktif.'
: 'Sambungkan app HP ke backend Spring Boot publik WalkGuide.',
style: TextStyle(
color: Colors.white.withValues(
alpha: 0.72,
),
height: 1.35,
),
),
],
),
),
Padding(
padding: EdgeInsets.all(compact ? 14 : 22),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
TextField(
controller: _url,
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _test(),
decoration: const InputDecoration(
labelText: 'Server URL',
hintText:
AppConstants.defaultServerUrl,
prefixIcon: Icon(
Icons.dns_outlined,
),
),
const SizedBox(width: 12),
Expanded(
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_HintChip(
icon: Icons.public_outlined,
label: 'Server: 202.46.28.170',
onTap: _usePublicUrl,
),
],
),
SizedBox(height: compact ? 12 : 16),
OutlinedButton.icon(
onPressed: _loading ? null : _test,
icon: _loading
? const SizedBox(
width: 18,
height: 18,
child:
CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(
Icons.radar_outlined,
),
label: const Text('Test Connection'),
),
if (_message != null) ...[
const SizedBox(height: 12),
_StatusBox(
success: _ok,
message: _message!,
),
],
if (_ok) ...[
const SizedBox(height: 12),
FilledButton.icon(
onPressed: _continue,
icon: const Icon(
Icons.arrow_forward_rounded,
),
label: Text(widget.editMode
? 'Simpan Server'
: 'Continue'),
),
],
if (widget.editMode) ...[
const SizedBox(height: 8),
TextButton.icon(
onPressed: () =>
context.go('/login'),
icon: const Icon(
Icons.arrow_back_rounded,
),
label: const Text('Kembali Login'),
),
],
if (!compact) ...[
const SizedBox(height: 18),
const Center(
child: Text(
'WalkGuide',
'v1.0.0 | Spring Boot + Flutter',
style: TextStyle(
color: Colors.white,
fontSize: compact ? 16 : 20,
fontWeight: FontWeight.w900,
fontSize: 11,
color: Color(0xFF94A3B8),
),
),
),
],
),
SizedBox(height: compact ? 14 : 18),
Text(
'Connect to Server',
style: TextStyle(
color: Colors.white,
fontSize: compact ? 22 : 30,
fontWeight: FontWeight.w900,
height: 1,
),
),
const SizedBox(height: 8),
Text(
'Sambungkan app HP ke backend Spring Boot yang sedang berjalan di laptop.',
style: TextStyle(
color: Colors.white
.withValues(alpha: 0.72),
height: 1.35,
),
),
],
],
),
),
),
Padding(
padding: EdgeInsets.all(compact ? 14 : 22),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
TextField(
controller: _url,
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _test(),
decoration: const InputDecoration(
labelText: 'Server URL',
hintText: 'http://127.0.0.1:8080',
prefixIcon: Icon(Icons.dns_outlined),
),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_HintChip(
icon: Icons.usb_outlined,
label: 'USB: 127.0.0.1',
onTap: _useUsbUrl,
),
_HintChip(
icon: Icons.phone_android_outlined,
label: 'Emulator: 10.0.2.2',
onTap: _useAndroidEmulatorUrl,
),
const _HintChip(
icon: Icons.wifi_tethering_outlined,
label: 'Wi-Fi: IP laptop',
),
],
),
SizedBox(height: compact ? 12 : 16),
OutlinedButton.icon(
onPressed: _loading ? null : _test,
icon: _loading
? const SizedBox(
width: 18,
height: 18,
child:
CircularProgressIndicator(
strokeWidth: 2),
)
: const Icon(Icons.radar_outlined),
label: const Text('Test Connection'),
),
if (_message != null) ...[
const SizedBox(height: 12),
_StatusBox(
success: _ok, message: _message!),
],
if (_ok) ...[
const SizedBox(height: 12),
FilledButton.icon(
onPressed: _continue,
icon: const Icon(
Icons.arrow_forward_rounded),
label: const Text('Continue'),
),
],
if (!compact) ...[
const SizedBox(height: 18),
const Center(
child: Text(
'v1.0.0 | Spring Boot + Flutter',
style: TextStyle(
fontSize: 11,
color: Color(0xFF94A3B8)),
),
),
],
],
),
),
],
],
),
),
),
),
@@ -309,30 +342,6 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
}
}
class _GlowBlob extends StatelessWidget {
final double size;
final Color color;
const _GlowBlob({required this.size, required this.color});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color.withValues(alpha: 0.18),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.22),
blurRadius: 60,
spreadRadius: 8),
],
),
);
}
}
class _HintChip extends StatelessWidget {
final IconData icon;
final String label;
@@ -388,12 +397,18 @@ class _StatusBox extends StatelessWidget {
),
child: Row(
children: [
Icon(success ? Icons.check_circle_outline : Icons.error_outline,
color: color, size: 20),
Icon(
success ? Icons.check_circle_outline : Icons.error_outline,
color: color,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(message,
style: TextStyle(color: color, fontWeight: FontWeight.w700))),
child: Text(
message,
style: TextStyle(color: color, fontWeight: FontWeight.w700),
),
),
],
),
);