@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
66import 'package:flutter_pecha/core/analytics/analytics_events.dart' ;
77import 'package:flutter_pecha/core/analytics/analytics_service.dart' ;
88import 'package:flutter_pecha/core/utils/app_logger.dart' ;
9+ import 'package:flutter_pecha/core/utils/network_image_utils.dart' ;
910import 'package:flutter_pecha/features/mala/data/datasources/mala_local_datasource.dart' ;
1011import 'package:flutter_pecha/features/mala/domain/entities/mantra.dart' ;
1112import 'package:flutter_pecha/features/mala/domain/usecases/mala_usecases.dart' ;
@@ -135,19 +136,19 @@ class MalaCounterNotifier extends StateNotifier<MalaCounterState> {
135136 beadImageUrl: fallbackImageUrl,
136137 beadImageBytes: localState.beadImageBytes,
137138 );
138- if (localState.beadImageBytes == null && fallbackImageUrl != null ) {
139- unawaited (_storeBeadImage (userId, fallbackImageUrl));
140- }
141139
142140 final result = await _getAccumulatorDetail (_presetId);
143141 if (! mounted) return ; // screen left mid-seed
142+
143+ String ? detailBeadImageUrl;
144144 result.fold (
145145 (failure) {
146146 _logger.warning ('Seed failed: ${failure .message }' );
147147 // Local counting stays enabled; dirty counts retry via sync manager.
148148 unawaited (_sync.flush (SyncReason .launch));
149149 },
150150 (detail) {
151+ detailBeadImageUrl = detail.beadImageUrl;
151152 // detail.accumulatorId is null when the user has no active accumulator
152153 // (fresh install, or after reset soft-deletes the session record).
153154 // detail.total maps to API `current_count` (active session), not
@@ -184,39 +185,114 @@ class MalaCounterNotifier extends StateNotifier<MalaCounterState> {
184185 beadImageUrl: beadImageUrl,
185186 beadImageBytes: localState.beadImageBytes,
186187 );
187- if (beadImageUrl != null ) {
188- unawaited (_storeBeadImage (userId, beadImageUrl));
189- }
190188
191189 // Push any offline tail captured before this seed.
192190 if (total > syncedTotal) unawaited (_sync.flush (SyncReason .launch));
193191 },
194192 );
195- }
196193
197- Future <void > _storeBeadImage (String userId, String imageUrl) async {
198- try {
199- final bytes = await _downloadImageBytes (imageUrl);
200- if (bytes.isEmpty) return ;
201- final current = _local.read (userId, _presetId);
202- await _local.write (
194+ unawaited (
195+ _refreshBeadImage (
203196 userId,
204- _presetId,
205- current.copyWith (
206- beadImageUrl: imageUrl,
207- beadImageBase64: base64Encode (bytes),
208- ),
209- );
197+ urlCandidates: [
198+ if (detailBeadImageUrl != null ) detailBeadImageUrl! ,
199+ if (fallbackImageUrl != null ) fallbackImageUrl,
200+ ],
201+ refetchDetailOnFailure: detailBeadImageUrl == null ,
202+ ),
203+ );
204+ }
205+
206+ /// Downloads bead artwork via [MalaRemoteDataSource.fetchImageBytes]
207+ /// (injected as [_downloadImageBytes] ) and persists bytes in Hive. Presigned
208+ /// S3 URLs expire, so stale cached URLs are retried against fresh catalogue /
209+ /// detail URLs when needed.
210+ Future <void > _refreshBeadImage (
211+ String userId, {
212+ List <String > urlCandidates = const [],
213+ bool refetchDetailOnFailure = true ,
214+ }) async {
215+ final localState = _local.read (userId, _presetId);
216+ final cachedBytes = localState.beadImageBytes;
217+ if (cachedBytes != null ) {
210218 if (! mounted) return ;
211219 state = state.copyWith (
212- beadImageUrl: imageUrl ,
213- beadImageBytes: Uint8List . fromList (bytes) ,
220+ beadImageUrl: localState.beadImageUrl ?? state.beadImageUrl ,
221+ beadImageBytes: cachedBytes ,
214222 );
223+ return ;
224+ }
225+
226+ final candidates = < String > [];
227+ void addCandidate (String ? url) {
228+ if (url == null || url.isEmpty) return ;
229+ final key = stableNetworkImageCacheKey (url);
230+ if (candidates.any ((c) => stableNetworkImageCacheKey (c) == key)) return ;
231+ candidates.add (url);
232+ }
233+
234+ for (final url in urlCandidates) {
235+ addCandidate (url);
236+ }
237+ addCandidate (_mantra.beadImageUrl);
238+ addCandidate (localState.beadImageUrl);
239+
240+ for (final url in candidates) {
241+ final bytes = await _tryDownloadBeadImage (url);
242+ if (bytes == null ) continue ;
243+ await _persistBeadImage (userId, url, bytes);
244+ return ;
245+ }
246+
247+ if (! refetchDetailOnFailure) return ;
248+
249+ final detailResult = await _getAccumulatorDetail (_presetId);
250+ await detailResult.fold (
251+ (_) async {},
252+ (detail) async {
253+ final freshUrl = detail.beadImageUrl;
254+ if (freshUrl == null || freshUrl.isEmpty) return ;
255+
256+ final bytes = await _tryDownloadBeadImage (freshUrl);
257+ if (bytes == null ) return ;
258+ await _persistBeadImage (userId, freshUrl, bytes);
259+ },
260+ );
261+ }
262+
263+ /// Network fetch delegated to [MalaRemoteDataSource.fetchImageBytes] .
264+ Future <List <int >?> _tryDownloadBeadImage (String url) async {
265+ try {
266+ final bytes = await _downloadImageBytes (url);
267+ if (bytes.isEmpty) return null ;
268+ return bytes;
215269 } catch (e) {
216- _logger.warning ('Failed to store mala bead image locally: $e ' );
270+ _logger.warning ('Bead image download failed for $url : $e ' );
271+ return null ;
217272 }
218273 }
219274
275+ Future <void > _persistBeadImage (
276+ String userId,
277+ String imageUrl,
278+ List <int > bytes,
279+ ) async {
280+ final current = _local.read (userId, _presetId);
281+ await _local.write (
282+ userId,
283+ _presetId,
284+ current.copyWith (
285+ beadImageUrl: imageUrl,
286+ beadImageBase64: base64Encode (bytes),
287+ ),
288+ );
289+ if (! mounted) return ;
290+ state = state.copyWith (
291+ beadImageUrl: imageUrl,
292+ beadImageBytes: Uint8List .fromList (bytes),
293+ );
294+ }
295+
220296 /// +1 recitation. No-op while seeding or resetting. Monotonic — never decrements.
221297 void incrementBead ({
222298 required bool soundEnabled,
0 commit comments