Update UI + Agora Call

This commit is contained in:
2026-05-27 19:36:01 +07:00
parent a629357e8c
commit 3cb32a4d69
42 changed files with 6516 additions and 1285 deletions
@@ -9,16 +9,6 @@ import '../../core/constants/app_constants.dart';
import '../../core/errors/friendly_error.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});
@@ -27,11 +17,17 @@ class ServerConnectScreen extends StatefulWidget {
}
class _ServerConnectScreenState extends State<ServerConnectScreen> {
final _url = TextEditingController();
final _url = TextEditingController(text: 'http://127.0.0.1:8080');
bool _loading = false;
bool _ok = false;
String? _message;
@override
void dispose() {
_url.dispose();
super.dispose();
}
Future<void> _test() async {
setState(() {
_loading = true;
@@ -47,8 +43,8 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
)).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.';
? 'Server aktif. WalkGuide siap tersambung.'
: 'Server merespons, tetapi format ping tidak valid.';
},
onError: (message) => _message = message,
fallback: 'Tidak bisa terhubung. Periksa URL dan jaringan.',
@@ -63,49 +59,219 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
if (mounted) context.go('/splash');
}
void _useUsbUrl() => setState(() => _url.text = 'http://127.0.0.1:8080');
@override
Widget build(BuildContext context) {
return _AuthFrame(
title: 'Connect to Server',
subtitle: 'Masukkan URL backend WalkGuide yang diberikan dosen.',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
return Scaffold(
backgroundColor: const Color(0xFFF5F8FC),
body: Stack(
children: [
TextField(
controller: _url,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'Server URL',
hintText: 'http://server-ip: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'),
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF071226),
Color(0xFF123D6B),
Color(0xFFF7FAFC)
],
stops: [0, 0.42, 1],
),
),
),
),
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)),
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) {
final compact = constraints.maxWidth < 390;
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(20, compact ? 20 : 36, 20, 24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 460),
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: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96),
borderRadius: BorderRadius.circular(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(28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding:
const EdgeInsets.fromLTRB(22, 22, 22, 20),
decoration: const BoxDecoration(
color: Color(0xFF071226),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF2563EB),
borderRadius:
BorderRadius.circular(16),
),
child: const Icon(
Icons.navigation_rounded,
color: Colors.white,
size: 28),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'WalkGuide Link',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w900,
),
),
),
],
),
const SizedBox(height: 18),
const Text(
'Connect to Server',
style: TextStyle(
color: Colors.white,
fontSize: 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: const EdgeInsets.all(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,
),
const _HintChip(
icon: Icons.wifi_tethering_outlined,
label: 'Wi-Fi: IP laptop',
),
],
),
const SizedBox(height: 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'),
),
],
const SizedBox(height: 18),
const Center(
child: Text(
'v1.0.0 | Spring Boot + Flutter',
style: TextStyle(
fontSize: 11,
color: Color(0xFF94A3B8)),
),
),
],
),
),
],
),
),
),
),
),
),
);
},
),
),
],
@@ -114,55 +280,62 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
}
}
// ---------------------------------------------------------------------------
// 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});
class _GlowBlob extends StatelessWidget {
final double size;
final Color color;
const _GlowBlob({required this.size, required this.color});
@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,
],
),
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;
final VoidCallback? onTap;
const _HintChip({required this.icon, required this.label, this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(999),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0xFFBFDBFE)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: const Color(0xFF1D4ED8)),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
color: Color(0xFF1D4ED8),
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
),
],
),
),
);
@@ -176,31 +349,23 @@ class _StatusBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DecoratedBox(
final color = success ? const Color(0xFF16A34A) : const Color(0xFFDC2626);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: success ? const Color(0xFFF0FDF4) : const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(10),
color: color.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.22)),
),
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: Row(
children: [
Icon(success ? Icons.check_circle_outline : Icons.error_outline,
color: color, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(message,
style: TextStyle(
color: success
? const Color(0xFF166534)
: const Color(0xFF991B1B))),
),
],
),
style: TextStyle(color: color, fontWeight: FontWeight.w700))),
],
),
);
}