820 lines
29 KiB
Dart
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');
|
|
}
|
|
}
|
|
|
|
}
|