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
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)
// Browser/Postman bisa pakai SockJS fallback: http://host:port/ws
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*") // Allow semua origin untuk testing HP di LAN
.withSockJS(); // SockJS fallback untuk browser compatibility
.setAllowedOriginPatterns("*"); // Allow semua origin untuk testing HP di LAN
// Endpoint fallback SockJS untuk browser tooling bila dibutuhkan.
registry.addEndpoint("/ws-sockjs")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}

View File

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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<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 -->
<!-- <item>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 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
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
@ -12,7 +12,7 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">#F8FAFC</item>
</style>
</resources>

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,11 @@ class _AuthInterceptor extends Interceptor {
@override
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;
try {
final refresh = await _storage.getRefreshToken();
@ -87,15 +91,21 @@ class _AuthInterceptor extends Interceptor {
// Retry original request
err.requestOptions.headers['Authorization'] =
'Bearer ${data['accessToken']}';
try {
final retryRes = await _dio.fetch(err.requestOptions);
_refreshing = false;
handler.resolve(retryRes);
} on DioException catch (retryErr) {
_refreshing = false;
handler.next(retryErr);
}
return;
}
} catch (_) {}
_refreshing = false;
} catch (_) {
await _storage.clearAll();
}
_refreshing = false;
}
handler.next(err);
}
}

View File

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

View File

@ -1,36 +1,107 @@
import 'package:flutter/services.dart';
import 'package:vibration/vibration.dart';
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 {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 500, 100, 500, 100, 500]);
await _vibrate(
pattern: [0, 500, 100, 500, 100, 500],
fallback: HapticFeedback.heavyImpact,
obstacle: true,
);
}
Future<void> obstacleClose() async {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 300, 100, 300]);
await _vibrate(
pattern: [0, 300, 100, 300],
fallback: HapticFeedback.mediumImpact,
obstacle: true,
);
}
Future<void> obstacleMedium() async {
if (!await _hasVibrator) return;
Vibration.vibrate(duration: 150);
await _vibrate(
duration: 150,
fallback: HapticFeedback.lightImpact,
obstacle: true,
);
}
Future<void> sosTriggered() async {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 1000, 200, 1000, 200, 1000]);
await _vibrate(
pattern: [0, 1000, 200, 1000, 200, 1000],
fallback: HapticFeedback.heavyImpact,
);
}
Future<void> callIncoming() async {
if (!await _hasVibrator) return;
Vibration.vibrate(pattern: [0, 500, 500, 500, 500, 500, 500, 500]);
await _vibrate(
pattern: [0, 500, 500, 500, 500, 500, 500, 500],
fallback: HapticFeedback.mediumImpact,
);
}
Future<void> success() async {
if (!await _hasVibrator) return;
Vibration.vibrate(duration: 80);
await _vibrate(
duration: 80,
fallback: HapticFeedback.selectionClick,
);
}
Future<void> stop() async => Vibration.cancel();

View File

@ -27,11 +27,14 @@ class HardwareShortcutBinding {
class HardwareShortcutListener {
final ApiClient _apiClient;
final Map<int, HardwareShortcutBinding> _bindings = {};
final Map<int, DateTime> _lastHandledAt = {};
bool _listening = false;
void Function(HardwareShortcutAction action)? _onAction;
void Function(int buttonCode, String buttonName)? _captureCallback;
static const Duration _repeatDebounce = Duration(milliseconds: 900);
HardwareShortcutListener(this._apiClient);
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;
}
@ -88,6 +92,12 @@ class HardwareShortcutListener {
final binding = _bindings[code];
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);
return true;
}
@ -103,7 +113,8 @@ HardwareShortcutBinding? _bindingFromJson(Map<String, dynamic> item) {
final action = _actionFromBackend(item['shortcutKey']?.toString());
final rawCode = item['buttonCode'];
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;
return HardwareShortcutBinding(
action: action,

View File

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

View File

@ -4,9 +4,15 @@ class TtsService {
final FlutterTts _tts = FlutterTts();
final List<String> _queue = [];
bool _speaking = false;
bool _initialized = false;
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.setPitch(pitch);
await _tts.setSpeechRate(rate);
@ -15,11 +21,25 @@ class TtsService {
_speaking = false;
_processQueue();
});
_initialized = true;
}
/// Tambah ke antrian - tidak memotong yg sedang bicara
void speak(String text) {
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);
if (!_speaking) _processQueue();
}
@ -27,6 +47,7 @@ class TtsService {
/// Potong TTS sekarang dan langsung ucapkan ini - untuk obstacle alert
Future<void> speakImmediate(String text) async {
if (text.isEmpty) return;
await init();
_queue.clear();
await _tts.stop();
_speaking = true;
@ -43,9 +64,20 @@ class TtsService {
String get lastSpoken => _lastSpoken;
bool get isSpeaking => _speaking;
Future<void> setLanguage(String lang) async => _tts.setLanguage(lang);
Future<void> setPitch(double pitch) async => _tts.setPitch(pitch);
Future<void> setRate(double rate) async => _tts.setSpeechRate(rate);
Future<void> setLanguage(String lang) async {
await init(language: lang);
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() {
if (_lastSpoken.isNotEmpty) speak(_lastSpoken);

View File

@ -1,15 +1,28 @@
import 'package:flutter/material.dart';
class AppColors {
static const primary = Color(0xFF2563EB);
static const primaryDark = Color(0xFF0F3EA8);
static const accent = Color(0xFF0891B2);
static const primaryBlue = Color(0xFF4A90D9);
static const softBlueBg = Color(0xFFEBF4FF);
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 danger = Color(0xFFDC2626);
static const success = Color(0xFF059669);
static const surface = Color(0xFFF7FAFC);
static const surfaceRaised = Color(0xFFFFFFFF);
static const text = Color(0xFF0F172A);
static const muted = Color(0xFF64748B);
static const surface = softBlueBg;
static const surfaceRaised = cardWhite;
static const text = textDark;
static const muted = textMuted;
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/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;
@ -103,7 +106,16 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
@override
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(
padding: const EdgeInsets.all(16),
child: Column(
@ -118,10 +130,7 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
children: [
Text(
'Activity Log',
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w800),
style: AppTextStyles.heading,
),
Text(
'${_items.length} aktivitas tercatat',
@ -155,7 +164,17 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
onSelected: (_) {
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,
labelStyle: TextStyle(
color: selected ? AppColors.primary : AppColors.muted,
@ -180,16 +199,23 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
? _EmptyPanel(filter: _selectedFilter)
: RefreshIndicator(
onRefresh: _load,
child: ListView.builder(
itemCount: _filtered.length,
itemBuilder: (ctx, i) =>
_LogCard(item: _filtered[i]),
child: ListView(
children: [
StaggerWrapper(
children: [
for (final item in _filtered)
_LogCard(item: item),
],
),
],
),
),
),
],
),
),
),
),
);
}
}
@ -228,7 +254,15 @@ class _LogCard extends StatelessWidget {
Widget build(BuildContext context) {
final meta = _logMeta(item.logType);
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -240,15 +274,10 @@ class _LogCard extends StatelessWidget {
height: 36,
decoration: BoxDecoration(
color: meta.color.withValues(alpha: 0.12),
shape: BoxShape.circle,
borderRadius: BorderRadius.circular(50),
),
child: Icon(meta.icon, color: meta.color, size: 18),
),
Container(
width: 1.5,
height: 20,
color: const Color(0xFFE2E8F0),
),
],
),
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: const EdgeInsets.only(top: 2),
child: Text(
@ -294,6 +324,7 @@ class _LogCard extends StatelessWidget {
),
],
),
),
);
}
@ -394,6 +425,13 @@ class _ErrorPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -410,6 +448,7 @@ class _ErrorPanel extends StatelessWidget {
),
],
),
),
);
}
}
@ -421,6 +460,13 @@ class _EmptyPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: Column(
mainAxisSize: MainAxisSize.min,
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 '../../core/ai/detection_export.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/utils/operation_guard.dart';
import '../../shared/widgets/animations/animations.dart';
import '../../shared/widgets/feature_page.dart';
class AiBenchmarkScreen extends StatefulWidget {
@ -125,8 +128,12 @@ class _AiBenchmarkScreenState extends State<AiBenchmarkScreen> {
enableAudio: false,
);
controller = activeController;
await activeController.initialize().timeout(const Duration(seconds: 5));
await activeController.takePicture().timeout(const Duration(seconds: 5));
await activeController
.initialize()
.timeout(const Duration(seconds: 5));
await activeController
.takePicture()
.timeout(const Duration(seconds: 5));
}
},
onError: (_) {},
@ -198,7 +205,11 @@ class _AiBenchmarkScreenState extends State<AiBenchmarkScreen> {
label: const Text('Clear log'),
),
const SizedBox(height: 16),
StaggerWrapper(
children: [
for (final run in _runs) _BenchmarkCard(run: run),
],
),
if (_runs.isEmpty)
const FeatureEmptyPanel(
icon: Icons.speed,
@ -224,9 +235,10 @@ class _BenchmarkCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE2E8F0)),
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -262,7 +274,8 @@ class _StatusBox extends StatelessWidget {
return DecoratedBox(
decoration: BoxDecoration(
color: success ? const Color(0xFFF0FDF4) : const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(14),
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: Padding(
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/websocket_service.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
@ -79,7 +82,8 @@ class _LoginScreenState extends State<LoginScreen> {
},
onError: (message) => _snack(context, message),
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);
}
@ -150,37 +154,25 @@ class _AuthFrame extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFEAF4FF),
backgroundColor: AppColors.softBlueBg,
body: LayoutBuilder(
builder: (context, constraints) {
final compact =
constraints.maxWidth < 480 || constraints.maxHeight < 720;
return Stack(
children: [
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.softBlueBg,
Colors.white,
AppColors.softPinkBg,
],
),
),
),
),
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: SafeArea(
child: Center(
child: SingleChildScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
@ -206,19 +198,10 @@ class _AuthFrame extends StatelessWidget {
child: RepaintBoundary(
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96),
color: AppColors.cardWhite,
borderRadius:
BorderRadius.circular(compact ? 22 : 30),
border: Border.all(
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),
),
],
BorderRadius.circular(compact ? 22 : 28),
boxShadow: AppDecorations.cardShadow,
),
child: Padding(
padding: EdgeInsets.fromLTRB(
@ -236,13 +219,15 @@ class _AuthFrame extends StatelessWidget {
width: compact ? 44 : 56,
height: compact ? 44 : 56,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF2563EB),
Color(0xFF0891B2)
],
),
gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x334A90D9),
blurRadius: 18,
offset: Offset(0, 8),
),
],
),
child: Icon(Icons.navigation_rounded,
color: Colors.white,
@ -256,8 +241,8 @@ class _AuthFrame extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFF0F172A),
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
),
@ -269,21 +254,22 @@ class _AuthFrame extends StatelessWidget {
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(999),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.shield_outlined,
size: 14, color: Color(0xFF1D4ED8)),
size: 14,
color: AppColors.primaryBlue),
SizedBox(width: 6),
Text(
'Secure Assistive Navigation',
style: TextStyle(
color: Color(0xFF1D4ED8),
color: AppColors.primaryBlue,
fontSize: 11,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
),
),
],
@ -294,13 +280,9 @@ class _AuthFrame extends StatelessWidget {
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.headlineMedium
?.copyWith(
style: AppTextStyles.heading.copyWith(
fontSize: compact ? 26 : null,
fontWeight: FontWeight.w900,
color: const Color(0xFF0F172A),
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
@ -309,7 +291,7 @@ class _AuthFrame extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFF64748B),
color: AppColors.muted,
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 '../../core/errors/friendly_error.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
@ -69,7 +73,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
},
onError: (message) => _snack(context, message),
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);
}
@ -128,7 +133,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _role == 'USER'
? const Color(0xFFEFF6FF)
? AppColors.softBlueBg
: const Color(0xFFF0FDF4),
borderRadius: BorderRadius.circular(999),
),
@ -234,18 +239,19 @@ class _RoleCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return BounceTap(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: selected ? const Color(0xFFEFF6FF) : Colors.white,
borderRadius: BorderRadius.circular(14),
color: selected ? AppColors.softBlueBg : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected ? const Color(0xFF1A56DB) : const Color(0xFFE2E8F0),
color: selected ? AppColors.primaryBlue : AppColors.border,
width: selected ? 2 : 1,
),
boxShadow: selected ? AppDecorations.cardShadow : null,
),
child: Row(
children: [
@ -253,10 +259,9 @@ class _RoleCard extends StatelessWidget {
width: 48,
height: 48,
decoration: BoxDecoration(
color: selected
? const Color(0xFF1A56DB)
: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(12),
color:
selected ? AppColors.primaryBlue : const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(50),
),
child: Icon(icon,
color: selected ? Colors.white : const Color(0xFF64748B)),
@ -267,16 +272,16 @@ class _RoleCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontWeight: FontWeight.w800, fontSize: 16)),
style: AppTextStyles.subheading.copyWith(fontSize: 16)),
Text(subtitle,
style: const TextStyle(
color: Color(0xFF64748B), fontSize: 13)),
color: AppColors.muted, fontSize: 13)),
],
),
),
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
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFEAF4FF),
backgroundColor: AppColors.softBlueBg,
body: LayoutBuilder(
builder: (context, constraints) {
final compact =
constraints.maxWidth < 480 || constraints.maxHeight < 720;
return Stack(
children: [
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.softBlueBg,
Colors.white,
AppColors.softPinkBg,
],
),
),
),
),
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: SafeArea(
child: Center(
child: SingleChildScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
@ -354,19 +347,10 @@ class _AuthFrame extends StatelessWidget {
child: RepaintBoundary(
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96),
color: AppColors.cardWhite,
borderRadius:
BorderRadius.circular(compact ? 22 : 30),
border: Border.all(
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),
),
],
BorderRadius.circular(compact ? 22 : 28),
boxShadow: AppDecorations.cardShadow,
),
child: Padding(
padding: EdgeInsets.fromLTRB(
@ -384,8 +368,15 @@ class _AuthFrame extends StatelessWidget {
width: compact ? 44 : 56,
height: compact ? 44 : 56,
decoration: BoxDecoration(
color: const Color(0xFF1D4ED8),
gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x334A90D9),
blurRadius: 18,
offset: Offset(0, 8),
),
],
),
child: Icon(Icons.navigation_rounded,
color: Colors.white,
@ -399,8 +390,8 @@ class _AuthFrame extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFF0F172A),
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
),
@ -411,13 +402,9 @@ class _AuthFrame extends StatelessWidget {
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.headlineMedium
?.copyWith(
style: AppTextStyles.heading.copyWith(
fontSize: compact ? 26 : null,
fontWeight: FontWeight.w900,
color: const Color(0xFF0F172A),
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
@ -426,7 +413,7 @@ class _AuthFrame extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFF64748B),
color: AppColors.muted,
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/websocket_service.dart';
import '../../core/storage/secure_storage.dart';
import '../../shared/widgets/walkguide_loading_screen.dart';
// ---------------------------------------------------------------------------
// SplashScreen
@ -39,32 +40,35 @@ class SplashScreen extends StatefulWidget {
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _animCtrl;
late final Animation<double> _fadeAnim;
late final AnimationController _screenCtrl;
late final Animation<double> _screenFade;
@override
void initState() {
super.initState();
_animCtrl = AnimationController(
_screenCtrl = AnimationController(
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();
}
@override
void dispose() {
_animCtrl.dispose();
_screenCtrl.dispose();
super.dispose();
}
Future<void> _route() async {
final routed = await runFriendlyAction(
() async {
// Animasi logo selalu tampil minimal 500ms agar tidak langsung flash.
await Future.delayed(const Duration(milliseconds: 500));
// Animasi logo selalu tampil minimal 900ms agar tidak langsung flash.
await Future.delayed(const Duration(milliseconds: 900));
final storage = sl<SecureStorage>();
final token = await storage.getAccessToken().timeout(
@ -77,7 +81,7 @@ class _SplashScreenState extends State<SplashScreen>
if (!mounted) return;
if (token == null || role == null) {
context.go('/login');
await _fadeOutThenGo('/login');
return;
}
@ -88,67 +92,28 @@ class _SplashScreenState extends State<SplashScreen>
sl<IncomingCallPollingService>().start();
}
// Auto-login: arahkan ke home sesuai role.
context.go(
await _fadeOutThenGo(
role == 'ROLE_GUARDIAN' ? '/guardian/dashboard' : '/user/walkguide',
);
},
onError: (_) {},
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
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF1A56DB),
body: Center(
child: FadeTransition(
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,
),
),
],
),
),
return FadeTransition(
opacity: _screenFade,
child: const WalkGuideLoadingScreen(
subtitle: 'Restoring your session',
),
);
}

View File

@ -10,8 +10,11 @@ import '../../core/services/call_service.dart';
import '../../core/services/haptic_service.dart';
import '../../core/services/tts_service.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 _kRed = Color(0xFFDC2626);
const _kMuted = Color(0xFF64748B);
@ -588,11 +591,21 @@ class _CallScaffold extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
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(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
const SizedBox(width: 48),
@ -614,6 +627,8 @@ class _CallScaffold extends StatelessWidget {
],
),
),
),
),
);
}
}
@ -666,7 +681,8 @@ class _Avatar extends StatelessWidget {
decoration: BoxDecoration(
shape: BoxShape.circle,
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),
);
@ -688,7 +704,7 @@ class _ControlButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return BounceTap(
onTap: onTap,
child: Column(
children: [
@ -718,7 +734,7 @@ class _EndCallButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return BounceTap(
onTap: onTap,
child: Column(
children: [
@ -754,7 +770,7 @@ class _RoundCallButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return BounceTap(
onTap: onTap,
child: Opacity(
opacity: onTap == null ? 0.4 : 1,

View File

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

View File

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

View File

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

View File

@ -8,6 +8,9 @@ import 'package:record/record.dart';
import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.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';
class GuardianSendNotifScreen extends StatefulWidget {
@ -132,19 +135,14 @@ class _GuardianSendNotifScreenState extends State<GuardianSendNotifScreen> {
subtitle: 'Kirim pesan singkat ke User yang sudah pairing',
child: ListView(
children: [
Container(
FadeSlideWrapper(
child: Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: const Color(0xFF1E293B).withValues(alpha: 0.06),
blurRadius: 22,
offset: const Offset(0, 12),
),
],
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@ -185,8 +183,8 @@ class _GuardianSendNotifScreenState extends State<GuardianSendNotifScreen> {
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE2E8F0)),
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: AppColors.border),
),
child: Row(
children: [
@ -229,7 +227,9 @@ class _GuardianSendNotifScreenState extends State<GuardianSendNotifScreen> {
),
FilledButton.icon(
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'),
style: FilledButton.styleFrom(
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/errors/friendly_error.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 '../../shared/widgets/feature_page.dart';
@ -48,6 +51,8 @@ class GuardianSettingsScreen extends StatelessWidget {
title: 'Guardian Settings',
subtitle: 'Account, pairing, AI tools, and server',
child: ListView(
children: [
StaggerWrapper(
children: [
_SettingsTile(
icon: Icons.link,
@ -67,6 +72,8 @@ class GuardianSettingsScreen extends StatelessWidget {
subtitle: 'Atur threshold deteksi dan label yang aktif.',
onTap: () => context.go('/guardian/ai-config'),
),
],
),
const SizedBox(height: 18),
OutlinedButton.icon(
onPressed: () => _changeServer(context),
@ -103,19 +110,28 @@ class _SettingsTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
return BounceTap(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE2E8F0)),
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: AppColors.border),
boxShadow: AppDecorations.cardShadow,
),
child: ListTile(
leading: Icon(icon, color: const Color(0xFF1D4ED8)),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w800)),
leading: Container(
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),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
),
);
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_text_styles.dart';
class HomeScreen extends StatelessWidget {
final String role;
@ -7,12 +10,24 @@ class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Dashboard Walk Guide')),
body: Center(
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.softBlueBg, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
role == 'ROLE_ADMIN' ? 'Selamat Datang Admin!' : 'Mode Walk Guide Siap!',
style: const TextStyle(fontSize: 24),
role == 'ROLE_ADMIN'
? '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/incoming_call_polling_service.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 '../../../shared/widgets/animations/animations.dart';
// âââ¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬ÃƒÂ¢Ã¢â¬ÂÃ¢âšÂ¬
// GUARDIAN DASHBOARD SCREEN
@ -382,7 +385,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF0F4FF),
backgroundColor: AppColors.softBlueBg,
body: SafeArea(
child: Column(
children: [
@ -398,6 +401,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: FadeSlideWrapper(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -420,6 +424,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
),
),
),
),
],
),
),
@ -1613,13 +1618,17 @@ class _KpiCard extends StatelessWidget {
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: highlight ? const Color(0xFFFFF1F2) : Colors.white,
borderRadius: BorderRadius.circular(12),
borderRadius: AppDecorations.cardRadius,
border: Border.all(
color: highlight ? const Color(0xFFFECACA) : const Color(0xFFE2E8F0),
width: 1,
),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8),
boxShadow: const [
BoxShadow(
color: Color(0x14000000),
blurRadius: 20,
offset: Offset(0, 4),
),
],
),
child: Column(
@ -1745,17 +1754,15 @@ class _QuickActionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: InkWell(
return BounceTap(
onTap: item.onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: AppDecorations.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -1793,7 +1800,6 @@ class _QuickActionCard extends StatelessWidget {
],
),
),
),
);
}
}

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.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 {
const ManualScreen({super.key});
@ -8,16 +12,38 @@ class ManualScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final commands = VoiceCommandKey.values.map((key) => key.name).toList();
return Scaffold(
appBar: AppBar(title: const Text('Manual')),
body: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: commands.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) => ListTile(
leading: const Icon(Icons.record_voice_over),
title: Text(commands[index]),
return FeaturePage(
title: 'Manual',
subtitle: 'Voice command yang tersedia',
child: ListView(
children: [
StaggerWrapper(
children: [
for (final command in commands)
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 '../../core/network/api_client.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 '../../shared/widgets/animations/animations.dart';
// helpers
@ -498,7 +502,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
Polyline(
points: _navState.routePoints,
strokeWidth: 5,
color: const Color(0xFF1A56DB),
color: AppColors.primaryBlue,
),
],
),
@ -536,8 +540,8 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
decoration: BoxDecoration(
shape: BoxShape.circle,
color: i == _navState.currentStepIndex
? const Color(0xFF1A56DB)
: const Color(0xFF93C5FD),
? AppColors.primaryBlue
: AppColors.gradientBlueStart,
border: Border.all(color: Colors.white, width: 2),
),
),
@ -552,6 +556,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
top: 12,
left: 12,
right: 12,
child: FadeSlideWrapper(
child: Column(
children: [
// search bar
@ -574,6 +579,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
],
),
),
),
// turn-by-turn banner (when navigating)
if (isNavigating && _navState.steps.isNotEmpty)
@ -610,7 +616,7 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
child: FloatingActionButton.small(
heroTag: 'nav_center',
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF1A56DB),
foregroundColor: AppColors.primaryBlue,
onPressed: _centerOnUser,
child: const Icon(Icons.my_location),
),
@ -621,7 +627,13 @@ class _NavigationModeScreenState extends State<NavigationModeScreen> {
Positioned.fill(
child: Container(
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
Widget build(BuildContext context) {
return Material(
elevation: 4,
borderRadius: BorderRadius.circular(14),
color: Colors.transparent,
child: Container(
decoration: AppDecorations.card,
child: TextField(
controller: controller,
focusNode: focusNode,
@ -659,7 +672,7 @@ class _SearchBar extends StatelessWidget {
textInputAction: TextInputAction.search,
decoration: InputDecoration(
hintText: 'Cari tujuan…',
prefixIcon: const Icon(Icons.search, color: Color(0xFF1A56DB)),
prefixIcon: const Icon(Icons.search, color: AppColors.primaryBlue),
suffixIcon: loading
? const Padding(
padding: EdgeInsets.all(12),
@ -675,8 +688,8 @@ class _SearchBar extends StatelessWidget {
onPressed: onClear,
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
border: const OutlineInputBorder(
borderRadius: AppDecorations.inputRadius,
borderSide: BorderSide.none,
),
filled: true,
@ -685,6 +698,7 @@ class _SearchBar extends StatelessWidget {
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
),
);
}
}
@ -698,29 +712,33 @@ class _SuggestionList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
elevation: 6,
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)),
color: Colors.transparent,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)),
child: Column(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)),
child: Container(
decoration: const BoxDecoration(
color: AppColors.cardWhite,
boxShadow: AppDecorations.cardShadow,
),
child: StaggerWrapper(
children: [
for (final place in items)
InkWell(
BounceTap(
onTap: () => onSelect(place),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
child: Row(
children: [
const Icon(Icons.place_outlined,
color: Color(0xFF64748B), size: 20),
color: AppColors.textMuted, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
place.displayName,
maxLines: 2,
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) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF1A56DB),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
decoration: const BoxDecoration(
gradient: AppDecorations.blueGradient,
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
),
child: Row(
children: [
@ -864,13 +877,8 @@ class _StatusBar extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: _bgColor,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
),
],
borderRadius: AppDecorations.cardRadius,
boxShadow: AppDecorations.cardShadow,
border: Border.all(
color: phase == _NavPhase.error
? const Color(0xFFFECACA)
@ -954,11 +962,11 @@ class _PulsingDotState extends State<_PulsingDot>
height: 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF1A56DB)
color: AppColors.primaryBlue
.withValues(alpha: _opacity.value * 0.4),
border: Border.all(
color:
const Color(0xFF1A56DB).withValues(alpha: _opacity.value),
AppColors.primaryBlue.withValues(alpha: _opacity.value),
width: 2,
),
),
@ -970,11 +978,11 @@ class _PulsingDotState extends State<_PulsingDot>
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF1A56DB),
color: AppColors.primaryBlue,
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: const Color(0xFF1A56DB).withValues(alpha: 0.4),
color: AppColors.primaryBlue.withValues(alpha: 0.4),
blurRadius: 6,
),
],

View File

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

View File

@ -8,6 +8,10 @@ import '../../app/injection_container.dart';
import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.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
@ -27,37 +31,11 @@ class UserPairingScreen extends StatefulWidget {
}
class _UserPairingScreenState extends State<UserPairingScreen> {
String? _uniqueId;
String? _pairingCode;
DateTime? _pairingCodeExpiresAt;
int? _pairingCodeSeconds;
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 {
setState(() => _regenerating = true);
await runFriendlyAction(
@ -90,7 +68,7 @@ class _UserPairingScreenState extends State<UserPairingScreen> {
return _Page(
title: 'Pairing',
subtitle: 'Bagikan pairing code sementara ini ke Guardian.',
child: Column(
child: StaggerWrapper(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_pairingCode == null || _pairingCode!.isEmpty)
@ -118,11 +96,6 @@ class _UserPairingScreenState extends State<UserPairingScreen> {
: const Icon(Icons.autorenew),
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),
_PairingStatusCard(allowUserResponse: true),
],
@ -348,26 +321,20 @@ class _PairingStatusCardState extends State<_PairingStatusCard> {
final cardColor = _active
? const Color(0xFFF0FDF4)
: pending
? const Color(0xFFEFF6FF)
? AppColors.softBlueBg
: const Color(0xFFFFFBEB);
final accent = _active
? const Color(0xFF059669)
: pending
? const Color(0xFF2563EB)
? AppColors.primaryBlue
: const Color(0xFFD97706);
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(22),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: accent.withValues(alpha: 0.28)),
boxShadow: [
BoxShadow(
color: accent.withValues(alpha: 0.10),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
boxShadow: AppDecorations.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@ -393,7 +360,7 @@ class _PairingStatusCardState extends State<_PairingStatusCard> {
Expanded(
child: Text(_status,
style: const TextStyle(
color: Color(0xFF0F172A),
color: AppColors.textDark,
fontWeight: FontWeight.w700,
height: 1.25)),
),
@ -465,7 +432,7 @@ class _Page extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF8FAFC), Color(0xFFEFF6FF)],
colors: [AppColors.softBlueBg, Colors.white],
),
),
child: Padding(
@ -486,15 +453,9 @@ class _Page extends StatelessWidget {
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: const Color(0xFF0F172A).withValues(alpha: 0.18),
blurRadius: 28,
offset: const Offset(0, 14),
),
],
boxShadow: AppDecorations.cardShadow,
),
child: Row(
children: [
@ -516,10 +477,8 @@ class _Page extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(
style: AppTextStyles.heading.copyWith(
fontSize: 24,
fontWeight: FontWeight.w900,
color: Colors.white,
)),
@ -535,7 +494,7 @@ class _Page extends StatelessWidget {
),
),
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),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22),
border: Border.all(color: const Color(0xFFDDEAFE)),
boxShadow: [
BoxShadow(
color: const Color(0xFF2563EB).withValues(alpha: 0.10),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
borderRadius: BorderRadius.circular(20),
boxShadow: AppDecorations.cardShadow,
),
child: Row(
children: [
@ -577,10 +529,10 @@ class _InfoCard extends StatelessWidget {
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
borderRadius: BorderRadius.circular(14),
color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(50),
),
child: Icon(icon, color: const Color(0xFF2563EB)),
child: Icon(icon, color: AppColors.primaryBlue),
),
const SizedBox(width: 14),
Expanded(
@ -589,19 +541,19 @@ class _InfoCard extends StatelessWidget {
children: [
Text(title,
style: const TextStyle(
color: Color(0xFF64748B), fontWeight: FontWeight.w700)),
color: AppColors.muted, fontWeight: FontWeight.w700)),
SelectableText(value,
style: const TextStyle(
fontSize: 25,
height: 1.1,
letterSpacing: 1.2,
fontWeight: FontWeight.w900,
color: Color(0xFF0F172A))),
color: AppColors.textDark)),
if (helper != null) ...[
const SizedBox(height: 6),
Text(helper!,
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/errors/friendly_error.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 {
final bool editMode;
@ -64,7 +68,7 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
},
onError: (message) => _message = message,
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);
}
@ -76,25 +80,27 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
if (mounted) context.go(widget.editMode ? '/login' : '/splash');
}
void _usePublicUrl() =>
void _useLocalUrl() =>
setState(() => _url.text = AppConstants.defaultServerUrl);
void _usePublicUrl() =>
setState(() => _url.text = AppConstants.publicServerUrl);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF061421),
backgroundColor: AppColors.softBlueBg,
body: Stack(
children: [
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF071226),
Color(0xFF0F3B57),
Color(0xFF0B6B6C),
AppColors.softBlueBg,
Colors.white,
AppColors.softPinkBg,
],
),
),
@ -140,20 +146,14 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96),
color: AppColors.cardWhite,
borderRadius: BorderRadius.circular(
compact ? 22 : 28,
),
border: Border.all(
color: Colors.white.withValues(alpha: 0.7),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 34,
offset: const Offset(0, 22),
),
],
boxShadow: AppDecorations.cardShadow,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
@ -170,7 +170,7 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
compact ? 14 : 20,
),
decoration: const BoxDecoration(
color: Color(0xFF071226),
color: AppColors.softBlueBg,
),
child: Column(
crossAxisAlignment:
@ -182,9 +182,17 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
width: compact ? 38 : 48,
height: compact ? 38 : 48,
decoration: BoxDecoration(
color: const Color(0xFF2563EB),
gradient:
AppDecorations.blueGradient,
borderRadius:
BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x334A90D9),
blurRadius: 18,
offset: Offset(0, 8),
),
],
),
child: Icon(
Icons.navigation_rounded,
@ -197,9 +205,9 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
child: Text(
'WalkGuide',
style: TextStyle(
color: Colors.white,
color: AppColors.textDark,
fontSize: compact ? 16 : 20,
fontWeight: FontWeight.w900,
fontWeight: FontWeight.w800,
),
),
),
@ -208,10 +216,9 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
SizedBox(height: compact ? 14 : 18),
Text(
'Connect to Server',
style: TextStyle(
color: Colors.white,
style: AppTextStyles.heading.copyWith(
fontSize: compact ? 22 : 30,
fontWeight: FontWeight.w900,
fontWeight: FontWeight.w800,
height: 1,
),
),
@ -219,11 +226,9 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
Text(
widget.editMode
? 'Ubah alamat backend Spring Boot WalkGuide yang aktif.'
: 'Sambungkan app HP ke backend Spring Boot publik WalkGuide.',
style: TextStyle(
color: Colors.white.withValues(
alpha: 0.72,
),
: 'Pilih backend WalkGuide yang aktif sebelum login atau register.',
style: AppTextStyles.body.copyWith(
color: AppColors.muted,
height: 1.35,
),
),
@ -255,9 +260,14 @@ class _ServerConnectScreenState extends State<ServerConnectScreen> {
spacing: 8,
runSpacing: 8,
children: [
_HintChip(
icon: Icons.usb_outlined,
label: 'USB: 127.0.0.1',
onTap: _useLocalUrl,
),
_HintChip(
icon: Icons.public_outlined,
label: 'Server: 202.46.28.170',
label: 'Kampus: 202.46.28.170',
onTap: _usePublicUrl,
),
],
@ -350,27 +360,27 @@ class _HintChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
return BounceTap(
onTap: onTap,
borderRadius: BorderRadius.circular(999),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0xFFBFDBFE)),
border:
Border.all(color: AppColors.primaryBlue.withValues(alpha: 0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: const Color(0xFF1D4ED8)),
Icon(icon, size: 14, color: AppColors.primaryBlue),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
color: Color(0xFF1D4ED8),
color: AppColors.primaryBlue,
fontSize: 12,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
),
),
],
@ -392,7 +402,7 @@ class _StatusBox extends StatelessWidget {
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withValues(alpha: 0.22)),
),
child: Row(

View File

@ -5,7 +5,6 @@ import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.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/tts_service.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;
// Colours (inline, tidak butuh import app_colors.dart)
const _kBlue = Color(0xFF1A56DB);
const _kRed = Color(0xFFDC2626);
const _kSurface = Color(0xFFF8FAFC);
const _kBorder = Color(0xFFE2E8F0);
const _kMuted = Color(0xFF64748B);
const _kText = Color(0xFF0F172A);
// Screen
@ -53,7 +52,6 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
// Account info (from SecureStorage)
String _displayName = '';
String _uniqueId = '';
// Pairing status
String _pairingStatus = '';
@ -75,7 +73,6 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
Future<void> _loadAccount() async {
final storage = sl<SecureStorage>();
_displayName = await storage.getDisplayName() ?? '';
_uniqueId = await storage.getUniqueUserId() ?? '';
}
Future<void> _loadSettings() async {
@ -91,6 +88,7 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
_ttsSpeed = (data['ttsSpeed'] as num?)?.toDouble() ?? 0.9;
_warnNoGuardian = data['warnNoGuardian'] as bool? ?? true;
_hapticEnabled = data['hapticEnabled'] as bool? ?? true;
sl<HapticService>().setEnabled(_hapticEnabled);
}
},
onError: (_) {},
@ -126,6 +124,7 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
// Apply TTS locally dulu
await sl<TtsService>().setLanguage(_ttsLanguage);
context.read<AppCubit>().setLocaleCode(_ttsLanguage);
sl<HapticService>().setEnabled(_hapticEnabled);
if (_hapticEnabled) {
await sl<HapticService>().success();
}
@ -169,7 +168,15 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
// build
@override
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
? const Center(child: CircularProgressIndicator())
: ListView(
@ -177,12 +184,11 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
children: [
// header
Text('Settings',
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w800)),
style: AppTextStyles.heading.copyWith(
fontWeight: FontWeight.w800,
)),
const Text('TTS, haptic, pairing, account',
style: TextStyle(color: _kMuted)),
style: TextStyle(color: AppColors.muted)),
const SizedBox(height: 20),
// 1. TTS Settings
@ -202,7 +208,8 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
),
items: const [
DropdownMenuItem(
value: 'id-ID', child: Text('Bahasa Indonesia')),
value: 'id-ID',
child: Text('Bahasa Indonesia')),
DropdownMenuItem(
value: 'en-US', child: Text('English (US)')),
],
@ -238,52 +245,6 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
children: [
Icon(
@ -313,6 +274,15 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
],
),
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(
width: double.infinity,
child: OutlinedButton.icon(
@ -335,18 +305,11 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.help_outline, color: _kBlue),
title: const Text('Daftar Voice Commands & Shortcuts'),
subtitle:
const Text('Lihat semua perintah suara yang tersedia'),
subtitle: const Text(
'Lihat semua perintah suara yang tersedia'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// Route /user/manual belum ada di router
// 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')),
);
context.go('/user/manual');
},
),
),
@ -374,7 +337,10 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
SwitchListTile(
contentPadding: EdgeInsets.zero,
value: _hapticEnabled,
onChanged: (v) => setState(() => _hapticEnabled = v),
onChanged: (v) {
sl<HapticService>().setEnabled(v);
setState(() => _hapticEnabled = v);
},
title: const Text('Haptic feedback'),
subtitle:
const Text('Getaran saat obstacle terdeteksi'),
@ -453,6 +419,7 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
const SizedBox(height: 32),
],
),
),
);
}
@ -489,11 +456,18 @@ class _SectionHeader extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
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),
Text(title,
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),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _kBorder),
borderRadius: BorderRadius.circular(20),
boxShadow: AppDecorations.cardShadow,
),
child: child,
);
@ -533,16 +507,22 @@ class _InfoRow extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
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),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12, color: _kMuted)),
Text(label,
style: const TextStyle(fontSize: 12, color: AppColors.muted)),
Text(value,
style: const TextStyle(
fontWeight: FontWeight.w700, color: _kText)),
fontWeight: FontWeight.w700, color: AppColors.textDark)),
],
),
),
@ -550,12 +530,12 @@ class _InfoRow extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: _kSurface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _kBorder),
color: AppColors.softBlueBg,
borderRadius: BorderRadius.circular(50),
border: Border.all(color: AppColors.border),
),
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/services/haptic_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';
Dio get _api => sl<ApiClient>().dio;
@ -252,7 +256,16 @@ class _SosScreenState extends State<SosScreen>
: compact
? 12.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(
padding: EdgeInsets.all(pagePadding),
child: Column(
@ -267,14 +280,13 @@ class _SosScreenState extends State<SosScreen>
children: [
Text(
'SOS',
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w800),
style: AppTextStyles.heading,
),
const Text(
Text(
'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) ...[
const Text(
'Riwayat SOS',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16),
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 16,
color: AppColors.textDark,
),
),
const SizedBox(height: 10),
Expanded(
@ -354,6 +370,8 @@ class _SosScreenState extends State<SosScreen>
],
),
),
),
),
);
},
);
@ -383,17 +401,32 @@ class _SosButton extends StatelessWidget {
: compact
? 154.0
: 200.0;
return SizedBox.square(
dimension: dimension,
child: FilledButton(
style: FilledButton.styleFrom(
shape: const CircleBorder(),
backgroundColor:
active ? const Color(0xFFB91C1C) : const Color(0xFFDC2626),
elevation: active ? 12 : 4,
shadowColor: const Color(0xFFDC2626).withValues(alpha: 0.5),
return BounceTap(
onTap: onPressed,
child: Semantics(
button: true,
label: 'Kirim SOS',
child: Container(
width: dimension,
height: dimension,
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(
mainAxisSize: MainAxisSize.min,
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),
shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFDC2626), width: 3),
boxShadow: AppDecorations.cardShadow,
),
child: const Center(
child: Column(
@ -470,8 +505,9 @@ class _ActiveSosBanner extends StatelessWidget {
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFFEE2E2),
borderRadius: BorderRadius.circular(12),
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFFCA5A5), width: 1.5),
boxShadow: AppDecorations.cardShadow,
),
child: Row(
children: [
@ -531,10 +567,21 @@ class _SosHistory extends StatelessWidget {
if (events.isEmpty) {
return _HistoryEmpty(onRefresh: onRefresh);
}
return ListView.separated(
itemCount: events.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (_, i) => _SosEventTile(event: events[i]),
return RefreshIndicator(
onRefresh: () async => onRefresh(),
child: ListView(
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),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: AppDecorations.cardShadow,
),
child: Row(
children: [
@ -662,9 +710,10 @@ class _HistoryEmpty extends StatelessWidget {
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(12),
color: AppColors.cardWhite,
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: AppDecorations.cardShadow,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,

View File

@ -15,6 +15,9 @@ import '../../core/errors/friendly_error.dart';
import '../../core/network/api_client.dart';
import '../../core/services/location_reporter_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';
// ---------------------------------------------------------------------------
@ -48,7 +51,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
_scanCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2200),
)..repeat();
);
_loadPairingStatus();
}
@ -73,8 +76,10 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
await _startCamera();
await sl<LocationReporterService>().start(walkGuideActive: true);
await _cubit.start();
_scanCtrl.repeat();
_cubit.updateStatus(_activeStatusText());
} else {
_scanCtrl.stop();
await _stopCamera();
await sl<LocationReporterService>().stop();
await _cubit.stop();
@ -151,7 +156,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
);
final controller = CameraController(
backCamera,
ResolutionPreset.medium,
ResolutionPreset.low,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.yuv420,
);
@ -195,7 +200,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
void _onCameraImage(CameraImage image) {
if (!_cubit.state.active || _processingFrame) return;
final now = DateTime.now();
if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 900)) {
if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 1500)) {
return;
}
_lastInferenceAt = now;
@ -258,6 +263,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
onPressed: () => context.go('/user/pairing'),
icon: const Icon(Icons.link)),
],
child: FadeSlideWrapper(
child: Column(
children: [
Expanded(
@ -290,7 +296,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
const SizedBox(width: 10),
_ActionSquare(
icon: Icons.sos_outlined,
color: const Color(0xFFDC2626),
color: AppColors.danger,
onTap: () async {
if (await _ensurePaired() && context.mounted) {
context.go('/user/sos');
@ -300,7 +306,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
const SizedBox(width: 10),
_ActionSquare(
icon: Icons.call_outlined,
color: const Color(0xFF059669),
color: AppColors.success,
onTap: () async {
if (await _ensurePaired() && context.mounted) {
context.go('/user/call');
@ -312,6 +318,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen>
],
),
),
),
);
}
}
@ -336,8 +343,12 @@ class _VisionPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cameraReady = camera != null && camera!.value.isInitialized;
return ClipRRect(
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
boxShadow: AppDecorations.cardShadow,
),
clipBehavior: Clip.antiAlias,
child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFF07111F)),
child: Stack(
@ -393,20 +404,21 @@ class _VisionPanel extends StatelessWidget {
),
),
Positioned(
top: 18,
left: 18,
right: 18,
child: Row(
top: 14,
left: 14,
right: 14,
child: Wrap(
spacing: 8,
runSpacing: 6,
children: [
_Pill(
text: state.active ? 'LIVE AI SCAN' : 'STANDBY',
text: state.active ? 'LIVE AI' : 'STANDBY',
color: state.active
? const Color(0xFF22C55E)
: const Color(0xFFF59E0B),
),
const SizedBox(width: 8),
_Pill(
text: paired ? 'GUARDIAN LINKED' : 'PAIRING REQUIRED',
text: paired ? 'LINKED' : 'PAIRING',
color: paired
? const Color(0xFF38BDF8)
: const Color(0xFFF97316),
@ -444,47 +456,60 @@ class _VisionPanel extends StatelessWidget {
),
if (!paired && !pairingLoading)
Positioned.fill(
child: DecoratedBox(
child: LayoutBuilder(
builder: (context, constraints) {
final compact = constraints.maxHeight < 270;
return DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFF020617).withValues(alpha: 0.72),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: compact ? 16 : 24,
vertical: compact ? 10 : 20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 70,
height: 70,
width: compact ? 46 : 64,
height: compact ? 46 : 64,
decoration: BoxDecoration(
color: const Color(0xFFFFFBEB)
.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(18),
border:
Border.all(color: const Color(0xFFF59E0B)),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFF59E0B)),
),
child: const Icon(Icons.link_off,
color: Color(0xFFFBBF24), size: 34),
child: Icon(Icons.link_off,
color: const Color(0xFFFBBF24),
size: compact ? 24 : 32),
),
const SizedBox(height: 14),
const Text(
SizedBox(height: compact ? 8 : 12),
Text(
'Guardian belum terhubung',
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontSize: compact ? 16 : 21,
fontWeight: FontWeight.w900,
),
),
if (!compact) ...[
const SizedBox(height: 6),
const Text(
'Pairing dulu supaya SOS, panggilan, dan pemantauan live punya penerima yang jelas.',
textAlign: TextAlign.center,
style:
TextStyle(color: Colors.white70, height: 1.35),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white70, height: 1.35),
),
const SizedBox(height: 16),
],
SizedBox(height: compact ? 10 : 14),
FilledButton.icon(
onPressed: onPairingTap,
icon: const Icon(Icons.link),
@ -494,6 +519,8 @@ class _VisionPanel extends StatelessWidget {
),
),
),
);
},
),
),
Positioned(
@ -624,15 +651,8 @@ class _MetricChip extends StatelessWidget {
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 14,
offset: const Offset(0, 8),
),
],
borderRadius: BorderRadius.circular(20),
boxShadow: AppDecorations.cardShadow,
),
child: Row(
children: [
@ -680,12 +700,20 @@ class _ActionSquare extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: color,
borderRadius: BorderRadius.circular(14),
child: InkWell(
return BounceTap(
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(
width: 54,
height: 50,
@ -816,7 +844,7 @@ class _Page extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF8FAFC), Color(0xFFEFF6FF)],
colors: [AppColors.softBlueBg, Colors.white],
),
),
child: Padding(
@ -835,12 +863,11 @@ class _Page extends StatelessWidget {
width: compact ? 38 : 46,
height: compact ? 38 : 46,
decoration: BoxDecoration(
color: const Color(0xFF2563EB),
gradient: AppDecorations.blueGradient,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color:
const Color(0xFF2563EB).withValues(alpha: 0.28),
color: AppColors.primaryBlue.withValues(alpha: 0.28),
blurRadius: 18,
offset: const Offset(0, 8),
),
@ -862,14 +889,14 @@ class _Page extends StatelessWidget {
.headlineSmall
?.copyWith(
fontWeight: FontWeight.w900,
color: const Color(0xFF0F172A),
color: AppColors.textDark,
fontSize: compact ? 22 : null,
)),
if (subtitle != null)
Text(subtitle!,
maxLines: 1,
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 '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/app.dart';
import 'core/constants/app_constants.dart';
import 'core/utils/init_guard.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
import 'shared/widgets/walkguide_loading_screen.dart';
Future<void> main() async {
await runZonedGuarded(
@ -21,18 +13,7 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
_installGlobalErrorUi();
await AppConstants.clearServerUrl();
if (!kIsWeb) {
final firebaseApp = await ignoreInitFailure(
() => Firebase.initializeApp(),
label: 'Firebase init',
);
if (firebaseApp != null) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
}
}
runApp(const WalkGuideBootApp());
try {
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() {
FlutterError.onError = (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
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@ -11,6 +13,8 @@ import '../../core/services/stt_service.dart';
import '../../core/services/tts_service.dart';
import '../../core/services/voice_command_handler.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import 'animations/animations.dart';
class UserShell extends StatefulWidget {
final Widget child;
@ -26,9 +30,6 @@ class _UserShellState extends State<UserShell> {
super.initState();
_loadVoiceCommands();
_startHardwareShortcuts();
WidgetsBinding.instance.addPostFrameCallback((_) {
_startVoiceListening();
});
sl<VoiceCommandHandler>().onCommand = (key) {
if (!mounted) return;
switch (key) {
@ -66,17 +67,6 @@ class _UserShellState extends State<UserShell> {
};
}
Future<void> _startVoiceListening() async {
await runFriendlyAction(
() async {
await sl<SttService>().init();
await sl<SttService>().startListening();
},
onError: (_) {},
fallback: 'Voice listener belum bisa dimuat.',
);
}
Future<void> _loadVoiceCommands() async {
await runFriendlyAction(
() async {
@ -139,6 +129,7 @@ class _UserShellState extends State<UserShell> {
@override
void dispose() {
sl<HardwareShortcutListener>().stopListening();
unawaited(sl<SttService>().stopListening());
super.dispose();
}
@ -198,15 +189,7 @@ class _AppShell extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
final useRail = constraints.maxWidth >= 760;
final content = AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
child: KeyedSubtree(
key: ValueKey(location),
child: child,
),
);
final content = child;
return Scaffold(
backgroundColor: AppColors.surface,
@ -257,73 +240,42 @@ class _RailNavigation extends StatelessWidget {
final itemHeight = compact ? 58.0 : 70.0;
return DecoratedBox(
decoration: const BoxDecoration(color: Colors.white),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Color(0x14000000),
blurRadius: 18,
offset: Offset(6, 0),
),
],
),
child: SafeArea(
right: false,
child: SizedBox(
width: width,
child: ListView.separated(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: compact ? 6 : 12,
),
itemCount: items.length,
separatorBuilder: (_, __) => SizedBox(height: compact ? 2 : 6),
itemBuilder: (context, index) {
final item = items[index];
final selected = index == selectedIndex;
return Semantics(
button: true,
selected: selected,
label: item.label,
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () => context.go(items[index].route),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
height: itemHeight,
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: selected
? const Color(0xFFEFF6FF)
: Colors.transparent,
borderRadius: BorderRadius.circular(18),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
selected ? item.selectedIcon : item.icon,
size: compact ? 23 : 25,
color: selected
? AppColors.primary
: const Color(0xFF334155),
for (var index = 0; index < items.length; index++)
Expanded(
child: Center(
child: _RailNavItem(
item: items[index],
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) {
final bottom = MediaQuery.of(context).padding.bottom;
final extraBottom = bottom > 12 ? 12.0 : bottom;
return DecoratedBox(
return Container(
margin: EdgeInsets.zero,
decoration: BoxDecoration(
color: Colors.white,
border: const Border(top: BorderSide(color: AppColors.border)),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 18,
offset: const Offset(0, -8),
),
AppDecorations.cardShadow.first,
],
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: SafeArea(
top: false,
child: SizedBox(
height: 68 + extraBottom,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 6),
itemBuilder: (context, index) {
final item = items[index];
final selected = index == selectedIndex;
child: Row(
children: [
for (var index = 0; index < items.length; index++)
Expanded(
child: _BottomNavItem(
item: items[index],
selected: index == selectedIndex,
),
),
],
),
),
),
),
);
}
}
class _RailNavItem extends StatelessWidget {
final _ShellItem item;
final bool selected;
final bool compact;
final double height;
const _RailNavItem({
required this.item,
required this.selected,
required this.compact,
required this.height,
});
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
selected: selected,
label: item.label,
child: InkWell(
borderRadius: BorderRadius.circular(12),
child: BounceTap(
onTap: () => context.go(item.route),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
width: 72,
padding: const EdgeInsets.symmetric(horizontal: 6),
curve: Curves.easeOutCubic,
height: height,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: selected
? const Color(0xFFEFF6FF)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: selected
? const Color(0xFFBFDBFE)
: Colors.transparent,
),
color: selected ? AppColors.softBlueBg : Colors.transparent,
borderRadius: BorderRadius.circular(24),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
selected ? item.selectedIcon : item.icon,
color: selected
? AppColors.primary
: const Color(0xFF64748B),
size: 22,
size: compact ? 21 : 24,
color: selected ? AppColors.primary : AppColors.muted,
),
const SizedBox(height: 4),
SizedBox(height: compact ? 1 : 4),
Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
fontWeight:
selected ? FontWeight.w800 : FontWeight.w600,
color: selected
? AppColors.primary
: const Color(0xFF64748B),
fontSize: compact ? 9.5 : 11.5,
height: 1,
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
color: selected ? AppColors.primary : AppColors.muted,
),
),
],
@ -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 '../../core/theme/app_colors.dart';
import '../../core/theme/app_decorations.dart';
import '../../core/theme/app_text_styles.dart';
import 'animations/animations.dart';
class FeaturePage extends StatelessWidget {
final String title;
@ -18,14 +21,23 @@ class FeaturePage extends StatelessWidget {
@override
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(
builder: (context, constraints) {
final short = constraints.maxHeight < 520;
final compact = constraints.maxWidth < 420 || short;
final wide = constraints.maxWidth >= 900;
final horizontal = compact ? 12.0 : 20.0;
return Padding(
return FadeSlideWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(
horizontal,
short ? 8 : 12,
@ -40,18 +52,7 @@ class FeaturePage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 12, end: 0),
duration: const Duration(milliseconds: 360),
curve: Curves.easeOutCubic,
builder: (_, offset, child) => Opacity(
opacity: (1 - offset / 12).clamp(0.0, 1.0),
child: Transform.translate(
offset: Offset(0, offset),
child: child,
),
),
child: compact
compact
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -81,7 +82,6 @@ class FeaturePage extends StatelessWidget {
if (trailing != null) trailing!,
],
),
),
SizedBox(height: short ? 8 : (compact ? 12 : 16)),
Expanded(
child: child,
@ -90,9 +90,11 @@ class FeaturePage extends StatelessWidget {
),
),
),
),
);
},
),
),
);
}
}
@ -118,10 +120,8 @@ class _FeatureHeading extends StatelessWidget {
title,
maxLines: short ? 1 : 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
style: AppTextStyles.heading.copyWith(
fontSize: compact ? 22 : null,
fontWeight: FontWeight.w900,
color: AppColors.text,
),
),
const SizedBox(height: 2),
@ -129,10 +129,9 @@ class _FeatureHeading extends StatelessWidget {
subtitle,
maxLines: short ? 1 : (compact ? 2 : 3),
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.muted,
style: AppTextStyles.body.copyWith(
color: AppColors.textMuted,
fontWeight: FontWeight.w500,
height: 1.25,
),
),
],
@ -157,8 +156,10 @@ class FeatureEmptyPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Container(
constraints: const BoxConstraints(maxWidth: 380),
padding: const EdgeInsets.all(24),
decoration: AppDecorations.card,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -167,7 +168,7 @@ class FeatureEmptyPanel extends StatelessWidget {
height: 72,
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(50),
border: Border.all(color: AppColors.border),
),
child: Icon(icon, size: 36, color: AppColors.primary),
@ -214,8 +215,9 @@ class FeatureErrorPanel extends StatelessWidget {
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(8),
borderRadius: AppDecorations.cardRadius,
border: Border.all(color: const Color(0xFFFECACA)),
boxShadow: AppDecorations.cardShadow,
),
child: Column(
mainAxisSize: MainAxisSize.min,

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"