feat(ui): add walk guide, server connect, pairing, auth, and splash screens
This commit is contained in:
+205
-1
@@ -1 +1,205 @@
|
||||
export '../screens.dart';
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../app/injection_container.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ServerConnectScreen
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Gerbang pertama aplikasi.
|
||||
// Muncul HANYA jika SharedPreferences tidak punya serverUrl tersimpan.
|
||||
// Setelah berhasil connect, tidak akan muncul lagi kecuali user reset via
|
||||
// Settings → "Change Server".
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class ServerConnectScreen extends StatefulWidget {
|
||||
const ServerConnectScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ServerConnectScreen> createState() => _ServerConnectScreenState();
|
||||
}
|
||||
|
||||
class _ServerConnectScreenState extends State<ServerConnectScreen> {
|
||||
final _url = TextEditingController(text: 'http://202.46.28.160:8080');
|
||||
bool _loading = false;
|
||||
bool _ok = false;
|
||||
String? _message;
|
||||
|
||||
Future<void> _test() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_ok = false;
|
||||
_message = null;
|
||||
});
|
||||
try {
|
||||
final clean = AppConstants.normalizeServerUrl(_url.text);
|
||||
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 dan siap dipakai.'
|
||||
: 'Server merespons dengan format tidak valid.';
|
||||
} catch (e) {
|
||||
_message = 'Tidak bisa terhubung. Periksa URL dan jaringan.';
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _continue() async {
|
||||
final clean = AppConstants.normalizeServerUrl(_url.text);
|
||||
await AppConstants.setServerUrl(clean);
|
||||
await sl<ApiClient>().init(clean);
|
||||
if (mounted) context.go('/splash');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _AuthFrame(
|
||||
title: 'Connect to Server',
|
||||
subtitle: 'Masukkan URL backend WalkGuide yang diberikan dosen.',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _url,
|
||||
keyboardType: TextInputType.url,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Server URL',
|
||||
hintText: 'http://202.46.28.160:8080',
|
||||
prefixIcon: Icon(Icons.dns_outlined),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _loading ? null : _test,
|
||||
icon: _loading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(Icons.wifi_tethering),
|
||||
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),
|
||||
label: const Text('Continue')),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
const Center(
|
||||
child: Text(
|
||||
'v1.0.0 | For Testing Purposes Only',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared private widgets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _AuthFrame extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget child;
|
||||
const _AuthFrame(
|
||||
{required this.title, required this.subtitle, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 460),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: const BorderSide(color: Color(0xFFE2E8F0))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Icon(Icons.navigation_rounded,
|
||||
color: Color(0xFF1A56DB), size: 42),
|
||||
const SizedBox(height: 14),
|
||||
Text(title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.w800)),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF64748B))),
|
||||
const SizedBox(height: 22),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusBox extends StatelessWidget {
|
||||
final bool success;
|
||||
final String message;
|
||||
const _StatusBox({required this.success, required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: success ? const Color(0xFFF0FDF4) : const Color(0xFFFEF2F2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
success ? Icons.check_circle_outline : Icons.error_outline,
|
||||
color:
|
||||
success ? const Color(0xFF166534) : const Color(0xFF991B1B),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(message,
|
||||
style: TextStyle(
|
||||
color: success
|
||||
? const Color(0xFF166534)
|
||||
: const Color(0xFF991B1B))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user