Skip to content

Commit a4ae6a2

Browse files
authored
feat: separate tabs for comments and stories in favorites screen. (#479)
1 parent 3413b16 commit a4ae6a2

File tree

12 files changed

+268
-145
lines changed

12 files changed

+268
-145
lines changed

lib/cubits/fav/fav_cubit.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class FavCubit extends Cubit<FavState> with Loggable {
1919
PreferenceRepository? preferenceRepository,
2020
HackerNewsRepository? hackerNewsRepository,
2121
HackerNewsWebRepository? hackerNewsWebRepository,
22+
SembastRepository? sembastRepository,
2223
}) : _authBloc = authBloc,
2324
_authRepository = authRepository ?? locator.get<AuthRepository>(),
2425
_preferenceRepository =
@@ -27,6 +28,8 @@ class FavCubit extends Cubit<FavState> with Loggable {
2728
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
2829
_hackerNewsWebRepository =
2930
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
31+
_sembastRepository =
32+
sembastRepository ?? locator.get<SembastRepository>(),
3033
super(FavState.init()) {
3134
init();
3235
}
@@ -36,8 +39,9 @@ class FavCubit extends Cubit<FavState> with Loggable {
3639
final PreferenceRepository _preferenceRepository;
3740
final HackerNewsRepository _hackerNewsRepository;
3841
final HackerNewsWebRepository _hackerNewsWebRepository;
42+
final SembastRepository _sembastRepository;
3943
late final StreamSubscription<String>? _usernameSubscription;
40-
static const int _pageSize = 20;
44+
static const int _pageSize = 100;
4145

4246
Future<void> init() async {
4347
_usernameSubscription = _authBloc.stream
@@ -55,6 +59,8 @@ class FavCubit extends Cubit<FavState> with Loggable {
5559
_hackerNewsRepository
5660
.fetchItemsStream(
5761
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
62+
getFromCache: (int id) =>
63+
_sembastRepository.getCachedItem(id: id),
5864
)
5965
.listen(_onItemLoaded)
6066
.onDone(() {
@@ -97,7 +103,10 @@ class FavCubit extends Cubit<FavState> with Loggable {
97103
void removeFav(int id) {
98104
_preferenceRepository
99105
..removeFav(username: username, id: id)
100-
..removeFav(username: '', id: id);
106+
..removeFav(
107+
username: '',
108+
id: id,
109+
);
101110

102111
emit(
103112
state.copyWith(
@@ -200,13 +209,17 @@ class FavCubit extends Cubit<FavState> with Loggable {
200209
}
201210

202211
void _onItemLoaded(Item item) {
212+
_sembastRepository.cacheItem(item);
203213
emit(
204214
state.copyWith(
205215
favItems: List<Item>.from(state.favItems)..add(item),
206216
),
207217
);
208218
}
209219

220+
void switchTab() =>
221+
emit(state.copyWith(isDisplayingStories: !state.isDisplayingStories));
222+
210223
@override
211224
Future<void> close() {
212225
_usernameSubscription?.cancel();

lib/cubits/fav/fav_state.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,39 @@ class FavState extends Equatable {
77
required this.status,
88
required this.mergeStatus,
99
required this.currentPage,
10+
required this.isDisplayingStories,
1011
});
1112

1213
FavState.init()
1314
: favIds = <int>[],
1415
favItems = <Item>[],
1516
status = Status.idle,
1617
mergeStatus = Status.idle,
17-
currentPage = 0;
18+
currentPage = 0,
19+
isDisplayingStories = true;
1820

1921
final List<int> favIds;
2022
final List<Item> favItems;
2123
final Status status;
2224
final Status mergeStatus;
2325
final int currentPage;
26+
final bool isDisplayingStories;
2427

2528
FavState copyWith({
2629
List<int>? favIds,
2730
List<Item>? favItems,
2831
Status? status,
2932
Status? mergeStatus,
3033
int? currentPage,
34+
bool? isDisplayingStories,
3135
}) {
3236
return FavState(
3337
favIds: favIds ?? this.favIds,
3438
favItems: favItems ?? this.favItems,
3539
status: status ?? this.status,
3640
mergeStatus: mergeStatus ?? this.mergeStatus,
3741
currentPage: currentPage ?? this.currentPage,
42+
isDisplayingStories: isDisplayingStories ?? this.isDisplayingStories,
3843
);
3944
}
4045

@@ -45,5 +50,6 @@ class FavState extends Equatable {
4550
currentPage,
4651
favIds,
4752
favItems,
53+
isDisplayingStories,
4854
];
4955
}

lib/models/item/buildable_comment.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class BuildableComment extends Comment with Buildable {
4141
BuildableComment copyWith({
4242
int? level,
4343
bool? hidden,
44+
int? kid,
4445
}) {
4546
return BuildableComment(
4647
id: id,
@@ -49,7 +50,7 @@ class BuildableComment extends Comment with Buildable {
4950
score: score,
5051
by: by,
5152
text: text,
52-
kids: kids,
53+
kids: kid == null ? kids : <int>[...kids, kid],
5354
dead: dead,
5455
deleted: deleted,
5556
hidden: hidden ?? this.hidden,

lib/models/item/comment.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Comment extends Item {
3636
Comment copyWith({
3737
int? level,
3838
bool? hidden,
39+
int? kid,
3940
}) {
4041
return Comment(
4142
id: id,
@@ -44,7 +45,7 @@ class Comment extends Item {
4445
score: score,
4546
by: by,
4647
text: text,
47-
kids: kids,
48+
kids: kid == null ? kids : <int>[...kids, kid],
4849
dead: dead,
4950
deleted: deleted,
5051
hidden: hidden ?? this.hidden,

lib/repositories/hacker_news_repository.dart

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -302,24 +302,32 @@ class HackerNewsRepository with Loggable {
302302

303303
/// Fetch a list of [Item] based on ids and return results
304304
/// using a stream.
305-
Stream<Item> fetchItemsStream({required List<int> ids}) async* {
305+
Stream<Item> fetchItemsStream({
306+
required List<int> ids,
307+
Future<Item?> Function(int)? getFromCache,
308+
}) async* {
306309
for (final int id in ids) {
307-
final Item? item =
308-
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
309-
if (json == null) return null;
310+
final Item? cachedItem = await getFromCache?.call(id);
311+
if (cachedItem != null) {
312+
yield cachedItem;
313+
} else {
314+
final Item? item =
315+
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
316+
if (json == null) return null;
310317

311-
if (json.isStory) {
312-
final Story story = Story.fromJson(json);
313-
return story;
314-
} else if (json.isComment) {
315-
final Comment comment = Comment.fromJson(json);
316-
return comment;
317-
}
318-
return null;
319-
});
318+
if (json.isStory) {
319+
final Story story = Story.fromJson(json);
320+
return story;
321+
} else if (json.isComment) {
322+
final Comment comment = Comment.fromJson(json);
323+
return comment;
324+
}
325+
return null;
326+
});
320327

321-
if (item != null) {
322-
yield item;
328+
if (item != null) {
329+
yield item;
330+
}
323331
}
324332
}
325333
}

lib/repositories/sembast_repository.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class SembastRepository with Loggable {
6767
return db;
6868
}
6969

70-
//#region Cached comments for time machine feature.
70+
//#region Cached comments for time machine feature and favorites screen.
7171
Future<Map<String, Object?>> cacheComment(Comment comment) async {
7272
final Database db = _database ?? await initializeDatabase();
7373
final StoreRef<int, Map<String, Object?>> store =
@@ -89,7 +89,34 @@ class SembastRepository with Loggable {
8989
}
9090
}
9191

92-
Future<int> deleteAllCachedComments() async {
92+
Future<Map<String, Object?>> cacheItem(Item item) async {
93+
final Database db = _database ?? await initializeDatabase();
94+
final StoreRef<int, Map<String, Object?>> store =
95+
intMapStoreFactory.store(_cachedCommentsKey);
96+
return store.record(item.id).put(db, item.toJson());
97+
}
98+
99+
Future<Item?> getCachedItem({required int id}) async {
100+
final Database db = _database ?? await initializeDatabase();
101+
final StoreRef<int, Map<String, Object?>> store =
102+
intMapStoreFactory.store(_cachedCommentsKey);
103+
final RecordSnapshot<int, Map<String, Object?>>? snapshot =
104+
await store.record(id).getSnapshot(db);
105+
if (snapshot != null) {
106+
final bool isStory = snapshot['type'] == 'story';
107+
if (isStory) {
108+
final Story story = Story.fromJson(snapshot.value);
109+
return story;
110+
} else {
111+
final Comment comment = Comment.fromJson(snapshot.value);
112+
return comment;
113+
}
114+
} else {
115+
return null;
116+
}
117+
}
118+
119+
Future<int> deleteAllCachedItems() async {
93120
final Database db = _database ?? await initializeDatabase();
94121
final StoreRef<int, Map<String, Object?>> store =
95122
intMapStoreFactory.store(_cachedCommentsKey);

lib/screens/profile/profile_screen.dart

Lines changed: 6 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import 'package:flutter/gestures.dart';
21
import 'package:flutter/material.dart';
32
import 'package:flutter_bloc/flutter_bloc.dart';
4-
import 'package:flutter_slidable/flutter_slidable.dart';
53
import 'package:go_router/go_router.dart';
64
import 'package:hacki/blocs/blocs.dart';
75
import 'package:hacki/config/constants.dart';
@@ -130,124 +128,12 @@ class _ProfileScreenState extends State<ProfileScreen>
130128
top: Dimens.pt50,
131129
child: Visibility(
132130
visible: pageType == PageType.fav,
133-
child: BlocConsumer<FavCubit, FavState>(
134-
listener: (BuildContext context, FavState favState) {
135-
if (favState.status == Status.success) {
136-
refreshControllerFav
137-
..refreshCompleted()
138-
..loadComplete();
139-
}
140-
},
141-
buildWhen: (FavState previous, FavState current) =>
142-
previous.favItems.length != current.favItems.length,
143-
builder: (BuildContext context, FavState favState) {
144-
Widget? header() => authState.isLoggedIn
145-
? BlocSelector<FavCubit, FavState, Status>(
146-
selector: (FavState state) => state.mergeStatus,
147-
builder: (
148-
BuildContext context,
149-
Status status,
150-
) {
151-
return TextButton(
152-
onPressed: () =>
153-
context.read<FavCubit>().merge(
154-
onError: (AppException e) =>
155-
showErrorSnackBar(e.message),
156-
onSuccess: () => showSnackBar(
157-
content: '''Sync completed.''',
158-
),
159-
),
160-
child: status == Status.inProgress
161-
? const SizedBox(
162-
height: Dimens.pt12,
163-
width: Dimens.pt12,
164-
child:
165-
CustomCircularProgressIndicator(
166-
strokeWidth: Dimens.pt2,
167-
),
168-
)
169-
: const Text('Sync from Hacker News'),
170-
);
171-
},
172-
)
173-
: null;
174-
175-
if (favState.favItems.isEmpty &&
176-
favState.status != Status.inProgress) {
177-
return Column(
178-
children: <Widget>[
179-
header() ?? const SizedBox.shrink(),
180-
const CenteredMessageView(
181-
content:
182-
'Your favorite stories will show up here.'
183-
'\nThey will be synced to your Hacker '
184-
'News account if you are logged in.',
185-
),
186-
],
187-
);
188-
}
189-
190-
return BlocBuilder<PreferenceCubit, PreferenceState>(
191-
buildWhen: (
192-
PreferenceState previous,
193-
PreferenceState current,
194-
) =>
195-
previous.isComplexStoryTileEnabled !=
196-
current.isComplexStoryTileEnabled ||
197-
previous.isMetadataEnabled !=
198-
current.isMetadataEnabled ||
199-
previous.isUrlEnabled != current.isUrlEnabled,
200-
builder: (
201-
BuildContext context,
202-
PreferenceState prefState,
203-
) {
204-
return ItemsListView<Item>(
205-
showWebPreviewOnStoryTile:
206-
prefState.isComplexStoryTileEnabled,
207-
showMetadataOnStoryTile:
208-
prefState.isMetadataEnabled,
209-
showFavicon: prefState.isFaviconEnabled,
210-
showUrl: prefState.isUrlEnabled,
211-
useSimpleTileForStory: true,
212-
refreshController: refreshControllerFav,
213-
items: favState.favItems,
214-
onRefresh: () {
215-
HapticFeedbackUtil.light();
216-
context.read<FavCubit>().refresh();
217-
},
218-
onLoadMore: () {
219-
context.read<FavCubit>().loadMore();
220-
},
221-
onTap: (Item item) => goToItemScreen(
222-
args: ItemScreenArgs(item: item),
223-
),
224-
header: header(),
225-
itemBuilder: (Widget child, Item item) {
226-
return Slidable(
227-
dragStartBehavior: DragStartBehavior.start,
228-
startActionPane: ActionPane(
229-
motion: const BehindMotion(),
230-
children: <Widget>[
231-
SlidableAction(
232-
onPressed: (_) {
233-
HapticFeedbackUtil.light();
234-
context
235-
.read<FavCubit>()
236-
.removeFav(item.id);
237-
},
238-
backgroundColor: Palette.red,
239-
foregroundColor: Palette.white,
240-
icon: Icons.close,
241-
),
242-
],
243-
),
244-
child: child,
245-
);
246-
},
247-
);
248-
},
249-
);
250-
},
131+
child: FavoritesScreen(
132+
refreshController: refreshControllerFav,
133+
authState: authState,
134+
onItemTap: (Item item) => goToItemScreen(
135+
args: ItemScreenArgs(item: item),
136+
),
251137
),
252138
),
253139
),

0 commit comments

Comments
 (0)