Skip to content

Commit 3244d08

Browse files
authored
Merge pull request #456 from OpenPecha/feat/bookmark-toggle-state
Feat/bookmark toggle state
2 parents 1507e67 + 92c0697 commit 3244d08

17 files changed

Lines changed: 621 additions & 119 deletions

File tree

lib/core/constants/app_assets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class AppAssets {
8787
static const IconData trash = PhosphorIconsRegular.trash;
8888
static const IconData fileText = PhosphorIconsRegular.fileText;
8989
static const IconData bookmarkSimple = PhosphorIconsRegular.bookmarkSimple;
90+
static const IconData bookmarkSimpleFill = PhosphorIconsFill.bookmarkSimple;
9091
static const IconData speakerSimpleHigh =
9192
PhosphorIconsRegular.speakerSimpleHigh;
9293
static const IconData vibrate = PhosphorIconsRegular.vibrate;

lib/features/home/presentation/screens/series_detail_screen.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import 'package:flutter_pecha/features/home/presentation/providers/series_provid
1010
import 'package:flutter_pecha/features/home/presentation/widgets/plan_list_view.dart';
1111
import 'package:flutter_pecha/features/home/presentation/widgets/series_more_bottom_sheet.dart';
1212
import 'package:flutter_pecha/features/plans/presentation/providers/user_plans_provider.dart';
13-
import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart';
13+
import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart';
14+
import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart';
1415
import 'package:flutter_pecha/shared/utils/helper_functions.dart';
1516
import 'package:flutter_riverpod/flutter_riverpod.dart';
1617
import 'package:go_router/go_router.dart';
@@ -40,6 +41,17 @@ class SeriesDetailScreen extends ConsumerWidget {
4041
) ??
4142
series;
4243

44+
if (resolvedSeries != null) {
45+
ref.watch(
46+
prefetchBookmarkExistsProvider(
47+
BookmarkTarget(
48+
type: BookmarkType.series,
49+
sourceId: resolvedSeries.id,
50+
),
51+
),
52+
);
53+
}
54+
4355
return Scaffold(
4456
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
4557
body: SafeArea(
@@ -152,12 +164,9 @@ class SeriesDetailScreen extends ConsumerWidget {
152164
void _openMoreSheet(BuildContext context, WidgetRef ref, Series series) {
153165
showSeriesMoreBottomSheet(
154166
context,
167+
seriesId: series.id,
168+
seriesName: series.title,
155169
onAddToPractices: () => _onAddToPractices(context, ref, series),
156-
onBookmark:
157-
() => BookmarkController(
158-
ref: ref,
159-
context: context,
160-
).bookmarkSeries(series.id, name: series.title),
161170
);
162171
}
163172

lib/features/home/presentation/widgets/series_more_bottom_sheet.dart

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,63 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
33
import 'package:flutter_pecha/core/constants/app_assets.dart';
4+
import 'package:flutter_pecha/core/extensions/context_ext.dart';
5+
import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart';
6+
import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart';
7+
import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart';
8+
import 'package:flutter_riverpod/flutter_riverpod.dart';
49

510
/// Bottom sheet opened from the three-dot (⋮) button on the series detail
611
/// screen.
712
///
813
/// Contains:
914
/// • "+ Add to my practices" action
10-
/// • Bookmark action
11-
///
12-
/// Both callbacks fire after the sheet is dismissed so any feedback
13-
/// (snackbar / login drawer) is shown on the underlying screen rather than
14-
/// behind the closing modal.
15-
class SeriesMoreBottomSheet extends StatelessWidget {
15+
/// • Bookmark toggle action
16+
class SeriesMoreBottomSheet extends ConsumerStatefulWidget {
1617
const SeriesMoreBottomSheet({
1718
super.key,
19+
required this.seriesId,
20+
required this.seriesName,
1821
this.onAddToPractices,
19-
this.onBookmark,
2022
});
2123

24+
final String seriesId;
25+
final String seriesName;
2226
final VoidCallback? onAddToPractices;
23-
final VoidCallback? onBookmark;
27+
28+
@override
29+
ConsumerState<SeriesMoreBottomSheet> createState() =>
30+
_SeriesMoreBottomSheetState();
31+
}
32+
33+
class _SeriesMoreBottomSheetState extends ConsumerState<SeriesMoreBottomSheet> {
34+
bool _isBookmarking = false;
35+
36+
BookmarkTarget get _bookmarkTarget => BookmarkTarget(
37+
type: BookmarkType.series,
38+
sourceId: widget.seriesId,
39+
);
40+
41+
Future<void> _toggleBookmark() async {
42+
if (_isBookmarking) return;
43+
setState(() => _isBookmarking = true);
44+
try {
45+
final nav = Navigator.of(context);
46+
final didToggle = await BookmarkController(
47+
ref: ref,
48+
context: context,
49+
).toggleSeries(widget.seriesId, name: widget.seriesName);
50+
if (mounted && didToggle) nav.pop();
51+
} finally {
52+
if (mounted) setState(() => _isBookmarking = false);
53+
}
54+
}
2455

2556
@override
2657
Widget build(BuildContext context) {
2758
final theme = Theme.of(context);
59+
final l10n = context.l10n;
60+
final isBookmarked = ref.watch(isBookmarkedProvider(_bookmarkTarget));
2861

2962
return SafeArea(
3063
top: false,
@@ -55,22 +88,33 @@ class SeriesMoreBottomSheet extends StatelessWidget {
5588
onTap: () {
5689
HapticFeedback.lightImpact();
5790
Navigator.of(context).pop();
58-
onAddToPractices?.call();
91+
widget.onAddToPractices?.call();
5992
},
6093
),
6194

6295
// ── Bookmark ───────────────────────────────────────────────────
6396
_SectionDivider(theme: theme),
6497
ListTile(
65-
leading: Icon(
66-
AppAssets.bookmarkSimple,
67-
color: theme.colorScheme.onSurface,
68-
),
69-
title: Text('Bookmark', style: theme.textTheme.bodyLarge),
98+
leading:
99+
_isBookmarking
100+
? SizedBox(
101+
width: 22,
102+
height: 22,
103+
child: CircularProgressIndicator(
104+
strokeWidth: 2,
105+
color: theme.colorScheme.onSurface,
106+
),
107+
)
108+
: Icon(
109+
isBookmarked
110+
? AppAssets.bookmarkSimpleFill
111+
: AppAssets.bookmarkSimple,
112+
color: theme.colorScheme.onSurface,
113+
),
114+
title: Text(l10n.bookmark, style: theme.textTheme.bodyLarge),
70115
onTap: () {
71116
HapticFeedback.lightImpact();
72-
Navigator.of(context).pop();
73-
onBookmark?.call();
117+
_toggleBookmark();
74118
},
75119
),
76120

@@ -93,8 +137,9 @@ class _SectionDivider extends StatelessWidget {
93137
/// Shows the series "more" bottom sheet.
94138
void showSeriesMoreBottomSheet(
95139
BuildContext context, {
140+
required String seriesId,
141+
required String seriesName,
96142
VoidCallback? onAddToPractices,
97-
VoidCallback? onBookmark,
98143
}) {
99144
showModalBottomSheet(
100145
context: context,
@@ -105,8 +150,9 @@ void showSeriesMoreBottomSheet(
105150
useRootNavigator: true,
106151
builder:
107152
(_) => SeriesMoreBottomSheet(
153+
seriesId: seriesId,
154+
seriesName: seriesName,
108155
onAddToPractices: onAddToPractices,
109-
onBookmark: onBookmark,
110156
),
111157
);
112158
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'package:flutter_pecha/features/mala/presentation/widgets/mala_beads.dart
1010
import 'package:flutter_pecha/features/mala/presentation/widgets/mala_skeleton.dart';
1111
import 'package:flutter_pecha/features/mala/presentation/widgets/mantra_switcher.dart';
1212
import 'package:flutter_pecha/features/mala/presentation/widgets/mala_settings_sheet.dart';
13+
import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart';
14+
import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart';
1315
import 'package:flutter_riverpod/flutter_riverpod.dart';
1416
import 'package:go_router/go_router.dart';
1517

@@ -91,6 +93,10 @@ class _MalaScreenState extends ConsumerState<MalaScreen> {
9193
);
9294
}
9395

96+
void _openMalaSettings(BuildContext context, Mantra mantra) {
97+
MalaSettingsSheet.show(context, mantra: mantra);
98+
}
99+
94100
Widget _buildLoaded(BuildContext context, List<Mantra> mantras) {
95101
if (mantras.isEmpty) {
96102
return const _MalaAppBarScaffold(
@@ -111,6 +117,12 @@ class _MalaScreenState extends ConsumerState<MalaScreen> {
111117
final mantra = mantras[_index];
112118
_trackOpened(mantra);
113119

120+
ref.watch(
121+
prefetchBookmarkExistsProvider(
122+
BookmarkTarget(type: BookmarkType.accumulator, sourceId: mantra.presetId),
123+
),
124+
);
125+
114126
final language = Localizations.localeOf(context).languageCode;
115127
final counter = ref.watch(malaCounterProvider(mantra));
116128
final notifier = ref.read(malaCounterProvider(mantra).notifier);
@@ -120,7 +132,7 @@ class _MalaScreenState extends ConsumerState<MalaScreen> {
120132
children: [
121133
_MalaAppBar(
122134
title: mantra.displayTitle(language),
123-
onMorePressed: () => MalaSettingsSheet.show(context, mantra: mantra),
135+
onMorePressed: () => _openMalaSettings(context, mantra),
124136
),
125137
Expanded(
126138
child: Padding(

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import 'package:flutter_pecha/core/widgets/destructive_confirmation_dialog.dart'
99
import 'package:flutter_pecha/features/mala/domain/entities/mantra.dart';
1010
import 'package:flutter_pecha/features/mala/presentation/providers/mala_providers.dart';
1111
import 'package:flutter_pecha/features/mala/presentation/providers/mala_settings_provider.dart';
12+
import 'package:flutter_pecha/features/practice/data/datasource/bookmark_remote_datasource.dart';
1213
import 'package:flutter_pecha/features/practice/presentation/controllers/bookmark_controller.dart';
14+
import 'package:flutter_pecha/features/practice/presentation/providers/bookmark_providers.dart';
1315
import 'package:flutter_pecha/shared/widgets/app_toggle_switch.dart';
1416
import 'package:flutter_riverpod/flutter_riverpod.dart';
1517
import 'package:go_router/go_router.dart';
@@ -37,6 +39,11 @@ class MalaSettingsSheet extends ConsumerWidget {
3739
final settings = ref.watch(malaSettingsProvider);
3840
final settingsNotifier = ref.read(malaSettingsProvider.notifier);
3941
final isOnline = ref.watch(connectivityNotifierProvider);
42+
final isBookmarked = ref.watch(
43+
isBookmarkedProvider(
44+
BookmarkTarget(type: BookmarkType.accumulator, sourceId: mantra.presetId),
45+
),
46+
);
4047
final dividerColor =
4148
isDark ? AppColors.cardBorderDark : AppColors.grey300;
4249
const destructiveColor = Color(0xFFB03027);
@@ -67,9 +74,12 @@ class MalaSettingsSheet extends ConsumerWidget {
6774
),
6875
Divider(height: 1, color: dividerColor),
6976
_MalaSettingsTile(
70-
icon: AppAssets.bookmarkSimple,
77+
icon:
78+
isBookmarked
79+
? AppAssets.bookmarkSimpleFill
80+
: AppAssets.bookmarkSimple,
7181
label: l10n.mala_add_to_bookmark,
72-
onTap: () => _onAddToBookmark(context, ref),
82+
onTap: () => _onToggleBookmark(context, ref),
7383
),
7484
Divider(height: 1, color: dividerColor),
7585
_MalaSettingsToggleTile(
@@ -111,19 +121,19 @@ class MalaSettingsSheet extends ConsumerWidget {
111121
router.pushNamed('edit-routine', extra: {'initialMantra': mantra});
112122
}
113123

114-
Future<void> _onAddToBookmark(BuildContext context, WidgetRef ref) async {
124+
Future<void> _onToggleBookmark(BuildContext context, WidgetRef ref) async {
115125
final language = ref.read(contentLanguageProvider);
116126
final navigator = Navigator.of(context);
117127

118-
// The controller handles the guest → login-drawer flow and shows its own
119-
// success/error snackbar, so it must run before the sheet is dismissed
120-
// (its context drives both). The sheet is closed once the call resolves.
121-
await BookmarkController(ref: ref, context: context).bookmarkMala(
128+
final didToggle = await BookmarkController(
129+
ref: ref,
130+
context: context,
131+
).toggleMala(
122132
mantra.presetId,
123133
name: mantra.displayTitle(language),
124134
);
125135

126-
if (context.mounted) navigator.pop();
136+
if (context.mounted && didToggle) navigator.pop();
127137
}
128138

129139
Future<void> _onResetCount(BuildContext context, WidgetRef ref) async {

lib/features/practice/data/datasource/bookmark_remote_datasource.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,30 @@ class BookmarkRemoteDatasource {
104104
return all;
105105
}
106106

107+
/// GET /users/me/bookmarks/exists
108+
///
109+
/// [sourceId] – entity id (text, segment, timer, accumulator, series, …)
110+
/// [type] – bookmark kind; pass when known so the check is unambiguous
111+
Future<BookmarkExistsResult> checkBookmarkExists({
112+
required String sourceId,
113+
BookmarkType? type,
114+
}) async {
115+
final response = await dio.get(
116+
'/users/me/bookmarks/exists',
117+
queryParameters: {
118+
'source_id': sourceId,
119+
if (type != null) 'type': type.value,
120+
},
121+
);
122+
final data = response.data;
123+
if (data is! Map<String, dynamic>) {
124+
throw const FormatException(
125+
'Unexpected /users/me/bookmarks/exists payload type',
126+
);
127+
}
128+
return BookmarkExistsResult.fromJson(data);
129+
}
130+
107131
/// DELETE /users/me/bookmarks/{bookmarkId}
108132
Future<void> deleteBookmark(String bookmarkId) async {
109133
await dio.delete('/users/me/bookmarks/$bookmarkId');

lib/features/practice/data/models/bookmark_models.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import 'package:flutter_pecha/shared/domain/value_objects/responsive_image.dart';
22

3+
/// Response from `GET /users/me/bookmarks/exists`.
4+
class BookmarkExistsResult {
5+
final bool exists;
6+
final String? id;
7+
8+
const BookmarkExistsResult({required this.exists, this.id});
9+
10+
factory BookmarkExistsResult.fromJson(Map<String, dynamic> json) {
11+
return BookmarkExistsResult(
12+
exists: json['exists'] as bool? ?? false,
13+
id: json['id'] as String?,
14+
);
15+
}
16+
}
17+
318
/// Models for `GET /users/me/bookmarks`.
419
///
520
/// Each row embeds a bookmark-specific object for its type (`text` / `plan` /

lib/features/practice/data/repositories/bookmark_repository.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ class BookmarkRepository {
3737
}
3838
}
3939

40+
Future<Either<Failure, BookmarkExistsResult>> checkBookmarkExists({
41+
required String sourceId,
42+
BookmarkType? type,
43+
}) async {
44+
try {
45+
final result = await remoteDatasource.checkBookmarkExists(
46+
sourceId: sourceId,
47+
type: type,
48+
);
49+
return Right(result);
50+
} catch (e) {
51+
return Left(
52+
ExceptionMapper.map(e, context: 'Failed to check bookmark status'),
53+
);
54+
}
55+
}
56+
4057
Future<Either<Failure, Unit>> deleteBookmark(String bookmarkId) async {
4158
try {
4259
await remoteDatasource.deleteBookmark(bookmarkId);

0 commit comments

Comments
 (0)