Files
999/lib/services/words_repository.dart
T

820 lines
29 KiB
Dart

import '../models/word_passport_model.dart';
import '../models/syncretic_session_model.dart';
import 'dart:io';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:shared_preferences/shared_preferences.dart';
import '../data/words_data.dart';
import '../models/history_entry_model.dart';
import '../models/word_model.dart';
import 'true_random_service.dart';
class WordsRepository {
static final WordsRepository _instance = WordsRepository._internal();
factory WordsRepository() => _instance;
WordsRepository._internal();
static const String _kHistoryKey = 'oracle_999_sessions_history_v2';
static const String _kMappingDateKey = 'oracle_daily_mapping_date_v1';
static const String _kMappingOrderKey = 'oracle_daily_mapping_order_v1';
static const String _kMappingSourceKey = 'oracle_daily_mapping_source_v1';
static const String _kCustomEditsKey = 'oracle_user_custom_edits_v1';
Map<String, Map<String, dynamic>> _userEdits = {};
List<WordItem> _sessionWords = [];
final List<OracleSession> _sessions = [];
Map<String, Map<String, dynamic>> _kbData = {};
Map<int, WordPassport> _passports = {};
WordPassport? getPassport(int wordId) => _passports[wordId];
Map<int, WordPassport> get allPassports => Map.unmodifiable(_passports);
bool _useRemoteShuffle = true;
String? _dailyMappingDate;
RandomSource _dailyMappingSource = RandomSource.local;
List<WordItem> get allWords => List.unmodifiable(_sessionWords);
List<OracleSession> get sessions => List.unmodifiable(_sessions);
List<OracleSession> get history => sessions; // Alias for compatibility
int get totalWords => _sessionWords.length;
int get customEditsCount => _userEdits.length;
Map<String, Map<String, dynamic>> get allCustomEdits => Map.unmodifiable(_userEdits);
String? get dailyMappingDate => _dailyMappingDate;
RandomSource get dailyMappingSource => _dailyMappingSource;
Future<void> init({bool remoteDailyShuffle = true}) async {
_useRemoteShuffle = remoteDailyShuffle;
await _loadKbAsset();
await _loadPassportsAsset();
await _loadUserEditsFromPrefs();
await _restoreOrFetchDailyMapping(force: false);
await _loadHistoryFromPrefs();
await _loadSyncreticSessionsFromPrefs();
}
Future<void> _loadPassportsAsset() async {
for (final path in ['assets/data/oracle_passports.json', '../assets/data/oracle_passports.json']) {
try {
final file = File(path);
if (file.existsSync()) {
final jsonStr = file.readAsStringSync();
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
_passports = decoded.map((k, v) => MapEntry(int.tryParse(k) ?? 0, WordPassport.fromMap(v as Map<String, dynamic>)));
return;
}
} catch (_) {}
}
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return;
}
try {
final jsonStr = await rootBundle.loadString('assets/data/oracle_passports.json');
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
_passports = decoded.map((k, v) => MapEntry(int.tryParse(k) ?? 0, WordPassport.fromMap(v as Map<String, dynamic>)));
} catch (_) {}
}
Future<void> _loadKbAsset() async {
// 1. In local dev/testing environments, load synchronously from filesystem
for (final path in ['assets/data/oracle_kb.json', '../assets/data/oracle_kb.json']) {
try {
final file = File(path);
if (file.existsSync()) {
final jsonStr = file.readAsStringSync();
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
_kbData = decoded.map((k, v) => MapEntry(k.toLowerCase().trim(), v as Map<String, dynamic>));
return;
}
} catch (_) {}
}
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return; // Avoid hanging on rootBundle during headless tests
}
// 2. In packaged release app, load via rootBundle
try {
final jsonStr = await rootBundle.loadString('assets/data/oracle_kb.json');
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
_kbData = decoded.map((k, v) => MapEntry(k.toLowerCase().trim(), v as Map<String, dynamic>));
} catch (e) {
debugPrint('Could not load oracle_kb.json asset: $e');
}
}
WordItem _enrichedWord(WordItem base) {
final key = base.word.toLowerCase().trim();
var result = base;
final kb = _kbData[key] ?? _kbData[base.id.toString()];
if (kb != null) {
final kbWord = (kb['word'] as String?)?.toLowerCase().trim();
if (kbWord == null || kbWord == key) {
result = result.enrichWithKb(kb);
}
}
final edit = _userEdits[key];
if (edit != null) {
result = result.copyWith(
core: edit['core'] as String? ?? result.core,
oracle: edit['oracle'] as String? ?? result.oracle,
shadow: edit['shadow'] as String? ?? result.shadow,
asks: edit['asks'] as String? ?? result.asks,
step: edit['step'] as String? ?? result.step,
cultures: edit.containsKey('cultures') ? _stringMap(edit['cultures']) : result.cultures,
languages: edit.containsKey('languages') ? _stringMap(edit['languages']) : result.languages,
layers: edit.containsKey('layers') ? _layerList(edit['layers']) : result.layers,
binds: edit.containsKey('binds') ? _stringList(edit['binds']) : result.binds,
isUserEdited: true,
);
}
return result;
}
Map<String, String> _stringMap(dynamic raw) {
if (raw is! Map) return {};
return raw.map((k, v) => MapEntry(k.toString(), v.toString()));
}
List<String> _stringList(dynamic raw) {
if (raw is! List) return [];
return raw.map((e) => e.toString().trim()).where((e) => e.isNotEmpty).toList();
}
List<SymbolLayer> _layerList(dynamic raw) {
if (raw is! List) return [];
return [
for (final item in raw)
if (item is Map)
SymbolLayer(
system: item['system']?.toString() ?? '',
title: item['title']?.toString() ?? '',
law: item['law']?.toString() ?? '',
),
].where((layer) => layer.system.isNotEmpty && layer.law.trim().isNotEmpty).toList();
}
Map<String, dynamic>? customEditOf(String word) {
final edit = _userEdits[word.toLowerCase().trim()];
if (edit == null) return null;
return Map<String, dynamic>.from(edit);
}
void _applyEditsToSessionWords() {
_sessionWords = _sessionWords.map((w) {
final base = kRussianTop1000Words.firstWhere(
(b) => b.id == w.id,
orElse: () => w,
);
return _enrichedWord(base).copyWith(sessionNumber: w.sessionNumber);
}).toList();
}
Future<void> savePassport(WordPassport passport) async {
_passports[passport.id] = passport;
final key = passport.word.toLowerCase().trim();
final previous = Map<String, dynamic>.from(_userEdits[key] ?? {});
previous['passport'] = passport.toMap();
if (passport.semanticCore.isNotEmpty) previous['core'] = passport.semanticCore;
if (passport.willVector.isNotEmpty) previous['asks'] = passport.willVector;
if (passport.shadowWarning.isNotEmpty) previous['shadow'] = passport.shadowWarning;
previous['updatedAt'] = DateTime.now().toUtc().toIso8601String();
_userEdits[key] = previous;
await _saveUserEditsToPrefs();
}
Future<void> saveCustomEdit(String word, {
required String core,
required String oracle,
required String shadow,
required String asks,
required String step,
Map<String, String>? cultures,
List<SymbolLayer>? layers,
Map<String, String>? languages,
List<String>? binds,
Map<String, String>? reports,
}) async {
final key = word.toLowerCase().trim();
final previous = Map<String, dynamic>.from(_userEdits[key] ?? {});
previous
..['core'] = core.trim()
..['oracle'] = oracle.trim()
..['shadow'] = shadow.trim()
..['asks'] = asks.trim()
..['step'] = step.trim()
..['updatedAt'] = DateTime.now().toUtc().toIso8601String();
if (cultures != null) previous['cultures'] = cultures;
if (layers != null) {
previous['layers'] = [
for (final layer in layers)
if (layer.law.trim().isNotEmpty)
{'system': layer.system, 'title': layer.title, 'law': layer.law.trim()},
];
}
if (languages != null) previous['languages'] = languages;
if (binds != null) previous['binds'] = binds;
if (reports != null) previous['reports'] = reports;
_userEdits[key] = previous;
await _saveUserEditsToPrefs();
_applyEditsToSessionWords();
}
Future<void> resetCustomEdit(String word) async {
final key = word.toLowerCase().trim();
if (_userEdits.remove(key) != null) {
await _saveUserEditsToPrefs();
_applyEditsToSessionWords();
}
}
Future<void> clearAllCustomEdits() async {
_userEdits.clear();
await _saveUserEditsToPrefs();
_applyEditsToSessionWords();
}
Future<int> importCustomEditsJson(String jsonStr) async {
try {
final decoded = json.decode(jsonStr);
var count = 0;
Map<String, dynamic> rawMap = {};
if (decoded is Map<String, dynamic>) {
if (decoded['symbols'] is Map<String, dynamic>) {
rawMap = Map<String, dynamic>.from(decoded['symbols'] as Map);
} else if (decoded['symbols'] is List) {
for (final item in decoded['symbols'] as List) {
if (item is Map && item['word'] != null) {
rawMap[item['word'].toString()] = Map<String, dynamic>.from(item);
}
}
} else {
rawMap = decoded;
}
} else if (decoded is List) {
for (final item in decoded) {
if (item is Map && item['word'] != null) {
rawMap[item['word'].toString()] = Map<String, dynamic>.from(item);
}
}
}
for (final entry in rawMap.entries) {
final key = entry.key.toLowerCase().trim();
if (entry.value is Map<String, dynamic>) {
final val = Map<String, dynamic>.from(entry.value as Map);
final core = val['core']?.toString().trim() ?? '';
final oracle = (val['oracle']?.toString() ?? val['description']?.toString() ?? '').trim();
final shadow = val['shadow']?.toString().trim() ?? '';
final asks = val['asks']?.toString().trim() ?? '';
final step = val['step']?.toString().trim() ?? '';
final cultures = val['cultures'] is Map ? _stringMap(val['cultures']) : null;
final languages = val['languages'] is Map ? _stringMap(val['languages']) : null;
final layers = val['layers'] is List ? _layerList(val['layers']) : null;
final binds = val['binds'] is List ? _stringList(val['binds']) : null;
final reports = val['reports'] is Map ? _stringMap(val['reports']) : null;
final hasBody = core.isNotEmpty ||
oracle.isNotEmpty ||
shadow.isNotEmpty ||
asks.isNotEmpty ||
step.isNotEmpty ||
(cultures != null && cultures.isNotEmpty) ||
(languages != null && languages.isNotEmpty) ||
(layers != null && layers.isNotEmpty);
if (hasBody) {
_userEdits[key] = {
'core': core,
'oracle': oracle,
'shadow': shadow,
'asks': asks,
'step': step,
'cultures': ?cultures,
'languages': ?languages,
if (layers != null)
'layers': [
for (final layer in layers)
{'system': layer.system, 'title': layer.title, 'law': layer.law},
],
'binds': ?binds,
'reports': ?reports,
'updatedAt': val['updatedAt']?.toString() ?? DateTime.now().toUtc().toIso8601String(),
};
count++;
}
}
}
if (count > 0) {
await _saveUserEditsToPrefs();
_applyEditsToSessionWords();
}
return count;
} catch (e) {
debugPrint('Error importing custom edits: $e');
return 0;
}
}
String exportCustomEditsJson() {
final exportMap = <String, dynamic>{};
for (final entry in _userEdits.entries) {
final key = entry.key;
final wordItem = getWordByName(key);
exportMap[key] = {
'word': key,
if (wordItem != null) 'id': wordItem.id,
if (wordItem != null) 'sessionNumber': wordItem.sessionNumber,
if (wordItem != null) 'emoji': wordItem.emoji,
if (wordItem != null) 'category': wordItem.category,
if (wordItem != null) 'pos': wordItem.partOfSpeech,
...entry.value,
};
}
return const JsonEncoder.withIndent(' ').convert({
'version': '1.0',
'type': 'oracle_custom_edits',
'exportedAt': DateTime.now().toUtc().toIso8601String(),
'count': exportMap.length,
'symbols': exportMap,
});
}
String exportFullLexiconJson() {
final list = _sessionWords.map((w) {
return {
'id': w.id,
'sessionNumber': w.sessionNumber,
'word': w.word,
'emoji': w.emoji,
'pos': w.partOfSpeech,
'category': w.category,
'core': w.core,
'oracle': w.oracle,
'shadow': w.shadow,
'asks': w.asks,
'step': w.step,
'binds': w.binds,
'cultures': w.cultures,
'languages': w.languages,
'layers': w.layers.map((l) => l.toMap()).toList(),
'description': w.description,
'family': w.family,
'operator': w.operator,
'valence': w.valence,
'jurisdictions': w.jurisdictions,
'isUserEdited': w.isUserEdited,
};
}).toList();
return const JsonEncoder.withIndent(' ').convert({
'version': '1.0',
'type': 'oracle_full_lexicon',
'exportedAt': DateTime.now().toUtc().toIso8601String(),
'count': list.length,
'symbols': list,
});
}
Future<void> _loadUserEditsFromPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_kCustomEditsKey);
if (raw != null) {
final map = json.decode(raw) as Map<String, dynamic>;
_userEdits = map.map((k, v) => MapEntry(k.toLowerCase().trim(), Map<String, dynamic>.from(v as Map)));
for (final entry in _userEdits.entries) {
if (entry.value['passport'] is Map) {
final p = WordPassport.fromMap(Map<String, dynamic>.from(entry.value['passport'] as Map));
_passports[p.id] = p;
}
}
}
} catch (_) {}
}
Future<void> _saveUserEditsToPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kCustomEditsKey, json.encode(_userEdits));
} catch (_) {}
}
void _initCanonicalWords() {
_sessionWords = [
for (var i = 0; i < kRussianTop1000Words.length; i++)
_enrichedWord(kRussianTop1000Words[i]).copyWith(sessionNumber: i),
];
_applyEditsToSessionWords();
}
Future<RandomSource> reshuffleSessionNumbers() async {
_initCanonicalWords();
await _healAllHistoryAndSessions();
return RandomSource.local;
}
Future<void> _healAllHistoryAndSessions() async {
await _loadHistoryFromPrefs();
await _loadSyncreticSessionsFromPrefs();
}
Future<RandomSource> _restoreOrFetchDailyMapping({required bool force}) async {
final today = utcDateStamp();
_initCanonicalWords();
_dailyMappingDate = today;
_dailyMappingSource = _useRemoteShuffle ? RandomSource.randomOrg : RandomSource.local;
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_kMappingOrderKey);
await prefs.setString(_kMappingDateKey, today);
await prefs.setString(_kMappingSourceKey, _dailyMappingSource.name);
} catch (_) {}
return _dailyMappingSource;
}
// Compatibility helpers
void applyIndexOrder(List<int> order) => _initCanonicalWords();
Future<bool> loadPersistedMapping(String today) async => true;
Future<void> persistMapping(String date, List<int> order, RandomSource source) async {}
static String utcDateStamp([DateTime? now]) {
final d = (now ?? DateTime.now()).toUtc();
final y = d.year.toString().padLeft(4, "0");
final m = d.month.toString().padLeft(2, "0");
final day = d.day.toString().padLeft(2, "0");
return "$y-$m-$day";
}
Future<void> deleteSession(String id) => removeSession(id);
WordItem? getWordBySessionNumber(int number) {
if (number < 0 || number >= _sessionWords.length) return null;
return _sessionWords[number];
}
WordItem? getWordByName(String name) {
final query = name.trim().toLowerCase();
for (final w in _sessionWords) {
if (w.word.toLowerCase() == query) return w;
}
return null;
}
WordItem? getWordById(int id) {
for (final w in _sessionWords) {
if (w.id == id) return w;
}
return null;
}
WordItem getRandomWord() {
final random = Random.secure();
final idx = random.nextInt(_sessionWords.length);
return _sessionWords[idx];
}
WordItem getNextWord(int currentSessionNumber) {
int nextNumber = (currentSessionNumber + 1) % _sessionWords.length;
return _sessionWords[nextNumber];
}
WordItem getPreviousWord(int currentSessionNumber) {
int prevNumber = (currentSessionNumber - 1 + _sessionWords.length) % _sessionWords.length;
return _sessionWords[prevNumber];
}
List<WordItem> search(String query) {
query = query.trim().toLowerCase();
if (query.isEmpty) return [];
final number = int.tryParse(query);
if (number != null && number >= 0 && number < _sessionWords.length) {
return [_sessionWords[number]];
}
final exact = <WordItem>[];
final startsWith = <WordItem>[];
final contains = <WordItem>[];
for (final w in _sessionWords) {
final lower = w.word.toLowerCase();
if (lower == query) {
exact.add(w);
} else if (lower.startsWith(query)) {
startsWith.add(w);
} else if (lower.contains(query) ||
w.description.toLowerCase().contains(query) ||
w.category.toLowerCase().contains(query) ||
w.partOfSpeech.toLowerCase().contains(query) ||
w.family.toLowerCase().contains(query) ||
w.operator.toLowerCase().contains(query) ||
w.core.toLowerCase().contains(query)) {
contains.add(w);
}
}
return [...exact, ...startsWith, ...contains];
}
List<String> getCategories() {
final set = <String>{'Все'};
for (final w in _sessionWords) {
if (w.category.isNotEmpty) {
set.add(w.category);
}
}
return set.toList();
}
List<String> getPartsOfSpeech() {
final set = <String>{'Все'};
for (final w in _sessionWords) {
if (w.partOfSpeech.isNotEmpty) {
set.add(w.partOfSpeech);
}
}
return set.toList();
}
List<String> getOperators() {
final set = <String>{'Все'};
for (final w in _sessionWords) {
if (w.operator.isNotEmpty) {
set.add(w.operator);
}
}
return set.toList();
}
// --- Session History Management ---
Future<void> saveSession(OracleSession session) async {
final existingIdx = _sessions.indexWhere((s) => s.id == session.id);
if (existingIdx != -1) {
_sessions[existingIdx] = session;
} else {
_sessions.insert(0, session);
}
if (_sessions.length > 500) {
_sessions.removeRange(500, _sessions.length);
}
await _saveHistoryToPrefs();
}
// Backward compatibility alias
Future<void> addHistoryEntry(OracleSession entry) => saveSession(entry);
Future<void> updateSession(String id, {String? aiInterpretation, String? question}) async {
final idx = _sessions.indexWhere((e) => e.id == id);
if (idx != -1) {
final old = _sessions[idx];
_sessions[idx] = old.copyWith(
aiInterpretation: aiInterpretation ?? old.aiInterpretation,
question: question ?? old.question,
);
await _saveHistoryToPrefs();
}
}
Future<void> updateHistoryEntry(String id, {String? aiInterpretation, String? question}) =>
updateSession(id, aiInterpretation: aiInterpretation, question: question);
Future<void> removeSession(String id) async {
_sessions.removeWhere((s) => s.id == id);
await _saveHistoryToPrefs();
}
// Backward compatibility alias
Future<void> deleteHistoryEntry(String id) => removeSession(id);
Future<void> clearHistory() async {
_sessions.clear();
await _saveHistoryToPrefs();
}
Future<void> _loadHistoryFromPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
final historyJson = prefs.getStringList(_kHistoryKey);
if (historyJson != null) {
_sessions.clear();
bool needsResave = false;
for (final itemStr in historyJson) {
try {
final map = json.decode(itemStr) as Map<String, dynamic>;
final session = OracleSession.fromMap(map);
// Heal session words to match their canonical sacred IDs
final healedWords = session.words.map((sw) {
final canonical = getWordByName(sw.word);
if (canonical != null && canonical.sessionNumber != sw.sessionNumber) {
needsResave = true;
return SessionWord(
word: canonical.word,
sessionNumber: canonical.sessionNumber,
partOfSpeech: canonical.partOfSpeech.isNotEmpty ? canonical.partOfSpeech : sw.partOfSpeech,
category: canonical.category.isNotEmpty ? canonical.category : sw.category,
symbolicMeaning: sw.symbolicMeaning.isNotEmpty ? sw.symbolicMeaning : canonical.displayMeaning,
positionTitle: sw.positionTitle,
);
}
return sw;
}).toList();
final healedMessages = session.messages.map((m) {
if (m.attachedWord != null) {
final canonical = getWordByName(m.attachedWord!.word);
if (canonical != null && canonical.sessionNumber != m.attachedWord!.sessionNumber) {
needsResave = true;
return SessionChatMessage(
id: m.id,
sender: m.sender,
text: m.text,
timestamp: m.timestamp,
attachedWord: SessionWord(
word: canonical.word,
sessionNumber: canonical.sessionNumber,
partOfSpeech: canonical.partOfSpeech.isNotEmpty ? canonical.partOfSpeech : m.attachedWord!.partOfSpeech,
category: canonical.category.isNotEmpty ? canonical.category : m.attachedWord!.category,
symbolicMeaning: m.attachedWord!.symbolicMeaning,
positionTitle: m.attachedWord!.positionTitle,
),
);
}
}
return m;
}).toList();
_sessions.add(session.copyWith(
words: healedWords,
messages: healedMessages,
));
} catch (_) {}
}
if (needsResave) {
await _saveHistoryToPrefs();
}
}
} catch (_) {}
}
Future<void> _saveHistoryToPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
final historyJson = _sessions.map((s) => json.encode(s.toMap())).toList();
await prefs.setStringList(_kHistoryKey, historyJson);
} catch (_) {}
}
static const String _kSyncreticSessionsKey = 'oracle_999_syncretic_sessions_v1';
List<SyncreticSession> _syncreticSessions = [];
SyncreticSession? _currentSyncreticSession;
List<SyncreticSession> get syncreticSessions => List.unmodifiable(_syncreticSessions);
SyncreticSession get currentSyncreticSession {
if (_currentSyncreticSession == null) {
final todayKey = DateTime.now().toIso8601String().substring(0, 10);
final existing = _syncreticSessions.where((s) => s.dateKey == todayKey);
if (existing.isNotEmpty) {
_currentSyncreticSession = existing.first;
} else {
_currentSyncreticSession = SyncreticSession(
id: 'syn_${DateTime.now().millisecondsSinceEpoch}',
date: DateTime.now(),
);
}
}
return _currentSyncreticSession!;
}
Future<void> addReadingToSyncreticSession(SyncreticReading reading, {String? question}) async {
final current = currentSyncreticSession;
final existingIdx = current.readings.indexWhere((r) => r.id == reading.id);
final List<SyncreticReading> updatedReadings;
if (existingIdx >= 0) {
updatedReadings = [...current.readings];
updatedReadings[existingIdx] = reading;
} else {
updatedReadings = [...current.readings, reading];
}
final updatedSession = current.copyWith(
readings: updatedReadings,
mainQuestion: (question != null && question.trim().isNotEmpty)
? question.trim()
: current.mainQuestion,
);
await saveSyncreticSession(updatedSession);
}
Future<void> removeReadingFromSyncreticSession(String readingId) async {
final current = currentSyncreticSession;
final updatedReadings = current.readings.where((r) => r.id != readingId).toList();
final updatedSession = current.copyWith(readings: updatedReadings);
await saveSyncreticSession(updatedSession);
}
Future<void> saveSyncreticSession(SyncreticSession session) async {
_currentSyncreticSession = session;
final idx = _syncreticSessions.indexWhere((s) => s.id == session.id);
if (idx >= 0) {
_syncreticSessions[idx] = session;
} else {
_syncreticSessions.insert(0, session);
}
await _saveSyncreticSessionsToPrefs();
}
Future<void> startNewSyncreticSession({String? question}) async {
final newSession = SyncreticSession(
id: 'syn_${DateTime.now().millisecondsSinceEpoch}',
date: DateTime.now(),
mainQuestion: question,
);
await saveSyncreticSession(newSession);
}
Future<void> _loadSyncreticSessionsFromPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
final list = prefs.getStringList(_kSyncreticSessionsKey);
if (list != null) {
bool needsResave = false;
_syncreticSessions = list
.map((str) {
try {
final session = SyncreticSession.fromJson(str);
final healedReadings = session.readings.map((r) {
final healedItems = r.items.map((item) {
if (item.linkedWord != null) {
final canonical = getWordByName(item.linkedWord!.word);
if (canonical != null && canonical.sessionNumber != item.linkedWord!.sessionNumber) {
needsResave = true;
var newTitle = item.title;
final numRe = RegExp(r'#\d{3}');
if (numRe.hasMatch(newTitle)) {
newTitle = newTitle.replaceFirst(numRe, '#${canonical.formattedSessionNumber}');
}
return ReadingSignItem(
title: newTitle,
subtitle: item.subtitle,
symbol: item.symbol,
lore: item.lore,
isReversed: item.isReversed,
linkedWord: canonical,
meta: item.meta,
);
}
}
return item;
}).toList();
return SyncreticReading(
id: r.id,
systemId: r.systemId,
systemName: r.systemName,
systemIcon: r.systemIcon,
timestamp: r.timestamp,
layoutTitle: r.layoutTitle,
inputMethod: r.inputMethod,
items: healedItems,
note: r.note,
question: r.question,
);
}).toList();
return session.copyWith(readings: healedReadings);
} catch (_) {
return null;
}
})
.whereType<SyncreticSession>()
.toList();
if (needsResave) {
await _saveSyncreticSessionsToPrefs();
}
}
} catch (e) {
debugPrint('Error loading syncretic sessions: $e');
}
}
Future<void> _saveSyncreticSessionsToPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
final list = _syncreticSessions.map((s) => s.toJson()).toList();
await prefs.setStringList(_kSyncreticSessionsKey, list);
} catch (e) {
debugPrint('Error saving syncretic sessions: $e');
}
}
}