Skip to content

Commit dc39bf1

Browse files
authored
Merge pull request #504 from OpenPecha/fix/mala-personal-lifetime-count
Fix/mala personal lifetime count
2 parents fc059d2 + 6950d4b commit dc39bf1

9 files changed

Lines changed: 97 additions & 26 deletions

File tree

lib/features/mala/data/datasources/mala_local_datasource.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@ class LocalMalaState {
1717
const LocalMalaState({
1818
this.total = 0,
1919
this.syncedTotal = 0,
20+
this.totalCounted = 0,
2021
this.accumulatorId,
2122
this.beadImageUrl,
2223
this.beadImageBase64,
2324
});
2425

2526
final int total;
2627
final int syncedTotal;
28+
29+
/// Lifetime `total_counted` baseline from the server, updated on sync.
30+
final int totalCounted;
2731
final String? accumulatorId;
2832

2933
/// Cached bead-image URL so the strand can render offline on a cold start,
@@ -49,12 +53,14 @@ class LocalMalaState {
4953
LocalMalaState copyWith({
5054
int? total,
5155
int? syncedTotal,
56+
int? totalCounted,
5257
String? accumulatorId,
5358
String? beadImageUrl,
5459
String? beadImageBase64,
5560
}) => LocalMalaState(
5661
total: total ?? this.total,
5762
syncedTotal: syncedTotal ?? this.syncedTotal,
63+
totalCounted: totalCounted ?? this.totalCounted,
5864
accumulatorId: accumulatorId ?? this.accumulatorId,
5965
beadImageUrl: beadImageUrl ?? this.beadImageUrl,
6066
beadImageBase64: beadImageBase64 ?? this.beadImageBase64,
@@ -63,6 +69,7 @@ class LocalMalaState {
6369
Map<String, dynamic> toJson() => {
6470
'total': total,
6571
'syncedTotal': syncedTotal,
72+
'totalCounted': totalCounted,
6673
if (accumulatorId != null) 'accumulatorId': accumulatorId,
6774
if (beadImageUrl != null) 'beadImageUrl': beadImageUrl,
6875
if (beadImageBase64 != null) 'beadImageBase64': beadImageBase64,
@@ -71,6 +78,7 @@ class LocalMalaState {
7178
factory LocalMalaState.fromJson(Map<String, dynamic> j) => LocalMalaState(
7279
total: (j['total'] as num?)?.toInt() ?? 0,
7380
syncedTotal: (j['syncedTotal'] as num?)?.toInt() ?? 0,
81+
totalCounted: (j['totalCounted'] as num?)?.toInt() ?? 0,
7482
accumulatorId: j['accumulatorId'] as String?,
7583
beadImageUrl: j['beadImageUrl'] as String?,
7684
beadImageBase64: j['beadImageBase64'] as String?,
@@ -199,6 +207,7 @@ class MalaLocalDataSource {
199207
userId,
200208
presetId,
201209
LocalMalaState(
210+
totalCounted: s.totalCounted,
202211
beadImageUrl: s.beadImageUrl,
203212
beadImageBase64: s.beadImageBase64,
204213
),

lib/features/mala/data/models/accumulator_model.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ class AccumulatorDetailModel {
233233
MalaCount toMalaCount() => MalaCount(
234234
accumulatorId: accumulatorId,
235235
total: currentCount,
236+
totalCounted: totalCounted,
236237
beadImageUrl: beadImageUrl,
237238
);
238239
}
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'package:equatable/equatable.dart';
22

3-
/// The current user's lifetime count for one accumulator.
3+
/// The current user's counts for one accumulator preset.
44
///
5-
/// Maps to the user-owned accumulator (`GET/POST/PUT /accumulators/user`),
6-
/// where [total] is its `current_count` and [accumulatorId] is the user
7-
/// accumulator id (distinct from the preset id).
5+
/// Maps to the user-owned accumulator (`GET/POST/PUT /accumulators/user`) and
6+
/// `GET /accumulators/{parent_id}` detail, where [total] is the active session
7+
/// `current_count`, [totalCounted] is lifetime `total_counted`, and
8+
/// [accumulatorId] is the user accumulator id (distinct from the preset id).
89
class MalaCount extends Equatable {
910
/// The user-owned accumulator id. Null before the user's accumulator has
1011
/// been created (lazy-created on first sync).
@@ -13,9 +14,12 @@ class MalaCount extends Equatable {
1314
/// The mantra this count is for (links to the preset / content).
1415
final String? mantraId;
1516

16-
/// Lifetime `current_count`.
17+
/// Active session `current_count`.
1718
final int total;
1819

20+
/// Lifetime `total_counted` across all sessions for this preset.
21+
final int totalCounted;
22+
1923
/// Per-user bead artwork (`mala_image_url`), when the user has customized it.
2024
/// Null falls back to the preset/mantra image.
2125
final String? beadImageUrl;
@@ -26,11 +30,12 @@ class MalaCount extends Equatable {
2630
this.accumulatorId,
2731
this.mantraId,
2832
required this.total,
33+
this.totalCounted = 0,
2934
this.beadImageUrl,
3035
this.updatedAt,
3136
});
3237

3338
@override
3439
List<Object?> get props =>
35-
[accumulatorId, mantraId, total, beadImageUrl, updatedAt];
40+
[accumulatorId, mantraId, total, totalCounted, beadImageUrl, updatedAt];
3641
}

lib/features/mala/presentation/providers/mala_counter_notifier.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
1717
class MalaCounterState {
1818
const MalaCounterState({
1919
this.total = 0,
20+
this.totalCounted = 0,
2021
this.beadsPerRound = kBeadsPerRound,
2122
this.isSeeding = true,
2223
this.seedFailed = false,
@@ -26,6 +27,9 @@ class MalaCounterState {
2627
});
2728

2829
final int total;
30+
31+
/// Lifetime `total_counted` baseline from `GET /accumulators/{parent_id}`.
32+
final int totalCounted;
2933
final int beadsPerRound;
3034

3135
/// Per-user bead artwork from the accumulator detail. Null falls back to the
@@ -50,6 +54,7 @@ class MalaCounterState {
5054

5155
MalaCounterState copyWith({
5256
int? total,
57+
int? totalCounted,
5358
int? beadsPerRound,
5459
bool? isSeeding,
5560
bool? seedFailed,
@@ -58,6 +63,7 @@ class MalaCounterState {
5863
Uint8List? beadImageBytes,
5964
}) => MalaCounterState(
6065
total: total ?? this.total,
66+
totalCounted: totalCounted ?? this.totalCounted,
6167
beadsPerRound: beadsPerRound ?? this.beadsPerRound,
6268
isSeeding: isSeeding ?? this.isSeeding,
6369
seedFailed: seedFailed ?? this.seedFailed,
@@ -112,6 +118,27 @@ class MalaCounterNotifier extends StateNotifier<MalaCounterState> {
112118

113119
String get _presetId => _mantra.presetId;
114120

121+
/// Lifetime total for [GroupAccumulationsSheet]: API `total_counted` baseline
122+
/// plus any unsynced session taps.
123+
int get displayLifetimeCount {
124+
final userId = _userId;
125+
if (userId == null) return state.totalCounted;
126+
final local = _local.read(userId, _presetId);
127+
final dirty = local.total - local.syncedTotal;
128+
if (dirty <= 0) return state.totalCounted;
129+
return state.totalCounted + dirty;
130+
}
131+
132+
/// Refreshes [MalaCounterState.totalCounted] after a successful personal sync.
133+
void handlePersonalCountSynced(String presetId) {
134+
if (presetId != _presetId || !mounted) return;
135+
final userId = _userId;
136+
if (userId == null) return;
137+
state = state.copyWith(
138+
totalCounted: _local.read(userId, _presetId).totalCounted,
139+
);
140+
}
141+
115142
static Future<List<int>> _emptyImageDownload(String _) async => const [];
116143

117144
/// Seed local state first, then reconcile remote state when available.
@@ -131,6 +158,7 @@ class MalaCounterNotifier extends StateNotifier<MalaCounterState> {
131158
final fallbackImageUrl = localState.beadImageUrl ?? _mantra.beadImageUrl;
132159
state = state.copyWith(
133160
total: localState.total,
161+
totalCounted: localState.totalCounted,
134162
isSeeding: false,
135163
seedFailed: false,
136164
beadImageUrl: fallbackImageUrl,
@@ -167,19 +195,22 @@ class MalaCounterNotifier extends StateNotifier<MalaCounterState> {
167195
final syncedTotal = serverAccId != null ? serverTotal : 0;
168196

169197
final beadImageUrl = detail.beadImageUrl ?? localState.beadImageUrl;
198+
final totalCounted = max(detail.totalCounted, localState.totalCounted);
170199
_local.write(
171200
userId,
172201
_presetId,
173202
localState.copyWith(
174203
total: total,
175204
syncedTotal: syncedTotal,
205+
totalCounted: totalCounted,
176206
accumulatorId: serverAccId ?? localState.accumulatorId,
177207
beadImageUrl: beadImageUrl,
178208
),
179209
);
180210

181211
state = state.copyWith(
182212
total: total,
213+
totalCounted: totalCounted,
183214
isSeeding: false,
184215
seedFailed: false,
185216
beadImageUrl: beadImageUrl,

lib/features/mala/presentation/providers/mala_providers.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,27 @@ final malaSoundPlayerProvider = Provider.autoDispose<MalaSoundPlayer>((ref) {
159159

160160
final malaCounterProvider = StateNotifierProvider.autoDispose
161161
.family<MalaCounterNotifier, MalaCounterState, Mantra>((ref, mantra) {
162-
return MalaCounterNotifier(
162+
final sync = ref.watch(malaSyncManagerProvider);
163+
final notifier = MalaCounterNotifier(
163164
mantra: mantra,
164165
local: ref.watch(malaLocalDataSourceProvider),
165166
getAccumulatorDetail: ref.watch(getAccumulatorDetailUseCaseProvider),
166167
deleteUserAccumulator: ref.watch(deleteUserAccumulatorUseCaseProvider),
167168
downloadImageBytes:
168169
ref.watch(malaRemoteDataSourceProvider).fetchImageBytes,
169-
sync: ref.watch(malaSyncManagerProvider),
170+
sync: sync,
170171
currentUserId: () => resolveMalaUserId(ref),
171172
analytics: ref.watch(analyticsServiceProvider),
172173
sound: ref.watch(malaSoundPlayerProvider),
173174
);
175+
void onPersonalCountSynced(String presetId) {
176+
notifier.handlePersonalCountSynced(presetId);
177+
}
178+
sync.onPersonalCountSynced = onPersonalCountSynced;
179+
ref.onDispose(() {
180+
if (sync.onPersonalCountSynced == onPersonalCountSynced) {
181+
sync.onPersonalCountSynced = null;
182+
}
183+
});
184+
return notifier;
174185
});

lib/features/mala/presentation/providers/mala_sync_manager.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ class MalaSyncManager with WidgetsBindingObserver {
7272
/// totals from `GET /accumulators/{id}/groups`.
7373
void Function(String groupAccumulatorId)? onGroupCountSynced;
7474

75+
/// Called after a personal session PUT succeeds so UI can refresh lifetime
76+
/// `total_counted` for [GroupAccumulationsSheet].
77+
void Function(String presetId)? onPersonalCountSynced;
78+
7579
/// Attach lifecycle + connectivity observers and flush any offline tail.
7680
void start() {
7781
if (_started) return;
@@ -305,7 +309,8 @@ class MalaSyncManager with WidgetsBindingObserver {
305309
}
306310

307311
Future<void> _pushTotal(String userId, String presetId, int sending) async {
308-
final s = _local.read(userId, presetId);
312+
final beforePush = _local.read(userId, presetId);
313+
final s = beforePush;
309314

310315
// First time only: mint the accumulator once via POST {parent_id}.
311316
// The new accumulator starts at 0; the absolute total is pushed by the
@@ -341,12 +346,18 @@ class MalaSyncManager with WidgetsBindingObserver {
341346
// Re-read: taps may have landed during the round-trip.
342347
final after = _local.read(userId, presetId);
343348
final confirmedTotal = max(count.total, sending);
349+
final syncedDelta = confirmedTotal - beforePush.syncedTotal;
350+
final nextTotalCounted =
351+
syncedDelta > 0
352+
? beforePush.totalCounted + syncedDelta
353+
: beforePush.totalCounted;
344354
await _local.write(
345355
userId,
346356
presetId,
347357
after.copyWith(
348358
total: max(after.total, count.total),
349359
syncedTotal: confirmedTotal,
360+
totalCounted: nextTotalCounted,
350361
accumulatorId: count.accumulatorId ?? accumulatorId,
351362
),
352363
);
@@ -357,6 +368,7 @@ class MalaSyncManager with WidgetsBindingObserver {
357368
'total': max(after.total, count.total),
358369
},
359370
);
371+
onPersonalCountSynced?.call(presetId);
360372
},
361373
);
362374
}

lib/features/mala/presentation/screens/mala_screen.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ class _MalaScreenState extends ConsumerState<MalaScreen> {
272272
const SizedBox(height: 12),
273273
GroupAccumulationsBar(
274274
presetId: mantra.presetId,
275-
userTotalCount: counter.total,
275+
personalLifetimeCount: notifier.displayLifetimeCount,
276276
),
277277
],
278278
),

lib/features/mala/presentation/widgets/group_accumulations_bar.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
1616
/// `GET /accumulators/{presetId}/groups?joined_only=true` returns groups.
1717
///
1818
/// Tapping opens [GroupAccumulationsSheet], which shows lifetime
19-
/// [AccumulatorGroup.userTotalCount] per group. The mala counter above uses
20-
/// session counts from [groupAccumulationCountsProvider] when a group is selected.
19+
/// [AccumulatorGroup.userTotalCount] per group and personal `total_counted`.
20+
/// The mala counter above uses session counts when a group is selected.
2121
class GroupAccumulationsBar extends ConsumerWidget {
2222
const GroupAccumulationsBar({
2323
super.key,
2424
required this.presetId,
25-
required this.userTotalCount,
25+
required this.personalLifetimeCount,
2626
});
2727

2828
final String presetId;
29-
/// Personal active session total (`MalaCounterState.total`), shown on the user row.
30-
final int userTotalCount;
29+
/// Personal lifetime total (`MalaCounterNotifier.displayLifetimeCount`).
30+
final int personalLifetimeCount;
3131

3232
static const barHeight = 40.0;
3333
static const _avatarSize = 28.0;
@@ -45,7 +45,7 @@ class GroupAccumulationsBar extends ConsumerWidget {
4545
return _GroupAccumulationsBarContent(
4646
presetId: presetId,
4747
groups: groups,
48-
userTotalCount: userTotalCount,
48+
personalLifetimeCount: personalLifetimeCount,
4949
avatarSize: _avatarSize,
5050
avatarOverlap: _avatarOverlap,
5151
);
@@ -61,14 +61,14 @@ class _GroupAccumulationsBarContent extends ConsumerWidget {
6161
const _GroupAccumulationsBarContent({
6262
required this.presetId,
6363
required this.groups,
64-
required this.userTotalCount,
64+
required this.personalLifetimeCount,
6565
required this.avatarSize,
6666
required this.avatarOverlap,
6767
});
6868

6969
final String presetId;
7070
final List<AccumulatorGroup> groups;
71-
final int userTotalCount;
71+
final int personalLifetimeCount;
7272
final double avatarSize;
7373
final double avatarOverlap;
7474

@@ -95,7 +95,7 @@ class _GroupAccumulationsBarContent extends ConsumerWidget {
9595
context,
9696
presetId: presetId,
9797
groups: groups,
98-
personalTotalCount: userTotalCount,
98+
personalLifetimeCount: personalLifetimeCount,
9999
),
100100
child: Container(
101101
height: GroupAccumulationsBar.barHeight,

lib/features/mala/presentation/widgets/group_accumulations_sheet.dart

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,27 @@ import 'package:intl/intl.dart';
1919
/// Group row counts show lifetime totals from
2020
/// `GET /accumulators/{presetId}/groups` (`user_total_count`), plus any unsynced
2121
/// local taps via [GroupAccumulationCountsNotifier.displayLifetimeCount]. The
22-
/// on-screen mala counter uses session counts from [groupAccumulationCountsProvider]
23-
/// instead; those reset to 0 on group DELETE while lifetime totals here do not.
22+
/// personal row shows lifetime `total_counted` from
23+
/// `GET /accumulators/{parent_id}` via [MalaCounterNotifier.displayLifetimeCount].
24+
/// The on-screen mala counter uses session counts instead; those reset to 0 on
25+
/// DELETE while lifetime totals here do not.
2426
class GroupAccumulationsSheet extends ConsumerStatefulWidget {
2527
const GroupAccumulationsSheet({
2628
super.key,
2729
required this.presetId,
2830
required this.groups,
29-
required this.personalTotalCount,
31+
required this.personalLifetimeCount,
3032
});
3133

3234
final String presetId;
3335
final List<AccumulatorGroup> groups;
34-
final int personalTotalCount;
36+
final int personalLifetimeCount;
3537

3638
static Future<void> show(
3739
BuildContext context, {
3840
required String presetId,
3941
required List<AccumulatorGroup> groups,
40-
required int personalTotalCount,
42+
required int personalLifetimeCount,
4143
}) {
4244
return showModalBottomSheet<void>(
4345
context: context,
@@ -48,7 +50,7 @@ class GroupAccumulationsSheet extends ConsumerStatefulWidget {
4850
(_) => GroupAccumulationsSheet(
4951
presetId: presetId,
5052
groups: groups,
51-
personalTotalCount: personalTotalCount,
53+
personalLifetimeCount: personalLifetimeCount,
5254
),
5355
);
5456
}
@@ -138,7 +140,7 @@ class _GroupAccumulationsSheetState
138140
title: user != null ? _userDisplayName(user) : '—',
139141
formattedCount: NumberFormat.decimalPattern(
140142
locale,
141-
).format(widget.personalTotalCount),
143+
).format(widget.personalLifetimeCount),
142144
),
143145
const SizedBox(height: 12),
144146
Divider(height: 1, thickness: 1, color: dividerColor),

0 commit comments

Comments
 (0)