Files
ingtrakapp/lib/services/storage_service.dart
T
2026-09-06 21:58:59 +02:00

174 lines
5.1 KiB
Dart

/// Thin wrapper around [SharedPreferences] for local key-value persistence.
library;
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/transaction_meta.dart';
import '../utils/constants.dart';
/// Persisted annotation store for transactions.
class TxMetaStoreData {
const TxMetaStoreData({
this.byId = const {},
this.categoryByDescription = const {},
this.exactTemplates = const {},
});
final Map<String, TxMeta> byId;
final Map<String, TxCategory> categoryByDescription;
final Map<String, TxExactTemplate> exactTemplates;
}
/// Loads and stores user preferences. Call [init] once at startup.
class StorageService {
StorageService({this._preferences});
SharedPreferences? _preferences;
Future<void> init() async {
_preferences ??= await SharedPreferences.getInstance();
}
SharedPreferences get _prefs {
final prefs = _preferences;
if (prefs == null) {
throw StateError('StorageService.init() must be called before use.');
}
return prefs;
}
String get themeModeName =>
_prefs.getString(StorageKeys.themeMode) ?? 'system';
Future<void> setThemeModeName(String value) {
return _prefs.setString(StorageKeys.themeMode, value);
}
bool get useRemoteData => _prefs.getBool(StorageKeys.useRemoteData) ?? false;
Future<void> setUseRemoteData(bool value) {
return _prefs.setBool(StorageKeys.useRemoteData, value);
}
TxMetaStoreData loadTxMetaStore() {
final raw = _prefs.getString(StorageKeys.txMeta);
if (raw != null && raw.isNotEmpty) {
return _decodeStore(raw);
}
// Migrate flat v1 map → v2 store.
final legacy = _prefs.getString(StorageKeys.txMetaLegacy);
if (legacy == null || legacy.isEmpty) return const TxMetaStoreData();
try {
final decoded = jsonDecode(legacy);
if (decoded is! Map) return const TxMetaStoreData();
final byId = <String, TxMeta>{};
final categoryByDescription = <String, TxCategory>{};
decoded.forEach((key, value) {
if (key is! String || value is! Map) return;
final meta = TxMeta.fromJson(Map<String, dynamic>.from(value));
byId[key] = meta;
if (meta.hasCategory) {
final desc = _descriptionFromLegacyId(key);
if (desc != null) {
categoryByDescription[desc] = meta.category;
}
}
});
return TxMetaStoreData(
byId: byId,
categoryByDescription: categoryByDescription,
);
} catch (_) {
return const TxMetaStoreData();
}
}
Future<void> saveTxMetaStore(TxMetaStoreData store) {
final encoded = {
'byId': {
for (final e in store.byId.entries) e.key: e.value.toJson(),
},
'categoryByDescription': {
for (final e in store.categoryByDescription.entries)
e.key: e.value.name,
},
'exactTemplates': {
for (final e in store.exactTemplates.entries) e.key: e.value.toJson(),
},
};
return _prefs.setString(StorageKeys.txMeta, jsonEncode(encoded));
}
/// Deprecated shim used by older call sites during transition.
Map<String, TxMeta> loadTxMeta() => loadTxMetaStore().byId;
Future<void> saveTxMeta(Map<String, TxMeta> meta) {
final current = loadTxMetaStore();
return saveTxMetaStore(
TxMetaStoreData(
byId: meta,
categoryByDescription: current.categoryByDescription,
exactTemplates: current.exactTemplates,
),
);
}
TxMetaStoreData _decodeStore(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) return const TxMetaStoreData();
final root = Map<String, dynamic>.from(decoded);
final byId = <String, TxMeta>{};
final byIdRaw = root['byId'];
if (byIdRaw is Map) {
byIdRaw.forEach((key, value) {
if (key is String && value is Map) {
byId[key] = TxMeta.fromJson(Map<String, dynamic>.from(value));
}
});
}
final categoryByDescription = <String, TxCategory>{};
final cats = root['categoryByDescription'];
if (cats is Map) {
cats.forEach((key, value) {
if (key is String && value is String) {
categoryByDescription[key] = TxCategoryX.fromName(value);
}
});
}
final exactTemplates = <String, TxExactTemplate>{};
final templates = root['exactTemplates'];
if (templates is Map) {
templates.forEach((key, value) {
if (key is String && value is Map) {
exactTemplates[key] =
TxExactTemplate.fromJson(Map<String, dynamic>.from(value));
}
});
}
return TxMetaStoreData(
byId: byId,
categoryByDescription: categoryByDescription,
exactTemplates: exactTemplates,
);
} catch (_) {
return const TxMetaStoreData();
}
}
/// Legacy id format: `date|currency|amount|description`.
String? _descriptionFromLegacyId(String id) {
final parts = id.split('|');
if (parts.length < 4) return null;
final description = parts.sublist(3).join('|');
return description.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' ');
}
}