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

View File

@ -36,11 +36,14 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override @Override
public void registerStompEndpoints(StompEndpointRegistry registry) { public void registerStompEndpoints(StompEndpointRegistry registry) {
// Endpoint WebSocket utama // Endpoint WebSocket utama untuk Flutter/stomp_dart_client.
// Flutter connect ke: ws://host:port/ws (tanpa SockJS) // Flutter connect ke: ws://host:port/ws (tanpa SockJS)
// Browser/Postman bisa pakai SockJS fallback: http://host:port/ws
registry.addEndpoint("/ws") registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*") // Allow semua origin untuk testing HP di LAN .setAllowedOriginPatterns("*"); // Allow semua origin untuk testing HP di LAN
.withSockJS(); // SockJS fallback untuk browser compatibility
// Endpoint fallback SockJS untuk browser tooling bila dibutuhkan.
registry.addEndpoint("/ws-sockjs")
.setAllowedOriginPatterns("*")
.withSockJS();
} }
} }

View File

@ -17,6 +17,7 @@
android:label="WalkGuide" android:label="WalkGuide"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen --> <!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" /> <item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here --> <!-- You can insert your own image assets here -->
<!-- <item> <!-- <item>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on --> <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
@ -12,7 +12,7 @@
running. running.
This Theme is only used starting with V2 of Flutter's Android embedding. --> This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item> <item name="android:windowBackground">#F8FAFC</item>
</style> </style>
</resources> </resources>

View File

@ -1,19 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_cubit.dart'; import 'app_cubit.dart';
import 'router.dart'; import 'router.dart';
import '../core/i18n/app_strings.dart'; import '../core/i18n/app_strings.dart';
import '../core/theme/app_colors.dart'; import '../core/theme/app_colors.dart';
import '../core/theme/app_decorations.dart';
import '../core/theme/app_text_styles.dart';
class WalkGuideApp extends StatelessWidget { class WalkGuideApp extends StatelessWidget {
const WalkGuideApp({super.key}); const WalkGuideApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const seed = AppColors.primary; const seed = AppColors.primaryBlue;
return BlocProvider( return BlocProvider(
create: (_) => AppCubit(), create: (_) => AppCubit(),
@ -55,7 +56,7 @@ class WalkGuideApp extends StatelessWidget {
error: AppColors.danger, error: AppColors.danger,
), ),
scaffoldBackgroundColor: AppColors.surface, scaffoldBackgroundColor: AppColors.surface,
textTheme: GoogleFonts.interTextTheme().apply( textTheme: AppTextStyles.textTheme.apply(
bodyColor: AppColors.text, bodyColor: AppColors.text,
displayColor: AppColors.text, displayColor: AppColors.text,
), ),
@ -73,14 +74,12 @@ class WalkGuideApp extends StatelessWidget {
elevation: 0, elevation: 0,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
), ),
cardTheme: CardThemeData( cardTheme: const CardThemeData(
elevation: 0, elevation: 0,
color: AppColors.surfaceRaised, color: AppColors.surfaceRaised,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder( margin: EdgeInsets.zero,
borderRadius: BorderRadius.circular(8), shape: AppDecorations.cardShape,
side: const BorderSide(color: AppColors.border),
),
), ),
dividerTheme: const DividerThemeData( dividerTheme: const DividerThemeData(
color: AppColors.border, color: AppColors.border,
@ -92,7 +91,7 @@ class WalkGuideApp extends StatelessWidget {
foregroundColor: AppColors.text, foregroundColor: AppColors.text,
backgroundColor: Colors.white, backgroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(14),
side: const BorderSide(color: AppColors.border), side: const BorderSide(color: AppColors.border),
), ),
), ),
@ -101,7 +100,7 @@ class WalkGuideApp extends StatelessWidget {
elevation: 0, elevation: 0,
height: 76, height: 76,
backgroundColor: Colors.white, backgroundColor: Colors.white,
indicatorColor: const Color(0xFFDDEAFE), indicatorColor: AppColors.softBlueBg,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
labelTextStyle: WidgetStateProperty.resolveWith( labelTextStyle: WidgetStateProperty.resolveWith(
(states) => TextStyle( (states) => TextStyle(
@ -117,9 +116,12 @@ class WalkGuideApp extends StatelessWidget {
backgroundColor: seed, backgroundColor: seed,
foregroundColor: Colors.white, foregroundColor: Colors.white,
minimumSize: const Size(0, 50), minimumSize: const Size(0, 50),
textStyle: const TextStyle(fontWeight: FontWeight.w800), textStyle: AppTextStyles.body.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(50),
), ),
), ),
), ),
@ -127,22 +129,25 @@ class WalkGuideApp extends StatelessWidget {
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 50), minimumSize: const Size(0, 50),
foregroundColor: seed, foregroundColor: seed,
textStyle: const TextStyle(fontWeight: FontWeight.w800), textStyle: AppTextStyles.body.copyWith(
color: seed,
fontWeight: FontWeight.w700,
),
side: const BorderSide(color: AppColors.border), side: const BorderSide(color: AppColors.border),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(50),
), ),
), ),
), ),
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
backgroundColor: AppColors.text, backgroundColor: AppColors.text,
contentTextStyle: GoogleFonts.inter( contentTextStyle: AppTextStyles.body.copyWith(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(14),
), ),
), ),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
@ -151,15 +156,15 @@ class WalkGuideApp extends StatelessWidget {
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 16), const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: AppColors.border), borderSide: const BorderSide(color: AppColors.border),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: AppColors.border), borderSide: const BorderSide(color: AppColors.border),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: seed, width: 1.5), borderSide: const BorderSide(color: seed, width: 1.5),
), ),
), ),

View File

@ -17,7 +17,6 @@ import '../core/services/voice_command_handler.dart';
import '../core/services/websocket_service.dart'; import '../core/services/websocket_service.dart';
import '../core/storage/local_database.dart'; import '../core/storage/local_database.dart';
import '../core/storage/secure_storage.dart'; import '../core/storage/secure_storage.dart';
import '../core/utils/init_guard.dart';
import '../features/notifications/application/notification_cubit.dart'; import '../features/notifications/application/notification_cubit.dart';
import '../features/notifications/data/repositories/notification_repository_impl.dart'; import '../features/notifications/data/repositories/notification_repository_impl.dart';
import '../features/notifications/domain/repositories/notification_repository.dart'; import '../features/notifications/domain/repositories/notification_repository.dart';
@ -82,6 +81,5 @@ Future<void> initDependencies() async {
await sl<ApiClient>().init(serverUrl); await sl<ApiClient>().init(serverUrl);
} }
await ignoreInitFailure(() => sl<TtsService>().init(), label: 'TTS init');
sl<VoiceCommandHandler>().loadDefaultCommands(); sl<VoiceCommandHandler>().loadDefaultCommands();
} }

View File

@ -25,6 +25,7 @@ import '../features/guardian_dashboard/presentation/screens/guardian_tools_scree
as guardian_tools; as guardian_tools;
import '../features/home/presentation/guardian_dashboard_screen.dart' import '../features/home/presentation/guardian_dashboard_screen.dart'
as guardian_home; as guardian_home;
import '../features/manual/manual_screen.dart' as manual;
import '../features/navigation_mode/presentation/screens/navigation_mode_screen.dart' import '../features/navigation_mode/presentation/screens/navigation_mode_screen.dart'
as nav; as nav;
import '../features/notifications/presentation/screens/notification_screen.dart' import '../features/notifications/presentation/screens/notification_screen.dart'
@ -41,7 +42,7 @@ import '../features/walk_guide/presentation/screens/walk_guide_screen.dart'
import '../shared/widgets/app_shells.dart'; import '../shared/widgets/app_shells.dart';
final GoRouter appRouter = GoRouter( final GoRouter appRouter = GoRouter(
initialLocation: '/splash', initialLocation: '/server-connect',
redirect: (context, state) async { redirect: (context, state) async {
final path = state.matchedLocation; final path = state.matchedLocation;
final serverUrl = await AppConstants.getServerUrl(); final serverUrl = await AppConstants.getServerUrl();
@ -141,6 +142,9 @@ final GoRouter appRouter = GoRouter(
GoRoute( GoRoute(
path: '/user/benchmark', path: '/user/benchmark',
builder: (_, __) => const benchmark.AiBenchmarkScreen()), builder: (_, __) => const benchmark.AiBenchmarkScreen()),
GoRoute(
path: '/user/manual',
builder: (_, __) => const manual.ManualScreen()),
], ],
), ),
ShellRoute( ShellRoute(

View File

@ -586,14 +586,31 @@ const Set<String> _walkGuideObstacleLabels = {
'bicycle', 'bicycle',
'car', 'car',
'motorcycle', 'motorcycle',
'truck',
'bus', 'bus',
'train', 'train',
'truck', 'boat',
'traffic light', 'traffic light',
'fire hydrant', 'fire hydrant',
'stop sign', 'stop sign',
'parking meter', 'parking meter',
'bench', 'bench',
'stairs',
'stair',
'pothole',
'curb',
'pole',
'bollard',
'cone',
'road cone',
'barrier',
'fence',
'door',
'trash can',
'signboard',
'crosswalk',
'sidewalk',
'wall',
'backpack', 'backpack',
'umbrella', 'umbrella',
'handbag', 'handbag',
@ -608,6 +625,7 @@ const Set<String> _walkGuideObstacleLabels = {
'bottle', 'bottle',
'cup', 'cup',
'book', 'book',
'object',
}; };
const Map<int, String> _cocoObstacleLabels = { const Map<int, String> _cocoObstacleLabels = {

View File

@ -1,7 +1,8 @@
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class AppConstants { class AppConstants {
static const String defaultServerUrl = 'http://202.46.28.170:8080'; static const String defaultServerUrl = 'http://127.0.0.1:8080';
static const String publicServerUrl = 'http://202.46.28.170:8080';
static const String _serverUrlKey = 'server_base_url'; static const String _serverUrlKey = 'server_base_url';
static const String _selectedYoloModelKey = 'selected_yolo_model'; static const String _selectedYoloModelKey = 'selected_yolo_model';
@ -10,7 +11,7 @@ class AppConstants {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_serverUrlKey); final saved = prefs.getString(_serverUrlKey);
if (saved == null || saved.trim().isEmpty) { if (saved == null || saved.trim().isEmpty) {
return defaultServerUrl; return null;
} }
return saved; return saved;
} }
@ -38,7 +39,7 @@ class AppConstants {
static Future<void> clearServerUrl() async { static Future<void> clearServerUrl() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString(_serverUrlKey, defaultServerUrl); await prefs.remove(_serverUrlKey);
} }
static String buildApiUrl(String baseUrl) => '$baseUrl/api/v1'; static String buildApiUrl(String baseUrl) => '$baseUrl/api/v1';

View File

@ -61,7 +61,11 @@ class _AuthInterceptor extends Interceptor {
@override @override
void onError(DioException err, ErrorInterceptorHandler handler) async { void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 && !_refreshing) { final status = err.response?.statusCode;
final canRefresh = (status == 401 || status == 403) &&
!_refreshing &&
!err.requestOptions.path.startsWith('/auth/');
if (canRefresh) {
_refreshing = true; _refreshing = true;
try { try {
final refresh = await _storage.getRefreshToken(); final refresh = await _storage.getRefreshToken();
@ -87,15 +91,21 @@ class _AuthInterceptor extends Interceptor {
// Retry original request // Retry original request
err.requestOptions.headers['Authorization'] = err.requestOptions.headers['Authorization'] =
'Bearer ${data['accessToken']}'; 'Bearer ${data['accessToken']}';
try {
final retryRes = await _dio.fetch(err.requestOptions); final retryRes = await _dio.fetch(err.requestOptions);
_refreshing = false; _refreshing = false;
handler.resolve(retryRes); handler.resolve(retryRes);
} on DioException catch (retryErr) {
_refreshing = false;
handler.next(retryErr);
}
return; return;
} }
} catch (_) {} } catch (_) {
_refreshing = false;
await _storage.clearAll(); await _storage.clearAll();
} }
_refreshing = false;
}
handler.next(err); handler.next(err);
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@ -7,6 +8,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../../app/router.dart'; import '../../app/router.dart';
import '../network/api_client.dart'; import '../network/api_client.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
class FcmService { class FcmService {
final ApiClient _apiClient; final ApiClient _apiClient;
final FlutterLocalNotificationsPlugin _localNotifications = final FlutterLocalNotificationsPlugin _localNotifications =
@ -17,6 +23,9 @@ class FcmService {
Future<void> init() async { Future<void> init() async {
if (kIsWeb) return; if (kIsWeb) return;
try { try {
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler);
final messaging = FirebaseMessaging.instance; final messaging = FirebaseMessaging.instance;
await _localNotifications.initialize( await _localNotifications.initialize(
const InitializationSettings( const InitializationSettings(

View File

@ -1,36 +1,107 @@
import 'package:flutter/services.dart';
import 'package:vibration/vibration.dart'; import 'package:vibration/vibration.dart';
class HapticService { class HapticService {
Future<bool> get _hasVibrator async => Vibration.hasVibrator(); bool _enabled = true;
DateTime _lastObstacleVibrationAt = DateTime.fromMillisecondsSinceEpoch(0);
static const _obstacleCooldown = Duration(seconds: 3);
bool get enabled => _enabled;
void setEnabled(bool enabled) {
_enabled = enabled;
if (!enabled) {
Vibration.cancel().ignore();
}
}
Future<bool> get _hasVibrator async {
try {
final hasVibrator = await Vibration.hasVibrator();
return hasVibrator == true;
} catch (_) {
return false;
}
}
bool _canRunObstacleVibration() {
final now = DateTime.now();
if (now.difference(_lastObstacleVibrationAt) < _obstacleCooldown) {
return false;
}
_lastObstacleVibrationAt = now;
return true;
}
Future<void> _vibrate({
int? duration,
List<int>? pattern,
required Future<void> Function() fallback,
bool obstacle = false,
}) async {
if (!_enabled) return;
if (obstacle && !_canRunObstacleVibration()) return;
try {
if (await _hasVibrator) {
if (pattern != null) {
await Vibration.vibrate(pattern: pattern);
} else if (duration != null) {
await Vibration.vibrate(duration: duration);
}
return;
}
} catch (_) {
// Use Flutter's platform haptics below when the vibration plugin fails.
}
await fallback();
}
Future<void> obstacleVeryClose() async { Future<void> obstacleVeryClose() async {
if (!await _hasVibrator) return; await _vibrate(
Vibration.vibrate(pattern: [0, 500, 100, 500, 100, 500]); pattern: [0, 500, 100, 500, 100, 500],
fallback: HapticFeedback.heavyImpact,
obstacle: true,
);
} }
Future<void> obstacleClose() async { Future<void> obstacleClose() async {
if (!await _hasVibrator) return; await _vibrate(
Vibration.vibrate(pattern: [0, 300, 100, 300]); pattern: [0, 300, 100, 300],
fallback: HapticFeedback.mediumImpact,
obstacle: true,
);
} }
Future<void> obstacleMedium() async { Future<void> obstacleMedium() async {
if (!await _hasVibrator) return; await _vibrate(
Vibration.vibrate(duration: 150); duration: 150,
fallback: HapticFeedback.lightImpact,
obstacle: true,
);
} }
Future<void> sosTriggered() async { Future<void> sosTriggered() async {
if (!await _hasVibrator) return; await _vibrate(
Vibration.vibrate(pattern: [0, 1000, 200, 1000, 200, 1000]); pattern: [0, 1000, 200, 1000, 200, 1000],
fallback: HapticFeedback.heavyImpact,
);
} }
Future<void> callIncoming() async { Future<void> callIncoming() async {
if (!await _hasVibrator) return; await _vibrate(
Vibration.vibrate(pattern: [0, 500, 500, 500, 500, 500, 500, 500]); pattern: [0, 500, 500, 500, 500, 500, 500, 500],
fallback: HapticFeedback.mediumImpact,
);
} }
Future<void> success() async { Future<void> success() async {
if (!await _hasVibrator) return; await _vibrate(
Vibration.vibrate(duration: 80); duration: 80,
fallback: HapticFeedback.selectionClick,
);
} }
Future<void> stop() async => Vibration.cancel(); Future<void> stop() async => Vibration.cancel();

View File

@ -27,11 +27,14 @@ class HardwareShortcutBinding {
class HardwareShortcutListener { class HardwareShortcutListener {
final ApiClient _apiClient; final ApiClient _apiClient;
final Map<int, HardwareShortcutBinding> _bindings = {}; final Map<int, HardwareShortcutBinding> _bindings = {};
final Map<int, DateTime> _lastHandledAt = {};
bool _listening = false; bool _listening = false;
void Function(HardwareShortcutAction action)? _onAction; void Function(HardwareShortcutAction action)? _onAction;
void Function(int buttonCode, String buttonName)? _captureCallback; void Function(int buttonCode, String buttonName)? _captureCallback;
static const Duration _repeatDebounce = Duration(milliseconds: 900);
HardwareShortcutListener(this._apiClient); HardwareShortcutListener(this._apiClient);
Future<void> startListening({ Future<void> startListening({
@ -68,7 +71,8 @@ class HardwareShortcutListener {
); );
} }
void captureNextButton(void Function(int buttonCode, String buttonName) onCapture) { void captureNextButton(
void Function(int buttonCode, String buttonName) onCapture) {
_captureCallback = onCapture; _captureCallback = onCapture;
} }
@ -88,6 +92,12 @@ class HardwareShortcutListener {
final binding = _bindings[code]; final binding = _bindings[code];
if (binding == null || !binding.enabled) return false; if (binding == null || !binding.enabled) return false;
final now = DateTime.now();
final lastHandled = _lastHandledAt[code];
if (lastHandled != null && now.difference(lastHandled) < _repeatDebounce) {
return true;
}
_lastHandledAt[code] = now;
_onAction?.call(binding.action); _onAction?.call(binding.action);
return true; return true;
} }
@ -103,7 +113,8 @@ HardwareShortcutBinding? _bindingFromJson(Map<String, dynamic> item) {
final action = _actionFromBackend(item['shortcutKey']?.toString()); final action = _actionFromBackend(item['shortcutKey']?.toString());
final rawCode = item['buttonCode']; final rawCode = item['buttonCode'];
final enabled = item['enabled'] != false; final enabled = item['enabled'] != false;
final code = rawCode is int ? rawCode : int.tryParse(rawCode?.toString() ?? ''); final code =
rawCode is int ? rawCode : int.tryParse(rawCode?.toString() ?? '');
if (action == null || code == null || code <= 0) return null; if (action == null || code == null || code <= 0) return null;
return HardwareShortcutBinding( return HardwareShortcutBinding(
action: action, action: action,

View File

@ -6,17 +6,24 @@ class SttService {
final SpeechToText _stt = SpeechToText(); final SpeechToText _stt = SpeechToText();
bool _available = false; bool _available = false;
bool _listening = false; bool _listening = false;
bool _shouldListen = false;
bool _initializing = false;
Function(String)? onResult; Function(String)? onResult;
Future<bool> init() async { Future<bool> init() async {
if (_available) return true;
if (_initializing) return _available;
_initializing = true;
_available = await _stt.initialize( _available = await _stt.initialize(
onError: (e) => _onError(e), onError: (e) => _onError(e),
onStatus: (s) => _onStatus(s), onStatus: (s) => _onStatus(s),
); );
_initializing = false;
return _available; return _available;
} }
Future<void> startListening() async { Future<void> startListening() async {
_shouldListen = true;
if (!_available || _listening) return; if (!_available || _listening) return;
_listening = true; _listening = true;
await _stt.listen( await _stt.listen(
@ -25,14 +32,15 @@ class SttService {
onResult?.call(result.recognizedWords.toLowerCase().trim()); onResult?.call(result.recognizedWords.toLowerCase().trim());
} }
}, },
listenFor: const Duration(seconds: 10), listenFor: const Duration(seconds: 60),
pauseFor: const Duration(seconds: 3), pauseFor: const Duration(seconds: 8),
localeId: 'id_ID', localeId: 'id_ID',
cancelOnError: false, cancelOnError: false,
); );
} }
Future<void> stopListening() async { Future<void> stopListening() async {
_shouldListen = false;
_listening = false; _listening = false;
await _stt.stop(); await _stt.stop();
} }
@ -42,15 +50,17 @@ class SttService {
void _onError(dynamic error) { void _onError(dynamic error) {
_listening = false; _listening = false;
// Auto-restart setelah error if (_shouldListen) {
Future.delayed(const Duration(seconds: 1), startListening); Future.delayed(const Duration(seconds: 2), startListening);
}
} }
void _onStatus(String status) { void _onStatus(String status) {
if (status == 'done' || status == 'notListening') { if (status == 'done' || status == 'notListening') {
_listening = false; _listening = false;
// Auto-restart agar selalu mendengarkan if (_shouldListen) {
Future.delayed(const Duration(milliseconds: 500), startListening); Future.delayed(const Duration(seconds: 2), startListening);
}
} }
} }
} }

View File

@ -4,9 +4,15 @@ class TtsService {
final FlutterTts _tts = FlutterTts(); final FlutterTts _tts = FlutterTts();
final List<String> _queue = []; final List<String> _queue = [];
bool _speaking = false; bool _speaking = false;
bool _initialized = false;
String _lastSpoken = ''; String _lastSpoken = '';
DateTime _lastQueuedAt = DateTime.fromMillisecondsSinceEpoch(0);
Future<void> init({String language = 'id-ID', double pitch = 1.0, double rate = 0.5}) async { Future<void> init(
{String language = 'id-ID',
double pitch = 1.0,
double rate = 0.5}) async {
if (_initialized) return;
await _tts.setLanguage(language); await _tts.setLanguage(language);
await _tts.setPitch(pitch); await _tts.setPitch(pitch);
await _tts.setSpeechRate(rate); await _tts.setSpeechRate(rate);
@ -15,11 +21,25 @@ class TtsService {
_speaking = false; _speaking = false;
_processQueue(); _processQueue();
}); });
_initialized = true;
} }
/// Tambah ke antrian - tidak memotong yg sedang bicara /// Tambah ke antrian - tidak memotong yg sedang bicara
void speak(String text) { void speak(String text) {
if (text.isEmpty) return; if (text.isEmpty) return;
if (!_initialized) {
init().then((_) => speak(text));
return;
}
final now = DateTime.now();
if (text == _lastSpoken &&
now.difference(_lastQueuedAt) < const Duration(milliseconds: 900)) {
return;
}
_lastQueuedAt = now;
if (_queue.length >= 3) {
_queue.removeAt(0);
}
_queue.add(text); _queue.add(text);
if (!_speaking) _processQueue(); if (!_speaking) _processQueue();
} }
@ -27,6 +47,7 @@ class TtsService {
/// Potong TTS sekarang dan langsung ucapkan ini - untuk obstacle alert /// Potong TTS sekarang dan langsung ucapkan ini - untuk obstacle alert
Future<void> speakImmediate(String text) async { Future<void> speakImmediate(String text) async {
if (text.isEmpty) return; if (text.isEmpty) return;
await init();
_queue.clear(); _queue.clear();
await _tts.stop(); await _tts.stop();
_speaking = true; _speaking = true;
@ -43,9 +64,20 @@ class TtsService {
String get lastSpoken => _lastSpoken; String get lastSpoken => _lastSpoken;
bool get isSpeaking => _speaking; bool get isSpeaking => _speaking;
Future<void> setLanguage(String lang) async => _tts.setLanguage(lang); Future<void> setLanguage(String lang) async {
Future<void> setPitch(double pitch) async => _tts.setPitch(pitch); await init(language: lang);
Future<void> setRate(double rate) async => _tts.setSpeechRate(rate); await _tts.setLanguage(lang);
}
Future<void> setPitch(double pitch) async {
await init();
await _tts.setPitch(pitch);
}
Future<void> setRate(double rate) async {
await init();
await _tts.setSpeechRate(rate);
}
void repeatLast() { void repeatLast() {
if (_lastSpoken.isNotEmpty) speak(_lastSpoken); if (_lastSpoken.isNotEmpty) speak(_lastSpoken);

View File

@ -1,15 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AppColors { class AppColors {
static const primary = Color(0xFF2563EB); static const primaryBlue = Color(0xFF4A90D9);
static const primaryDark = Color(0xFF0F3EA8); static const softBlueBg = Color(0xFFEBF4FF);
static const accent = Color(0xFF0891B2); static const softPinkBg = Color(0xFFFFF0F5);
static const softYellowBg = Color(0xFFFFFBEB);
static const cardWhite = Color(0xFFFFFFFF);
static const textDark = Color(0xFF2D3748);
static const textMuted = Color(0xFFA0AEC0);
static const gold = Color(0xFFF6C90E);
static const gradientBlueStart = Color(0xFF6BB8FF);
static const gradientBlueEnd = Color(0xFF4A90D9);
static const gradientPinkStart = Color(0xFFFFB3C6);
static const gradientPinkEnd = Color(0xFFFF6B9D);
static const primary = primaryBlue;
static const primaryDark = Color(0xFF256FB8);
static const accent = Color(0xFFFF6B9D);
static const warning = Color(0xFFD97706); static const warning = Color(0xFFD97706);
static const danger = Color(0xFFDC2626); static const danger = Color(0xFFDC2626);
static const success = Color(0xFF059669); static const success = Color(0xFF059669);
static const surface = Color(0xFFF7FAFC); static const surface = softBlueBg;
static const surfaceRaised = Color(0xFFFFFFFF); static const surfaceRaised = cardWhite;
static const text = Color(0xFF0F172A); static const text = textDark;
static const muted = Color(0xFF64748B); static const muted = textMuted;
static const border = Color(0xFFE2E8F0); static const border = Color(0xFFE2E8F0);
} }

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
class AppDecorations {
static const cardRadius = BorderRadius.all(Radius.circular(20));
static const pillRadius = BorderRadius.all(Radius.circular(50));
static const inputRadius = BorderRadius.all(Radius.circular(14));
static const iconCircleSize = 44.0;
static const cardShadow = [
BoxShadow(
color: Color(0x14000000),
blurRadius: 20,
offset: Offset(0, 4),
),
];
static const avatarShadow = [
BoxShadow(
color: Color(0x18000000),
blurRadius: 18,
offset: Offset(0, 6),
),
];
static const blueGradient = LinearGradient(
colors: [AppColors.gradientBlueStart, AppColors.gradientBlueEnd],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
static const pinkGradient = LinearGradient(
colors: [AppColors.gradientPinkStart, AppColors.gradientPinkEnd],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
static const cardShape = RoundedRectangleBorder(
borderRadius: cardRadius,
);
static BoxDecoration get card => const BoxDecoration(
color: AppColors.cardWhite,
borderRadius: cardRadius,
boxShadow: cardShadow,
);
static BoxDecoration pillGradient({
Gradient gradient = blueGradient,
}) =>
BoxDecoration(
gradient: gradient,
borderRadius: pillRadius,
);
static BoxDecoration iconCircle({
Color color = AppColors.softBlueBg,
}) =>
BoxDecoration(
color: color,
borderRadius: pillRadius,
);
static BoxDecoration avatar({
Color borderColor = Colors.white,
}) =>
BoxDecoration(
shape: BoxShape.circle,
color: AppColors.cardWhite,
border: Border.all(color: borderColor, width: 3),
boxShadow: avatarShadow,
);
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
class AppTextStyles {
static TextStyle get heading => GoogleFonts.poppins(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.textDark,
letterSpacing: 0,
height: 1.2,
);
static TextStyle get subheading => GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textDark,
letterSpacing: 0,
height: 1.25,
);
static TextStyle get body => GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.textDark,
letterSpacing: 0,
height: 1.45,
);
static TextStyle get caption => GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.textMuted,
letterSpacing: 0,
height: 1.35,
);
static TextTheme get textTheme => GoogleFonts.poppinsTextTheme().copyWith(
headlineSmall: heading,
titleMedium: subheading,
bodyMedium: body,
bodySmall: caption,
labelLarge: body.copyWith(fontWeight: FontWeight.w700),
);
}

View File

@ -8,6 +8,9 @@ import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import '../../shared/widgets/animations/animations.dart';
Dio get _api => sl<ApiClient>().dio; Dio get _api => sl<ApiClient>().dio;
@ -103,7 +106,16 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: FadeSlideWrapper(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@ -118,10 +130,7 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
children: [ children: [
Text( Text(
'Activity Log', 'Activity Log',
style: Theme.of(context) style: AppTextStyles.heading,
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w800),
), ),
Text( Text(
'${_items.length} aktivitas tercatat', '${_items.length} aktivitas tercatat',
@ -155,7 +164,17 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
onSelected: (_) { onSelected: (_) {
setState(() => _applyFilter(f)); setState(() => _applyFilter(f));
}, },
selectedColor: AppColors.primary.withValues(alpha: 0.15), selectedColor:
AppColors.primary.withValues(alpha: 0.15),
backgroundColor: AppColors.cardWhite,
side: BorderSide(
color: selected
? AppColors.primary.withValues(alpha: 0.4)
: AppColors.border,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
checkmarkColor: AppColors.primary, checkmarkColor: AppColors.primary,
labelStyle: TextStyle( labelStyle: TextStyle(
color: selected ? AppColors.primary : AppColors.muted, color: selected ? AppColors.primary : AppColors.muted,
@ -180,16 +199,23 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
? _EmptyPanel(filter: _selectedFilter) ? _EmptyPanel(filter: _selectedFilter)
: RefreshIndicator( : RefreshIndicator(
onRefresh: _load, onRefresh: _load,
child: ListView.builder( child: ListView(
itemCount: _filtered.length, children: [
itemBuilder: (ctx, i) => StaggerWrapper(
_LogCard(item: _filtered[i]), children: [
for (final item in _filtered)
_LogCard(item: item),
],
),
],
), ),
), ),
), ),
], ],
), ),
), ),
),
),
); );
} }
} }
@ -228,7 +254,15 @@ class _LogCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final meta = _logMeta(item.logType); final meta = _logMeta(item.logType);
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 10),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -240,15 +274,10 @@ class _LogCard extends StatelessWidget {
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
color: meta.color.withValues(alpha: 0.12), color: meta.color.withValues(alpha: 0.12),
shape: BoxShape.circle, borderRadius: BorderRadius.circular(50),
), ),
child: Icon(meta.icon, color: meta.color, size: 18), child: Icon(meta.icon, color: meta.color, size: 18),
), ),
Container(
width: 1.5,
height: 20,
color: const Color(0xFFE2E8F0),
),
], ],
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -278,7 +307,8 @@ class _LogCard extends StatelessWidget {
), ),
], ],
), ),
if (item.description != null && item.description!.isNotEmpty) if (item.description != null &&
item.description!.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: 2), padding: const EdgeInsets.only(top: 2),
child: Text( child: Text(
@ -294,6 +324,7 @@ class _LogCard extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
@ -394,6 +425,13 @@ class _ErrorPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -410,6 +448,7 @@ class _ErrorPanel extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }
@ -421,6 +460,13 @@ class _EmptyPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -437,6 +483,7 @@ class _EmptyPanel extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }

View File

@ -9,8 +9,11 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../../app/injection_container.dart'; import '../../app/injection_container.dart';
import '../../core/ai/detection_export.dart'; import '../../core/ai/detection_export.dart';
import '../../core/constants/app_constants.dart'; import '../../core/constants/app_constants.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/utils/operation_guard.dart'; import '../../core/utils/operation_guard.dart';
import '../../shared/widgets/animations/animations.dart';
import '../../shared/widgets/feature_page.dart'; import '../../shared/widgets/feature_page.dart';
class AiBenchmarkScreen extends StatefulWidget { class AiBenchmarkScreen extends StatefulWidget {
@ -125,8 +128,12 @@ class _AiBenchmarkScreenState extends State<AiBenchmarkScreen> {
enableAudio: false, enableAudio: false,
); );
controller = activeController; controller = activeController;
await activeController.initialize().timeout(const Duration(seconds: 5)); await activeController
await activeController.takePicture().timeout(const Duration(seconds: 5)); .initialize()
.timeout(const Duration(seconds: 5));
await activeController
.takePicture()
.timeout(const Duration(seconds: 5));
} }
}, },
onError: (_) {}, onError: (_) {},
@ -198,7 +205,11 @@ class _AiBenchmarkScreenState extends State<AiBenchmarkScreen> {
label: const Text('Clear log'), label: const Text('Clear log'),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
StaggerWrapper(
children: [
for (final run in _runs) _BenchmarkCard(run: run), for (final run in _runs) _BenchmarkCard(run: run),
],
),
if (_runs.isEmpty) if (_runs.isEmpty)
const FeatureEmptyPanel( const FeatureEmptyPanel(
icon: Icons.speed, icon: Icons.speed,
@ -224,9 +235,10 @@ class _BenchmarkCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppColors.cardWhite,
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -262,7 +274,8 @@ class _StatusBox extends StatelessWidget {
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: success ? const Color(0xFFF0FDF4) : const Color(0xFFFEF2F2), color: success ? const Color(0xFFF0FDF4) : const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(14), borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),

View File

@ -19,6 +19,9 @@ import '../../core/services/offline_queue_service.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/services/websocket_service.dart'; import '../../core/services/websocket_service.dart';
import '../../core/storage/secure_storage.dart'; import '../../core/storage/secure_storage.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// LoginScreen // LoginScreen
@ -79,7 +82,8 @@ class _LoginScreenState extends State<LoginScreen> {
}, },
onError: (message) => _snack(context, message), onError: (message) => _snack(context, message),
fallback: 'Login gagal. Periksa email dan password kamu.', fallback: 'Login gagal. Periksa email dan password kamu.',
connectionHint: 'Tidak bisa ke server. Pakai URL backend publik/aktif.', connectionHint:
'Tidak bisa ke server. Cek URL backend di Connect Server dan pastikan server aktif.',
); );
if (mounted) setState(() => _loading = false); if (mounted) setState(() => _loading = false);
} }
@ -150,37 +154,25 @@ class _AuthFrame extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFEAF4FF), backgroundColor: AppColors.softBlueBg,
body: LayoutBuilder( body: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final compact = final compact =
constraints.maxWidth < 480 || constraints.maxHeight < 720; constraints.maxWidth < 480 || constraints.maxHeight < 720;
return Stack( return DecoratedBox(
children: [ decoration: const BoxDecoration(
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topCenter,
end: Alignment.bottomRight, end: Alignment.bottomCenter,
colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)], colors: [
AppColors.softBlueBg,
Colors.white,
AppColors.softPinkBg,
],
), ),
), ),
), child: SafeArea(
), child: Center(
Positioned(
top: compact ? -70 : -90,
right: compact ? -70 : -60,
child: Container(
width: compact ? 180 : 260,
height: compact ? 180 : 260,
decoration: BoxDecoration(
color: const Color(0xFF2563EB).withValues(alpha: 0.12),
shape: BoxShape.circle,
),
),
),
Center(
child: SingleChildScrollView( child: SingleChildScrollView(
keyboardDismissBehavior: keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag, ScrollViewKeyboardDismissBehavior.onDrag,
@ -206,19 +198,10 @@ class _AuthFrame extends StatelessWidget {
child: RepaintBoundary( child: RepaintBoundary(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96), color: AppColors.cardWhite,
borderRadius: borderRadius:
BorderRadius.circular(compact ? 22 : 30), BorderRadius.circular(compact ? 22 : 28),
border: Border.all( boxShadow: AppDecorations.cardShadow,
color: Colors.white.withValues(alpha: 0.8)),
boxShadow: [
BoxShadow(
color: const Color(0xFF1E3A8A)
.withValues(alpha: 0.14),
blurRadius: compact ? 24 : 40,
offset: const Offset(0, 18),
),
],
), ),
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
@ -236,13 +219,15 @@ class _AuthFrame extends StatelessWidget {
width: compact ? 44 : 56, width: compact ? 44 : 56,
height: compact ? 44 : 56, height: compact ? 44 : 56,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: const LinearGradient( gradient: AppDecorations.blueGradient,
colors: [
Color(0xFF2563EB),
Color(0xFF0891B2)
],
),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x334A90D9),
blurRadius: 18,
offset: Offset(0, 8),
),
],
), ),
child: Icon(Icons.navigation_rounded, child: Icon(Icons.navigation_rounded,
color: Colors.white, color: Colors.white,
@ -256,8 +241,8 @@ class _AuthFrame extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w800,
color: Color(0xFF0F172A), color: AppColors.textDark,
), ),
), ),
), ),
@ -269,21 +254,22 @@ class _AuthFrame extends StatelessWidget {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6), horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.shield_outlined, Icon(Icons.shield_outlined,
size: 14, color: Color(0xFF1D4ED8)), size: 14,
color: AppColors.primaryBlue),
SizedBox(width: 6), SizedBox(width: 6),
Text( Text(
'Secure Assistive Navigation', 'Secure Assistive Navigation',
style: TextStyle( style: TextStyle(
color: Color(0xFF1D4ED8), color: AppColors.primaryBlue,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
), ),
), ),
], ],
@ -294,13 +280,9 @@ class _AuthFrame extends StatelessWidget {
title, title,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: AppTextStyles.heading.copyWith(
.textTheme
.headlineMedium
?.copyWith(
fontSize: compact ? 26 : null, fontSize: compact ? 26 : null,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w800,
color: const Color(0xFF0F172A),
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@ -309,7 +291,7 @@ class _AuthFrame extends StatelessWidget {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF64748B), color: AppColors.muted,
height: 1.35, height: 1.35,
), ),
), ),
@ -324,7 +306,7 @@ class _AuthFrame extends StatelessWidget {
), ),
), ),
), ),
], ),
); );
}, },
), ),

View File

@ -7,6 +7,10 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../../app/injection_container.dart'; import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import '../../shared/widgets/animations/animations.dart';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// RegisterScreen // RegisterScreen
@ -69,7 +73,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
}, },
onError: (message) => _snack(context, message), onError: (message) => _snack(context, message),
fallback: 'Registrasi gagal. Periksa data akun kamu.', fallback: 'Registrasi gagal. Periksa data akun kamu.',
connectionHint: 'Tidak bisa ke server. Pakai URL backend publik/aktif.', connectionHint:
'Tidak bisa ke server. Cek URL backend di Connect Server dan pastikan server aktif.',
); );
if (mounted) setState(() => _loading = false); if (mounted) setState(() => _loading = false);
} }
@ -128,7 +133,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _role == 'USER' color: _role == 'USER'
? const Color(0xFFEFF6FF) ? AppColors.softBlueBg
: const Color(0xFFF0FDF4), : const Color(0xFFF0FDF4),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
@ -234,18 +239,19 @@ class _RoleCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return BounceTap(
onTap: onTap, onTap: onTap,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? const Color(0xFFEFF6FF) : Colors.white, color: selected ? AppColors.softBlueBg : Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: selected ? const Color(0xFF1A56DB) : const Color(0xFFE2E8F0), color: selected ? AppColors.primaryBlue : AppColors.border,
width: selected ? 2 : 1, width: selected ? 2 : 1,
), ),
boxShadow: selected ? AppDecorations.cardShadow : null,
), ),
child: Row( child: Row(
children: [ children: [
@ -253,10 +259,9 @@ class _RoleCard extends StatelessWidget {
width: 48, width: 48,
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected color:
? const Color(0xFF1A56DB) selected ? AppColors.primaryBlue : const Color(0xFFF1F5F9),
: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(50),
borderRadius: BorderRadius.circular(12),
), ),
child: Icon(icon, child: Icon(icon,
color: selected ? Colors.white : const Color(0xFF64748B)), color: selected ? Colors.white : const Color(0xFF64748B)),
@ -267,16 +272,16 @@ class _RoleCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, Text(title,
style: const TextStyle( style: AppTextStyles.subheading.copyWith(fontSize: 16)),
fontWeight: FontWeight.w800, fontSize: 16)),
Text(subtitle, Text(subtitle,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF64748B), fontSize: 13)), color: AppColors.muted, fontSize: 13)),
], ],
), ),
), ),
if (selected) if (selected)
const Icon(Icons.check_circle_rounded, color: Color(0xFF1A56DB)), const Icon(Icons.check_circle_rounded,
color: AppColors.primaryBlue),
], ],
), ),
), ),
@ -298,37 +303,25 @@ class _AuthFrame extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFEAF4FF), backgroundColor: AppColors.softBlueBg,
body: LayoutBuilder( body: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final compact = final compact =
constraints.maxWidth < 480 || constraints.maxHeight < 720; constraints.maxWidth < 480 || constraints.maxHeight < 720;
return Stack( return DecoratedBox(
children: [ decoration: const BoxDecoration(
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topCenter,
end: Alignment.bottomRight, end: Alignment.bottomCenter,
colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)], colors: [
AppColors.softBlueBg,
Colors.white,
AppColors.softPinkBg,
],
), ),
), ),
), child: SafeArea(
), child: Center(
Positioned(
top: compact ? -70 : -90,
right: compact ? -70 : -60,
child: Container(
width: compact ? 180 : 260,
height: compact ? 180 : 260,
decoration: BoxDecoration(
color: const Color(0xFF2563EB).withValues(alpha: 0.12),
shape: BoxShape.circle,
),
),
),
Center(
child: SingleChildScrollView( child: SingleChildScrollView(
keyboardDismissBehavior: keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag, ScrollViewKeyboardDismissBehavior.onDrag,
@ -354,19 +347,10 @@ class _AuthFrame extends StatelessWidget {
child: RepaintBoundary( child: RepaintBoundary(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96), color: AppColors.cardWhite,
borderRadius: borderRadius:
BorderRadius.circular(compact ? 22 : 30), BorderRadius.circular(compact ? 22 : 28),
border: Border.all( boxShadow: AppDecorations.cardShadow,
color: Colors.white.withValues(alpha: 0.8)),
boxShadow: [
BoxShadow(
color: const Color(0xFF1E3A8A)
.withValues(alpha: 0.14),
blurRadius: compact ? 24 : 40,
offset: const Offset(0, 18),
),
],
), ),
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
@ -384,8 +368,15 @@ class _AuthFrame extends StatelessWidget {
width: compact ? 44 : 56, width: compact ? 44 : 56,
height: compact ? 44 : 56, height: compact ? 44 : 56,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1D4ED8), gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x334A90D9),
blurRadius: 18,
offset: Offset(0, 8),
),
],
), ),
child: Icon(Icons.navigation_rounded, child: Icon(Icons.navigation_rounded,
color: Colors.white, color: Colors.white,
@ -399,8 +390,8 @@ class _AuthFrame extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w800,
color: Color(0xFF0F172A), color: AppColors.textDark,
), ),
), ),
), ),
@ -411,13 +402,9 @@ class _AuthFrame extends StatelessWidget {
title, title,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: AppTextStyles.heading.copyWith(
.textTheme
.headlineMedium
?.copyWith(
fontSize: compact ? 26 : null, fontSize: compact ? 26 : null,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w800,
color: const Color(0xFF0F172A),
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@ -426,7 +413,7 @@ class _AuthFrame extends StatelessWidget {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF64748B), color: AppColors.muted,
height: 1.35, height: 1.35,
), ),
), ),
@ -441,7 +428,7 @@ class _AuthFrame extends StatelessWidget {
), ),
), ),
), ),
], ),
); );
}, },
), ),

View File

@ -13,6 +13,7 @@ import '../../core/services/incoming_call_polling_service.dart';
import '../../core/services/offline_queue_service.dart'; import '../../core/services/offline_queue_service.dart';
import '../../core/services/websocket_service.dart'; import '../../core/services/websocket_service.dart';
import '../../core/storage/secure_storage.dart'; import '../../core/storage/secure_storage.dart';
import '../../shared/widgets/walkguide_loading_screen.dart';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// SplashScreen // SplashScreen
@ -39,32 +40,35 @@ class SplashScreen extends StatefulWidget {
class _SplashScreenState extends State<SplashScreen> class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final AnimationController _animCtrl; late final AnimationController _screenCtrl;
late final Animation<double> _fadeAnim; late final Animation<double> _screenFade;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_animCtrl = AnimationController( _screenCtrl = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 700), duration: const Duration(milliseconds: 260),
value: 1,
);
_screenFade = CurvedAnimation(
parent: _screenCtrl,
curve: Curves.easeOutCubic,
); );
_fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeIn);
_animCtrl.forward();
_route(); _route();
} }
@override @override
void dispose() { void dispose() {
_animCtrl.dispose(); _screenCtrl.dispose();
super.dispose(); super.dispose();
} }
Future<void> _route() async { Future<void> _route() async {
final routed = await runFriendlyAction( final routed = await runFriendlyAction(
() async { () async {
// Animasi logo selalu tampil minimal 500ms agar tidak langsung flash. // Animasi logo selalu tampil minimal 900ms agar tidak langsung flash.
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 900));
final storage = sl<SecureStorage>(); final storage = sl<SecureStorage>();
final token = await storage.getAccessToken().timeout( final token = await storage.getAccessToken().timeout(
@ -77,7 +81,7 @@ class _SplashScreenState extends State<SplashScreen>
if (!mounted) return; if (!mounted) return;
if (token == null || role == null) { if (token == null || role == null) {
context.go('/login'); await _fadeOutThenGo('/login');
return; return;
} }
@ -88,67 +92,28 @@ class _SplashScreenState extends State<SplashScreen>
sl<IncomingCallPollingService>().start(); sl<IncomingCallPollingService>().start();
} }
// Auto-login: arahkan ke home sesuai role. // Auto-login: arahkan ke home sesuai role.
context.go( await _fadeOutThenGo(
role == 'ROLE_GUARDIAN' ? '/guardian/dashboard' : '/user/walkguide', role == 'ROLE_GUARDIAN' ? '/guardian/dashboard' : '/user/walkguide',
); );
}, },
onError: (_) {}, onError: (_) {},
fallback: 'Sesi belum bisa dipulihkan.', fallback: 'Sesi belum bisa dipulihkan.',
); );
if (!routed && mounted) context.go('/login'); if (!routed && mounted) await _fadeOutThenGo('/login');
}
Future<void> _fadeOutThenGo(String route) async {
if (!mounted) return;
await _screenCtrl.reverse();
if (mounted) context.go(route);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return FadeTransition(
backgroundColor: const Color(0xFF1A56DB), opacity: _screenFade,
body: Center( child: const WalkGuideLoadingScreen(
child: FadeTransition( subtitle: 'Restoring your session',
opacity: _fadeAnim,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Logo / icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(28),
),
child: const Icon(
Icons.navigation_rounded,
color: Colors.white,
size: 60,
),
),
const SizedBox(height: 24),
const Text(
'WalkGuide',
style: TextStyle(
color: Colors.white,
fontSize: 34,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
),
),
const SizedBox(height: 6),
const Text(
'AI-powered navigation for everyone',
style: TextStyle(color: Colors.white70, fontSize: 13),
),
const SizedBox(height: 48),
const SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
),
],
),
),
), ),
); );
} }

View File

@ -10,8 +10,11 @@ import '../../core/services/call_service.dart';
import '../../core/services/haptic_service.dart'; import '../../core/services/haptic_service.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/storage/secure_storage.dart'; import '../../core/storage/secure_storage.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../shared/widgets/animations/animations.dart';
const _kBlue = Color(0xFF1A56DB); const _kBlue = AppColors.primaryBlue;
const _kGreen = Color(0xFF16A34A); const _kGreen = Color(0xFF16A34A);
const _kRed = Color(0xFFDC2626); const _kRed = Color(0xFFDC2626);
const _kMuted = Color(0xFF64748B); const _kMuted = Color(0xFF64748B);
@ -588,11 +591,21 @@ class _CallScaffold extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: _kBg, backgroundColor: _kBg,
body: SafeArea( body: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [_kBg, Color(0xFF172554)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: FadeSlideWrapper(
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 48), const SizedBox(width: 48),
@ -614,6 +627,8 @@ class _CallScaffold extends StatelessWidget {
], ],
), ),
), ),
),
),
); );
} }
} }
@ -666,7 +681,8 @@ class _Avatar extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: color.withValues(alpha: 0.2), color: color.withValues(alpha: 0.2),
border: Border.all(color: color, width: 3), border: Border.all(color: Colors.white, width: 3),
boxShadow: AppDecorations.avatarShadow,
), ),
child: Icon(icon, color: Colors.white, size: 56), child: Icon(icon, color: Colors.white, size: 56),
); );
@ -688,7 +704,7 @@ class _ControlButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return BounceTap(
onTap: onTap, onTap: onTap,
child: Column( child: Column(
children: [ children: [
@ -718,7 +734,7 @@ class _EndCallButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return BounceTap(
onTap: onTap, onTap: onTap,
child: Column( child: Column(
children: [ children: [
@ -754,7 +770,7 @@ class _RoundCallButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return BounceTap(
onTap: onTap, onTap: onTap,
child: Opacity( child: Opacity(
opacity: onTap == null ? 0.4 : 1, opacity: onTap == null ? 0.4 : 1,

View File

@ -9,6 +9,10 @@ import 'package:intl/intl.dart';
import '../../../app/injection_container.dart'; import '../../../app/injection_container.dart';
import '../../../core/errors/friendly_error.dart'; import '../../../core/errors/friendly_error.dart';
import '../../../core/network/api_client.dart'; import '../../../core/network/api_client.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_decorations.dart';
import '../../../core/theme/app_text_styles.dart';
import '../../../shared/widgets/animations/animations.dart';
Dio get _api => sl<ApiClient>().dio; Dio get _api => sl<ApiClient>().dio;
@ -142,13 +146,22 @@ class _GuardianActivityLogScreenState extends State<GuardianActivityLogScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: FadeSlideWrapper(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header // ââ Header ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -157,11 +170,7 @@ class _GuardianActivityLogScreenState extends State<GuardianActivityLogScreen> {
children: [ children: [
Text( Text(
'User Logs', 'User Logs',
style: GoogleFonts.outfit( style: AppTextStyles.heading,
fontSize: 22,
fontWeight: FontWeight.w800,
color: const Color(0xFF0F172A),
),
), ),
Text( Text(
_needsPairing _needsPairing
@ -185,7 +194,7 @@ class _GuardianActivityLogScreenState extends State<GuardianActivityLogScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Filter chips // ââ Filter chips âââââââââââââââââââââââââââââââââââââââââââââââââ
if (!_needsPairing && !_loading && _error == null) if (!_needsPairing && !_loading && _error == null)
SizedBox( SizedBox(
height: 36, height: 36,
@ -225,7 +234,7 @@ class _GuardianActivityLogScreenState extends State<GuardianActivityLogScreen> {
if (!_needsPairing && !_loading && _error == null) if (!_needsPairing && !_loading && _error == null)
const SizedBox(height: 16), const SizedBox(height: 16),
// Body // ââ Body âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Expanded( Expanded(
child: _loading child: _loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
@ -238,16 +247,23 @@ class _GuardianActivityLogScreenState extends State<GuardianActivityLogScreen> {
: RefreshIndicator( : RefreshIndicator(
onRefresh: _load, onRefresh: _load,
color: const Color(0xFF1A56DB), color: const Color(0xFF1A56DB),
child: ListView.builder( child: ListView(
itemCount: _filtered.length, children: [
itemBuilder: (ctx, i) => StaggerWrapper(
_LogCard(item: _filtered[i]), children: [
for (final item in _filtered)
_LogCard(item: item),
],
),
],
), ),
), ),
), ),
], ],
), ),
), ),
),
),
); );
} }
@ -258,8 +274,9 @@ class _GuardianActivityLogScreenState extends State<GuardianActivityLogScreen> {
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFBEB), color: const Color(0xFFFFFBEB),
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFFDE68A)), border: Border.all(color: const Color(0xFFFDE68A)),
boxShadow: AppDecorations.cardShadow,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -338,9 +355,9 @@ class _GuardianActivityLogScreenState extends State<GuardianActivityLogScreen> {
} }
} }
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// DATA MODEL // DATA MODEL
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class _LogItem { class _LogItem {
final int id; final int id;
@ -365,9 +382,9 @@ class _LogItem {
); );
} }
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// LOG CARD // LOG CARD
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class _LogCard extends StatelessWidget { class _LogCard extends StatelessWidget {
final _LogItem item; final _LogItem item;
@ -377,7 +394,15 @@ class _LogCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final meta = _logMeta(item.logType); final meta = _logMeta(item.logType);
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 10),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -389,15 +414,10 @@ class _LogCard extends StatelessWidget {
height: 38, height: 38,
decoration: BoxDecoration( decoration: BoxDecoration(
color: meta.color.withValues(alpha: 0.12), color: meta.color.withValues(alpha: 0.12),
shape: BoxShape.circle, borderRadius: BorderRadius.circular(50),
), ),
child: Icon(meta.icon, color: meta.color, size: 18), child: Icon(meta.icon, color: meta.color, size: 18),
), ),
Container(
width: 1.5,
height: 22,
color: const Color(0xFFE2E8F0),
),
], ],
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -429,7 +449,8 @@ class _LogCard extends StatelessWidget {
), ),
], ],
), ),
if (item.description != null && item.description!.isNotEmpty) if (item.description != null &&
item.description!.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: 3), padding: const EdgeInsets.only(top: 3),
child: Text( child: Text(
@ -447,6 +468,7 @@ class _LogCard extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
@ -459,9 +481,9 @@ class _LogCard extends StatelessWidget {
} }
} }
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// LOG METADATA // LOG METADATA
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class _LogMeta { class _LogMeta {
final IconData icon; final IconData icon;

View File

@ -9,7 +9,11 @@ import 'package:google_fonts/google_fonts.dart';
import '../../../app/injection_container.dart'; import '../../../app/injection_container.dart';
import '../../../core/errors/friendly_error.dart'; import '../../../core/errors/friendly_error.dart';
import '../../../core/network/api_client.dart'; import '../../../core/network/api_client.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_decorations.dart';
import '../../../core/theme/app_text_styles.dart';
import '../../../core/utils/operation_guard.dart'; import '../../../core/utils/operation_guard.dart';
import '../../../shared/widgets/animations/animations.dart';
Dio get _api => sl<ApiClient>().dio; Dio get _api => sl<ApiClient>().dio;
@ -77,7 +81,8 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
}, },
onError: (error) => setState(() { onError: (error) => setState(() {
_error = error is DioException _error = error is DioException
? friendlyDioMessage(error, fallback: 'Gagal memuat konfigurasi AI.') ? friendlyDioMessage(error,
fallback: 'Gagal memuat konfigurasi AI.')
: 'Gagal memuat konfigurasi AI. Coba refresh lagi.'; : 'Gagal memuat konfigurasi AI. Coba refresh lagi.';
}), }),
); );
@ -136,13 +141,22 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: FadeSlideWrapper(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header // ââ Header ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -151,11 +165,7 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
children: [ children: [
Text( Text(
'AI Config', 'AI Config',
style: GoogleFonts.outfit( style: AppTextStyles.heading,
fontSize: 22,
fontWeight: FontWeight.w800,
color: const Color(0xFF0F172A),
),
), ),
Text( Text(
'Konfigurasi deteksi YOLO untuk User', 'Konfigurasi deteksi YOLO untuk User',
@ -183,7 +193,7 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Body // ââ Body âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Expanded( Expanded(
child: _loading child: _loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
@ -196,15 +206,20 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
], ],
), ),
), ),
),
),
); );
} }
Widget _buildConfigForm() { Widget _buildConfigForm() {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: StaggerWrapper(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Confidence Threshold Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ââ Confidence Threshold ââââââââââââââââââââââââââââââââââââââââââ
_SectionCard( _SectionCard(
title: 'Confidence Threshold', title: 'Confidence Threshold',
subtitle: subtitle:
@ -223,7 +238,8 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4), horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A56DB).withValues(alpha: 0.1), color:
const Color(0xFF1A56DB).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
@ -243,7 +259,8 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
max: 0.9, max: 0.9,
divisions: 8, divisions: 8,
activeColor: const Color(0xFF1A56DB), activeColor: const Color(0xFF1A56DB),
onChanged: (v) => setState(() => _confidenceThreshold = v), onChanged: (v) =>
setState(() => _confidenceThreshold = v),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -261,7 +278,7 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Alert Distances // ââ Alert Distances âââââââââââââââââââââââââââââââââââââââââââââââ
_SectionCard( _SectionCard(
title: 'Jarak Peringatan', title: 'Jarak Peringatan',
subtitle: 'Batas jarak (meter) untuk level peringatan', subtitle: 'Batas jarak (meter) untuk level peringatan',
@ -285,13 +302,15 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
const SizedBox(width: 6), const SizedBox(width: 6),
Text('Jarak Dekat', Text('Jarak Dekat',
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 13, color: const Color(0xFF0F172A))), fontSize: 13,
color: const Color(0xFF0F172A))),
]), ]),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4), horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFDC2626).withValues(alpha: 0.1), color:
const Color(0xFFDC2626).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
@ -330,13 +349,15 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
const SizedBox(width: 6), const SizedBox(width: 6),
Text('Jarak Sedang', Text('Jarak Sedang',
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 13, color: const Color(0xFF0F172A))), fontSize: 13,
color: const Color(0xFF0F172A))),
]), ]),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4), horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFD97706).withValues(alpha: 0.1), color:
const Color(0xFFD97706).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
@ -356,14 +377,15 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
max: 8.0, max: 8.0,
divisions: 7, divisions: 7,
activeColor: const Color(0xFFD97706), activeColor: const Color(0xFFD97706),
onChanged: (v) => setState(() => _alertDistanceMedium = v), onChanged: (v) =>
setState(() => _alertDistanceMedium = v),
), ),
], ],
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Max Inference FPS // ââ Max Inference FPS âââââââââââââââââââââââââââââââââââââââââââââ
_SectionCard( _SectionCard(
title: 'Max Inference FPS', title: 'Max Inference FPS',
subtitle: subtitle:
@ -382,7 +404,8 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4), horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF059669).withValues(alpha: 0.1), color:
const Color(0xFF059669).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
@ -421,7 +444,7 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Enabled Labels // ââ Enabled Labels ââââââââââââââââââââââââââââââââââââââââââââââââ
_SectionCard( _SectionCard(
title: 'Label yang Diaktifkan', title: 'Label yang Diaktifkan',
subtitle: 'Jenis objek yang akan dideteksi AI', subtitle: 'Jenis objek yang akan dideteksi AI',
@ -435,10 +458,11 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
return GestureDetector( return GestureDetector(
onTap: () => setState(() => _enabledLabels = label), onTap: () => setState(() => _enabledLabels = label),
child: Container( child: Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 16, vertical: 8), horizontal: 16, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? const Color(0xFF7C3AED) : Colors.white, color:
selected ? const Color(0xFF7C3AED) : Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: selected color: selected
@ -451,8 +475,9 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: color: selected
selected ? Colors.white : const Color(0xFF64748B), ? Colors.white
: const Color(0xFF64748B),
), ),
), ),
), ),
@ -462,7 +487,7 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Save button // ââ Save button âââââââââââââââââââââââââââââââââââââââââââââââââââ
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton.icon( child: FilledButton.icon(
@ -486,6 +511,8 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
), ),
],
),
); );
} }
@ -496,8 +523,9 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFBEB), color: const Color(0xFFFFFBEB),
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFFDE68A)), border: Border.all(color: const Color(0xFFFDE68A)),
boxShadow: AppDecorations.cardShadow,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -552,9 +580,9 @@ class _GuardianAiConfigScreenState extends State<GuardianAiConfigScreen> {
} }
} }
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// SECTION CARD // SECTION CARD
// // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class _SectionCard extends StatelessWidget { class _SectionCard extends StatelessWidget {
final String title; final String title;
@ -576,16 +604,10 @@ class _SectionCard extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppColors.cardWhite,
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: AppColors.border),
boxShadow: [ boxShadow: AppDecorations.cardShadow,
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -596,7 +618,7 @@ class _SectionCard extends StatelessWidget {
height: 34, height: 34,
decoration: BoxDecoration( decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.1), color: iconColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(50),
), ),
child: Icon(icon, color: iconColor, size: 18), child: Icon(icon, color: iconColor, size: 18),
), ),

View File

@ -8,6 +8,9 @@ import 'package:latlong2/latlong.dart';
import '../../app/injection_container.dart'; import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../shared/widgets/animations/animations.dart';
import '../../shared/widgets/feature_page.dart'; import '../../shared/widgets/feature_page.dart';
class GuardianMapScreen extends StatefulWidget { class GuardianMapScreen extends StatefulWidget {
@ -106,8 +109,9 @@ class _GuardianMapCard extends StatelessWidget {
final center = _pointFrom(location) ?? final center = _pointFrom(location) ??
(points.isNotEmpty ? points.first : null) ?? (points.isNotEmpty ? points.first : null) ??
const LatLng(-7.2575, 112.7521); const LatLng(-7.2575, 112.7521);
return ClipRRect( return Container(
borderRadius: BorderRadius.circular(20), decoration: AppDecorations.card,
clipBehavior: Clip.antiAlias,
child: FlutterMap( child: FlutterMap(
options: MapOptions(initialCenter: center, initialZoom: 16), options: MapOptions(initialCenter: center, initialZoom: 16),
children: [ children: [
@ -121,7 +125,7 @@ class _GuardianMapCard extends StatelessWidget {
Polyline( Polyline(
points: points, points: points,
strokeWidth: 4, strokeWidth: 4,
color: const Color(0xFF2563EB), color: AppColors.primaryBlue,
), ),
], ],
), ),
@ -171,10 +175,18 @@ class _TimelineList extends StatelessWidget {
), ),
); );
} }
return ListView.separated( return ListView(
itemCount: segments.length, children: [
separatorBuilder: (_, __) => const SizedBox(height: 10), StaggerWrapper(
itemBuilder: (_, index) => _TimelineCard(segment: segments[index]), children: [
for (final segment in segments)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _TimelineCard(segment: segment),
),
],
),
],
); );
} }
} }
@ -189,9 +201,10 @@ class _TimelineCard extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppColors.cardWhite,
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
), ),
child: Row( child: Row(
children: [ children: [
@ -199,10 +212,10 @@ class _TimelineCard extends StatelessWidget {
width: 42, width: 42,
height: 42, height: 42,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(50),
), ),
child: Icon(segment.icon, color: const Color(0xFF1D4ED8)), child: Icon(segment.icon, color: AppColors.primaryBlue),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(

View File

@ -8,6 +8,9 @@ import 'package:record/record.dart';
import '../../app/injection_container.dart'; import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../shared/widgets/animations/animations.dart';
import '../../shared/widgets/feature_page.dart'; import '../../shared/widgets/feature_page.dart';
class GuardianSendNotifScreen extends StatefulWidget { class GuardianSendNotifScreen extends StatefulWidget {
@ -132,19 +135,14 @@ class _GuardianSendNotifScreenState extends State<GuardianSendNotifScreen> {
subtitle: 'Kirim pesan singkat ke User yang sudah pairing', subtitle: 'Kirim pesan singkat ke User yang sudah pairing',
child: ListView( child: ListView(
children: [ children: [
Container( FadeSlideWrapper(
child: Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppColors.cardWhite,
borderRadius: BorderRadius.circular(20), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: AppColors.border),
boxShadow: [ boxShadow: AppDecorations.cardShadow,
BoxShadow(
color: const Color(0xFF1E293B).withValues(alpha: 0.06),
blurRadius: 22,
offset: const Offset(0, 12),
),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -185,8 +183,8 @@ class _GuardianSendNotifScreenState extends State<GuardianSendNotifScreen> {
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8FAFC), color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: AppColors.border),
), ),
child: Row( child: Row(
children: [ children: [
@ -229,7 +227,9 @@ class _GuardianSendNotifScreenState extends State<GuardianSendNotifScreen> {
), ),
FilledButton.icon( FilledButton.icon(
onPressed: _loading ? null : _toggleRecording, onPressed: _loading ? null : _toggleRecording,
icon: Icon(_recording ? Icons.stop : Icons.fiber_manual_record), icon: Icon(_recording
? Icons.stop
: Icons.fiber_manual_record),
label: Text(_recording ? 'Stop' : 'Record'), label: Text(_recording ? 'Stop' : 'Record'),
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: _recording backgroundColor: _recording
@ -260,6 +260,7 @@ class _GuardianSendNotifScreenState extends State<GuardianSendNotifScreen> {
], ],
), ),
), ),
),
], ],
), ),
); );

View File

@ -9,6 +9,9 @@ import '../../app/injection_container.dart';
import '../../core/constants/app_constants.dart'; import '../../core/constants/app_constants.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../shared/widgets/animations/animations.dart';
import '../../core/storage/secure_storage.dart'; import '../../core/storage/secure_storage.dart';
import '../../shared/widgets/feature_page.dart'; import '../../shared/widgets/feature_page.dart';
@ -48,6 +51,8 @@ class GuardianSettingsScreen extends StatelessWidget {
title: 'Guardian Settings', title: 'Guardian Settings',
subtitle: 'Account, pairing, AI tools, and server', subtitle: 'Account, pairing, AI tools, and server',
child: ListView( child: ListView(
children: [
StaggerWrapper(
children: [ children: [
_SettingsTile( _SettingsTile(
icon: Icons.link, icon: Icons.link,
@ -67,6 +72,8 @@ class GuardianSettingsScreen extends StatelessWidget {
subtitle: 'Atur threshold deteksi dan label yang aktif.', subtitle: 'Atur threshold deteksi dan label yang aktif.',
onTap: () => context.go('/guardian/ai-config'), onTap: () => context.go('/guardian/ai-config'),
), ),
],
),
const SizedBox(height: 18), const SizedBox(height: 18),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => _changeServer(context), onPressed: () => _changeServer(context),
@ -103,19 +110,28 @@ class _SettingsTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return BounceTap(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppColors.cardWhite,
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
), ),
child: ListTile( child: ListTile(
leading: Icon(icon, color: const Color(0xFF1D4ED8)), leading: Container(
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w800)), width: 44,
height: 44,
decoration: AppDecorations.iconCircle(),
child: Icon(icon, color: AppColors.primaryBlue),
),
title:
Text(title, style: const TextStyle(fontWeight: FontWeight.w800)),
subtitle: Text(subtitle), subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: onTap, ),
), ),
); );
} }

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_text_styles.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends StatelessWidget {
final String role; final String role;
@ -7,12 +10,24 @@ class HomeScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return DecoratedBox(
appBar: AppBar(title: const Text('Dashboard Walk Guide')), decoration: const BoxDecoration(
body: Center( gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text( child: Text(
role == 'ROLE_ADMIN' ? 'Selamat Datang Admin!' : 'Mode Walk Guide Siap!', role == 'ROLE_ADMIN'
style: const TextStyle(fontSize: 24), ? 'Selamat Datang Admin!'
: 'Mode Walk Guide Siap!',
textAlign: TextAlign.center,
style: AppTextStyles.heading,
),
), ),
), ),
); );

View File

@ -14,7 +14,10 @@ import '../../../core/network/api_client.dart';
import '../../../core/services/websocket_service.dart'; import '../../../core/services/websocket_service.dart';
import '../../../core/services/incoming_call_polling_service.dart'; import '../../../core/services/incoming_call_polling_service.dart';
import '../../../core/storage/secure_storage.dart'; import '../../../core/storage/secure_storage.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_decorations.dart';
import '../../../core/utils/operation_guard.dart'; import '../../../core/utils/operation_guard.dart';
import '../../../shared/widgets/animations/animations.dart';
// âââ¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ // âââ¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬
// GUARDIAN DASHBOARD SCREEN // GUARDIAN DASHBOARD SCREEN
@ -382,7 +385,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF0F4FF), backgroundColor: AppColors.softBlueBg,
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
@ -398,6 +401,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: FadeSlideWrapper(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -420,6 +424,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
), ),
), ),
), ),
),
], ],
), ),
), ),
@ -1613,13 +1618,17 @@ class _KpiCard extends StatelessWidget {
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: highlight ? const Color(0xFFFFF1F2) : Colors.white, color: highlight ? const Color(0xFFFFF1F2) : Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: AppDecorations.cardRadius,
border: Border.all( border: Border.all(
color: highlight ? const Color(0xFFFECACA) : const Color(0xFFE2E8F0), color: highlight ? const Color(0xFFFECACA) : const Color(0xFFE2E8F0),
width: 1, width: 1,
), ),
boxShadow: [ boxShadow: const [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8), BoxShadow(
color: Color(0x14000000),
blurRadius: 20,
offset: Offset(0, 4),
),
], ],
), ),
child: Column( child: Column(
@ -1745,17 +1754,15 @@ class _QuickActionCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return BounceTap(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: item.onTap, onTap: item.onTap,
borderRadius: BorderRadius.circular(12),
child: Container( child: Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: AppDecorations.cardShadow,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1793,7 +1800,6 @@ class _QuickActionCard extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/services/voice_command_handler.dart'; import '../../core/services/voice_command_handler.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../shared/widgets/animations/animations.dart';
import '../../shared/widgets/feature_page.dart';
class ManualScreen extends StatelessWidget { class ManualScreen extends StatelessWidget {
const ManualScreen({super.key}); const ManualScreen({super.key});
@ -8,16 +12,38 @@ class ManualScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final commands = VoiceCommandKey.values.map((key) => key.name).toList(); final commands = VoiceCommandKey.values.map((key) => key.name).toList();
return Scaffold( return FeaturePage(
appBar: AppBar(title: const Text('Manual')), title: 'Manual',
body: ListView.separated( subtitle: 'Voice command yang tersedia',
padding: const EdgeInsets.all(16), child: ListView(
itemCount: commands.length, children: [
separatorBuilder: (_, __) => const Divider(height: 1), StaggerWrapper(
itemBuilder: (context, index) => ListTile( children: [
leading: const Icon(Icons.record_voice_over), for (final command in commands)
title: Text(commands[index]), Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
), ),
child: ListTile(
leading: Container(
width: 44,
height: 44,
decoration: AppDecorations.iconCircle(),
child: const Icon(
Icons.record_voice_over,
color: AppColors.primaryBlue,
),
),
title: Text(command),
),
),
],
),
],
), ),
); );
} }

View File

@ -16,7 +16,11 @@ import 'package:latlong2/latlong.dart';
import '../../app/injection_container.dart'; import '../../app/injection_container.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import '../../core/utils/operation_guard.dart'; import '../../core/utils/operation_guard.dart';
import '../../shared/widgets/animations/animations.dart';
// helpers // helpers
@ -498,7 +502,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
Polyline( Polyline(
points: _navState.routePoints, points: _navState.routePoints,
strokeWidth: 5, strokeWidth: 5,
color: const Color(0xFF1A56DB), color: AppColors.primaryBlue,
), ),
], ],
), ),
@ -536,8 +540,8 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: i == _navState.currentStepIndex color: i == _navState.currentStepIndex
? const Color(0xFF1A56DB) ? AppColors.primaryBlue
: const Color(0xFF93C5FD), : AppColors.gradientBlueStart,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 2),
), ),
), ),
@ -552,6 +556,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
top: 12, top: 12,
left: 12, left: 12,
right: 12, right: 12,
child: FadeSlideWrapper(
child: Column( child: Column(
children: [ children: [
// search bar // search bar
@ -574,6 +579,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
], ],
), ),
), ),
),
// turn-by-turn banner (when navigating) // turn-by-turn banner (when navigating)
if (isNavigating && _navState.steps.isNotEmpty) if (isNavigating && _navState.steps.isNotEmpty)
@ -610,7 +616,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
child: FloatingActionButton.small( child: FloatingActionButton.small(
heroTag: 'nav_center', heroTag: 'nav_center',
backgroundColor: Colors.white, backgroundColor: Colors.white,
foregroundColor: const Color(0xFF1A56DB), foregroundColor: AppColors.primaryBlue,
onPressed: _centerOnUser, onPressed: _centerOnUser,
child: const Icon(Icons.my_location), child: const Icon(Icons.my_location),
), ),
@ -621,7 +627,13 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black26, color: Colors.black26,
child: const Center(child: CircularProgressIndicator()), child: Center(
child: Container(
padding: const EdgeInsets.all(20),
decoration: AppDecorations.card,
child: const CircularProgressIndicator(),
),
),
), ),
), ),
], ],
@ -650,8 +662,9 @@ class _SearchBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
elevation: 4, color: Colors.transparent,
borderRadius: BorderRadius.circular(14), child: Container(
decoration: AppDecorations.card,
child: TextField( child: TextField(
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
@ -659,7 +672,7 @@ class _SearchBar extends StatelessWidget {
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Cari tujuan…', hintText: 'Cari tujuan…',
prefixIcon: const Icon(Icons.search, color: Color(0xFF1A56DB)), prefixIcon: const Icon(Icons.search, color: AppColors.primaryBlue),
suffixIcon: loading suffixIcon: loading
? const Padding( ? const Padding(
padding: EdgeInsets.all(12), padding: EdgeInsets.all(12),
@ -675,8 +688,8 @@ class _SearchBar extends StatelessWidget {
onPressed: onClear, onPressed: onClear,
) )
: null, : null,
border: OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: AppDecorations.inputRadius,
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
filled: true, filled: true,
@ -685,6 +698,7 @@ class _SearchBar extends StatelessWidget {
const EdgeInsets.symmetric(horizontal: 16, vertical: 14), const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
), ),
), ),
),
); );
} }
} }
@ -698,29 +712,33 @@ class _SuggestionList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
elevation: 6, color: Colors.transparent,
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)), borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)),
child: Column( child: Container(
decoration: const BoxDecoration(
color: AppColors.cardWhite,
boxShadow: AppDecorations.cardShadow,
),
child: StaggerWrapper(
children: [ children: [
for (final place in items) for (final place in items)
InkWell( BounceTap(
onTap: () => onSelect(place), onTap: () => onSelect(place),
child: Padding( child: Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 16, vertical: 12), horizontal: 16, vertical: 12),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.place_outlined, const Icon(Icons.place_outlined,
color: Color(0xFF64748B), size: 20), color: AppColors.textMuted, size: 20),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
place.displayName, place.displayName,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14), style: AppTextStyles.body,
), ),
), ),
], ],
@ -730,6 +748,7 @@ class _SuggestionList extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }
@ -761,16 +780,10 @@ class _TurnCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: const BoxDecoration(
color: const Color(0xFF1A56DB), gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(16), borderRadius: AppDecorations.cardRadius,
boxShadow: [ boxShadow: AppDecorations.cardShadow,
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
), ),
child: Row( child: Row(
children: [ children: [
@ -864,13 +877,8 @@ class _StatusBar extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _bgColor, color: _bgColor,
borderRadius: BorderRadius.circular(14), borderRadius: AppDecorations.cardRadius,
boxShadow: [ boxShadow: AppDecorations.cardShadow,
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
),
],
border: Border.all( border: Border.all(
color: phase == _NavPhase.error color: phase == _NavPhase.error
? const Color(0xFFFECACA) ? const Color(0xFFFECACA)
@ -954,11 +962,11 @@ class _PulsingDotState extends State<_PulsingDot>
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xFF1A56DB) color: AppColors.primaryBlue
.withValues(alpha: _opacity.value * 0.4), .withValues(alpha: _opacity.value * 0.4),
border: Border.all( border: Border.all(
color: color:
const Color(0xFF1A56DB).withValues(alpha: _opacity.value), AppColors.primaryBlue.withValues(alpha: _opacity.value),
width: 2, width: 2,
), ),
), ),
@ -970,11 +978,11 @@ class _PulsingDotState extends State<_PulsingDot>
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xFF1A56DB), color: AppColors.primaryBlue,
border: Border.all(color: Colors.white, width: 3), border: Border.all(color: Colors.white, width: 3),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: const Color(0xFF1A56DB).withValues(alpha: 0.4), color: AppColors.primaryBlue.withValues(alpha: 0.4),
blurRadius: 6, blurRadius: 6,
), ),
], ],

View File

@ -13,6 +13,9 @@ import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import '../../shared/widgets/animations/animations.dart';
import 'application/notification_cubit.dart'; import 'application/notification_cubit.dart';
import 'domain/entities/guardian_notification.dart'; import 'domain/entities/guardian_notification.dart';
@ -124,7 +127,16 @@ class _NotificationScreenState extends State<NotificationScreen> {
builder: (context, state) { builder: (context, state) {
final items = state.items.map(_NotifItem.fromEntity).toList(); final items = state.items.map(_NotifItem.fromEntity).toList();
final unreadCount = items.where((n) => !n.isRead).length; final unreadCount = items.where((n) => !n.isRead).length;
return SafeArea( return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: FadeSlideWrapper(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@ -141,10 +153,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
children: [ children: [
Text( Text(
'Notifications', 'Notifications',
style: Theme.of(context) style: AppTextStyles.heading,
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w800),
), ),
if (unreadCount > 0) ...[ if (unreadCount > 0) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
@ -166,7 +175,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
? const SizedBox( ? const SizedBox(
width: 14, width: 14,
height: 14, height: 14,
child: CircularProgressIndicator(strokeWidth: 2)) child: CircularProgressIndicator(
strokeWidth: 2))
: const Icon(Icons.done_all, size: 18), : const Icon(Icons.done_all, size: 18),
label: const Text('Baca semua'), label: const Text('Baca semua'),
), ),
@ -184,26 +194,40 @@ class _NotificationScreenState extends State<NotificationScreen> {
child: state.loading child: state.loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: state.error != null : state.error != null
? _ErrorPanel(message: state.error!, onRetry: _load) ? _ErrorPanel(
message: state.error!, onRetry: _load)
: items.isEmpty : items.isEmpty
? const _EmptyPanel() ? const _EmptyPanel()
: RefreshIndicator( : RefreshIndicator(
onRefresh: _load, onRefresh: _load,
child: ListView.separated( child: ListView(
itemCount: items.length, children: [
separatorBuilder: (_, __) => StaggerWrapper(
const SizedBox(height: 10), children: [
itemBuilder: (ctx, i) => _NotifCard( for (final item in items)
notif: items[i], Padding(
onMarkRead: () => _markRead(items[i].id), padding:
onReadAloud: () => _readAloud(items[i]), const EdgeInsets.only(
bottom: 10),
child: _NotifCard(
notif: item,
onMarkRead: () =>
_markRead(item.id),
onReadAloud: () =>
_readAloud(item),
), ),
), ),
],
),
],
),
), ),
), ),
], ],
), ),
), ),
),
),
); );
}, },
); );
@ -271,7 +295,7 @@ class _NotifCard extends StatelessWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
decoration: BoxDecoration( decoration: BoxDecoration(
color: unread ? const Color(0xFFEFF6FF) : Colors.white, color: unread ? const Color(0xFFEFF6FF) : Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: AppDecorations.cardRadius,
border: Border.all( border: Border.all(
color: unread color: unread
? AppColors.primary.withValues(alpha: 0.3) ? AppColors.primary.withValues(alpha: 0.3)
@ -280,9 +304,9 @@ class _NotifCard extends StatelessWidget {
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.04), color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8, blurRadius: 20,
offset: const Offset(0, 2), offset: const Offset(0, 4),
), ),
], ],
), ),
@ -301,7 +325,7 @@ class _NotifCard extends StatelessWidget {
color: isVoice color: isVoice
? AppColors.success.withValues(alpha: 0.12) ? AppColors.success.withValues(alpha: 0.12)
: AppColors.primary.withValues(alpha: 0.12), : AppColors.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(50),
), ),
child: Icon( child: Icon(
isVoice ? Icons.mic : Icons.message, isVoice ? Icons.mic : Icons.message,
@ -413,6 +437,7 @@ class _UnreadBadge extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primary, color: AppColors.primary,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
boxShadow: AppDecorations.cardShadow,
), ),
child: Text( child: Text(
'$count', '$count',
@ -431,6 +456,13 @@ class _ErrorPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -447,6 +479,7 @@ class _ErrorPanel extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }
@ -456,8 +489,15 @@ class _EmptyPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Center( return Center(
child: Column( child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: const Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.notifications_none, size: 64, color: AppColors.muted), Icon(Icons.notifications_none, size: 64, color: AppColors.muted),
@ -472,6 +512,7 @@ class _EmptyPanel extends StatelessWidget {
style: TextStyle(color: AppColors.muted)), style: TextStyle(color: AppColors.muted)),
], ],
), ),
),
); );
} }
} }

View File

@ -8,6 +8,10 @@ import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/storage/secure_storage.dart'; import '../../core/storage/secure_storage.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import '../../shared/widgets/animations/animations.dart';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// UserPairingScreen // UserPairingScreen
@ -27,37 +31,11 @@ class UserPairingScreen extends StatefulWidget {
} }
class _UserPairingScreenState extends State<UserPairingScreen> { class _UserPairingScreenState extends State<UserPairingScreen> {
String? _uniqueId;
String? _pairingCode; String? _pairingCode;
DateTime? _pairingCodeExpiresAt; DateTime? _pairingCodeExpiresAt;
int? _pairingCodeSeconds; int? _pairingCodeSeconds;
bool _regenerating = false; bool _regenerating = false;
@override
void initState() {
super.initState();
_loadUniqueId();
}
Future<void> _loadUniqueId() async {
var value = await sl<SecureStorage>().getUniqueUserId();
if (value == null || value.isEmpty) {
await runFriendlyAction(
() async {
final res = await sl<ApiClient>()
.dio
.get('/user/profile')
.timeout(const Duration(seconds: 5));
final data = res.data['data'];
if (data is Map) value = data['uniqueUserId']?.toString();
},
onError: (_) {},
fallback: 'Profil belum bisa dimuat.',
);
}
if (mounted) setState(() => _uniqueId = value);
}
Future<void> _regeneratePairingCode() async { Future<void> _regeneratePairingCode() async {
setState(() => _regenerating = true); setState(() => _regenerating = true);
await runFriendlyAction( await runFriendlyAction(
@ -90,7 +68,7 @@ class _UserPairingScreenState extends State<UserPairingScreen> {
return _Page( return _Page(
title: 'Pairing', title: 'Pairing',
subtitle: 'Bagikan pairing code sementara ini ke Guardian.', subtitle: 'Bagikan pairing code sementara ini ke Guardian.',
child: Column( child: StaggerWrapper(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (_pairingCode == null || _pairingCode!.isEmpty) if (_pairingCode == null || _pairingCode!.isEmpty)
@ -118,11 +96,6 @@ class _UserPairingScreenState extends State<UserPairingScreen> {
: const Icon(Icons.autorenew), : const Icon(Icons.autorenew),
label: Text(_regenerating ? 'Generating...' : 'Generate New Code'), label: Text(_regenerating ? 'Generating...' : 'Generate New Code'),
), ),
if (_uniqueId != null && _uniqueId!.isNotEmpty) ...[
const SizedBox(height: 8),
Text('Account ID: $_uniqueId',
style: const TextStyle(color: Color(0xFF64748B), fontSize: 12)),
],
const SizedBox(height: 16), const SizedBox(height: 16),
_PairingStatusCard(allowUserResponse: true), _PairingStatusCard(allowUserResponse: true),
], ],
@ -348,26 +321,20 @@ class _PairingStatusCardState extends State<_PairingStatusCard> {
final cardColor = _active final cardColor = _active
? const Color(0xFFF0FDF4) ? const Color(0xFFF0FDF4)
: pending : pending
? const Color(0xFFEFF6FF) ? AppColors.softBlueBg
: const Color(0xFFFFFBEB); : const Color(0xFFFFFBEB);
final accent = _active final accent = _active
? const Color(0xFF059669) ? const Color(0xFF059669)
: pending : pending
? const Color(0xFF2563EB) ? AppColors.primaryBlue
: const Color(0xFFD97706); : const Color(0xFFD97706);
return Container( return Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardColor, color: cardColor,
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(20),
border: Border.all(color: accent.withValues(alpha: 0.28)), border: Border.all(color: accent.withValues(alpha: 0.28)),
boxShadow: [ boxShadow: AppDecorations.cardShadow,
BoxShadow(
color: accent.withValues(alpha: 0.10),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -393,7 +360,7 @@ class _PairingStatusCardState extends State<_PairingStatusCard> {
Expanded( Expanded(
child: Text(_status, child: Text(_status,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF0F172A), color: AppColors.textDark,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
height: 1.25)), height: 1.25)),
), ),
@ -465,7 +432,7 @@ class _Page extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Color(0xFFF8FAFC), Color(0xFFEFF6FF)], colors: [AppColors.softBlueBg, Colors.white],
), ),
), ),
child: Padding( child: Padding(
@ -486,15 +453,9 @@ class _Page extends StatelessWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF0F172A), gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: AppDecorations.cardShadow,
BoxShadow(
color: const Color(0xFF0F172A).withValues(alpha: 0.18),
blurRadius: 28,
offset: const Offset(0, 14),
),
],
), ),
child: Row( child: Row(
children: [ children: [
@ -516,10 +477,8 @@ class _Page extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, Text(title,
style: Theme.of(context) style: AppTextStyles.heading.copyWith(
.textTheme fontSize: 24,
.headlineSmall
?.copyWith(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Colors.white, color: Colors.white,
)), )),
@ -535,7 +494,7 @@ class _Page extends StatelessWidget {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Expanded(child: child), Expanded(child: FadeSlideWrapper(child: child)),
], ],
), ),
), ),
@ -561,15 +520,8 @@ class _InfoCard extends StatelessWidget {
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFDDEAFE)), boxShadow: AppDecorations.cardShadow,
boxShadow: [
BoxShadow(
color: const Color(0xFF2563EB).withValues(alpha: 0.10),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
), ),
child: Row( child: Row(
children: [ children: [
@ -577,10 +529,10 @@ class _InfoCard extends StatelessWidget {
width: 48, width: 48,
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(50),
), ),
child: Icon(icon, color: const Color(0xFF2563EB)), child: Icon(icon, color: AppColors.primaryBlue),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded( Expanded(
@ -589,19 +541,19 @@ class _InfoCard extends StatelessWidget {
children: [ children: [
Text(title, Text(title,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF64748B), fontWeight: FontWeight.w700)), color: AppColors.muted, fontWeight: FontWeight.w700)),
SelectableText(value, SelectableText(value,
style: const TextStyle( style: const TextStyle(
fontSize: 25, fontSize: 25,
height: 1.1, height: 1.1,
letterSpacing: 1.2, letterSpacing: 1.2,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFF0F172A))), color: AppColors.textDark)),
if (helper != null) ...[ if (helper != null) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
Text(helper!, Text(helper!,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF64748B), fontSize: 12)), color: AppColors.muted, fontSize: 12)),
], ],
])), ])),
], ],

View File

@ -8,6 +8,10 @@ import '../../app/injection_container.dart';
import '../../core/constants/app_constants.dart'; import '../../core/constants/app_constants.dart';
import '../../core/errors/friendly_error.dart'; import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import '../../shared/widgets/animations/animations.dart';
class ServerConnectScreen extends StatefulWidget { class ServerConnectScreen extends StatefulWidget {
final bool editMode; final bool editMode;
@ -64,7 +68,7 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
}, },
onError: (message) => _message = message, onError: (message) => _message = message,
fallback: fallback:
'Tidak bisa terhubung ke server publik. Pastikan backend aktif dan jaringan HP stabil.', 'Tidak bisa terhubung ke server. Pastikan backend aktif, URL benar, dan jaringan HP stabil.',
); );
if (mounted) setState(() => _loading = false); if (mounted) setState(() => _loading = false);
} }
@ -76,25 +80,27 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
if (mounted) context.go(widget.editMode ? '/login' : '/splash'); if (mounted) context.go(widget.editMode ? '/login' : '/splash');
} }
void _usePublicUrl() => void _useLocalUrl() =>
setState(() => _url.text = AppConstants.defaultServerUrl); setState(() => _url.text = AppConstants.defaultServerUrl);
void _usePublicUrl() =>
setState(() => _url.text = AppConstants.publicServerUrl);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF061421), backgroundColor: AppColors.softBlueBg,
body: Stack( body: Stack(
children: [ children: [
const Positioned.fill( const Positioned.fill(
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topCenter,
end: Alignment.bottomRight, end: Alignment.bottomCenter,
colors: [ colors: [
Color(0xFF071226), AppColors.softBlueBg,
Color(0xFF0F3B57), Colors.white,
Color(0xFF0B6B6C), AppColors.softPinkBg,
], ],
), ),
), ),
@ -140,20 +146,14 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
), ),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96), color: AppColors.cardWhite,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
compact ? 22 : 28, compact ? 22 : 28,
), ),
border: Border.all( border: Border.all(
color: Colors.white.withValues(alpha: 0.7), color: Colors.white.withValues(alpha: 0.7),
), ),
boxShadow: [ boxShadow: AppDecorations.cardShadow,
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 34,
offset: const Offset(0, 22),
),
],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
@ -170,7 +170,7 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
compact ? 14 : 20, compact ? 14 : 20,
), ),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xFF071226), color: AppColors.softBlueBg,
), ),
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
@ -182,9 +182,17 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
width: compact ? 38 : 48, width: compact ? 38 : 48,
height: compact ? 38 : 48, height: compact ? 38 : 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF2563EB), gradient:
AppDecorations.blueGradient,
borderRadius: borderRadius:
BorderRadius.circular(16), BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x334A90D9),
blurRadius: 18,
offset: Offset(0, 8),
),
],
), ),
child: Icon( child: Icon(
Icons.navigation_rounded, Icons.navigation_rounded,
@ -197,9 +205,9 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
child: Text( child: Text(
'WalkGuide', 'WalkGuide',
style: TextStyle( style: TextStyle(
color: Colors.white, color: AppColors.textDark,
fontSize: compact ? 16 : 20, fontSize: compact ? 16 : 20,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w800,
), ),
), ),
), ),
@ -208,10 +216,9 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
SizedBox(height: compact ? 14 : 18), SizedBox(height: compact ? 14 : 18),
Text( Text(
'Connect to Server', 'Connect to Server',
style: TextStyle( style: AppTextStyles.heading.copyWith(
color: Colors.white,
fontSize: compact ? 22 : 30, fontSize: compact ? 22 : 30,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w800,
height: 1, height: 1,
), ),
), ),
@ -219,11 +226,9 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
Text( Text(
widget.editMode widget.editMode
? 'Ubah alamat backend Spring Boot WalkGuide yang aktif.' ? 'Ubah alamat backend Spring Boot WalkGuide yang aktif.'
: 'Sambungkan app HP ke backend Spring Boot publik WalkGuide.', : 'Pilih backend WalkGuide yang aktif sebelum login atau register.',
style: TextStyle( style: AppTextStyles.body.copyWith(
color: Colors.white.withValues( color: AppColors.muted,
alpha: 0.72,
),
height: 1.35, height: 1.35,
), ),
), ),
@ -255,9 +260,14 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
_HintChip(
icon: Icons.usb_outlined,
label: 'USB: 127.0.0.1',
onTap: _useLocalUrl,
),
_HintChip( _HintChip(
icon: Icons.public_outlined, icon: Icons.public_outlined,
label: 'Server: 202.46.28.170', label: 'Kampus: 202.46.28.170',
onTap: _usePublicUrl, onTap: _usePublicUrl,
), ),
], ],
@ -350,27 +360,27 @@ class _HintChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return BounceTap(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(999),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0xFFBFDBFE)), border:
Border.all(color: AppColors.primaryBlue.withValues(alpha: 0.2)),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, size: 14, color: const Color(0xFF1D4ED8)), Icon(icon, size: 14, color: AppColors.primaryBlue),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
label, label,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF1D4ED8), color: AppColors.primaryBlue,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
), ),
), ),
], ],
@ -392,7 +402,7 @@ class _StatusBox extends StatelessWidget {
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withValues(alpha: 0.08), color: color.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withValues(alpha: 0.22)), border: Border.all(color: color.withValues(alpha: 0.22)),
), ),
child: Row( child: Row(

View File

@ -5,7 +5,6 @@ import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -17,16 +16,16 @@ import '../../core/network/api_client.dart';
import '../../core/services/haptic_service.dart'; import '../../core/services/haptic_service.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/storage/secure_storage.dart'; import '../../core/storage/secure_storage.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
Dio get _api => sl<ApiClient>().dio; Dio get _api => sl<ApiClient>().dio;
// Colours (inline, tidak butuh import app_colors.dart) // Colours (inline, tidak butuh import app_colors.dart)
const _kBlue = Color(0xFF1A56DB); const _kBlue = Color(0xFF1A56DB);
const _kRed = Color(0xFFDC2626); const _kRed = Color(0xFFDC2626);
const _kSurface = Color(0xFFF8FAFC);
const _kBorder = Color(0xFFE2E8F0);
const _kMuted = Color(0xFF64748B); const _kMuted = Color(0xFF64748B);
const _kText = Color(0xFF0F172A);
// Screen // Screen
@ -53,7 +52,6 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
// Account info (from SecureStorage) // Account info (from SecureStorage)
String _displayName = ''; String _displayName = '';
String _uniqueId = '';
// Pairing status // Pairing status
String _pairingStatus = ''; String _pairingStatus = '';
@ -75,7 +73,6 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
Future<void> _loadAccount() async { Future<void> _loadAccount() async {
final storage = sl<SecureStorage>(); final storage = sl<SecureStorage>();
_displayName = await storage.getDisplayName() ?? ''; _displayName = await storage.getDisplayName() ?? '';
_uniqueId = await storage.getUniqueUserId() ?? '';
} }
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
@ -91,6 +88,7 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
_ttsSpeed = (data['ttsSpeed'] as num?)?.toDouble() ?? 0.9; _ttsSpeed = (data['ttsSpeed'] as num?)?.toDouble() ?? 0.9;
_warnNoGuardian = data['warnNoGuardian'] as bool? ?? true; _warnNoGuardian = data['warnNoGuardian'] as bool? ?? true;
_hapticEnabled = data['hapticEnabled'] as bool? ?? true; _hapticEnabled = data['hapticEnabled'] as bool? ?? true;
sl<HapticService>().setEnabled(_hapticEnabled);
} }
}, },
onError: (_) {}, onError: (_) {},
@ -126,6 +124,7 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
// Apply TTS locally dulu // Apply TTS locally dulu
await sl<TtsService>().setLanguage(_ttsLanguage); await sl<TtsService>().setLanguage(_ttsLanguage);
context.read<AppCubit>().setLocaleCode(_ttsLanguage); context.read<AppCubit>().setLocaleCode(_ttsLanguage);
sl<HapticService>().setEnabled(_hapticEnabled);
if (_hapticEnabled) { if (_hapticEnabled) {
await sl<HapticService>().success(); await sl<HapticService>().success();
} }
@ -169,7 +168,15 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
// build // build
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.softBlueBg, Colors.white],
),
),
child: SafeArea(
child: _loading child: _loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: ListView( : ListView(
@ -177,12 +184,11 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
children: [ children: [
// header // header
Text('Settings', Text('Settings',
style: Theme.of(context) style: AppTextStyles.heading.copyWith(
.textTheme fontWeight: FontWeight.w800,
.headlineSmall )),
?.copyWith(fontWeight: FontWeight.w800)),
const Text('TTS, haptic, pairing, account', const Text('TTS, haptic, pairing, account',
style: TextStyle(color: _kMuted)), style: TextStyle(color: AppColors.muted)),
const SizedBox(height: 20), const SizedBox(height: 20),
// 1. TTS Settings // 1. TTS Settings
@ -202,7 +208,8 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
), ),
items: const [ items: const [
DropdownMenuItem( DropdownMenuItem(
value: 'id-ID', child: Text('Bahasa Indonesia')), value: 'id-ID',
child: Text('Bahasa Indonesia')),
DropdownMenuItem( DropdownMenuItem(
value: 'en-US', child: Text('English (US)')), value: 'en-US', child: Text('English (US)')),
], ],
@ -238,52 +245,6 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// unique ID
if (_uniqueId.isNotEmpty) ...[
const Text('Unique User ID',
style: TextStyle(
fontSize: 12,
color: _kMuted,
fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: _uniqueId));
_snack('ID disalin ke clipboard.');
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
const Icon(Icons.qr_code_2,
color: _kBlue, size: 20),
const SizedBox(width: 10),
Text(
_uniqueId,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
letterSpacing: 2,
color: _kBlue),
),
const Spacer(),
const Icon(Icons.copy,
color: _kMuted, size: 16),
],
),
),
),
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 12),
],
// pairing status
Row( Row(
children: [ children: [
Icon( Icon(
@ -313,6 +274,15 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text(
'Kode pairing dibuat dari menu Pairing dan hanya valid sementara. Jangan pakai Unique User ID untuk pairing.',
style: TextStyle(
color: _kMuted,
fontSize: 12,
height: 1.35,
),
),
const SizedBox(height: 12),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: OutlinedButton.icon( child: OutlinedButton.icon(
@ -335,18 +305,11 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.help_outline, color: _kBlue), leading: const Icon(Icons.help_outline, color: _kBlue),
title: const Text('Daftar Voice Commands & Shortcuts'), title: const Text('Daftar Voice Commands & Shortcuts'),
subtitle: subtitle: const Text(
const Text('Lihat semua perintah suara yang tersedia'), 'Lihat semua perintah suara yang tersedia'),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
// Route /user/manual belum ada di router context.go('/user/manual');
// tambahkan GoRoute('/user/manual', ManualScreen) di router.dart
// lalu ganti baris ini dengan: context.go('/user/manual');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Manual screen: tambah route /user/manual di router.dart')),
);
}, },
), ),
), ),
@ -374,7 +337,10 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
value: _hapticEnabled, value: _hapticEnabled,
onChanged: (v) => setState(() => _hapticEnabled = v), onChanged: (v) {
sl<HapticService>().setEnabled(v);
setState(() => _hapticEnabled = v);
},
title: const Text('Haptic feedback'), title: const Text('Haptic feedback'),
subtitle: subtitle:
const Text('Getaran saat obstacle terdeteksi'), const Text('Getaran saat obstacle terdeteksi'),
@ -453,6 +419,7 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
const SizedBox(height: 32), const SizedBox(height: 32),
], ],
), ),
),
); );
} }
@ -489,11 +456,18 @@ class _SectionHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Icon(icon, size: 18, color: _kBlue), Container(
width: 36,
height: 36,
decoration: AppDecorations.iconCircle(color: AppColors.softBlueBg),
child: Icon(icon, size: 18, color: AppColors.primaryBlue),
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(title, Text(title,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w800, fontSize: 14, color: _kBlue)), fontWeight: FontWeight.w800,
fontSize: 14,
color: AppColors.primaryBlue)),
], ],
); );
} }
@ -510,8 +484,8 @@ class _Card extends StatelessWidget {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(20),
border: Border.all(color: _kBorder), boxShadow: AppDecorations.cardShadow,
), ),
child: child, child: child,
); );
@ -533,16 +507,22 @@ class _InfoRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Icon(icon, size: 18, color: _kMuted), Container(
width: 36,
height: 36,
decoration: AppDecorations.iconCircle(color: AppColors.softBlueBg),
child: Icon(icon, size: 18, color: AppColors.primaryBlue),
),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, style: const TextStyle(fontSize: 12, color: _kMuted)), Text(label,
style: const TextStyle(fontSize: 12, color: AppColors.muted)),
Text(value, Text(value,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w700, color: _kText)), fontWeight: FontWeight.w700, color: AppColors.textDark)),
], ],
), ),
), ),
@ -550,12 +530,12 @@ class _InfoRow extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _kSurface, color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(50),
border: Border.all(color: _kBorder), border: Border.all(color: AppColors.border),
), ),
child: Text(note!, child: Text(note!,
style: const TextStyle(fontSize: 11, color: _kMuted)), style: const TextStyle(fontSize: 11, color: AppColors.muted)),
), ),
], ],
); );

View File

@ -14,6 +14,10 @@ import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/services/haptic_service.dart'; import '../../core/services/haptic_service.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import '../../shared/widgets/animations/animations.dart';
import 'application/sos_cubit.dart'; import 'application/sos_cubit.dart';
Dio get _api => sl<ApiClient>().dio; Dio get _api => sl<ApiClient>().dio;
@ -252,7 +256,16 @@ class _SosScreenState extends State<SosScreen>
: compact : compact
? 12.0 ? 12.0
: 24.0; : 24.0;
return SafeArea( return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: FadeSlideWrapper(
child: Padding( child: Padding(
padding: EdgeInsets.all(pagePadding), padding: EdgeInsets.all(pagePadding),
child: Column( child: Column(
@ -267,14 +280,13 @@ class _SosScreenState extends State<SosScreen>
children: [ children: [
Text( Text(
'SOS', 'SOS',
style: Theme.of(context) style: AppTextStyles.heading,
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w800),
), ),
const Text( Text(
'Emergency alert ke Guardian', 'Emergency alert ke Guardian',
style: TextStyle(color: Color(0xFF64748B)), style: AppTextStyles.body.copyWith(
color: AppColors.textMuted,
),
), ),
], ],
), ),
@ -338,7 +350,11 @@ class _SosScreenState extends State<SosScreen>
if (!landscapeTight) ...[ if (!landscapeTight) ...[
const Text( const Text(
'Riwayat SOS', 'Riwayat SOS',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16), style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 16,
color: AppColors.textDark,
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Expanded( Expanded(
@ -354,6 +370,8 @@ class _SosScreenState extends State<SosScreen>
], ],
), ),
), ),
),
),
); );
}, },
); );
@ -383,17 +401,32 @@ class _SosButton extends StatelessWidget {
: compact : compact
? 154.0 ? 154.0
: 200.0; : 200.0;
return SizedBox.square( return BounceTap(
dimension: dimension, onTap: onPressed,
child: FilledButton( child: Semantics(
style: FilledButton.styleFrom( button: true,
shape: const CircleBorder(), label: 'Kirim SOS',
backgroundColor: child: Container(
active ? const Color(0xFFB91C1C) : const Color(0xFFDC2626), width: dimension,
elevation: active ? 12 : 4, height: dimension,
shadowColor: const Color(0xFFDC2626).withValues(alpha: 0.5), decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: active
? const [Color(0xFFDC2626), Color(0xFF991B1B)]
: const [Color(0xFFFF5A5A), Color(0xFFDC2626)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
), ),
onPressed: onPressed, boxShadow: [
BoxShadow(
color: AppColors.danger.withValues(alpha: active ? 0.34 : 0.22),
blurRadius: active ? 28 : 18,
offset: const Offset(0, 10),
),
],
),
alignment: Alignment.center,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -415,6 +448,7 @@ class _SosButton extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }
@ -439,6 +473,7 @@ class _SendingIndicator extends StatelessWidget {
color: const Color(0xFFDC2626).withValues(alpha: 0.15), color: const Color(0xFFDC2626).withValues(alpha: 0.15),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFDC2626), width: 3), border: Border.all(color: const Color(0xFFDC2626), width: 3),
boxShadow: AppDecorations.cardShadow,
), ),
child: const Center( child: const Center(
child: Column( child: Column(
@ -470,8 +505,9 @@ class _ActiveSosBanner extends StatelessWidget {
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFEE2E2), color: const Color(0xFFFEE2E2),
borderRadius: BorderRadius.circular(12), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFFCA5A5), width: 1.5), border: Border.all(color: const Color(0xFFFCA5A5), width: 1.5),
boxShadow: AppDecorations.cardShadow,
), ),
child: Row( child: Row(
children: [ children: [
@ -531,10 +567,21 @@ class _SosHistory extends StatelessWidget {
if (events.isEmpty) { if (events.isEmpty) {
return _HistoryEmpty(onRefresh: onRefresh); return _HistoryEmpty(onRefresh: onRefresh);
} }
return ListView.separated( return RefreshIndicator(
itemCount: events.length, onRefresh: () async => onRefresh(),
separatorBuilder: (_, __) => const SizedBox(height: 8), child: ListView(
itemBuilder: (_, i) => _SosEventTile(event: events[i]), children: [
StaggerWrapper(
children: [
for (final event in events)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _SosEventTile(event: event),
),
],
),
],
),
); );
} }
} }
@ -570,8 +617,9 @@ class _SosEventTile extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: AppDecorations.cardShadow,
), ),
child: Row( child: Row(
children: [ children: [
@ -662,9 +710,10 @@ class _HistoryEmpty extends StatelessWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8FAFC), color: AppColors.cardWhite,
borderRadius: BorderRadius.circular(12), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: AppDecorations.cardShadow,
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -15,6 +15,9 @@ import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart'; import '../../core/network/api_client.dart';
import '../../core/services/location_reporter_service.dart'; import '../../core/services/location_reporter_service.dart';
import '../../core/services/tts_service.dart'; import '../../core/services/tts_service.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../shared/widgets/animations/animations.dart';
import 'application/walk_guide_cubit.dart'; import 'application/walk_guide_cubit.dart';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -48,7 +51,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
_scanCtrl = AnimationController( _scanCtrl = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 2200), duration: const Duration(milliseconds: 2200),
)..repeat(); );
_loadPairingStatus(); _loadPairingStatus();
} }
@ -73,8 +76,10 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
await _startCamera(); await _startCamera();
await sl<LocationReporterService>().start(walkGuideActive: true); await sl<LocationReporterService>().start(walkGuideActive: true);
await _cubit.start(); await _cubit.start();
_scanCtrl.repeat();
_cubit.updateStatus(_activeStatusText()); _cubit.updateStatus(_activeStatusText());
} else { } else {
_scanCtrl.stop();
await _stopCamera(); await _stopCamera();
await sl<LocationReporterService>().stop(); await sl<LocationReporterService>().stop();
await _cubit.stop(); await _cubit.stop();
@ -151,7 +156,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
); );
final controller = CameraController( final controller = CameraController(
backCamera, backCamera,
ResolutionPreset.medium, ResolutionPreset.low,
enableAudio: false, enableAudio: false,
imageFormatGroup: ImageFormatGroup.yuv420, imageFormatGroup: ImageFormatGroup.yuv420,
); );
@ -195,7 +200,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
void _onCameraImage(CameraImage image) { void _onCameraImage(CameraImage image) {
if (!_cubit.state.active || _processingFrame) return; if (!_cubit.state.active || _processingFrame) return;
final now = DateTime.now(); final now = DateTime.now();
if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 900)) { if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 1500)) {
return; return;
} }
_lastInferenceAt = now; _lastInferenceAt = now;
@ -258,6 +263,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
onPressed: () => context.go('/user/pairing'), onPressed: () => context.go('/user/pairing'),
icon: const Icon(Icons.link)), icon: const Icon(Icons.link)),
], ],
child: FadeSlideWrapper(
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
@ -290,7 +296,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
const SizedBox(width: 10), const SizedBox(width: 10),
_ActionSquare( _ActionSquare(
icon: Icons.sos_outlined, icon: Icons.sos_outlined,
color: const Color(0xFFDC2626), color: AppColors.danger,
onTap: () async { onTap: () async {
if (await _ensurePaired() && context.mounted) { if (await _ensurePaired() && context.mounted) {
context.go('/user/sos'); context.go('/user/sos');
@ -300,7 +306,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
const SizedBox(width: 10), const SizedBox(width: 10),
_ActionSquare( _ActionSquare(
icon: Icons.call_outlined, icon: Icons.call_outlined,
color: const Color(0xFF059669), color: AppColors.success,
onTap: () async { onTap: () async {
if (await _ensurePaired() && context.mounted) { if (await _ensurePaired() && context.mounted) {
context.go('/user/call'); context.go('/user/call');
@ -312,6 +318,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
], ],
), ),
), ),
),
); );
} }
} }
@ -336,8 +343,12 @@ class _VisionPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cameraReady = camera != null && camera!.value.isInitialized; final cameraReady = camera != null && camera!.value.isInitialized;
return ClipRRect( return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
boxShadow: AppDecorations.cardShadow,
),
clipBehavior: Clip.antiAlias,
child: DecoratedBox( child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFF07111F)), decoration: const BoxDecoration(color: Color(0xFF07111F)),
child: Stack( child: Stack(
@ -393,20 +404,21 @@ class _VisionPanel extends StatelessWidget {
), ),
), ),
Positioned( Positioned(
top: 18, top: 14,
left: 18, left: 14,
right: 18, right: 14,
child: Row( child: Wrap(
spacing: 8,
runSpacing: 6,
children: [ children: [
_Pill( _Pill(
text: state.active ? 'LIVE AI SCAN' : 'STANDBY', text: state.active ? 'LIVE AI' : 'STANDBY',
color: state.active color: state.active
? const Color(0xFF22C55E) ? const Color(0xFF22C55E)
: const Color(0xFFF59E0B), : const Color(0xFFF59E0B),
), ),
const SizedBox(width: 8),
_Pill( _Pill(
text: paired ? 'GUARDIAN LINKED' : 'PAIRING REQUIRED', text: paired ? 'LINKED' : 'PAIRING',
color: paired color: paired
? const Color(0xFF38BDF8) ? const Color(0xFF38BDF8)
: const Color(0xFFF97316), : const Color(0xFFF97316),
@ -444,47 +456,60 @@ class _VisionPanel extends StatelessWidget {
), ),
if (!paired && !pairingLoading) if (!paired && !pairingLoading)
Positioned.fill( Positioned.fill(
child: DecoratedBox( child: LayoutBuilder(
builder: (context, constraints) {
final compact = constraints.maxHeight < 270;
return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF020617).withValues(alpha: 0.72), color: const Color(0xFF020617).withValues(alpha: 0.72),
), ),
child: Center( child: Center(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(24), padding: EdgeInsets.symmetric(
horizontal: compact ? 16 : 24,
vertical: compact ? 10 : 20,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 70, width: compact ? 46 : 64,
height: 70, height: compact ? 46 : 64,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFBEB) color: const Color(0xFFFFFBEB)
.withValues(alpha: 0.14), .withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(16),
border: border: Border.all(
Border.all(color: const Color(0xFFF59E0B)), color: const Color(0xFFF59E0B)),
), ),
child: const Icon(Icons.link_off, child: Icon(Icons.link_off,
color: Color(0xFFFBBF24), size: 34), color: const Color(0xFFFBBF24),
size: compact ? 24 : 32),
), ),
const SizedBox(height: 14), SizedBox(height: compact ? 8 : 12),
const Text( Text(
'Guardian belum terhubung', 'Guardian belum terhubung',
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 22, fontSize: compact ? 16 : 21,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
), ),
), ),
if (!compact) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
const Text( const Text(
'Pairing dulu supaya SOS, panggilan, dan pemantauan live punya penerima yang jelas.', 'Pairing dulu supaya SOS, panggilan, dan pemantauan live punya penerima yang jelas.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: maxLines: 2,
TextStyle(color: Colors.white70, height: 1.35), overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white70, height: 1.35),
), ),
const SizedBox(height: 16), ],
SizedBox(height: compact ? 10 : 14),
FilledButton.icon( FilledButton.icon(
onPressed: onPairingTap, onPressed: onPairingTap,
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
@ -494,6 +519,8 @@ class _VisionPanel extends StatelessWidget {
), ),
), ),
), ),
);
},
), ),
), ),
Positioned( Positioned(
@ -624,15 +651,8 @@ class _MetricChip extends StatelessWidget {
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE2E8F0)), boxShadow: AppDecorations.cardShadow,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 14,
offset: const Offset(0, 8),
),
],
), ),
child: Row( child: Row(
children: [ children: [
@ -680,12 +700,20 @@ class _ActionSquare extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return BounceTap(
color: color,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(14), child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(50),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.22),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
),
child: SizedBox( child: SizedBox(
width: 54, width: 54,
height: 50, height: 50,
@ -816,7 +844,7 @@ class _Page extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Color(0xFFF8FAFC), Color(0xFFEFF6FF)], colors: [AppColors.softBlueBg, Colors.white],
), ),
), ),
child: Padding( child: Padding(
@ -835,12 +863,11 @@ class _Page extends StatelessWidget {
width: compact ? 38 : 46, width: compact ? 38 : 46,
height: compact ? 38 : 46, height: compact ? 38 : 46,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF2563EB), gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: color: AppColors.primaryBlue.withValues(alpha: 0.28),
const Color(0xFF2563EB).withValues(alpha: 0.28),
blurRadius: 18, blurRadius: 18,
offset: const Offset(0, 8), offset: const Offset(0, 8),
), ),
@ -862,14 +889,14 @@ class _Page extends StatelessWidget {
.headlineSmall .headlineSmall
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: const Color(0xFF0F172A), color: AppColors.textDark,
fontSize: compact ? 22 : null, fontSize: compact ? 22 : null,
)), )),
if (subtitle != null) if (subtitle != null)
Text(subtitle!, Text(subtitle!,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF64748B))), style: const TextStyle(color: AppColors.muted)),
], ],
), ),
), ),

View File

@ -2,18 +2,10 @@ import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'app/injection_container.dart'; import 'app/injection_container.dart';
import 'app/app.dart'; import 'app/app.dart';
import 'core/constants/app_constants.dart'; import 'core/constants/app_constants.dart';
import 'core/utils/init_guard.dart'; import 'shared/widgets/walkguide_loading_screen.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
Future<void> main() async { Future<void> main() async {
await runZonedGuarded( await runZonedGuarded(
@ -21,18 +13,7 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
_installGlobalErrorUi(); _installGlobalErrorUi();
await AppConstants.clearServerUrl(); await AppConstants.clearServerUrl();
runApp(const WalkGuideBootApp());
if (!kIsWeb) {
final firebaseApp = await ignoreInitFailure(
() => Firebase.initializeApp(),
label: 'Firebase init',
);
if (firebaseApp != null) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
}
}
try { try {
await initDependencies(); await initDependencies();
@ -51,6 +32,20 @@ Future<void> main() async {
); );
} }
class WalkGuideBootApp extends StatelessWidget {
const WalkGuideBootApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: WalkGuideLoadingScreen(
subtitle: 'Preparing connection setup',
),
);
}
}
void _installGlobalErrorUi() { void _installGlobalErrorUi() {
FlutterError.onError = (details) { FlutterError.onError = (details) {
FlutterError.presentError(details); FlutterError.presentError(details);

View File

@ -0,0 +1,3 @@
export 'bounce_tap.dart';
export 'fade_slide_wrapper.dart';
export 'stagger_wrapper.dart';

View File

@ -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,
),
),
);
}
}

View File

@ -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,
),
),
);
}
}

View File

@ -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,
),
),
);
}
}

View File

@ -1,5 +1,7 @@
// ignore_for_file: prefer_const_constructors, sort_child_properties_last // ignore_for_file: prefer_const_constructors, sort_child_properties_last
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.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/tts_service.dart';
import '../../core/services/voice_command_handler.dart'; import '../../core/services/voice_command_handler.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import 'animations/animations.dart';
class UserShell extends StatefulWidget { class UserShell extends StatefulWidget {
final Widget child; final Widget child;
@ -26,9 +30,6 @@ class _UserShellState extends State<UserShell> {
super.initState(); super.initState();
_loadVoiceCommands(); _loadVoiceCommands();
_startHardwareShortcuts(); _startHardwareShortcuts();
WidgetsBinding.instance.addPostFrameCallback((_) {
_startVoiceListening();
});
sl<VoiceCommandHandler>().onCommand = (key) { sl<VoiceCommandHandler>().onCommand = (key) {
if (!mounted) return; if (!mounted) return;
switch (key) { 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 { Future<void> _loadVoiceCommands() async {
await runFriendlyAction( await runFriendlyAction(
() async { () async {
@ -139,6 +129,7 @@ class _UserShellState extends State<UserShell> {
@override @override
void dispose() { void dispose() {
sl<HardwareShortcutListener>().stopListening(); sl<HardwareShortcutListener>().stopListening();
unawaited(sl<SttService>().stopListening());
super.dispose(); super.dispose();
} }
@ -198,15 +189,7 @@ class _AppShell extends StatelessWidget {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final useRail = constraints.maxWidth >= 760; final useRail = constraints.maxWidth >= 760;
final content = AnimatedSwitcher( final content = child;
duration: const Duration(milliseconds: 180),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
child: KeyedSubtree(
key: ValueKey(location),
child: child,
),
);
return Scaffold( return Scaffold(
backgroundColor: AppColors.surface, backgroundColor: AppColors.surface,
@ -257,73 +240,42 @@ class _RailNavigation extends StatelessWidget {
final itemHeight = compact ? 58.0 : 70.0; final itemHeight = compact ? 58.0 : 70.0;
return DecoratedBox( 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( child: SafeArea(
right: false, right: false,
child: SizedBox( child: SizedBox(
width: width, width: width,
child: ListView.separated( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: compact ? 6 : 12, 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( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( for (var index = 0; index < items.length; index++)
selected ? item.selectedIcon : item.icon, Expanded(
size: compact ? 23 : 25, child: Center(
color: selected child: _RailNavItem(
? AppColors.primary item: items[index],
: const Color(0xFF334155), selected: index == selectedIndex,
compact: compact,
height: itemHeight,
), ),
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),
), ),
), ),
], ],
), ),
), ),
), ),
);
},
),
),
), ),
); );
}, },
@ -344,74 +296,89 @@ class _BottomScrollNavigation extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).padding.bottom; final bottom = MediaQuery.of(context).padding.bottom;
final extraBottom = bottom > 12 ? 12.0 : bottom; final extraBottom = bottom > 12 ? 12.0 : bottom;
return DecoratedBox( return Container(
margin: EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: const Border(top: BorderSide(color: AppColors.border)), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [ boxShadow: [
BoxShadow( AppDecorations.cardShadow.first,
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 18,
offset: const Offset(0, -8),
),
], ],
), ),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: SafeArea( child: SafeArea(
top: false, top: false,
child: SizedBox( child: SizedBox(
height: 68 + extraBottom, height: 68 + extraBottom,
child: ListView.separated( child: Row(
scrollDirection: Axis.horizontal, children: [
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), for (var index = 0; index < items.length; index++)
itemCount: items.length, Expanded(
separatorBuilder: (_, __) => const SizedBox(width: 6), child: _BottomNavItem(
itemBuilder: (context, index) { item: items[index],
final item = items[index]; selected: index == selectedIndex,
final 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( return Semantics(
button: true, button: true,
selected: selected, selected: selected,
label: item.label, label: item.label,
child: InkWell( child: BounceTap(
borderRadius: BorderRadius.circular(12),
onTap: () => context.go(item.route), onTap: () => context.go(item.route),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 160), duration: const Duration(milliseconds: 160),
width: 72, curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(horizontal: 6), height: height,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected color: selected ? AppColors.softBlueBg : Colors.transparent,
? const Color(0xFFEFF6FF) borderRadius: BorderRadius.circular(24),
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: selected
? const Color(0xFFBFDBFE)
: Colors.transparent,
),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
selected ? item.selectedIcon : item.icon, selected ? item.selectedIcon : item.icon,
color: selected size: compact ? 21 : 24,
? AppColors.primary color: selected ? AppColors.primary : AppColors.muted,
: const Color(0xFF64748B),
size: 22,
), ),
const SizedBox(height: 4), SizedBox(height: compact ? 1 : 4),
Text( Text(
item.label, item.label,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: compact ? 9.5 : 11.5,
fontWeight: height: 1,
selected ? FontWeight.w800 : FontWeight.w600, fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
color: selected color: selected ? AppColors.primary : AppColors.muted,
? AppColors.primary
: const Color(0xFF64748B),
), ),
), ),
], ],
@ -419,7 +386,64 @@ class _BottomScrollNavigation extends StatelessWidget {
), ),
), ),
); );
}, }
}
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,
),
),
],
),
), ),
), ),
), ),

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/theme/app_colors.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 { class FeaturePage extends StatelessWidget {
final String title; final String title;
@ -18,14 +21,23 @@ class FeaturePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final short = constraints.maxHeight < 520; final short = constraints.maxHeight < 520;
final compact = constraints.maxWidth < 420 || short; final compact = constraints.maxWidth < 420 || short;
final wide = constraints.maxWidth >= 900; final wide = constraints.maxWidth >= 900;
final horizontal = compact ? 12.0 : 20.0; final horizontal = compact ? 12.0 : 20.0;
return Padding( return FadeSlideWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
horizontal, horizontal,
short ? 8 : 12, short ? 8 : 12,
@ -40,18 +52,7 @@ class FeaturePage extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TweenAnimationBuilder<double>( compact
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( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -81,7 +82,6 @@ class FeaturePage extends StatelessWidget {
if (trailing != null) trailing!, if (trailing != null) trailing!,
], ],
), ),
),
SizedBox(height: short ? 8 : (compact ? 12 : 16)), SizedBox(height: short ? 8 : (compact ? 12 : 16)),
Expanded( Expanded(
child: child, child: child,
@ -90,9 +90,11 @@ class FeaturePage extends StatelessWidget {
), ),
), ),
), ),
),
); );
}, },
), ),
),
); );
} }
} }
@ -118,10 +120,8 @@ class _FeatureHeading extends StatelessWidget {
title, title,
maxLines: short ? 1 : 2, maxLines: short ? 1 : 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineSmall?.copyWith( style: AppTextStyles.heading.copyWith(
fontSize: compact ? 22 : null, fontSize: compact ? 22 : null,
fontWeight: FontWeight.w900,
color: AppColors.text,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@ -129,10 +129,9 @@ class _FeatureHeading extends StatelessWidget {
subtitle, subtitle,
maxLines: short ? 1 : (compact ? 2 : 3), maxLines: short ? 1 : (compact ? 2 : 3),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: AppTextStyles.body.copyWith(
color: AppColors.muted, color: AppColors.textMuted,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
height: 1.25,
), ),
), ),
], ],
@ -157,8 +156,10 @@ class FeatureEmptyPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: ConstrainedBox( child: Container(
constraints: const BoxConstraints(maxWidth: 360), constraints: const BoxConstraints(maxWidth: 380),
padding: const EdgeInsets.all(24),
decoration: AppDecorations.card,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -167,7 +168,7 @@ class FeatureEmptyPanel extends StatelessWidget {
height: 72, height: 72,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.08), color: AppColors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(50),
border: Border.all(color: AppColors.border), border: Border.all(color: AppColors.border),
), ),
child: Icon(icon, size: 36, color: AppColors.primary), child: Icon(icon, size: 36, color: AppColors.primary),
@ -214,8 +215,9 @@ class FeatureErrorPanel extends StatelessWidget {
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFEF2F2), color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(8), borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFFECACA)), border: Border.all(color: const Color(0xFFFECACA)),
boxShadow: AppDecorations.cardShadow,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -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,
),
),
);
},
),
);
},
);
}
}

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/battery_plus-6.2.3/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/connectivity_plus-6.1.5/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/device_info_plus-11.5.0/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_local_notifications_linux-4.0.1/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/record_linux-1.3.0/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_linux-2.4.1/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/

View File

@ -0,0 +1 @@
C:/Users/Evan/AppData/Local/Pub/Cache/hosted/pub.dev/tflite_flutter-0.12.1/

View File

@ -0,0 +1,23 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
}

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -0,0 +1,27 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
record_linux
sqlite3_flutter_libs
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
tflite_flutter
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@ -0,0 +1,48 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import agora_rtc_engine
import audio_session
import battery_plus
import connectivity_plus
import device_info_plus
import firebase_core
import firebase_messaging
import flutter_local_notifications
import flutter_secure_storage_macos
import flutter_tts
import geolocator_apple
import iris_method_channel
import just_audio
import path_provider_foundation
import record_darwin
import shared_preferences_foundation
import speech_to_text
import sqflite_darwin
import sqlite3_flutter_libs
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AgoraRtcNgPlugin.register(with: registry.registrar(forPlugin: "AgoraRtcNgPlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
IrisMethodChannelPlugin.register(with: registry.registrar(forPlugin: "IrisMethodChannelPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
}

View File

@ -0,0 +1,12 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=D:\Tools\Flutter\flutter
FLUTTER_APPLICATION_PATH=D:\CodeSpace\Final Project Gabungan - Broken Test\walkguide-mobile\walkguide_app
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
FLUTTER_CLI_BUILD_MODE=debug
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=.dart_tool/package_config.json

View File

@ -0,0 +1,13 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=D:\Tools\Flutter\flutter"
export "FLUTTER_APPLICATION_PATH=D:\CodeSpace\Final Project Gabungan - Broken Test\walkguide-mobile\walkguide_app"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1"
export "FLUTTER_CLI_BUILD_MODE=debug"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=.dart_tool/package_config.json"