import 'package:flutter/foundation.dart'; import '../data/remote/question_source_remote_store.dart'; import '../models/question_source_preference.dart'; /// Question source mix persisted via `/v1/me/question-sources`. class QuestionSourceConfigRepository { QuestionSourceConfigRepository._(); static final QuestionSourceConfigRepository instance = QuestionSourceConfigRepository._(); static const List defaultPreferences = [ QuestionSourcePreference( id: 'market_history', label: 'Market question', description: 'Prospective price-move guessing game', ratio: 50, ), QuestionSourcePreference( id: 'spelling', label: 'Spelling', description: 'Spell-check quiz from curated word lists', ratio: 50, ), ]; final QuestionSourceRemoteStore _remote = QuestionSourceRemoteStore(); final ValueNotifier> preferences = ValueNotifier>( List.from(defaultPreferences), ); String? _activeUid; List get current => mergeWithDefaults(preferences.value); Future startSession(String firebaseUid) async { if (_activeUid == firebaseUid) { return; } _activeUid = firebaseUid; await loadFromRemote(); } Future endSession() async { _activeUid = null; resetToDefaults(); } Future loadFromRemote() async { try { final List? remote = await _remote.fetchPreferences(); if (remote != null) { replaceAll(remote); } } catch (_) { replaceAll(defaultPreferences); } } Future saveAll(List next) async { final List normalized = mergeWithDefaults(next); if (_activeUid == null) { replaceAll(normalized); return; } final List saved = await _remote.savePreferences(normalized); replaceAll(saved); } void replaceAll(List next) { preferences.value = mergeWithDefaults(next); } /// Keeps saved toggles/weights while picking up new default sources and labels. static List mergeWithDefaults( List saved, ) { final Map byId = { for (final QuestionSourcePreference source in saved) source.id: source, }; return defaultPreferences .map((QuestionSourcePreference defaults) { final QuestionSourcePreference? existing = byId[defaults.id]; if (existing == null) { return defaults; } return existing.copyWith( label: defaults.label, description: defaults.description, comingSoon: defaults.comingSoon, enabled: defaults.comingSoon ? false : existing.enabled, ); }) .toList(growable: false); } void resetToDefaults() { replaceAll(defaultPreferences); } /// True when spelling is the only enabled, non–coming-soon source. static bool isSpellingOnlyEnabled(List sources) { final Iterable enabled = sources.where( (QuestionSourcePreference s) => s.enabled && !s.comingSoon, ); return enabled.length == 1 && enabled.first.id == 'spelling'; } /// True when market history is the only enabled, non–coming-soon source. static bool isMarketOnlyEnabled(List sources) { final Iterable enabled = sources.where( (QuestionSourcePreference s) => s.enabled && !s.comingSoon, ); return enabled.length == 1 && enabled.first.id == 'market_history'; } static bool isMarketHistoryEnabled(List sources) { for (final QuestionSourcePreference source in sources) { if (source.id == 'market_history') { return source.enabled && !source.comingSoon; } } return false; } static bool isSpellingEnabled(List sources) { for (final QuestionSourcePreference source in sources) { if (source.id == 'spelling') { return source.enabled && !source.comingSoon; } } return false; } /// True when spelling is enabled and its effective mix is 100%. static bool isSpellingFullyWeighted(List sources) { if (!isSpellingEnabled(sources)) { return false; } return (effectiveMix(sources)['spelling'] ?? 0) >= 100; } /// Effective share (0–100) for each enabled source; empty when none enabled. static Map effectiveMix( List sources, ) { final Iterable enabled = sources.where((QuestionSourcePreference s) => s.enabled); final int totalWeight = enabled.fold(0, (int sum, QuestionSourcePreference s) => sum + s.ratio); if (totalWeight <= 0) { return {}; } return { for (final QuestionSourcePreference source in enabled) source.id: ((source.ratio / totalWeight) * 100).round(), }; } }