POLISHING STUFF and Smarter AI, and alot i mean.. ALOT OF BUG FIX AND OPTIMIZE
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,14 +91,20 @@ class _AuthInterceptor extends Interceptor {
|
||||
// Retry original request
|
||||
err.requestOptions.headers['Authorization'] =
|
||||
'Bearer ${data['accessToken']}';
|
||||
final retryRes = await _dio.fetch(err.requestOptions);
|
||||
_refreshing = false;
|
||||
handler.resolve(retryRes);
|
||||
try {
|
||||
final retryRes = await _dio.fetch(err.requestOptions);
|
||||
_refreshing = false;
|
||||
handler.resolve(retryRes);
|
||||
} on DioException catch (retryErr) {
|
||||
_refreshing = false;
|
||||
handler.next(retryErr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
await _storage.clearAll();
|
||||
}
|
||||
_refreshing = false;
|
||||
await _storage.clearAll();
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -58,4 +90,4 @@ class TtsService {
|
||||
_lastSpoken = text;
|
||||
_tts.speak(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user