feat: implement splash and onboarding flows, update app assets, and add project design documentation

This commit is contained in:
Hamza-Ayed
2026-07-24 04:15:07 +03:00
parent 9f7bc31408
commit 213b459912
208 changed files with 23801 additions and 1066 deletions
@@ -0,0 +1,173 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
enum FavoriteType { home, work }
class SavedPlace {
final String id;
final String name;
final String? address;
final double lat;
final double lng;
int useCount;
DateTime lastUsedAt;
FavoriteType? favoriteType;
SavedPlace({
required this.id,
required this.name,
this.address,
required this.lat,
required this.lng,
this.useCount = 0,
DateTime? lastUsedAt,
this.favoriteType,
}) : lastUsedAt = lastUsedAt ?? DateTime.now();
/// Rounding to 4 decimal places (~11m precision) for deduplication.
static double _roundCoord(double value) =>
(value * 10000).roundToDouble() / 10000;
String get dedupeKey =>
'${_roundCoord(lat)},${_roundCoord(lng)}';
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'address': address,
'lat': lat,
'lng': lng,
'useCount': useCount,
'lastUsedAt': lastUsedAt.toIso8601String(),
'favoriteType': favoriteType?.name,
};
factory SavedPlace.fromJson(Map<String, dynamic> json) => SavedPlace(
id: json['id'] as String,
name: json['name'] as String,
address: json['address'] as String?,
lat: (json['lat'] as num).toDouble(),
lng: (json['lng'] as num).toDouble(),
useCount: json['useCount'] as int? ?? 0,
lastUsedAt: DateTime.parse(json['lastUsedAt'] as String),
favoriteType: json['favoriteType'] != null
? FavoriteType.values.firstWhere(
(e) => e.name == json['favoriteType'],
)
: null,
);
}
class SavedPlacesStore {
static const _key = 'saved_places';
static const _maxItems = 50;
Future<List<SavedPlace>> getAll() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_key);
if (raw == null) return [];
final list = (jsonDecode(raw) as List)
.map((e) => SavedPlace.fromJson(e as Map<String, dynamic>))
.toList();
return _sort(list);
}
Future<void> add(SavedPlace place) async {
final prefs = await SharedPreferences.getInstance();
final list = await getAll();
final existingIndex = list.indexWhere(
(p) => p.dedupeKey == place.dedupeKey && p.favoriteType == null,
);
if (existingIndex != -1) {
list[existingIndex].useCount++;
list[existingIndex].lastUsedAt = DateTime.now();
} else {
place.useCount = 1;
list.add(place);
}
await _enforceCap(list);
await _save(prefs, list);
}
Future<void> remove(String placeId) async {
final prefs = await SharedPreferences.getInstance();
final list = await getAll();
list.removeWhere((p) => p.id == placeId);
await _save(prefs, list);
}
Future<void> setFavorite(String placeId, FavoriteType type) async {
final prefs = await SharedPreferences.getInstance();
final list = await getAll();
// Clear any existing favorite of this type across all items.
for (final p in list) {
if (p.favoriteType == type) {
p.favoriteType = null;
}
}
final idx = list.indexWhere((p) => p.id == placeId);
if (idx != -1) {
list[idx].favoriteType = type;
}
await _save(prefs, list);
}
Future<void> clearFavorite(FavoriteType type) async {
final prefs = await SharedPreferences.getInstance();
final list = await getAll();
for (final p in list) {
if (p.favoriteType == type) {
p.favoriteType = null;
}
}
await _save(prefs, list);
}
/// Removes the lowest-scoring non-favorite item when the list exceeds the cap.
/// Score = useCount (higher is better), ties broken by lastUsedAt (newer is better).
Future<void> _enforceCap(List<SavedPlace> list) async {
if (list.length <= _maxItems) return;
final nonFavorites = list.where((p) => p.favoriteType == null).toList()
..sort((a, b) {
if (a.useCount != b.useCount) {
return a.useCount.compareTo(b.useCount); // ascending — lowest first
}
return a.lastUsedAt.compareTo(b.lastUsedAt); // oldest first
});
final toRemove = list.length - _maxItems;
final idsToRemove =
nonFavorites.take(toRemove).map((p) => p.id).toSet();
list.removeWhere((p) => idsToRemove.contains(p.id));
}
List<SavedPlace> _sort(List<SavedPlace> list) {
final favoriteOrder = {FavoriteType.home: 0, FavoriteType.work: 1};
list.sort((a, b) {
if (a.favoriteType != null && b.favoriteType == null) return -1;
if (a.favoriteType == null && b.favoriteType != null) return 1;
if (a.favoriteType != null && b.favoriteType != null) {
return favoriteOrder[a.favoriteType]!
.compareTo(favoriteOrder[b.favoriteType]!);
}
if (a.useCount != b.useCount) {
return b.useCount.compareTo(a.useCount); // descending
}
return b.lastUsedAt.compareTo(a.lastUsedAt); // newer first
});
return list;
}
Future<void> _save(SharedPreferences prefs, List<SavedPlace> list) async {
final encoded = jsonEncode(list.map((p) => p.toJson()).toList());
await prefs.setString(_key, encoded);
}
}
@@ -9,7 +9,17 @@ class TokenStore {
await _s.write(key: 'refresh_token', value: refresh);
}
Future<void> saveUser(String userJson) async {
await _s.write(key: 'cached_user', value: userJson);
}
Future<String?> readUser() => _s.read(key: 'cached_user');
Future<String?> access() => _s.read(key: 'access_token');
Future<String?> refresh() => _s.read(key: 'refresh_token');
Future<bool> hasToken() async {
final token = await access();
return token != null && token.isNotEmpty;
}
Future<void> clear() => _s.deleteAll();
}