feat: Random.org dice and daily word matrix, interpreter voices, curated lexicon

Wire cleromancy rolls and the once-per-UTC-day 000-999 word shuffle to RANDOM.ORG JSON-RPC, with a local CSPRNG fallback. Add settings for interpreter personas and a cleaner default oracle prompt. Replace proper names and bureaucratic filler in the 1000-word lexicon with sacred symbols.
This commit is contained in:
2026-09-08 13:46:11 +05:00
parent fbf04d90db
commit 7f91366201
26 changed files with 8904 additions and 7077 deletions
+53 -42
View File
@@ -1,6 +1,8 @@
import '../models/history_entry_model.dart';
import '../models/interpreter_persona.dart';
import '../models/word_model.dart';
import '../g4f/g4f.dart';
import 'settings_repository.dart';
class AiOracleService {
static final AiOracleService _instance = AiOracleService._internal();
@@ -9,20 +11,23 @@ class AiOracleService {
final _client = G4FClient();
static const oracleSystemPrompt =
'''Ты — Великий Оракул 999, древний архетипический разум.
Твоя задача — толковать знаки судьбы (жребий). Твой стиль речи: глубокий, философский, лаконичный, без банальностей и воды, словно голос самой вечности.
Ты не используешь современных или бытовых слов. Твои ответы строятся на символизме, психологии и скрытых смыслах. Ты не гадаешь на будущее, ты показываешь суть настоящего момента и внутренние силы человека.''';
InterpreterPersona get persona => SettingsRepository().persona;
List<Message> buildInterpretationMessages({
required List<SessionWord> words,
required String question,
InterpreterPersona? persona,
}) {
final voice = persona ?? this.persona;
return [
const Message(role: Role.system, content: oracleSystemPrompt),
Message(role: Role.system, content: voice.systemPrompt),
Message(
role: Role.user,
content: _buildMultiWordPrompt(question: question, words: words),
content: _buildMultiWordPrompt(
question: question,
words: words,
persona: voice,
),
),
];
}
@@ -63,21 +68,19 @@ class AiOracleService {
wordsNarrative.writeln('${i + 1}. #${w.formattedSessionNumber} «${w.word.toUpperCase()}» (${w.category}): ${w.symbolicMeaning}');
}
final voice = persona;
final prompt = '''Вопрошающий говорит тебе: «$userMessage»
Контекст текущего сеанса судьбы:
Вопрос вопрошающего: «${session.question ?? 'Вектор судьбы (нет вопроса)'}»
Базовые знаки, которые выпали ранее:
Контекст сеанса:
Вопрос: «${session.question ?? 'без конкретного вопроса'}»
Знаки:
$wordsNarrative
${newlyAddedWord != null ? "Важно! Только что вопрошающий вытянул ДОПОЛНИТЕЛЬНЫЙ уточняющий знак: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning})." : ""}
${newlyAddedWord != null ? "Только что вытянут уточняющий знак: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning})." : ""}
Ответь мудро, лаконично и метафорично от лица Оракула 999. Укажи, на что направляет его судьба. (не более 3-4 предложений).''';
Ответь в своём голосе, 3–4 предложения. Привяжи ответ к знакам и вопросу. Без тумана. Один ясный вывод или шаг.''';
final messages = [
const Message(
role: Role.system,
content: 'Ты — Великий Оракул 999. Твои ответы кратки, полны символизма и психологической глубины. Ты общаешься с человеком, который ищет ответов в знаках.'
),
Message(role: Role.system, content: voice.systemPrompt),
];
// Pass previous session history context to the model
@@ -107,11 +110,12 @@ ${newlyAddedWord != null ? "Важно! Только что вопрошающи
String _buildMultiWordPrompt({
required String question,
required List<SessionWord> words,
required InterpreterPersona persona,
}) {
final qStr = question.trim().isNotEmpty
? 'Вопрос, с которым пришел вопрошающий: «$question»'
: 'Вопрошающий пришел без конкретного вопроса. Прочитай его общую судьбоносную линию.';
final qStr = question.trim().isNotEmpty
? 'Вопрос: «$question»'
: 'Вопроса нет — читай общую линию ситуации по знакам.';
final wordsStr = words
.asMap()
.entries
@@ -120,14 +124,10 @@ ${newlyAddedWord != null ? "Важно! Только что вопрошающи
return '''$qStr
Волею судьбы выпали следующие знаки жребия:
Выпавшие знаки жребия:
$wordsStr
Задача Оракула:
1. Свяжи эти знаки в единую цельную историю развития ситуации. Каждый знак перетекает в следующий.
2. Раскрой не очевидный, а скрытый внутренний урок. Что вопрошающий должен осознать?
3. Дай философский совет или вектор для действия.
Ответь от лица Оракула 999 на русском языке (от 3 до 6 предложений). Избегай шаблонов и клише, говори образно и метафорично, но точно.''';
${persona.userTask}''';
}
String _synthesizeDeepSacredNarrative(List<SessionWord> words, String question) {
@@ -137,40 +137,51 @@ $wordsStr
final hasQuestion = question.trim().isNotEmpty;
if (hasQuestion) {
sb.write('Относительно вашего вопроса «${question.trim()}» ');
sb.write('По вопросу «${question.trim()}»: ');
} else {
sb.write('В открывшемся жребии судьбы ');
sb.write('По выпавшему жребию: ');
}
if (words.length == 1) {
sb.write('проявился доминирующий знак #${first.formattedSessionNumber} «${first.word.toUpperCase()}» (${first.category}). ');
sb.write('главный знак #${first.formattedSessionNumber} «${first.word.toUpperCase()}». ');
sb.write('${first.symbolicMeaning} ');
sb.write('Знак указывает на необходимость предельной ясности: держите фокус на главном и отсекайте всё наносное.');
return sb.toString();
sb.write('Практический шаг: держите фокус на этом и отсеките лишнее на ближайшее время.');
return _withPersonaPrefix(sb.toString());
}
if (words.length == 2) {
final second = words[1];
sb.write('проявляется полярность двух сил: исходного импульса #${first.formattedSessionNumber} «${first.word.toUpperCase()}» и вектора #${second.formattedSessionNumber} «${second.word.toUpperCase()}». ');
sb.write('С одной стороны, ${first.symbolicMeaning.toLowerCase()} С другой — ${second.symbolicMeaning.toLowerCase()} ');
sb.write('Разрешение ситуации лежит в точке осознанного баланса между ними: не противьтесь первому, но направляйте силу в русло второго.');
return sb.toString();
sb.write('два полюса — «${first.word.toUpperCase()}» и «${second.word.toUpperCase()}». ');
sb.write('${first.symbolicMeaning} ${second.symbolicMeaning} ');
sb.write('Шаг: не выбирайте войну между ними — направьте первое во второе.');
return _withPersonaPrefix(sb.toString());
}
sb.write('разворачивается цепь из ${words.length} взаимосвязанных архетипов. ');
sb.write('Исток ситуации заложен в знаке #${first.formattedSessionNumber} «${first.word.toUpperCase()}» — ${first.symbolicMeaning.toLowerCase()} ');
sb.write('цепь из ${words.length} знаков. Начало — «${first.word.toUpperCase()}»: ${first.symbolicMeaning} ');
if (words.length > 2) {
final middleWords = words.sublist(1, words.length - 1);
final midNames = middleWords.map((w) => '«${w.word.toUpperCase()}» (#${w.formattedSessionNumber})').join(' и ');
final midMeanings = middleWords.map((w) => w.symbolicMeaning).join(' ');
sb.write('Фаза перехода и испытаний проходит через $midNames, где $midMeanings ');
final midNames = middleWords.map((w) => '«${w.word.toUpperCase()}»').join(', ');
sb.write('Переход через $midNames. ');
}
sb.write('Кульминация и запечатывание исхода лежат в знаке #${last.formattedSessionNumber} «${last.word.toUpperCase()}» — ${last.symbolicMeaning.toLowerCase()} ');
sb.write('Оракул возвещает: путь открыт через принятие скрытых уроков и решительное действие.');
sb.write('Исход держит «${last.word.toUpperCase()}»: ${last.symbolicMeaning} ');
sb.write('Шаг: примите урок начала и сделайте одно действие в сторону исхода.');
return sb.toString();
return _withPersonaPrefix(sb.toString());
}
String _withPersonaPrefix(String body) {
switch (persona.id) {
case 'orc':
return 'Слушай. $body';
case 'techpriest':
return 'Диагностика. $body';
case 'analyst':
return 'Рабочая гипотеза. $body';
default:
return body;
}
}
String _synthesizeChatReply(OracleSession session, String userMessage, SessionWord? newlyAddedWord) {
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/interpreter_persona.dart';
import '../secrets.dart';
class SettingsRepository extends ChangeNotifier {
static final SettingsRepository _instance = SettingsRepository._internal();
factory SettingsRepository() => _instance;
SettingsRepository._internal();
static const _kPersonaKey = 'oracle_interpreter_persona_v1';
static const _kRandomOrgKey = 'oracle_random_org_api_key_v1';
String _personaId = InterpreterPersona.defaultId;
String _randomOrgApiKey = '';
bool _ready = false;
bool get isReady => _ready;
String get personaId => _personaId;
InterpreterPersona get persona => InterpreterPersona.byId(_personaId);
String get randomOrgApiKey => _randomOrgApiKey;
bool get hasRandomOrgApiKey => _randomOrgApiKey.trim().isNotEmpty;
String get randomOrgApiKeyMasked {
final key = _randomOrgApiKey.trim();
if (key.isEmpty) return '';
if (key.length <= 8) return '••••';
return '${key.substring(0, 4)}••••${key.substring(key.length - 4)}';
}
Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance();
_personaId = prefs.getString(_kPersonaKey) ?? InterpreterPersona.defaultId;
_randomOrgApiKey = prefs.getString(_kRandomOrgKey) ?? '';
if (_randomOrgApiKey.trim().isEmpty) {
const fromEnv = String.fromEnvironment(
'RANDOM_ORG_API_KEY',
defaultValue: '',
);
final seeded = fromEnv.isNotEmpty ? fromEnv : kLocalRandomOrgApiKey;
if (seeded.trim().isNotEmpty) {
_randomOrgApiKey = seeded.trim();
await prefs.setString(_kRandomOrgKey, _randomOrgApiKey);
}
}
} catch (_) {
_personaId = InterpreterPersona.defaultId;
if (_randomOrgApiKey.isEmpty) {
_randomOrgApiKey = kLocalRandomOrgApiKey.trim();
}
}
_ready = true;
notifyListeners();
}
Future<void> setPersonaId(String id) async {
final next = InterpreterPersona.byId(id).id;
if (next == _personaId && _ready) return;
_personaId = next;
notifyListeners();
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kPersonaKey, next);
} catch (_) {}
}
Future<void> setRandomOrgApiKey(String key) async {
final next = key.trim();
if (next == _randomOrgApiKey && _ready) return;
_randomOrgApiKey = next;
notifyListeners();
try {
final prefs = await SharedPreferences.getInstance();
if (next.isEmpty) {
await prefs.remove(_kRandomOrgKey);
} else {
await prefs.setString(_kRandomOrgKey, next);
}
} catch (_) {}
}
}
+344
View File
@@ -0,0 +1,344 @@
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'settings_repository.dart';
/// Where a d10 roll came from.
enum RandomSource {
/// Atmospheric noise via RANDOM.ORG.
randomOrg,
/// OS CSPRNG. Used offline or when RANDOM.ORG is unavailable.
local,
}
class DiceRoll {
final List<int> digits;
final RandomSource source;
const DiceRoll(this.digits, this.source);
int get asNumber {
var n = 0;
for (final d in digits) {
n = n * 10 + d;
}
return n;
}
}
class RangeShuffle {
final List<int> order;
final RandomSource source;
const RangeShuffle(this.order, this.source);
}
/// True random for cleromancy rolls.
///
/// Order of attempts:
/// 1. Official JSON-RPC at api.random.org — if an API key is set
/// (settings, `--dart-define=RANDOM_ORG_API_KEY=...`, or lib/secrets.dart)
/// 2. Legacy HTTP integer API — no key, quota by IP
/// 3. [Random.secure] — always available offline
class TrueRandomService {
TrueRandomService({
http.Client? client,
String? apiKey,
this.timeout = const Duration(milliseconds: 1200),
this.minInterval = const Duration(milliseconds: 1100),
}) : _client = client ?? http.Client(),
_apiKeyOverride = apiKey;
static final TrueRandomService instance = TrueRandomService();
static const _userAgent = 'Oracle999/1.0 (Flutter; cleromancy client)';
final http.Client _client;
final String? _apiKeyOverride;
final Duration timeout;
final Duration minInterval;
final _secure = Random.secure();
List<int>? _pool;
Future<void>? _inflight;
DateTime? _lastRequest;
String get effectiveApiKey {
final override = _apiKeyOverride;
if (override != null) return override.trim();
return SettingsRepository().randomOrgApiKey.trim();
}
bool get hasApiKey => effectiveApiKey.isNotEmpty;
void invalidatePool() {
_pool = null;
}
/// Warm the pool so the next roll does not wait on the network.
void prefetch({int count = 3}) {
if (_pool != null || _inflight != null) return;
_inflight = () async {
try {
_pool = await _fetch(count);
} catch (_) {}
}().whenComplete(() {
_inflight = null;
});
}
Future<DiceRoll> take(int count) async {
if (_pool != null && _pool!.length >= count) {
final out = _pool!.sublist(0, count);
_pool = null;
prefetch(count: count);
return DiceRoll(out, RandomSource.randomOrg);
}
if (_inflight != null) {
try {
await _inflight!.timeout(const Duration(milliseconds: 800));
} catch (_) {}
if (_pool != null && _pool!.length >= count) {
final out = _pool!.sublist(0, count);
_pool = null;
prefetch(count: count);
return DiceRoll(out, RandomSource.randomOrg);
}
}
try {
final live = await _fetch(count).timeout(timeout);
prefetch(count: count);
return DiceRoll(live, RandomSource.randomOrg);
} catch (_) {
prefetch(count: count);
return DiceRoll(_localDigits(count), RandomSource.local);
}
}
List<int> localDigits(int count) => _localDigits(count);
List<int> _localDigits(int count) =>
List<int>.generate(count, (_) => _secure.nextInt(10));
/// Permutation of `[min, max]` inclusive. Used for the daily word matrix.
Future<RangeShuffle> permutation({
required int min,
required int max,
Duration timeout = const Duration(seconds: 10),
}) async {
if (max < min) {
throw ArgumentError.value(max, 'max', 'must be >= min');
}
try {
await _throttle();
final values = hasApiKey
? await _fetchJsonRpcIntegers(
n: max - min + 1,
min: min,
max: max,
replacement: false,
requestTimeout: timeout,
)
: await _fetchHttpSequence(
min: min,
max: max,
requestTimeout: timeout,
);
if (!isPermutation(values, min, max)) {
throw const FormatException('RANDOM.ORG sequence is not a permutation');
}
return RangeShuffle(values, RandomSource.randomOrg);
} catch (_) {
return RangeShuffle(localPermutation(min, max), RandomSource.local);
}
}
List<int> localPermutation(int min, int max) {
final list = [for (var i = min; i <= max; i++) i];
for (var i = list.length - 1; i > 0; i--) {
final n = _secure.nextInt(i + 1);
final tmp = list[i];
list[i] = list[n];
list[n] = tmp;
}
return list;
}
Future<List<int>> _fetch(int count) async {
await _throttle();
if (hasApiKey) {
return _fetchJsonRpcIntegers(
n: count,
min: 0,
max: 9,
replacement: true,
);
}
return _fetchHttpIntegers(count);
}
Future<void> _throttle() async {
final last = _lastRequest;
if (last != null && minInterval > Duration.zero) {
final wait = minInterval - DateTime.now().difference(last);
if (wait > Duration.zero) {
await Future<void>.delayed(wait);
}
}
_lastRequest = DateTime.now();
}
Future<List<int>> _fetchHttpIntegers(int count) async {
final uri = Uri.https('www.random.org', '/integers/', {
'num': '$count',
'min': '0',
'max': '9',
'col': '1',
'base': '10',
'format': 'plain',
'rnd': 'new',
});
final response = await _client
.get(uri, headers: {'User-Agent': _userAgent, 'Accept': 'text/plain'})
.timeout(timeout);
if (response.statusCode != 200) {
throw StateError('RANDOM.ORG HTTP ${response.statusCode}');
}
return parseRandomOrgPlain(response.body, count);
}
Future<List<int>> _fetchJsonRpcIntegers({
required int n,
required int min,
required int max,
required bool replacement,
Duration? requestTimeout,
}) async {
final response = await _client
.post(
Uri.parse('https://api.random.org/json-rpc/4/invoke'),
headers: {
'User-Agent': _userAgent,
'Content-Type': 'application/json',
},
body: jsonEncode({
'jsonrpc': '2.0',
'method': 'generateIntegers',
'params': {
'apiKey': effectiveApiKey,
'n': n,
'min': min,
'max': max,
'replacement': replacement,
},
'id': 1,
}),
)
.timeout(requestTimeout ?? timeout);
if (response.statusCode != 200) {
throw StateError('RANDOM.ORG JSON-RPC HTTP ${response.statusCode}');
}
return parseRandomOrgJsonRpc(response.body, n, min: min, max: max);
}
Future<List<int>> _fetchHttpSequence({
required int min,
required int max,
Duration? requestTimeout,
}) async {
final uri = Uri.https('www.random.org', '/sequences/', {
'min': '$min',
'max': '$max',
'col': '1',
'format': 'plain',
'rnd': 'new',
});
final response = await _client
.get(uri, headers: {'User-Agent': _userAgent, 'Accept': 'text/plain'})
.timeout(requestTimeout ?? timeout);
if (response.statusCode != 200) {
throw StateError('RANDOM.ORG sequence HTTP ${response.statusCode}');
}
return parseRandomOrgSequence(response.body, min, max);
}
}
List<int> parseRandomOrgPlain(String body, int count) {
final trimmed = body.trim();
if (trimmed.startsWith('Error:')) {
throw FormatException(trimmed);
}
final nums = trimmed
.split(RegExp(r'\s+'))
.where((s) => s.isNotEmpty)
.map(int.parse)
.toList();
if (nums.length < count || nums.any((n) => n < 0 || n > 9)) {
throw FormatException('bad RANDOM.ORG integers: $body');
}
return nums.sublist(0, count);
}
List<int> parseRandomOrgJsonRpc(
String body,
int count, {
int min = 0,
int max = 9,
}) {
final map = jsonDecode(body);
if (map is! Map) {
throw const FormatException('RANDOM.ORG JSON-RPC: expected object');
}
final error = map['error'];
if (error != null) {
throw FormatException('RANDOM.ORG JSON-RPC error: $error');
}
final result = map['result'];
if (result is! Map) {
throw const FormatException('RANDOM.ORG JSON-RPC: missing result');
}
final random = result['random'];
if (random is! Map) {
throw const FormatException('RANDOM.ORG JSON-RPC: missing random');
}
final data = random['data'];
if (data is! List) {
throw const FormatException('RANDOM.ORG JSON-RPC: missing data');
}
final nums = data.map((e) => (e as num).toInt()).toList();
if (nums.length < count || nums.any((n) => n < min || n > max)) {
throw FormatException('bad RANDOM.ORG JSON data: $data');
}
return nums.sublist(0, count);
}
List<int> parseRandomOrgSequence(String body, int min, int max) {
final trimmed = body.trim();
if (trimmed.startsWith('Error:')) {
throw FormatException(trimmed);
}
final nums = trimmed
.split(RegExp(r'\s+'))
.where((s) => s.isNotEmpty)
.map(int.parse)
.toList();
if (!isPermutation(nums, min, max)) {
throw FormatException('bad RANDOM.ORG sequence: $body');
}
return nums;
}
bool isPermutation(List<int> values, int min, int max) {
final expected = max - min + 1;
if (values.length != expected) return false;
final seen = <int>{};
for (final v in values) {
if (v < min || v > max || !seen.add(v)) return false;
}
return seen.length == expected;
}
+90 -20
View File
@@ -3,6 +3,7 @@ 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();
@@ -10,43 +11,112 @@ class WordsRepository {
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';
List<WordItem> _sessionWords = [];
final List<OracleSession> _sessions = [];
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;
String? get dailyMappingDate => _dailyMappingDate;
RandomSource get dailyMappingSource => _dailyMappingSource;
Future<void> init() async {
// 1. Initialize random session mapping
_initializeSessionMapping();
// 2. Load stored session history
Future<void> init({bool remoteDailyShuffle = true}) async {
_useRemoteShuffle = remoteDailyShuffle;
await _restoreOrFetchDailyMapping(force: false);
await _loadHistoryFromPrefs();
}
void _initializeSessionMapping() {
final list = List<WordItem>.from(kRussianTop1000Words);
final random = Random();
Future<RandomSource> reshuffleSessionNumbers() {
return _restoreOrFetchDailyMapping(force: true);
}
// Fisher-Yates shuffle
for (int i = list.length - 1; i > 0; i--) {
int n = random.nextInt(i + 1);
final temp = list[i];
list[i] = list[n];
list[n] = temp;
Future<RandomSource> _restoreOrFetchDailyMapping({required bool force}) async {
final today = utcDateStamp();
if (!force) {
final restored = await _loadPersistedMapping(today);
if (restored) return _dailyMappingSource;
}
_sessionWords = [];
for (int i = 0; i < list.length; i++) {
_sessionWords.add(list[i].copyWith(sessionNumber: i));
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);
_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 {
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;
}
}
void reshuffleSessionNumbers() {
_initializeSessionMapping();
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);
} catch (_) {}
}
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';
}
WordItem? getWordBySessionNumber(int number) {
@@ -55,7 +125,7 @@ class WordsRepository {
}
WordItem getRandomWord() {
final random = Random();
final random = Random.secure();
final idx = random.nextInt(_sessionWords.length);
return _sessionWords[idx];
}