Skip to content

Commit

Permalink
feat: separate tabs for comments and stories in favorites screen. (#479)
Browse files Browse the repository at this point in the history
  • Loading branch information
Livinglist authored Sep 22, 2024
1 parent 3413b16 commit a4ae6a2
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 145 deletions.
17 changes: 15 additions & 2 deletions lib/cubits/fav/fav_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class FavCubit extends Cubit<FavState> with Loggable {
PreferenceRepository? preferenceRepository,
HackerNewsRepository? hackerNewsRepository,
HackerNewsWebRepository? hackerNewsWebRepository,
SembastRepository? sembastRepository,
}) : _authBloc = authBloc,
_authRepository = authRepository ?? locator.get<AuthRepository>(),
_preferenceRepository =
Expand All @@ -27,6 +28,8 @@ class FavCubit extends Cubit<FavState> with Loggable {
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_hackerNewsWebRepository =
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
_sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(),
super(FavState.init()) {
init();
}
Expand All @@ -36,8 +39,9 @@ class FavCubit extends Cubit<FavState> with Loggable {
final PreferenceRepository _preferenceRepository;
final HackerNewsRepository _hackerNewsRepository;
final HackerNewsWebRepository _hackerNewsWebRepository;
final SembastRepository _sembastRepository;
late final StreamSubscription<String>? _usernameSubscription;
static const int _pageSize = 20;
static const int _pageSize = 100;

Future<void> init() async {
_usernameSubscription = _authBloc.stream
Expand All @@ -55,6 +59,8 @@ class FavCubit extends Cubit<FavState> with Loggable {
_hackerNewsRepository
.fetchItemsStream(
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
getFromCache: (int id) =>
_sembastRepository.getCachedItem(id: id),
)
.listen(_onItemLoaded)
.onDone(() {
Expand Down Expand Up @@ -97,7 +103,10 @@ class FavCubit extends Cubit<FavState> with Loggable {
void removeFav(int id) {
_preferenceRepository
..removeFav(username: username, id: id)
..removeFav(username: '', id: id);
..removeFav(
username: '',
id: id,
);

emit(
state.copyWith(
Expand Down Expand Up @@ -200,13 +209,17 @@ class FavCubit extends Cubit<FavState> with Loggable {
}

void _onItemLoaded(Item item) {
_sembastRepository.cacheItem(item);
emit(
state.copyWith(
favItems: List<Item>.from(state.favItems)..add(item),
),
);
}

void switchTab() =>
emit(state.copyWith(isDisplayingStories: !state.isDisplayingStories));

@override
Future<void> close() {
_usernameSubscription?.cancel();
Expand Down
8 changes: 7 additions & 1 deletion lib/cubits/fav/fav_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,39 @@ class FavState extends Equatable {
required this.status,
required this.mergeStatus,
required this.currentPage,
required this.isDisplayingStories,
});

FavState.init()
: favIds = <int>[],
favItems = <Item>[],
status = Status.idle,
mergeStatus = Status.idle,
currentPage = 0;
currentPage = 0,
isDisplayingStories = true;

final List<int> favIds;
final List<Item> favItems;
final Status status;
final Status mergeStatus;
final int currentPage;
final bool isDisplayingStories;

FavState copyWith({
List<int>? favIds,
List<Item>? favItems,
Status? status,
Status? mergeStatus,
int? currentPage,
bool? isDisplayingStories,
}) {
return FavState(
favIds: favIds ?? this.favIds,
favItems: favItems ?? this.favItems,
status: status ?? this.status,
mergeStatus: mergeStatus ?? this.mergeStatus,
currentPage: currentPage ?? this.currentPage,
isDisplayingStories: isDisplayingStories ?? this.isDisplayingStories,
);
}

Expand All @@ -45,5 +50,6 @@ class FavState extends Equatable {
currentPage,
favIds,
favItems,
isDisplayingStories,
];
}
3 changes: 2 additions & 1 deletion lib/models/item/buildable_comment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class BuildableComment extends Comment with Buildable {
BuildableComment copyWith({
int? level,
bool? hidden,
int? kid,
}) {
return BuildableComment(
id: id,
Expand All @@ -49,7 +50,7 @@ class BuildableComment extends Comment with Buildable {
score: score,
by: by,
text: text,
kids: kids,
kids: kid == null ? kids : <int>[...kids, kid],
dead: dead,
deleted: deleted,
hidden: hidden ?? this.hidden,
Expand Down
3 changes: 2 additions & 1 deletion lib/models/item/comment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Comment extends Item {
Comment copyWith({
int? level,
bool? hidden,
int? kid,
}) {
return Comment(
id: id,
Expand All @@ -44,7 +45,7 @@ class Comment extends Item {
score: score,
by: by,
text: text,
kids: kids,
kids: kid == null ? kids : <int>[...kids, kid],
dead: dead,
deleted: deleted,
hidden: hidden ?? this.hidden,
Expand Down
38 changes: 23 additions & 15 deletions lib/repositories/hacker_news_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -302,24 +302,32 @@ class HackerNewsRepository with Loggable {

/// Fetch a list of [Item] based on ids and return results
/// using a stream.
Stream<Item> fetchItemsStream({required List<int> ids}) async* {
Stream<Item> fetchItemsStream({
required List<int> ids,
Future<Item?> Function(int)? getFromCache,
}) async* {
for (final int id in ids) {
final Item? item =
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
if (json == null) return null;
final Item? cachedItem = await getFromCache?.call(id);
if (cachedItem != null) {
yield cachedItem;
} else {
final Item? item =
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
if (json == null) return null;

if (json.isStory) {
final Story story = Story.fromJson(json);
return story;
} else if (json.isComment) {
final Comment comment = Comment.fromJson(json);
return comment;
}
return null;
});
if (json.isStory) {
final Story story = Story.fromJson(json);
return story;
} else if (json.isComment) {
final Comment comment = Comment.fromJson(json);
return comment;
}
return null;
});

if (item != null) {
yield item;
if (item != null) {
yield item;
}
}
}
}
Expand Down
31 changes: 29 additions & 2 deletions lib/repositories/sembast_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class SembastRepository with Loggable {
return db;
}

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

Future<int> deleteAllCachedComments() async {
Future<Map<String, Object?>> cacheItem(Item item) async {
final Database db = _database ?? await initializeDatabase();
final StoreRef<int, Map<String, Object?>> store =
intMapStoreFactory.store(_cachedCommentsKey);
return store.record(item.id).put(db, item.toJson());
}

Future<Item?> getCachedItem({required int id}) async {
final Database db = _database ?? await initializeDatabase();
final StoreRef<int, Map<String, Object?>> store =
intMapStoreFactory.store(_cachedCommentsKey);
final RecordSnapshot<int, Map<String, Object?>>? snapshot =
await store.record(id).getSnapshot(db);
if (snapshot != null) {
final bool isStory = snapshot['type'] == 'story';
if (isStory) {
final Story story = Story.fromJson(snapshot.value);
return story;
} else {
final Comment comment = Comment.fromJson(snapshot.value);
return comment;
}
} else {
return null;
}
}

Future<int> deleteAllCachedItems() async {
final Database db = _database ?? await initializeDatabase();
final StoreRef<int, Map<String, Object?>> store =
intMapStoreFactory.store(_cachedCommentsKey);
Expand Down
126 changes: 6 additions & 120 deletions lib/screens/profile/profile_screen.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
Expand Down Expand Up @@ -130,124 +128,12 @@ class _ProfileScreenState extends State<ProfileScreen>
top: Dimens.pt50,
child: Visibility(
visible: pageType == PageType.fav,
child: BlocConsumer<FavCubit, FavState>(
listener: (BuildContext context, FavState favState) {
if (favState.status == Status.success) {
refreshControllerFav
..refreshCompleted()
..loadComplete();
}
},
buildWhen: (FavState previous, FavState current) =>
previous.favItems.length != current.favItems.length,
builder: (BuildContext context, FavState favState) {
Widget? header() => authState.isLoggedIn
? BlocSelector<FavCubit, FavState, Status>(
selector: (FavState state) => state.mergeStatus,
builder: (
BuildContext context,
Status status,
) {
return TextButton(
onPressed: () =>
context.read<FavCubit>().merge(
onError: (AppException e) =>
showErrorSnackBar(e.message),
onSuccess: () => showSnackBar(
content: '''Sync completed.''',
),
),
child: status == Status.inProgress
? const SizedBox(
height: Dimens.pt12,
width: Dimens.pt12,
child:
CustomCircularProgressIndicator(
strokeWidth: Dimens.pt2,
),
)
: const Text('Sync from Hacker News'),
);
},
)
: null;

if (favState.favItems.isEmpty &&
favState.status != Status.inProgress) {
return Column(
children: <Widget>[
header() ?? const SizedBox.shrink(),
const CenteredMessageView(
content:
'Your favorite stories will show up here.'
'\nThey will be synced to your Hacker '
'News account if you are logged in.',
),
],
);
}

return BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (
PreferenceState previous,
PreferenceState current,
) =>
previous.isComplexStoryTileEnabled !=
current.isComplexStoryTileEnabled ||
previous.isMetadataEnabled !=
current.isMetadataEnabled ||
previous.isUrlEnabled != current.isUrlEnabled,
builder: (
BuildContext context,
PreferenceState prefState,
) {
return ItemsListView<Item>(
showWebPreviewOnStoryTile:
prefState.isComplexStoryTileEnabled,
showMetadataOnStoryTile:
prefState.isMetadataEnabled,
showFavicon: prefState.isFaviconEnabled,
showUrl: prefState.isUrlEnabled,
useSimpleTileForStory: true,
refreshController: refreshControllerFav,
items: favState.favItems,
onRefresh: () {
HapticFeedbackUtil.light();
context.read<FavCubit>().refresh();
},
onLoadMore: () {
context.read<FavCubit>().loadMore();
},
onTap: (Item item) => goToItemScreen(
args: ItemScreenArgs(item: item),
),
header: header(),
itemBuilder: (Widget child, Item item) {
return Slidable(
dragStartBehavior: DragStartBehavior.start,
startActionPane: ActionPane(
motion: const BehindMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) {
HapticFeedbackUtil.light();
context
.read<FavCubit>()
.removeFav(item.id);
},
backgroundColor: Palette.red,
foregroundColor: Palette.white,
icon: Icons.close,
),
],
),
child: child,
);
},
);
},
);
},
child: FavoritesScreen(
refreshController: refreshControllerFav,
authState: authState,
onItemTap: (Item item) => goToItemScreen(
args: ItemScreenArgs(item: item),
),
),
),
),
Expand Down
Loading

0 comments on commit a4ae6a2

Please sign in to comment.