feat: implement splash and onboarding flows, update app assets, and add project design documentation
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user