1682578501
- activeSeason now returns the first season (Season 1 'First Bloom') so a new player starts at spring instead of the newest season. - Bundle the owner-picked CC0 tracks menu.mp3 + season_001.mp3 (BGM now audible). - Settings footer shows 'v1.0.0 (build 3)' so test builds are identifiable. 180 tests green, analyze clean.
135 lines
4.7 KiB
Dart
135 lines
4.7 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../data/content_repository.dart';
|
|
import '../data/save_repository.dart';
|
|
import '../data/streak.dart';
|
|
import '../game/models/season.dart';
|
|
import '../services/ad_service.dart';
|
|
import '../services/analytics_service.dart';
|
|
import '../services/audio_service.dart';
|
|
import '../services/consent_service.dart';
|
|
import '../services/iap_service.dart';
|
|
import '../services/music_service.dart';
|
|
import 'ads_notifier.dart';
|
|
import 'endless_best_notifier.dart';
|
|
import 'music_notifier.dart';
|
|
import 'sound_notifier.dart';
|
|
import 'game_session_notifier.dart';
|
|
import 'progress_notifier.dart';
|
|
import 'season_flow_notifier.dart';
|
|
import 'streak_notifier.dart';
|
|
import 'tutorial_notifier.dart';
|
|
|
|
final gameSessionProvider =
|
|
NotifierProvider<GameSessionNotifier, GameViewState?>(
|
|
GameSessionNotifier.new,
|
|
);
|
|
|
|
final soundEnabledProvider =
|
|
NotifierProvider<SoundEnabledNotifier, bool>(SoundEnabledNotifier.new);
|
|
|
|
final audioServiceProvider = Provider<AudioService>((ref) {
|
|
final service = AudioService(enabled: ref.read(soundEnabledProvider));
|
|
ref.listen<bool>(soundEnabledProvider, (_, next) => service.enabled = next);
|
|
ref.onDispose(service.dispose);
|
|
return service;
|
|
});
|
|
|
|
final musicEnabledProvider =
|
|
NotifierProvider<MusicEnabledNotifier, bool>(MusicEnabledNotifier.new);
|
|
|
|
final musicServiceProvider = Provider<MusicService>((ref) {
|
|
final service = MusicService(enabled: ref.read(musicEnabledProvider));
|
|
ref.listen<bool>(musicEnabledProvider, (_, next) => service.enabled = next);
|
|
ref.onDispose(service.dispose);
|
|
return service;
|
|
});
|
|
|
|
/// Overridden with the opened repository in main() (and in tests).
|
|
final saveRepositoryProvider = Provider<SaveRepository>(
|
|
(ref) => throw UnimplementedError('override with an opened SaveRepository'),
|
|
);
|
|
|
|
final progressProvider =
|
|
NotifierProvider<ProgressNotifier, Map<String, StageProgress>>(
|
|
ProgressNotifier.new,
|
|
);
|
|
|
|
final seasonFlowProvider = NotifierProvider<SeasonFlowNotifier, SeasonFlow?>(
|
|
SeasonFlowNotifier.new,
|
|
);
|
|
|
|
final contentRepositoryProvider =
|
|
Provider<ContentRepository>((ref) => ContentRepository());
|
|
|
|
final seasonsProvider = FutureProvider<List<SeasonPack>>((ref) {
|
|
// Watching (not awaiting) the one-shot sync makes this provider re-run
|
|
// once when the sync completes, picking up freshly cached packs. Local
|
|
// content loads immediately; the network never blocks this future.
|
|
ref.watch(seasonRefreshProvider);
|
|
return ref.read(contentRepositoryProvider).availableSeasons();
|
|
});
|
|
|
|
/// One background content sync per app session. Home listens and refreshes
|
|
/// the season list when new packs arrived.
|
|
final seasonRefreshProvider = FutureProvider<bool>(
|
|
(ref) => ref.read(contentRepositoryProvider).refresh(),
|
|
);
|
|
|
|
/// The season players land in by default: Season 1, the start of the journey,
|
|
/// so a new player experiences the content in order rather than being dropped
|
|
/// into the newest season. (availableSeasons is sorted by seasonId ascending,
|
|
/// so `.first` is Season 1.) A season selector to reach later seasons is a
|
|
/// planned follow-up.
|
|
SeasonPack activeSeason(List<SeasonPack> seasons) => seasons.first;
|
|
|
|
final streakProvider = NotifierProvider<StreakNotifier, StreakState>(
|
|
StreakNotifier.new,
|
|
);
|
|
|
|
final tutorialProvider = NotifierProvider<TutorialNotifier, TutorialStep?>(
|
|
TutorialNotifier.new,
|
|
);
|
|
|
|
final endlessBestProvider = NotifierProvider<EndlessBestNotifier, int>(
|
|
EndlessBestNotifier.new,
|
|
);
|
|
|
|
final adsRemovedProvider =
|
|
NotifierProvider<AdsRemovedNotifier, bool>(AdsRemovedNotifier.new);
|
|
|
|
/// Reads ownership live from [adsRemovedProvider]; a mid-session purchase
|
|
/// takes effect on the next ad decision without re-wiring.
|
|
final adServiceProvider = Provider<AdService>((ref) {
|
|
final service = AdService(adsRemoved: () => ref.read(adsRemovedProvider));
|
|
ref.onDispose(service.dispose);
|
|
return service;
|
|
});
|
|
|
|
final consentServiceProvider = Provider<ConsentService>(
|
|
(ref) => ConsentService(ref.read(adServiceProvider)),
|
|
);
|
|
|
|
/// A verified remove_ads purchase/restore grants the entitlement through the
|
|
/// notifier (persists + flips state), which AdService and the banner observe.
|
|
final iapServiceProvider = Provider<IapService>((ref) {
|
|
final service = IapService(
|
|
onEntitlementGranted: () =>
|
|
ref.read(adsRemovedProvider.notifier).grant(),
|
|
);
|
|
ref.onDispose(service.dispose);
|
|
service.initialize();
|
|
return service;
|
|
});
|
|
|
|
final analyticsProvider = Provider<AnalyticsService>(
|
|
(ref) => AnalyticsService(DebugAnalyticsBackend()),
|
|
);
|
|
|
|
/// The visual theme of whatever season is in play; fallback outside seasons
|
|
/// (home, endless). Pure model — UI converts via ThemeColors.
|
|
final activeThemeProvider = Provider<SeasonTheme>((ref) {
|
|
final flow = ref.watch(seasonFlowProvider);
|
|
return flow?.pack.theme ?? SeasonTheme.fallback;
|
|
});
|