mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
bedc3b66ec | |||
3e3941380d | |||
bbed4e0e75 | |||
a4ae6a20e1 | |||
3413b1686d |
@ -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 =
|
||||
@ -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();
|
||||
}
|
||||
@ -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
|
||||
@ -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(() {
|
||||
@ -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(
|
||||
@ -200,6 +209,7 @@ 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),
|
||||
@ -207,6 +217,9 @@ class FavCubit extends Cubit<FavState> with Loggable {
|
||||
);
|
||||
}
|
||||
|
||||
void switchTab() =>
|
||||
emit(state.copyWith(isDisplayingStories: !state.isDisplayingStories));
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_usernameSubscription?.cancel();
|
||||
|
@ -7,6 +7,7 @@ class FavState extends Equatable {
|
||||
required this.status,
|
||||
required this.mergeStatus,
|
||||
required this.currentPage,
|
||||
required this.isDisplayingStories,
|
||||
});
|
||||
|
||||
FavState.init()
|
||||
@ -14,13 +15,21 @@ class FavState extends Equatable {
|
||||
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;
|
||||
|
||||
List<Comment> get favComments =>
|
||||
favItems.whereType<Comment>().toList(growable: false);
|
||||
|
||||
List<Story> get favStories =>
|
||||
favItems.whereType<Story>().toList(growable: false);
|
||||
|
||||
FavState copyWith({
|
||||
List<int>? favIds,
|
||||
@ -28,6 +37,7 @@ class FavState extends Equatable {
|
||||
Status? status,
|
||||
Status? mergeStatus,
|
||||
int? currentPage,
|
||||
bool? isDisplayingStories,
|
||||
}) {
|
||||
return FavState(
|
||||
favIds: favIds ?? this.favIds,
|
||||
@ -35,6 +45,7 @@ class FavState extends Equatable {
|
||||
status: status ?? this.status,
|
||||
mergeStatus: mergeStatus ?? this.mergeStatus,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
isDisplayingStories: isDisplayingStories ?? this.isDisplayingStories,
|
||||
);
|
||||
}
|
||||
|
||||
@ -45,5 +56,6 @@ class FavState extends Equatable {
|
||||
currentPage,
|
||||
favIds,
|
||||
favItems,
|
||||
isDisplayingStories,
|
||||
];
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ class BuildableComment extends Comment with Buildable {
|
||||
BuildableComment copyWith({
|
||||
int? level,
|
||||
bool? hidden,
|
||||
int? kid,
|
||||
}) {
|
||||
return BuildableComment(
|
||||
id: id,
|
||||
@ -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,
|
||||
|
@ -36,6 +36,7 @@ class Comment extends Item {
|
||||
Comment copyWith({
|
||||
int? level,
|
||||
bool? hidden,
|
||||
int? kid,
|
||||
}) {
|
||||
return Comment(
|
||||
id: id,
|
||||
@ -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,
|
||||
|
@ -110,11 +110,11 @@ final class SwipeGesturePreference extends BooleanPreference {
|
||||
String get key => 'swipeGestureMode';
|
||||
|
||||
@override
|
||||
String get title => 'Swipe Gesture';
|
||||
String get title => 'Swipe Gesture for Switching Tabs';
|
||||
|
||||
@override
|
||||
String get subtitle =>
|
||||
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
|
||||
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu and double tap to open the url (if complex tile is disabled).''';
|
||||
}
|
||||
|
||||
final class NotificationModePreference extends BooleanPreference {
|
||||
|
@ -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;
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (item != null) {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -401,7 +401,8 @@ class HackerNewsWebRepository with Loggable {
|
||||
/// Get comment age.
|
||||
final Element? cmtAgeElement =
|
||||
element.querySelector(_commentAgeSelector);
|
||||
final String? ageString = cmtAgeElement?.attributes['title'];
|
||||
final String? ageString =
|
||||
cmtAgeElement?.attributes['title']?.split(' ').firstOrNull;
|
||||
|
||||
final int? timestamp = ageString == null
|
||||
? null
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
@ -10,6 +11,8 @@ import 'package:sembast/sembast.dart';
|
||||
import 'package:sembast/sembast_io.dart';
|
||||
|
||||
/// [SembastRepository] is for storing stories and comments for faster loading.
|
||||
/// This is currently used by [TimeMachineCubit], [NotificationCubit] and
|
||||
/// [FavCubit].
|
||||
///
|
||||
/// Sembast [Database] is used as its database and is being stored in the
|
||||
/// documents directory assigned by host system which you can retrieve
|
||||
@ -67,7 +70,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 =
|
||||
@ -89,7 +92,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);
|
||||
|
@ -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';
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
187
lib/screens/profile/widgets/favorites_screen.dart
Normal file
187
lib/screens/profile/widgets/favorites_screen.dart
Normal file
@ -0,0 +1,187 @@
|
||||
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:hacki/blocs/auth/auth_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/profile/widgets/centered_message_view.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
class FavoritesScreen extends StatelessWidget {
|
||||
const FavoritesScreen({
|
||||
required this.refreshController,
|
||||
required this.authState,
|
||||
required this.onItemTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final RefreshController refreshController;
|
||||
final AuthState authState;
|
||||
final void Function(Item) onItemTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<FavCubit, FavState>(
|
||||
listener: (BuildContext context, FavState favState) {
|
||||
if (favState.status == Status.success) {
|
||||
refreshController
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
}
|
||||
},
|
||||
buildWhen: (FavState previous, FavState current) =>
|
||||
previous.favItems.length != current.favItems.length ||
|
||||
previous.isDisplayingStories != current.isDisplayingStories,
|
||||
builder: (BuildContext context, FavState favState) {
|
||||
Widget? header() => Column(
|
||||
children: <Widget>[
|
||||
if (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) =>
|
||||
context.showErrorSnackBar(e.message),
|
||||
onSuccess: () => context.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',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
selected: favState.isDisplayingStories,
|
||||
label: 'Story',
|
||||
onSelected: (_) => context.read<FavCubit>().switchTab(),
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
selected: !favState.isDisplayingStories,
|
||||
label: 'Comment',
|
||||
onSelected: (_) => context.read<FavCubit>().switchTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
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.',
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
if (favState.isDisplayingStories && favState.favStories.isEmpty) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
header() ?? const SizedBox.shrink(),
|
||||
const CenteredMessageView(
|
||||
content: 'No favorite story.',
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (!favState.isDisplayingStories &&
|
||||
favState.favComments.isEmpty) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
header() ?? const SizedBox.shrink(),
|
||||
const CenteredMessageView(
|
||||
content: 'No favorite comment.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: refreshController,
|
||||
items: favState.isDisplayingStories
|
||||
? favState.favStories
|
||||
: favState.favComments,
|
||||
onRefresh: () {
|
||||
HapticFeedbackUtil.light();
|
||||
context.read<FavCubit>().refresh();
|
||||
},
|
||||
onLoadMore: () {
|
||||
context.read<FavCubit>().loadMore();
|
||||
},
|
||||
onTap: onItemTap,
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -606,7 +606,7 @@ class _SettingsState extends State<Settings> with ItemActionMixin, Loggable {
|
||||
context.pop();
|
||||
locator
|
||||
.get<SembastRepository>()
|
||||
.deleteAllCachedComments()
|
||||
.deleteAllCachedItems()
|
||||
.whenComplete(
|
||||
locator.get<OfflineRepository>().deleteAll,
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
export 'centered_message_view.dart';
|
||||
export 'enter_offline_mode_list_tile.dart';
|
||||
export 'favorites_screen.dart';
|
||||
export 'inbox_view.dart';
|
||||
export 'offline_list_tile.dart';
|
||||
export 'settings.dart';
|
||||
|
@ -82,6 +82,13 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
FadeIn(
|
||||
child: InkWell(
|
||||
onTap: () => onTap(e),
|
||||
|
||||
/// If swipe gesture is enabled on home screen, use
|
||||
/// long press instead of slide action to trigger
|
||||
/// the action menu.
|
||||
onLongPress: swipeGestureEnabled
|
||||
? () => onMoreTapped?.call(e, context.rect)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: Dimens.pt8,
|
||||
|
@ -123,20 +123,41 @@ class LinkView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: imageUri ?? Constants.favicon(url),
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
cacheKey: imageUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
if (url.isEmpty) {
|
||||
return FadeIn(
|
||||
child: Center(
|
||||
child: _HackerNewsImage(
|
||||
height: layoutHeight,
|
||||
: () {
|
||||
if (imageUri?.isNotEmpty ?? false) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: imageUri!,
|
||||
fit:
|
||||
isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
cacheKey: imageUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
if (url.isEmpty) {
|
||||
return FadeIn(
|
||||
child: Center(
|
||||
child: _HackerNewsImage(
|
||||
height: layoutHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: Constants.favicon(url),
|
||||
fit: BoxFit.scaleDown,
|
||||
cacheKey: iconUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
return const FadeIn(
|
||||
child: Icon(
|
||||
Icons.public,
|
||||
size: Dimens.pt20,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (url.isNotEmpty) {
|
||||
return Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: Constants.favicon(url),
|
||||
@ -152,8 +173,16 @@ class LinkView extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
} else {
|
||||
return FadeIn(
|
||||
child: Center(
|
||||
child: _HackerNewsImage(
|
||||
height: layoutHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -128,7 +128,7 @@ class StoryTile extends StatelessWidget {
|
||||
excludeSemantics: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
onDoubleTap: () {
|
||||
if (story.url.isNotEmpty) {
|
||||
LinkUtil.launch(
|
||||
story.url,
|
||||
|
@ -3,7 +3,18 @@ import 'package:hacki/models/models.dart' show Comment;
|
||||
class CommentCache {
|
||||
static final Map<int, Comment> _comments = <int, Comment>{};
|
||||
|
||||
void cacheComment(Comment comment) => _comments[comment.id] = comment;
|
||||
void cacheComment(Comment comment) {
|
||||
_comments[comment.id] = comment;
|
||||
|
||||
/// Comments fetched from `HackerNewsWebRepository` doesn't have populated
|
||||
/// `kids` field, this is why we need to update that of the parent
|
||||
/// comment here.
|
||||
final int parentId = comment.parent;
|
||||
final Comment? parent = _comments[parentId];
|
||||
if (parent == null || parent.kids.contains(comment.id)) return;
|
||||
final Comment updatedParent = parent.copyWith(kid: comment.id);
|
||||
_comments[parentId] = updatedParent;
|
||||
}
|
||||
|
||||
Comment? getComment(int id) => _comments[id];
|
||||
|
||||
|
@ -250,10 +250,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0"
|
||||
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.6.0"
|
||||
version: "5.7.0"
|
||||
dio_smart_retry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1,6 +1,6 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 2.9.2+150
|
||||
version: 2.9.5+153
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@ -17,7 +17,7 @@ dependencies:
|
||||
collection: ^1.17.1
|
||||
connectivity_plus: ^6.0.3
|
||||
device_info_plus: ^10.1.0
|
||||
dio: ^5.4.3+1
|
||||
dio: ^5.7.0
|
||||
dio_smart_retry: ^6.0.0
|
||||
equatable: ^2.0.5
|
||||
fast_gbk: ^1.0.0
|
||||
|
Reference in New Issue
Block a user