feat: session content sync trigger and newest-season selection
Add seasonRefreshProvider (once-per-session FutureProvider) and activeSeason() helper; HomeScreen listens and invalidates seasonsProvider when new packs arrive; season_map_screen and season_title_screen switch from list.first to activeSeason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,16 @@ final seasonsProvider = FutureProvider<List<SeasonPack>>(
|
|||||||
(ref) => ref.read(contentRepositoryProvider).availableSeasons(),
|
(ref) => 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: the newest available.
|
||||||
|
/// (availableSeasons is sorted by seasonId ascending.)
|
||||||
|
SeasonPack activeSeason(List<SeasonPack> seasons) => seasons.last;
|
||||||
|
|
||||||
final streakProvider = NotifierProvider<StreakNotifier, StreakState>(
|
final streakProvider = NotifierProvider<StreakNotifier, StreakState>(
|
||||||
StreakNotifier.new,
|
StreakNotifier.new,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
ref.listen(seasonRefreshProvider, (_, next) {
|
||||||
|
if (next is AsyncData<bool> && next.value == true) {
|
||||||
|
ref.invalidate(seasonsProvider);
|
||||||
|
}
|
||||||
|
});
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final streak = ref.watch(streakProvider);
|
final streak = ref.watch(streakProvider);
|
||||||
final best = ref.watch(endlessBestProvider);
|
final best = ref.watch(endlessBestProvider);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class SeasonMapScreen extends ConsumerWidget {
|
|||||||
loading: () =>
|
loading: () =>
|
||||||
const Scaffold(body: Center(child: CircularProgressIndicator())),
|
const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||||
error: (e, _) => Scaffold(body: Center(child: Text('$e'))),
|
error: (e, _) => Scaffold(body: Center(child: Text('$e'))),
|
||||||
data: (list) => _JourneyMap(pack: list.first),
|
data: (list) => _JourneyMap(pack: activeSeason(list)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class _SeasonTitleScreenState extends ConsumerState<SeasonTitleScreen> {
|
|||||||
_auto?.cancel();
|
_auto?.cancel();
|
||||||
_auto = Timer(const Duration(milliseconds: 1600), _go);
|
_auto = Timer(const Duration(milliseconds: 1600), _go);
|
||||||
}
|
}
|
||||||
final pack = list.first;
|
final pack = activeSeason(list);
|
||||||
final locale = Localizations.localeOf(context).languageCode;
|
final locale = Localizations.localeOf(context).languageCode;
|
||||||
final number = int.tryParse(pack.seasonId.split('_').last) ?? 1;
|
final number = int.tryParse(pack.seasonId.split('_').last) ?? 1;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:block_seasons/data/content_repository.dart';
|
||||||
|
import 'package:block_seasons/game/models/season.dart';
|
||||||
|
import 'package:block_seasons/game/models/stage.dart';
|
||||||
|
import 'package:block_seasons/state/providers.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
class _FakeRepo extends ContentRepository {
|
||||||
|
_FakeRepo(this.result);
|
||||||
|
final bool result;
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> refresh() async {
|
||||||
|
calls++;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SeasonPack _pack(String id) => SeasonPack(
|
||||||
|
schemaVersion: 1,
|
||||||
|
seasonId: id,
|
||||||
|
version: 1,
|
||||||
|
title: const {'en': 'Test Season', 'ko': '테스트 시즌'},
|
||||||
|
theme: SeasonTheme.fallback,
|
||||||
|
stages: [
|
||||||
|
StageConfig(
|
||||||
|
id: 's1',
|
||||||
|
seed: 1,
|
||||||
|
moveLimit: 10,
|
||||||
|
preset: const [],
|
||||||
|
objectives: const [],
|
||||||
|
stars: const StarThresholds(twoMovesLeft: 2, threeMovesLeft: 4),
|
||||||
|
generatorProfile: 'mid',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('seasonRefreshProvider runs refresh once and exposes the result',
|
||||||
|
() async {
|
||||||
|
final repo = _FakeRepo(true);
|
||||||
|
final container = ProviderContainer(
|
||||||
|
overrides: [contentRepositoryProvider.overrideWithValue(repo)],
|
||||||
|
);
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
expect(await container.read(seasonRefreshProvider.future), isTrue);
|
||||||
|
// Re-reading does not re-run (FutureProvider caches).
|
||||||
|
expect(await container.read(seasonRefreshProvider.future), isTrue);
|
||||||
|
expect(repo.calls, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('activeSeason picks the newest by id', () {
|
||||||
|
final p1 = _pack('season_001');
|
||||||
|
final p2 = _pack('season_002');
|
||||||
|
expect(activeSeason([p1, p2]).seasonId, 'season_002');
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user