feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
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';
|
||||
@@ -14,9 +20,17 @@ class WordsRepository {
|
||||
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;
|
||||
@@ -25,105 +39,441 @@ class WordsRepository {
|
||||
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<RandomSource> reshuffleSessionNumbers() {
|
||||
return _restoreOrFetchDailyMapping(force: true);
|
||||
|
||||
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();
|
||||
if (!force) {
|
||||
final restored = await _loadPersistedMapping(today);
|
||||
if (restored) return _dailyMappingSource;
|
||||
}
|
||||
|
||||
final lastId = kRussianTop1000Words.length - 1;
|
||||
RangeShuffle shuffle;
|
||||
if (_useRemoteShuffle) {
|
||||
shuffle = await TrueRandomService.instance.permutation(
|
||||
min: 0,
|
||||
max: lastId,
|
||||
);
|
||||
} else {
|
||||
shuffle = RangeShuffle(
|
||||
TrueRandomService.instance.localPermutation(0, lastId),
|
||||
RandomSource.local,
|
||||
);
|
||||
}
|
||||
|
||||
_applyIndexOrder(shuffle.order);
|
||||
_initCanonicalWords();
|
||||
_dailyMappingDate = today;
|
||||
_dailyMappingSource = shuffle.source;
|
||||
await _persistMapping(today, shuffle.order, shuffle.source);
|
||||
return shuffle.source;
|
||||
}
|
||||
|
||||
void _applyIndexOrder(List<int> order) {
|
||||
_sessionWords = [
|
||||
for (var i = 0; i < order.length; i++)
|
||||
kRussianTop1000Words[order[i]].copyWith(sessionNumber: i),
|
||||
];
|
||||
}
|
||||
|
||||
Future<bool> _loadPersistedMapping(String today) async {
|
||||
_dailyMappingSource = _useRemoteShuffle ? RandomSource.randomOrg : RandomSource.local;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final date = prefs.getString(_kMappingDateKey);
|
||||
final stored = prefs.getStringList(_kMappingOrderKey);
|
||||
if (date != today || stored == null) return false;
|
||||
final order = stored.map(int.parse).toList();
|
||||
if (!isPermutation(order, 0, kRussianTop1000Words.length - 1)) {
|
||||
return false;
|
||||
}
|
||||
_applyIndexOrder(order);
|
||||
_dailyMappingDate = date;
|
||||
final sourceName = prefs.getString(_kMappingSourceKey);
|
||||
_dailyMappingSource = sourceName == RandomSource.randomOrg.name
|
||||
? RandomSource.randomOrg
|
||||
: RandomSource.local;
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMapping(
|
||||
String date,
|
||||
List<int> order,
|
||||
RandomSource source,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kMappingDateKey, date);
|
||||
await prefs.setStringList(
|
||||
_kMappingOrderKey,
|
||||
order.map((e) => e.toString()).toList(),
|
||||
);
|
||||
await prefs.setString(_kMappingSourceKey, source.name);
|
||||
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';
|
||||
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);
|
||||
@@ -162,7 +512,10 @@ class WordsRepository {
|
||||
} else if (lower.contains(query) ||
|
||||
w.description.toLowerCase().contains(query) ||
|
||||
w.category.toLowerCase().contains(query) ||
|
||||
w.partOfSpeech.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);
|
||||
}
|
||||
}
|
||||
@@ -190,6 +543,16 @@ class WordsRepository {
|
||||
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 {
|
||||
@@ -223,12 +586,13 @@ class WordsRepository {
|
||||
Future<void> updateHistoryEntry(String id, {String? aiInterpretation, String? question}) =>
|
||||
updateSession(id, aiInterpretation: aiInterpretation, question: question);
|
||||
|
||||
Future<void> deleteSession(String id) async {
|
||||
_sessions.removeWhere((e) => e.id == id);
|
||||
Future<void> removeSession(String id) async {
|
||||
_sessions.removeWhere((s) => s.id == id);
|
||||
await _saveHistoryToPrefs();
|
||||
}
|
||||
|
||||
Future<void> deleteHistoryEntry(String id) => deleteSession(id);
|
||||
// Backward compatibility alias
|
||||
Future<void> deleteHistoryEntry(String id) => removeSession(id);
|
||||
|
||||
Future<void> clearHistory() async {
|
||||
_sessions.clear();
|
||||
@@ -238,14 +602,65 @@ class WordsRepository {
|
||||
Future<void> _loadHistoryFromPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = prefs.getStringList(_kHistoryKey);
|
||||
_sessions.clear();
|
||||
if (jsonList != null) {
|
||||
for (final item in jsonList) {
|
||||
final historyJson = prefs.getStringList(_kHistoryKey);
|
||||
if (historyJson != null) {
|
||||
_sessions.clear();
|
||||
bool needsResave = false;
|
||||
for (final itemStr in historyJson) {
|
||||
try {
|
||||
_sessions.add(OracleSession.fromJson(item));
|
||||
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 (_) {}
|
||||
}
|
||||
@@ -253,8 +668,145 @@ class WordsRepository {
|
||||
Future<void> _saveHistoryToPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = _sessions.map((e) => e.toJson()).toList();
|
||||
await prefs.setStringList(_kHistoryKey, jsonList);
|
||||
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> 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user