mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
71aa42118d | |||
4f21d3e6bd | |||
96d0fe9e5e | |||
69eee3e278 | |||
36bcd996c0 | |||
5fc39d8b8b | |||
5dce7787e1 | |||
8888dde792 | |||
6c8fc4cf87 | |||
ae9cc109db | |||
c8976ed17b | |||
ff7e115418 | |||
0310507c96 | |||
58c646e232 | |||
08328e2ca1 | |||
86b7228ffd | |||
e103c88ca6 | |||
94323a04e0 | |||
4776c375a1 |
@ -76,6 +76,15 @@ final class SharedPrefsCore {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func remove(key: String?) -> Bool{
|
||||||
|
if let key = key {
|
||||||
|
let keyStore = NSUbiquitousKeyValueStore()
|
||||||
|
keyStore.removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
||||||
@ -87,6 +96,14 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
|||||||
|
|
||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
switch call.method {
|
switch call.method {
|
||||||
|
case "remove":
|
||||||
|
if let params = call.arguments as? [String: Any] {
|
||||||
|
let key = params[keyKey] as? String
|
||||||
|
|
||||||
|
let res = SharedPrefsCore.shared.remove(key: key)
|
||||||
|
result(res)
|
||||||
|
}
|
||||||
|
|
||||||
case "setBool":
|
case "setBool":
|
||||||
if let params = call.arguments as? [String: Any] {
|
if let params = call.arguments as? [String: Any] {
|
||||||
let val = params[valKey] as? Bool
|
let val = params[valKey] as? Bool
|
||||||
|
@ -15,6 +15,14 @@ class SyncedSharedPreferences {
|
|||||||
const MethodChannel(channel),
|
const MethodChannel(channel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<bool?> remove({
|
||||||
|
required String key,
|
||||||
|
}) async {
|
||||||
|
return _channel.invokeMethod('remove', <String, dynamic>{
|
||||||
|
'key': key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool?> setBool({
|
Future<bool?> setBool({
|
||||||
required String key,
|
required String key,
|
||||||
required bool val,
|
required bool val,
|
||||||
|
5
fastlane/metadata/android/en-US/changelogs/129.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/129.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
- Ability to use manual pagination on home screen.
|
||||||
|
- Ability to use Material 3 (experimental).
|
||||||
|
- Ability to search in thread.
|
||||||
|
- Ability to customize text scale factor.
|
||||||
|
- Ability to customize app's accent color.
|
4
fastlane/metadata/android/en-US/changelogs/131.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/131.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- New comment indicator.
|
||||||
|
- Ability to mark stories as read from home page.
|
||||||
|
- Text rendering improvements.
|
||||||
|
- Performance improvements.
|
4
fastlane/metadata/android/en-US/changelogs/132.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/132.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- New comment indicator.
|
||||||
|
- Ability to mark stories as read from home page.
|
||||||
|
- Text rendering improvements.
|
||||||
|
- Performance improvements.
|
@ -159,7 +159,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
|
||||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||||
|
@ -11,13 +11,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
AuthBloc({
|
AuthBloc({
|
||||||
AuthRepository? authRepository,
|
AuthRepository? authRepository,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
SembastRepository? sembastRepository,
|
SembastRepository? sembastRepository,
|
||||||
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
|
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||||
_preferenceRepository =
|
_preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
super(const AuthState.init()) {
|
super(const AuthState.init()) {
|
||||||
@ -31,7 +31,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
|
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
final SembastRepository _sembastRepository;
|
final SembastRepository _sembastRepository;
|
||||||
|
|
||||||
Future<void> onInitialize(
|
Future<void> onInitialize(
|
||||||
@ -41,7 +41,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
await _authRepository.loggedIn.then((bool loggedIn) async {
|
await _authRepository.loggedIn.then((bool loggedIn) async {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
final String? username = await _authRepository.username;
|
final String? username = await _authRepository.username;
|
||||||
User? user = await _storiesRepository.fetchUser(id: username!);
|
User? user = await _hackerNewsRepository.fetchUser(id: username!);
|
||||||
|
|
||||||
/// According to Hacker News' API documentation,
|
/// According to Hacker News' API documentation,
|
||||||
/// if user has no public activity (posting a comment or story),
|
/// if user has no public activity (posting a comment or story),
|
||||||
@ -89,7 +89,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (successful) {
|
if (successful) {
|
||||||
final User? user = await _storiesRepository.fetchUser(id: event.username);
|
final User? user =
|
||||||
|
await _hackerNewsRepository.fetchUser(id: event.username);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
user: user ?? User.emptyWithId(event.username),
|
user: user ?? User.emptyWithId(event.username),
|
||||||
|
@ -19,15 +19,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
required PreferenceCubit preferenceCubit,
|
required PreferenceCubit preferenceCubit,
|
||||||
required FilterCubit filterCubit,
|
required FilterCubit filterCubit,
|
||||||
OfflineRepository? offlineRepository,
|
OfflineRepository? offlineRepository,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
}) : _preferenceCubit = preferenceCubit,
|
}) : _preferenceCubit = preferenceCubit,
|
||||||
_filterCubit = filterCubit,
|
_filterCubit = filterCubit,
|
||||||
_offlineRepository =
|
_offlineRepository =
|
||||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
_preferenceRepository =
|
_preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_logger = logger ?? locator.get<Logger>(),
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
@ -37,6 +37,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
on<StoriesLoadMore>(onLoadMore);
|
on<StoriesLoadMore>(onLoadMore);
|
||||||
on<StoryLoaded>(onStoryLoaded);
|
on<StoryLoaded>(onStoryLoaded);
|
||||||
on<StoryRead>(onStoryRead);
|
on<StoryRead>(onStoryRead);
|
||||||
|
on<StoryUnread>(onStoryUnread);
|
||||||
on<StoriesLoaded>(onStoriesLoaded);
|
on<StoriesLoaded>(onStoriesLoaded);
|
||||||
on<StoriesDownload>(onDownload);
|
on<StoriesDownload>(onDownload);
|
||||||
on<StoriesCancelDownload>(onCancelDownload);
|
on<StoriesCancelDownload>(onCancelDownload);
|
||||||
@ -49,7 +50,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
final PreferenceCubit _preferenceCubit;
|
final PreferenceCubit _preferenceCubit;
|
||||||
final FilterCubit _filterCubit;
|
final FilterCubit _filterCubit;
|
||||||
final OfflineRepository _offlineRepository;
|
final OfflineRepository _offlineRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
DeviceScreenType? deviceScreenType;
|
DeviceScreenType? deviceScreenType;
|
||||||
@ -113,13 +114,14 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
add(StoriesLoaded(type: type));
|
add(StoriesLoaded(type: type));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
|
final List<int> ids =
|
||||||
|
await _hackerNewsRepository.fetchStoryIds(type: type);
|
||||||
emit(
|
emit(
|
||||||
state
|
state
|
||||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||||
);
|
);
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
|
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
|
||||||
.listen((Story story) {
|
.listen((Story story) {
|
||||||
add(StoryLoaded(story: story, type: type));
|
add(StoryLoaded(story: story, type: type));
|
||||||
@ -196,7 +198,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
add(StoriesLoaded(type: event.type));
|
add(StoriesLoaded(type: event.type));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchStoriesStream(
|
.fetchStoriesStream(
|
||||||
ids: state.storyIdsByType[event.type]!.sublist(
|
ids: state.storyIdsByType[event.type]!.sublist(
|
||||||
lower,
|
lower,
|
||||||
@ -273,7 +275,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
..remove(StoryType.latest);
|
..remove(StoryType.latest);
|
||||||
|
|
||||||
for (final StoryType type in prioritizedTypes) {
|
for (final StoryType type in prioritizedTypes) {
|
||||||
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
|
final List<int> ids =
|
||||||
|
await _hackerNewsRepository.fetchStoryIds(type: type);
|
||||||
await _offlineRepository.cacheStoryIds(type: type, ids: ids);
|
await _offlineRepository.cacheStoryIds(type: type, ids: ids);
|
||||||
prioritizedIds.addAll(ids);
|
prioritizedIds.addAll(ids);
|
||||||
}
|
}
|
||||||
@ -293,7 +296,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final Set<int> latestIds = <int>{};
|
final Set<int> latestIds = <int>{};
|
||||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
final List<int> ids = await _hackerNewsRepository.fetchStoryIds(
|
||||||
type: StoryType.latest,
|
type: StoryType.latest,
|
||||||
);
|
);
|
||||||
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
|
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
|
||||||
@ -347,7 +350,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.d('fetching story $id');
|
_logger.d('fetching story $id');
|
||||||
final Story? story = await _storiesRepository.fetchStory(id: id);
|
final Story? story = await _hackerNewsRepository.fetchStory(id: id);
|
||||||
|
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
if (isPrioritized) {
|
if (isPrioritized) {
|
||||||
@ -377,7 +380,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
/// In other words, we are prioritizing the story itself instead of
|
/// In other words, we are prioritizing the story itself instead of
|
||||||
/// the comments in the story.
|
/// the comments in the story.
|
||||||
late final StreamSubscription<Comment>? downloadStream;
|
late final StreamSubscription<Comment>? downloadStream;
|
||||||
downloadStream = _storiesRepository
|
downloadStream = _hackerNewsRepository
|
||||||
.fetchAllChildrenComments(ids: story.kids)
|
.fetchAllChildrenComments(ids: story.kids)
|
||||||
.whereType<Comment>()
|
.whereType<Comment>()
|
||||||
.listen(
|
.listen(
|
||||||
@ -460,7 +463,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
StoryRead event,
|
StoryRead event,
|
||||||
Emitter<StoriesState> emit,
|
Emitter<StoriesState> emit,
|
||||||
) async {
|
) async {
|
||||||
unawaited(_preferenceRepository.updateHasRead(event.story.id));
|
unawaited(_preferenceRepository.addHasRead(event.story.id));
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -469,6 +472,19 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> onStoryUnread(
|
||||||
|
StoryUnread event,
|
||||||
|
Emitter<StoriesState> emit,
|
||||||
|
) async {
|
||||||
|
unawaited(_preferenceRepository.removeHasRead(event.story.id));
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
readStoriesIds: <int>{...state.readStoriesIds}..remove(event.story.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> onClearAllReadStories(
|
Future<void> onClearAllReadStories(
|
||||||
ClearAllReadStories event,
|
ClearAllReadStories event,
|
||||||
Emitter<StoriesState> emit,
|
Emitter<StoriesState> emit,
|
||||||
|
@ -95,6 +95,15 @@ class StoryRead extends StoriesEvent {
|
|||||||
List<Object?> get props => <Object?>[story];
|
List<Object?> get props => <Object?>[story];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StoryUnread extends StoriesEvent {
|
||||||
|
StoryUnread({required this.story});
|
||||||
|
|
||||||
|
final Story story;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[story];
|
||||||
|
}
|
||||||
|
|
||||||
class ClearAllReadStories extends StoriesEvent {
|
class ClearAllReadStories extends StoriesEvent {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[];
|
List<Object?> get props => <Object?>[];
|
||||||
|
@ -23,12 +23,12 @@ Future<void> setUpLocator() async {
|
|||||||
output: LogUtil.logOutput(logOutputFile),
|
output: LogUtil.logOutput(logOutputFile),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
..registerSingleton<SembastRepository>(SembastRepository())
|
||||||
|
..registerSingleton<HackerNewsRepository>(HackerNewsRepository())
|
||||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||||
..registerSingleton<SearchRepository>(SearchRepository())
|
..registerSingleton<SearchRepository>(SearchRepository())
|
||||||
..registerSingleton<AuthRepository>(AuthRepository())
|
..registerSingleton<AuthRepository>(AuthRepository())
|
||||||
..registerSingleton<PostRepository>(PostRepository())
|
..registerSingleton<PostRepository>(PostRepository())
|
||||||
..registerSingleton<SembastRepository>(SembastRepository())
|
|
||||||
..registerSingleton<OfflineRepository>(OfflineRepository())
|
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||||
..registerSingleton<DraftCache>(DraftCache())
|
..registerSingleton<DraftCache>(DraftCache())
|
||||||
..registerSingleton<CommentCache>(CommentCache())
|
..registerSingleton<CommentCache>(CommentCache())
|
||||||
|
@ -32,18 +32,15 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
required CommentsOrder defaultCommentsOrder,
|
required CommentsOrder defaultCommentsOrder,
|
||||||
CommentCache? commentCache,
|
CommentCache? commentCache,
|
||||||
OfflineRepository? offlineRepository,
|
OfflineRepository? offlineRepository,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
SembastRepository? sembastRepository,
|
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
}) : _filterCubit = filterCubit,
|
}) : _filterCubit = filterCubit,
|
||||||
_collapseCache = collapseCache,
|
_collapseCache = collapseCache,
|
||||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||||
_offlineRepository =
|
_offlineRepository =
|
||||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
_sembastRepository =
|
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
|
||||||
_logger = logger ?? locator.get<Logger>(),
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
super(
|
super(
|
||||||
CommentsState.init(
|
CommentsState.init(
|
||||||
@ -58,8 +55,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
final CollapseCache _collapseCache;
|
final CollapseCache _collapseCache;
|
||||||
final CommentCache _commentCache;
|
final CommentCache _commentCache;
|
||||||
final OfflineRepository _offlineRepository;
|
final OfflineRepository _offlineRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
final SembastRepository _sembastRepository;
|
|
||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
|
|
||||||
final ItemScrollController itemScrollController = ItemScrollController();
|
final ItemScrollController itemScrollController = ItemScrollController();
|
||||||
@ -96,7 +92,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_streamSubscription = _storiesRepository
|
_streamSubscription = _hackerNewsRepository
|
||||||
.fetchAllCommentsRecursivelyStream(
|
.fetchAllCommentsRecursivelyStream(
|
||||||
ids: targetAncestors!.last.kids,
|
ids: targetAncestors!.last.kids,
|
||||||
level: targetAncestors.last.level + 1,
|
level: targetAncestors.last.level + 1,
|
||||||
@ -122,7 +118,10 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
final Item item = state.item;
|
final Item item = state.item;
|
||||||
final Item updatedItem = state.isOfflineReading
|
final Item updatedItem = state.isOfflineReading
|
||||||
? item
|
? item
|
||||||
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
|
: await _hackerNewsRepository
|
||||||
|
.fetchItem(id: item.id)
|
||||||
|
.then(_toBuildable)
|
||||||
|
.onError((_, __) => item) ??
|
||||||
item;
|
item;
|
||||||
final List<int> kids = _sortKids(updatedItem.kids);
|
final List<int> kids = _sortKids(updatedItem.kids);
|
||||||
|
|
||||||
@ -135,12 +134,13 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
} else {
|
} else {
|
||||||
switch (state.fetchMode) {
|
switch (state.fetchMode) {
|
||||||
case FetchMode.lazy:
|
case FetchMode.lazy:
|
||||||
commentStream = _storiesRepository.fetchCommentsStream(
|
commentStream = _hackerNewsRepository.fetchCommentsStream(
|
||||||
ids: kids,
|
ids: kids,
|
||||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
);
|
);
|
||||||
case FetchMode.eager:
|
case FetchMode.eager:
|
||||||
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
|
commentStream =
|
||||||
|
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
|
||||||
ids: kids,
|
ids: kids,
|
||||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
);
|
);
|
||||||
@ -187,16 +187,16 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
final Item item = state.item;
|
final Item item = state.item;
|
||||||
final Item updatedItem =
|
final Item updatedItem =
|
||||||
await _storiesRepository.fetchItem(id: item.id) ?? item;
|
await _hackerNewsRepository.fetchItem(id: item.id) ?? item;
|
||||||
final List<int> kids = _sortKids(updatedItem.kids);
|
final List<int> kids = _sortKids(updatedItem.kids);
|
||||||
|
|
||||||
late final Stream<Comment> commentStream;
|
late final Stream<Comment> commentStream;
|
||||||
if (state.fetchMode == FetchMode.lazy) {
|
if (state.fetchMode == FetchMode.lazy) {
|
||||||
commentStream = _storiesRepository.fetchCommentsStream(
|
commentStream = _hackerNewsRepository.fetchCommentsStream(
|
||||||
ids: kids,
|
ids: kids,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
|
commentStream = _hackerNewsRepository.fetchAllCommentsRecursivelyStream(
|
||||||
ids: kids,
|
ids: kids,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -245,14 +245,17 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
/// Ignoring because the subscription will be cancelled in close()
|
/// Ignoring because the subscription will be cancelled in close()
|
||||||
// ignore: cancel_subscriptions
|
// ignore: cancel_subscriptions
|
||||||
final StreamSubscription<Comment> streamSubscription =
|
final StreamSubscription<Comment> streamSubscription =
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchCommentsStream(ids: comment.kids)
|
.fetchCommentsStream(ids: comment.kids)
|
||||||
.asyncMap(_toBuildableComment)
|
.asyncMap(_toBuildableComment)
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
.listen((Comment cmt) {
|
.listen((Comment cmt) {
|
||||||
_collapseCache.addKid(cmt.id, to: cmt.parent);
|
_collapseCache.addKid(cmt.id, to: cmt.parent);
|
||||||
_commentCache.cacheComment(cmt);
|
_commentCache.cacheComment(cmt);
|
||||||
_sembastRepository.cacheComment(cmt);
|
|
||||||
|
final Map<int, Comment> updatedIdToCommentMap =
|
||||||
|
Map<int, Comment>.from(state.idToCommentMap);
|
||||||
|
updatedIdToCommentMap[comment.id] = comment;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -260,6 +263,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
state.comments.indexOf(comment) + offset + 1,
|
state.comments.indexOf(comment) + offset + 1,
|
||||||
cmt.copyWith(level: level),
|
cmt.copyWith(level: level),
|
||||||
),
|
),
|
||||||
|
idToCommentMap: updatedIdToCommentMap,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
offset++;
|
offset++;
|
||||||
@ -289,7 +293,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
HapticFeedbackUtil.light();
|
HapticFeedbackUtil.light();
|
||||||
emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress));
|
emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress));
|
||||||
final Item? parent =
|
final Item? parent =
|
||||||
await _storiesRepository.fetchItem(id: state.item.parent);
|
await _hackerNewsRepository.fetchItem(id: state.item.parent);
|
||||||
|
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
return;
|
return;
|
||||||
@ -310,7 +314,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
Future<void> loadRootThread() async {
|
Future<void> loadRootThread() async {
|
||||||
HapticFeedbackUtil.light();
|
HapticFeedbackUtil.light();
|
||||||
emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress));
|
emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress));
|
||||||
final Story? parent = await _storiesRepository
|
final Story? parent = await _hackerNewsRepository
|
||||||
.fetchParentStory(id: state.item.id)
|
.fetchParentStory(id: state.item.id)
|
||||||
.then(_toBuildableStory);
|
.then(_toBuildableStory);
|
||||||
|
|
||||||
@ -370,7 +374,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll to next root level comment.
|
/// Scroll to next root level comment.
|
||||||
void scrollToNextRoot() {
|
void scrollToNextRoot({VoidCallback? onError}) {
|
||||||
final int totalComments = state.comments.length;
|
final int totalComments = state.comments.length;
|
||||||
final List<Comment> onScreenComments = itemPositionsListener
|
final List<Comment> onScreenComments = itemPositionsListener
|
||||||
.itemPositions.value
|
.itemPositions.value
|
||||||
@ -422,6 +426,10 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.status == CommentsStatus.allLoaded) {
|
||||||
|
onError?.call();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll to previous root level comment.
|
/// Scroll to previous root level comment.
|
||||||
@ -460,27 +468,49 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void search(String query) {
|
void search(String query, {String author = ''}) {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
|
|
||||||
if (query.isEmpty) return;
|
late final bool Function(Comment cmt) conditionSatisfied;
|
||||||
|
|
||||||
final String lowercaseQuery = query.toLowerCase();
|
final String lowercaseQuery = query.toLowerCase();
|
||||||
|
if (query.isEmpty && author.isEmpty) {
|
||||||
|
return;
|
||||||
|
} else if (author.isEmpty) {
|
||||||
|
conditionSatisfied =
|
||||||
|
(Comment cmt) => cmt.text.toLowerCase().contains(lowercaseQuery);
|
||||||
|
} else if (query.isEmpty) {
|
||||||
|
conditionSatisfied = (Comment cmt) => cmt.by == author;
|
||||||
|
} else {
|
||||||
|
conditionSatisfied = (Comment cmt) =>
|
||||||
|
cmt.text.toLowerCase().contains(lowercaseQuery) && cmt.by == author;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
inThreadSearchQuery: query,
|
||||||
|
inThreadSearchAuthor: author,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
for (final int i in 0.to(state.comments.length, inclusive: false)) {
|
for (final int i in 0.to(state.comments.length, inclusive: false)) {
|
||||||
final Comment cmt = state.comments.elementAt(i);
|
final Comment cmt = state.comments.elementAt(i);
|
||||||
if (cmt.text.toLowerCase().contains(lowercaseQuery)) {
|
if (conditionSatisfied(cmt)) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
matchedComments: <int>[...state.matchedComments, i],
|
matchedComments: <int>[...state.matchedComments, i],
|
||||||
inThreadSearchQuery: query,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetSearch() =>
|
void resetSearch() => emit(
|
||||||
emit(state.copyWith(matchedComments: <int>[], inThreadSearchQuery: ''));
|
state.copyWith(
|
||||||
|
matchedComments: <int>[],
|
||||||
|
inThreadSearchQuery: '',
|
||||||
|
inThreadSearchAuthor: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
List<int> _sortKids(List<int> kids) {
|
List<int> _sortKids(List<int> kids) {
|
||||||
switch (state.order) {
|
switch (state.order) {
|
||||||
@ -507,8 +537,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
_collapseCache.addKid(comment.id, to: comment.parent);
|
_collapseCache.addKid(comment.id, to: comment.parent);
|
||||||
_commentCache.cacheComment(comment);
|
_commentCache.cacheComment(comment);
|
||||||
_sembastRepository.cacheComment(comment);
|
|
||||||
|
|
||||||
|
// Hide comment that matches any of the filter keywords.
|
||||||
final bool hidden = _filterCubit.state.keywords.any(
|
final bool hidden = _filterCubit.state.keywords.any(
|
||||||
(String keyword) => comment.text.toLowerCase().contains(keyword),
|
(String keyword) => comment.text.toLowerCase().contains(keyword),
|
||||||
);
|
);
|
||||||
@ -517,7 +547,16 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
comment.copyWith(hidden: hidden),
|
comment.copyWith(hidden: hidden),
|
||||||
];
|
];
|
||||||
|
|
||||||
emit(state.copyWith(comments: updatedComments));
|
final Map<int, Comment> updatedIdToCommentMap =
|
||||||
|
Map<int, Comment>.from(state.idToCommentMap);
|
||||||
|
updatedIdToCommentMap[comment.id] = comment;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
comments: updatedComments,
|
||||||
|
idToCommentMap: updatedIdToCommentMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ class CommentsState extends Equatable {
|
|||||||
required this.item,
|
required this.item,
|
||||||
required this.comments,
|
required this.comments,
|
||||||
required this.matchedComments,
|
required this.matchedComments,
|
||||||
|
required this.idToCommentMap,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.fetchParentStatus,
|
required this.fetchParentStatus,
|
||||||
required this.fetchRootStatus,
|
required this.fetchRootStatus,
|
||||||
@ -22,6 +23,7 @@ class CommentsState extends Equatable {
|
|||||||
required this.isOfflineReading,
|
required this.isOfflineReading,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
required this.inThreadSearchQuery,
|
required this.inThreadSearchQuery,
|
||||||
|
required this.inThreadSearchAuthor,
|
||||||
});
|
});
|
||||||
|
|
||||||
CommentsState.init({
|
CommentsState.init({
|
||||||
@ -31,15 +33,18 @@ class CommentsState extends Equatable {
|
|||||||
required this.order,
|
required this.order,
|
||||||
}) : comments = <Comment>[],
|
}) : comments = <Comment>[],
|
||||||
matchedComments = <int>[],
|
matchedComments = <int>[],
|
||||||
|
idToCommentMap = <int, Comment>{},
|
||||||
status = CommentsStatus.idle,
|
status = CommentsStatus.idle,
|
||||||
fetchParentStatus = CommentsStatus.idle,
|
fetchParentStatus = CommentsStatus.idle,
|
||||||
fetchRootStatus = CommentsStatus.idle,
|
fetchRootStatus = CommentsStatus.idle,
|
||||||
onlyShowTargetComment = false,
|
onlyShowTargetComment = false,
|
||||||
currentPage = 0,
|
currentPage = 0,
|
||||||
inThreadSearchQuery = '';
|
inThreadSearchQuery = '',
|
||||||
|
inThreadSearchAuthor = '';
|
||||||
|
|
||||||
final Item item;
|
final Item item;
|
||||||
final List<Comment> comments;
|
final List<Comment> comments;
|
||||||
|
final Map<int, Comment> idToCommentMap;
|
||||||
final CommentsStatus status;
|
final CommentsStatus status;
|
||||||
final CommentsStatus fetchParentStatus;
|
final CommentsStatus fetchParentStatus;
|
||||||
final CommentsStatus fetchRootStatus;
|
final CommentsStatus fetchRootStatus;
|
||||||
@ -49,6 +54,7 @@ class CommentsState extends Equatable {
|
|||||||
final bool isOfflineReading;
|
final bool isOfflineReading;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
final String inThreadSearchQuery;
|
final String inThreadSearchQuery;
|
||||||
|
final String inThreadSearchAuthor;
|
||||||
|
|
||||||
/// Indexes of comments that matches the query for in-thread search.
|
/// Indexes of comments that matches the query for in-thread search.
|
||||||
final List<int> matchedComments;
|
final List<int> matchedComments;
|
||||||
@ -57,6 +63,7 @@ class CommentsState extends Equatable {
|
|||||||
Item? item,
|
Item? item,
|
||||||
List<Comment>? comments,
|
List<Comment>? comments,
|
||||||
List<int>? matchedComments,
|
List<int>? matchedComments,
|
||||||
|
Map<int, Comment>? idToCommentMap,
|
||||||
CommentsStatus? status,
|
CommentsStatus? status,
|
||||||
CommentsStatus? fetchParentStatus,
|
CommentsStatus? fetchParentStatus,
|
||||||
CommentsStatus? fetchRootStatus,
|
CommentsStatus? fetchRootStatus,
|
||||||
@ -66,6 +73,7 @@ class CommentsState extends Equatable {
|
|||||||
bool? isOfflineReading,
|
bool? isOfflineReading,
|
||||||
int? currentPage,
|
int? currentPage,
|
||||||
String? inThreadSearchQuery,
|
String? inThreadSearchQuery,
|
||||||
|
String? inThreadSearchAuthor,
|
||||||
}) {
|
}) {
|
||||||
return CommentsState(
|
return CommentsState(
|
||||||
item: item ?? this.item,
|
item: item ?? this.item,
|
||||||
@ -81,11 +89,40 @@ class CommentsState extends Equatable {
|
|||||||
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||||
currentPage: currentPage ?? this.currentPage,
|
currentPage: currentPage ?? this.currentPage,
|
||||||
inThreadSearchQuery: inThreadSearchQuery ?? this.inThreadSearchQuery,
|
inThreadSearchQuery: inThreadSearchQuery ?? this.inThreadSearchQuery,
|
||||||
|
inThreadSearchAuthor: inThreadSearchAuthor ?? this.inThreadSearchAuthor,
|
||||||
|
idToCommentMap: idToCommentMap ?? this.idToCommentMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
|
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
|
||||||
|
|
||||||
|
static final Map<int, bool> _isResponseCache = <int, bool>{};
|
||||||
|
|
||||||
|
bool isResponse(Comment comment) {
|
||||||
|
if (_isResponseCache.containsKey(comment.id)) {
|
||||||
|
return _isResponseCache[comment.id]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.isRoot) {
|
||||||
|
_isResponseCache[comment.id] = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Comment? precedingComment = idToCommentMap[comment.parent];
|
||||||
|
if (precedingComment == null) {
|
||||||
|
_isResponseCache[comment.id] = false;
|
||||||
|
return false;
|
||||||
|
} else if (item.id == precedingComment.parent && item.by == comment.by) {
|
||||||
|
_isResponseCache[comment.id] = true;
|
||||||
|
return true;
|
||||||
|
} else if (idToCommentMap[precedingComment.parent]?.by == comment.by) {
|
||||||
|
_isResponseCache[comment.id] = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
_isResponseCache[comment.id] = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
item,
|
item,
|
||||||
@ -100,5 +137,7 @@ class CommentsState extends Equatable {
|
|||||||
comments,
|
comments,
|
||||||
matchedComments,
|
matchedComments,
|
||||||
inThreadSearchQuery,
|
inThreadSearchQuery,
|
||||||
|
inThreadSearchAuthor,
|
||||||
|
idToCommentMap,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,13 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
required AuthBloc authBloc,
|
required AuthBloc authBloc,
|
||||||
AuthRepository? authRepository,
|
AuthRepository? authRepository,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
}) : _authBloc = authBloc,
|
}) : _authBloc = authBloc,
|
||||||
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||||
_preferenceRepository =
|
_preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
super(FavState.init()) {
|
super(FavState.init()) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
final AuthBloc _authBloc;
|
final AuthBloc _authBloc;
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
String? _username;
|
String? _username;
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchItemsStream(
|
.fetchItemsStream(
|
||||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||||
)
|
)
|
||||||
@ -73,7 +73,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Item? item = await _storiesRepository.fetchItem(id: id);
|
final Item? item = await _hackerNewsRepository.fetchItem(id: id);
|
||||||
|
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
upper = len;
|
upper = len;
|
||||||
}
|
}
|
||||||
|
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchItemsStream(
|
.fetchItemsStream(
|
||||||
ids: state.favIds.sublist(
|
ids: state.favIds.sublist(
|
||||||
lower,
|
lower,
|
||||||
@ -149,7 +149,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
|
|
||||||
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
||||||
emit(state.copyWith(favIds: favIds));
|
emit(state.copyWith(favIds: favIds));
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchItemsStream(
|
.fetchItemsStream(
|
||||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||||
)
|
)
|
||||||
|
@ -10,16 +10,16 @@ part 'history_state.dart';
|
|||||||
class HistoryCubit extends Cubit<HistoryState> {
|
class HistoryCubit extends Cubit<HistoryState> {
|
||||||
HistoryCubit({
|
HistoryCubit({
|
||||||
required AuthBloc authBloc,
|
required AuthBloc authBloc,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
}) : _authBloc = authBloc,
|
}) : _authBloc = authBloc,
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
super(HistoryState.init()) {
|
super(HistoryState.init()) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final AuthBloc _authBloc;
|
final AuthBloc _authBloc;
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
@ -27,7 +27,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
|||||||
if (authState.isLoggedIn) {
|
if (authState.isLoggedIn) {
|
||||||
final String username = authState.username;
|
final String username = authState.username;
|
||||||
|
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchSubmitted(userId: username)
|
.fetchSubmitted(userId: username)
|
||||||
.then((List<int>? submittedIds) {
|
.then((List<int>? submittedIds) {
|
||||||
emit(
|
emit(
|
||||||
@ -38,7 +38,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (submittedIds != null) {
|
if (submittedIds != null) {
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchItemsStream(
|
.fetchItemsStream(
|
||||||
ids: submittedIds.sublist(
|
ids: submittedIds.sublist(
|
||||||
0,
|
0,
|
||||||
@ -66,7 +66,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
|||||||
upper = len;
|
upper = len;
|
||||||
}
|
}
|
||||||
|
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchItemsStream(
|
.fetchItemsStream(
|
||||||
ids: state.submittedIds.sublist(
|
ids: state.submittedIds.sublist(
|
||||||
lower,
|
lower,
|
||||||
@ -93,12 +93,12 @@ class HistoryCubit extends Cubit<HistoryState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchSubmitted(userId: username)
|
.fetchSubmitted(userId: username)
|
||||||
.then((List<int>? submittedIds) {
|
.then((List<int>? submittedIds) {
|
||||||
emit(state.copyWith(submittedIds: submittedIds));
|
emit(state.copyWith(submittedIds: submittedIds));
|
||||||
if (submittedIds != null) {
|
if (submittedIds != null) {
|
||||||
_storiesRepository
|
_hackerNewsRepository
|
||||||
.fetchItemsStream(
|
.fetchItemsStream(
|
||||||
ids: submittedIds.sublist(
|
ids: submittedIds.sublist(
|
||||||
0,
|
0,
|
||||||
|
@ -16,13 +16,13 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
NotificationCubit({
|
NotificationCubit({
|
||||||
required AuthBloc authBloc,
|
required AuthBloc authBloc,
|
||||||
required PreferenceCubit preferenceCubit,
|
required PreferenceCubit preferenceCubit,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
SembastRepository? sembastRepository,
|
SembastRepository? sembastRepository,
|
||||||
}) : _authBloc = authBloc,
|
}) : _authBloc = authBloc,
|
||||||
_preferenceCubit = preferenceCubit,
|
_preferenceCubit = preferenceCubit,
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
_preferenceRepository =
|
_preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
@ -54,7 +54,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
|
|
||||||
final AuthBloc _authBloc;
|
final AuthBloc _authBloc;
|
||||||
final PreferenceCubit _preferenceCubit;
|
final PreferenceCubit _preferenceCubit;
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final SembastRepository _sembastRepository;
|
final SembastRepository _sembastRepository;
|
||||||
String? _username;
|
String? _username;
|
||||||
@ -82,7 +82,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
|
|
||||||
for (final int id in commentsToBeLoaded) {
|
for (final int id in commentsToBeLoaded) {
|
||||||
Comment? comment = await _sembastRepository.getComment(id: id);
|
Comment? comment = await _sembastRepository.getComment(id: id);
|
||||||
comment ??= await _storiesRepository.fetchComment(id: id);
|
comment ??= await _hackerNewsRepository.fetchComment(id: id);
|
||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -160,7 +160,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
|
|
||||||
for (final int id in commentsToBeLoaded) {
|
for (final int id in commentsToBeLoaded) {
|
||||||
Comment? comment = await _sembastRepository.getComment(id: id);
|
Comment? comment = await _sembastRepository.getComment(id: id);
|
||||||
comment ??= await _storiesRepository.fetchComment(id: id);
|
comment ??= await _hackerNewsRepository.fetchComment(id: id);
|
||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
emit(state.copyWith(comments: <Comment>[...state.comments, comment]));
|
emit(state.copyWith(comments: <Comment>[...state.comments, comment]));
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchReplies() {
|
Future<void> _fetchReplies() {
|
||||||
return _storiesRepository
|
return _hackerNewsRepository
|
||||||
.fetchSubmitted(userId: _authBloc.state.username)
|
.fetchSubmitted(userId: _authBloc.state.username)
|
||||||
.then((List<int>? submittedItems) async {
|
.then((List<int>? submittedItems) async {
|
||||||
if (submittedItems != null) {
|
if (submittedItems != null) {
|
||||||
@ -194,7 +194,9 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (final int id in subscribedItems) {
|
for (final int id in subscribedItems) {
|
||||||
await _storiesRepository.fetchItem(id: id).then((Item? item) async {
|
await _hackerNewsRepository
|
||||||
|
.fetchItem(id: id)
|
||||||
|
.then((Item? item) async {
|
||||||
final List<int> kids = item?.kids ?? <int>[];
|
final List<int> kids = item?.kids ?? <int>[];
|
||||||
final List<int> previousKids =
|
final List<int> previousKids =
|
||||||
(await _sembastRepository.kids(of: id)) ?? <int>[];
|
(await _sembastRepository.kids(of: id)) ?? <int>[];
|
||||||
@ -216,7 +218,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
...state.unreadCommentsIds,
|
...state.unreadCommentsIds,
|
||||||
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
|
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
|
||||||
);
|
);
|
||||||
await _storiesRepository
|
await _hackerNewsRepository
|
||||||
.fetchComment(id: newCommentId)
|
.fetchComment(id: newCommentId)
|
||||||
.then((Comment? comment) {
|
.then((Comment? comment) {
|
||||||
if (comment != null && !comment.dead && !comment.deleted) {
|
if (comment != null && !comment.dead && !comment.deleted) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
@ -9,28 +10,33 @@ part 'pin_state.dart';
|
|||||||
class PinCubit extends Cubit<PinState> {
|
class PinCubit extends Cubit<PinState> {
|
||||||
PinCubit({
|
PinCubit({
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
}) : _preferenceRepository =
|
}) : _preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
super(PinState.init()) {
|
super(PinState.init()) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
emit(PinState.init());
|
emit(PinState.init());
|
||||||
_preferenceRepository.pinnedStoriesIds.then((List<int> ids) {
|
_preferenceRepository.pinnedStoriesIds.then((List<int> ids) {
|
||||||
emit(state.copyWith(pinnedStoriesIds: ids));
|
emit(state.copyWith(pinnedStoriesIds: ids));
|
||||||
|
|
||||||
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
|
_hackerNewsRepository
|
||||||
|
.fetchStoriesStream(ids: ids)
|
||||||
|
.listen(_onStoryFetched);
|
||||||
}).whenComplete(() => emit(state.copyWith(status: Status.success)));
|
}).whenComplete(() => emit(state.copyWith(status: Status.success)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void pinStory(Story story) {
|
void pinStory(
|
||||||
|
Story story, {
|
||||||
|
VoidCallback? onDone,
|
||||||
|
}) {
|
||||||
if (!state.pinnedStoriesIds.contains(story.id)) {
|
if (!state.pinnedStoriesIds.contains(story.id)) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -39,10 +45,14 @@ class PinCubit extends Cubit<PinState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
||||||
|
onDone?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void unpinStory(Story story) {
|
void unpinStory(
|
||||||
|
Story story, {
|
||||||
|
VoidCallback? onDone,
|
||||||
|
}) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
pinnedStoriesIds: <int>[...state.pinnedStoriesIds]..remove(story.id),
|
pinnedStoriesIds: <int>[...state.pinnedStoriesIds]..remove(story.id),
|
||||||
@ -50,6 +60,7 @@ class PinCubit extends Cubit<PinState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
||||||
|
onDone?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh() {
|
void refresh() {
|
||||||
|
@ -11,13 +11,13 @@ part 'poll_state.dart';
|
|||||||
class PollCubit extends Cubit<PollState> {
|
class PollCubit extends Cubit<PollState> {
|
||||||
PollCubit({
|
PollCubit({
|
||||||
required Story story,
|
required Story story,
|
||||||
StoriesRepository? storiesRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
}) : _story = story,
|
}) : _story = story,
|
||||||
_storiesRepository =
|
_hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
super(PollState.init());
|
super(PollState.init());
|
||||||
|
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
final Story _story;
|
final Story _story;
|
||||||
|
|
||||||
Future<void> init({
|
Future<void> init({
|
||||||
@ -33,7 +33,7 @@ class PollCubit extends Cubit<PollState> {
|
|||||||
|
|
||||||
if (pollOptionsIds.isEmpty || refresh) {
|
if (pollOptionsIds.isEmpty || refresh) {
|
||||||
final Story? updatedStory =
|
final Story? updatedStory =
|
||||||
await _storiesRepository.fetchStory(id: _story.id);
|
await _hackerNewsRepository.fetchStory(id: _story.id);
|
||||||
|
|
||||||
if (updatedStory != null) {
|
if (updatedStory != null) {
|
||||||
pollOptionsIds = updatedStory.parts;
|
pollOptionsIds = updatedStory.parts;
|
||||||
@ -47,7 +47,7 @@ class PollCubit extends Cubit<PollState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pollOptionsIds.isNotEmpty) {
|
if (pollOptionsIds.isNotEmpty) {
|
||||||
final List<PollOption> pollOptions = (await _storiesRepository
|
final List<PollOption> pollOptions = (await _hackerNewsRepository
|
||||||
.fetchPollOptionsStream(ids: pollOptionsIds)
|
.fetchPollOptionsStream(ids: pollOptionsIds)
|
||||||
.toSet())
|
.toSet())
|
||||||
.toList();
|
.toList();
|
||||||
|
@ -72,7 +72,11 @@ class PreferenceState extends Equatable {
|
|||||||
|
|
||||||
bool get material3Enabled => _isOn<Material3Preference>();
|
bool get material3Enabled => _isOn<Material3Preference>();
|
||||||
|
|
||||||
bool get paginationEnabled => _isOn<PaginationPreference>();
|
bool get manualPaginationEnabled => _isOn<ManualPaginationPreference>();
|
||||||
|
|
||||||
|
bool get trueDarkModeEnabled => _isOn<TrueDarkModePreference>();
|
||||||
|
|
||||||
|
bool get hapticFeedbackEnabled => _isOn<HapticFeedbackPreference>();
|
||||||
|
|
||||||
double get textScaleFactor =>
|
double get textScaleFactor =>
|
||||||
preferences.singleWhereType<TextScaleFactorPreference>().val;
|
preferences.singleWhereType<TextScaleFactorPreference>().val;
|
||||||
|
@ -102,6 +102,18 @@ class SearchCubit extends Cubit<SearchState> {
|
|||||||
search(state.params.query);
|
search(state.params.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onExactMatchToggled() {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
params: state.params.copyWith(
|
||||||
|
exactMatch: !state.params.exactMatch,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
search(state.params.query);
|
||||||
|
}
|
||||||
|
|
||||||
void onDateTimeRangeUpdated(DateTime start, DateTime end) {
|
void onDateTimeRangeUpdated(DateTime start, DateTime end) {
|
||||||
final DateTime updatedStart = start.copyWith(
|
final DateTime updatedStart = start.copyWith(
|
||||||
second: 0,
|
second: 0,
|
||||||
|
@ -7,16 +7,16 @@ import 'package:hacki/repositories/repositories.dart';
|
|||||||
part 'user_state.dart';
|
part 'user_state.dart';
|
||||||
|
|
||||||
class UserCubit extends Cubit<UserState> {
|
class UserCubit extends Cubit<UserState> {
|
||||||
UserCubit({StoriesRepository? storiesRepository})
|
UserCubit({HackerNewsRepository? hackerNewsRepository})
|
||||||
: _storiesRepository =
|
: _hackerNewsRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||||
super(const UserState.init());
|
super(const UserState.init());
|
||||||
|
|
||||||
final StoriesRepository _storiesRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
|
|
||||||
void init({required String userId}) {
|
void init({required String userId}) {
|
||||||
emit(state.copyWith(status: Status.inProgress));
|
emit(state.copyWith(status: Status.inProgress));
|
||||||
_storiesRepository.fetchUser(id: userId).then((User? user) {
|
_hackerNewsRepository.fetchUser(id: userId).then((User? user) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
user: user ?? User.emptyWithId(userId),
|
user: user ?? User.emptyWithId(userId),
|
||||||
|
@ -19,6 +19,7 @@ import 'package:hacki/config/locator.dart';
|
|||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/services/fetcher.dart';
|
import 'package:hacki/services/fetcher.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
import 'package:hacki/utils/haptic_feedback_util.dart';
|
||||||
import 'package:hacki/utils/theme_util.dart';
|
import 'package:hacki/utils/theme_util.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
@ -229,16 +230,22 @@ class HackiApp extends StatelessWidget {
|
|||||||
)..init(),
|
)..init(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<PreferenceCubit, PreferenceState>(
|
child: BlocConsumer<PreferenceCubit, PreferenceState>(
|
||||||
|
listenWhen: (PreferenceState previous, PreferenceState current) =>
|
||||||
|
previous.hapticFeedbackEnabled != current.hapticFeedbackEnabled,
|
||||||
|
listener: (_, PreferenceState state) {
|
||||||
|
HapticFeedbackUtil.enabled = state.hapticFeedbackEnabled;
|
||||||
|
},
|
||||||
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
||||||
previous.appColor != current.appColor ||
|
previous.appColor != current.appColor ||
|
||||||
previous.font != current.font ||
|
previous.font != current.font ||
|
||||||
previous.textScaleFactor != current.textScaleFactor ||
|
previous.textScaleFactor != current.textScaleFactor ||
|
||||||
previous.material3Enabled != current.material3Enabled,
|
previous.material3Enabled != current.material3Enabled ||
|
||||||
|
previous.trueDarkModeEnabled != current.trueDarkModeEnabled,
|
||||||
builder: (BuildContext context, PreferenceState state) {
|
builder: (BuildContext context, PreferenceState state) {
|
||||||
return AdaptiveTheme(
|
return AdaptiveTheme(
|
||||||
key: ValueKey<String>(
|
key: ValueKey<String>(
|
||||||
'''${state.appColor}${state.font}${state.material3Enabled}''',
|
'''${state.appColor}${state.font}${state.material3Enabled}${state.trueDarkModeEnabled}''',
|
||||||
),
|
),
|
||||||
light: ThemeData(
|
light: ThemeData(
|
||||||
primaryColor: state.appColor,
|
primaryColor: state.appColor,
|
||||||
@ -254,7 +261,7 @@ class HackiApp extends StatelessWidget {
|
|||||||
primarySwatch: state.appColor,
|
primarySwatch: state.appColor,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
),
|
),
|
||||||
canvasColor: Palette.black,
|
canvasColor: state.trueDarkModeEnabled ? Palette.black : null,
|
||||||
fontFamily: state.font.name,
|
fontFamily: state.font.name,
|
||||||
),
|
),
|
||||||
initial: savedThemeMode ?? AdaptiveThemeMode.system,
|
initial: savedThemeMode ?? AdaptiveThemeMode.system,
|
||||||
|
@ -17,6 +17,7 @@ class BuildableComment extends Comment with Buildable {
|
|||||||
required super.deleted,
|
required super.deleted,
|
||||||
required super.hidden,
|
required super.hidden,
|
||||||
required super.level,
|
required super.level,
|
||||||
|
required super.isFromCache,
|
||||||
required this.elements,
|
required this.elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ class BuildableComment extends Comment with Buildable {
|
|||||||
deleted: comment.deleted,
|
deleted: comment.deleted,
|
||||||
level: comment.level,
|
level: comment.level,
|
||||||
hidden: comment.hidden,
|
hidden: comment.hidden,
|
||||||
|
isFromCache: comment.isFromCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -53,6 +55,7 @@ class BuildableComment extends Comment with Buildable {
|
|||||||
hidden: hidden ?? this.hidden,
|
hidden: hidden ?? this.hidden,
|
||||||
level: level ?? this.level,
|
level: level ?? this.level,
|
||||||
elements: elements,
|
elements: elements,
|
||||||
|
isFromCache: isFromCache,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ class Comment extends Item {
|
|||||||
required super.deleted,
|
required super.deleted,
|
||||||
required super.hidden,
|
required super.hidden,
|
||||||
required this.level,
|
required this.level,
|
||||||
|
required this.isFromCache,
|
||||||
}) : super(
|
}) : super(
|
||||||
descendants: 0,
|
descendants: 0,
|
||||||
parts: <int>[],
|
parts: <int>[],
|
||||||
@ -21,9 +22,12 @@ class Comment extends Item {
|
|||||||
type: '',
|
type: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
Comment.fromJson(super.json, {this.level = 0}) : super.fromJson();
|
Comment.fromJson(super.json, {this.level = 0})
|
||||||
|
: isFromCache = json['fromCache'] == true,
|
||||||
|
super.fromJson();
|
||||||
|
|
||||||
final int level;
|
final int level;
|
||||||
|
final bool isFromCache;
|
||||||
|
|
||||||
String get metadata => '''by $by $timeAgo''';
|
String get metadata => '''by $by $timeAgo''';
|
||||||
|
|
||||||
@ -45,6 +49,7 @@ class Comment extends Item {
|
|||||||
deleted: deleted,
|
deleted: deleted,
|
||||||
hidden: hidden ?? this.hidden,
|
hidden: hidden ?? this.hidden,
|
||||||
level: level ?? this.level,
|
level: level ?? this.level,
|
||||||
|
isFromCache: isFromCache,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,10 +41,12 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
|||||||
const CollapseModePreference(),
|
const CollapseModePreference(),
|
||||||
const ReaderModePreference(),
|
const ReaderModePreference(),
|
||||||
const CustomTabPreference(),
|
const CustomTabPreference(),
|
||||||
const EyeCandyModePreference(),
|
const ManualPaginationPreference(),
|
||||||
const Material3Preference(),
|
|
||||||
const PaginationPreference(),
|
|
||||||
const SwipeGesturePreference(),
|
const SwipeGesturePreference(),
|
||||||
|
const HapticFeedbackPreference(),
|
||||||
|
const EyeCandyModePreference(),
|
||||||
|
const TrueDarkModePreference(),
|
||||||
|
const Material3Preference(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -68,6 +70,8 @@ const bool _notificationModeDefaultValue = true;
|
|||||||
const bool _swipeGestureModeDefaultValue = false;
|
const bool _swipeGestureModeDefaultValue = false;
|
||||||
const bool _displayModeDefaultValue = true;
|
const bool _displayModeDefaultValue = true;
|
||||||
const bool _eyeCandyModeDefaultValue = false;
|
const bool _eyeCandyModeDefaultValue = false;
|
||||||
|
const bool _trueDarkModeDefaultValue = false;
|
||||||
|
const bool _hapticFeedbackModeDefaultValue = true;
|
||||||
const bool _readerModeDefaultValue = true;
|
const bool _readerModeDefaultValue = true;
|
||||||
const bool _markReadStoriesModeDefaultValue = true;
|
const bool _markReadStoriesModeDefaultValue = true;
|
||||||
const bool _metadataModeDefaultValue = true;
|
const bool _metadataModeDefaultValue = true;
|
||||||
@ -101,7 +105,7 @@ class SwipeGesturePreference extends BooleanPreference {
|
|||||||
String get key => 'swipeGestureMode';
|
String get key => 'swipeGestureMode';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'Enable Swipe Gesture';
|
String get title => 'Swipe Gesture';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subtitle =>
|
String get subtitle =>
|
||||||
@ -289,20 +293,20 @@ class EyeCandyModePreference extends BooleanPreference {
|
|||||||
String get subtitle => 'some sort of magic.';
|
String get subtitle => 'some sort of magic.';
|
||||||
}
|
}
|
||||||
|
|
||||||
class PaginationPreference extends BooleanPreference {
|
class ManualPaginationPreference extends BooleanPreference {
|
||||||
const PaginationPreference({bool? val})
|
const ManualPaginationPreference({bool? val})
|
||||||
: super(val: val ?? _paginationModeDefaultValue);
|
: super(val: val ?? _paginationModeDefaultValue);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PaginationPreference copyWith({required bool? val}) {
|
ManualPaginationPreference copyWith({required bool? val}) {
|
||||||
return PaginationPreference(val: val);
|
return ManualPaginationPreference(val: val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get key => 'paginationMode';
|
String get key => 'paginationMode';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'Enable Pagination';
|
String get title => 'Manual Pagination';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subtitle => '''so you can get stuff done.''';
|
String get subtitle => '''so you can get stuff done.''';
|
||||||
@ -321,7 +325,7 @@ class Material3Preference extends BooleanPreference {
|
|||||||
String get key => 'material3Mode';
|
String get key => 'material3Mode';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'Enable Material 3';
|
String get title => 'Material 3';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subtitle =>
|
String get subtitle =>
|
||||||
@ -355,6 +359,47 @@ class CustomTabPreference extends BooleanPreference {
|
|||||||
bool get isDisplayable => Platform.isAndroid;
|
bool get isDisplayable => Platform.isAndroid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TrueDarkModePreference extends BooleanPreference {
|
||||||
|
const TrueDarkModePreference({bool? val})
|
||||||
|
: super(val: val ?? _trueDarkModeDefaultValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TrueDarkModePreference copyWith({required bool? val}) {
|
||||||
|
return TrueDarkModePreference(val: val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => 'trueDarkMode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'True Dark Mode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subtitle => 'real dark.';
|
||||||
|
}
|
||||||
|
|
||||||
|
class HapticFeedbackPreference extends BooleanPreference {
|
||||||
|
const HapticFeedbackPreference({bool? val})
|
||||||
|
: super(val: val ?? _hapticFeedbackModeDefaultValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
HapticFeedbackPreference copyWith({required bool? val}) {
|
||||||
|
return HapticFeedbackPreference(val: val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => 'hapticFeedbackMode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'Haptic Feedback';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subtitle => '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isDisplayable => Platform.isIOS;
|
||||||
|
}
|
||||||
|
|
||||||
class FetchModePreference extends IntPreference {
|
class FetchModePreference extends IntPreference {
|
||||||
FetchModePreference({int? val}) : super(val: val ?? _fetchModeDefaultValue);
|
FetchModePreference({int? val}) : super(val: val ?? _fetchModeDefaultValue);
|
||||||
|
|
||||||
@ -444,7 +489,7 @@ class StoryMarkingModePreference extends IntPreference {
|
|||||||
String get key => 'storyMarkingMode';
|
String get key => 'storyMarkingMode';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'Mark a Story as Read on';
|
String get title => 'Mark as Read on';
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppColorPreference extends IntPreference {
|
class AppColorPreference extends IntPreference {
|
||||||
|
@ -8,31 +8,36 @@ class SearchParams extends Equatable {
|
|||||||
required this.filters,
|
required this.filters,
|
||||||
required this.query,
|
required this.query,
|
||||||
required this.page,
|
required this.page,
|
||||||
this.sorted = false,
|
required this.sorted,
|
||||||
|
required this.exactMatch,
|
||||||
});
|
});
|
||||||
|
|
||||||
SearchParams.init()
|
SearchParams.init()
|
||||||
: filters = <SearchFilter>{},
|
: filters = <SearchFilter>{},
|
||||||
query = '',
|
query = '',
|
||||||
page = 0,
|
page = 0,
|
||||||
sorted = false;
|
sorted = false,
|
||||||
|
exactMatch = false;
|
||||||
|
|
||||||
final Set<SearchFilter> filters;
|
final Set<SearchFilter> filters;
|
||||||
final String query;
|
final String query;
|
||||||
final int page;
|
final int page;
|
||||||
final bool sorted;
|
final bool sorted;
|
||||||
|
final bool exactMatch;
|
||||||
|
|
||||||
SearchParams copyWith({
|
SearchParams copyWith({
|
||||||
Set<SearchFilter>? filters,
|
Set<SearchFilter>? filters,
|
||||||
String? query,
|
String? query,
|
||||||
int? page,
|
int? page,
|
||||||
bool? sorted,
|
bool? sorted,
|
||||||
|
bool? exactMatch,
|
||||||
}) {
|
}) {
|
||||||
return SearchParams(
|
return SearchParams(
|
||||||
filters: filters ?? this.filters,
|
filters: filters ?? this.filters,
|
||||||
query: query ?? this.query,
|
query: query ?? this.query,
|
||||||
page: page ?? this.page,
|
page: page ?? this.page,
|
||||||
sorted: sorted ?? this.sorted,
|
sorted: sorted ?? this.sorted,
|
||||||
|
exactMatch: exactMatch ?? this.exactMatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +48,7 @@ class SearchParams extends Equatable {
|
|||||||
query: query,
|
query: query,
|
||||||
page: page,
|
page: page,
|
||||||
sorted: sorted,
|
sorted: sorted,
|
||||||
|
exactMatch: exactMatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,16 +60,19 @@ class SearchParams extends Equatable {
|
|||||||
query: query,
|
query: query,
|
||||||
page: page,
|
page: page,
|
||||||
sorted: sorted,
|
sorted: sorted,
|
||||||
|
exactMatch: exactMatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get filteredQuery {
|
String get filteredQuery {
|
||||||
final StringBuffer buffer = StringBuffer();
|
final StringBuffer buffer = StringBuffer();
|
||||||
|
final String encodedQuery =
|
||||||
|
Uri.encodeComponent(exactMatch ? '"$query"' : query);
|
||||||
|
|
||||||
if (sorted) {
|
if (sorted) {
|
||||||
buffer.write('search_by_date?query=${Uri.encodeComponent(query)}');
|
buffer.write('search_by_date?query=$encodedQuery');
|
||||||
} else {
|
} else {
|
||||||
buffer.write('search?query=${Uri.encodeComponent(query)}');
|
buffer.write('search?query=$encodedQuery');
|
||||||
}
|
}
|
||||||
|
|
||||||
final Iterable<NumericFilter> numericFilters =
|
final Iterable<NumericFilter> numericFilters =
|
||||||
@ -111,5 +120,6 @@ class SearchParams extends Equatable {
|
|||||||
query,
|
query,
|
||||||
page,
|
page,
|
||||||
sorted,
|
sorted,
|
||||||
|
exactMatch,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@ enum StoryMarkingMode {
|
|||||||
tap('tapping'),
|
tap('tapping'),
|
||||||
// Mark a story as read after user scrolls past or taps on it, whichever
|
// Mark a story as read after user scrolls past or taps on it, whichever
|
||||||
// happens the first.
|
// happens the first.
|
||||||
scrollPastOrTap('scrolling past or tapping');
|
scrollPastOrTap('scrolling past or tapping'),
|
||||||
|
swipeGestureOnly('swipe gesture only');
|
||||||
|
|
||||||
const StoryMarkingMode(this.label);
|
const StoryMarkingMode(this.label);
|
||||||
|
|
||||||
|
@ -1,19 +1,31 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
/// [StoriesRepository] is for fetching
|
/// [HackerNewsRepository] is for fetching
|
||||||
/// [Item] such as [Story], [PollOption], [Comment] or [User].
|
/// [Item] such as [Story], [PollOption], [Comment] or [User].
|
||||||
///
|
///
|
||||||
/// You can learn more about the Hacker News API at
|
/// You can learn more about the Hacker News API at
|
||||||
/// https://github.com/HackerNews/API.
|
/// https://github.com/HackerNews/API.
|
||||||
class StoriesRepository {
|
class HackerNewsRepository {
|
||||||
StoriesRepository({
|
HackerNewsRepository({
|
||||||
FirebaseClient? firebaseClient,
|
FirebaseClient? firebaseClient,
|
||||||
}) : _firebaseClient = firebaseClient ?? FirebaseClient.anonymous();
|
SembastRepository? sembastRepository,
|
||||||
|
Logger? logger,
|
||||||
|
}) : _firebaseClient = firebaseClient ?? FirebaseClient.anonymous(),
|
||||||
|
_sembastRepository =
|
||||||
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
|
_logger = logger ?? locator.get<Logger>();
|
||||||
|
|
||||||
final FirebaseClient _firebaseClient;
|
final FirebaseClient _firebaseClient;
|
||||||
|
final SembastRepository _sembastRepository;
|
||||||
|
final Logger _logger;
|
||||||
static const String _baseUrl = 'https://hacker-news.firebaseio.com/v0/';
|
static const String _baseUrl = 'https://hacker-news.firebaseio.com/v0/';
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> _fetchItemJson(int id) async {
|
Future<Map<String, dynamic>?> _fetchItemJson(int id) async {
|
||||||
@ -34,11 +46,10 @@ class StoriesRepository {
|
|||||||
await _fetchItemJson(id).then((Map<String, dynamic>? json) {
|
await _fetchItemJson(id).then((Map<String, dynamic>? json) {
|
||||||
if (json == null) return null;
|
if (json == null) return null;
|
||||||
|
|
||||||
final String type = json['type'] as String;
|
if (json.isStory) {
|
||||||
if (type == 'story' || type == 'job' || type == 'poll') {
|
|
||||||
final Story story = Story.fromJson(json);
|
final Story story = Story.fromJson(json);
|
||||||
return story;
|
return story;
|
||||||
} else if (type == 'comment') {
|
} else if (json.isComment) {
|
||||||
final Comment comment = Comment.fromJson(json);
|
final Comment comment = Comment.fromJson(json);
|
||||||
return comment;
|
return comment;
|
||||||
}
|
}
|
||||||
@ -57,11 +68,10 @@ class StoriesRepository {
|
|||||||
|
|
||||||
final Map<String, dynamic> json = val as Map<String, dynamic>;
|
final Map<String, dynamic> json = val as Map<String, dynamic>;
|
||||||
|
|
||||||
final String type = json['type'] as String;
|
if (json.isStory) {
|
||||||
if (type == 'story' || type == 'job' || type == 'poll') {
|
|
||||||
final Story story = Story.fromJson(json);
|
final Story story = Story.fromJson(json);
|
||||||
return story;
|
return story;
|
||||||
} else if (type == 'comment') {
|
} else if (json.isComment) {
|
||||||
final Comment comment = Comment.fromJson(json);
|
final Comment comment = Comment.fromJson(json);
|
||||||
return comment;
|
return comment;
|
||||||
}
|
}
|
||||||
@ -226,7 +236,17 @@ class StoriesRepository {
|
|||||||
if (json == null) return null;
|
if (json == null) return null;
|
||||||
|
|
||||||
final Comment comment = Comment.fromJson(json, level: level);
|
final Comment comment = Comment.fromJson(json, level: level);
|
||||||
|
|
||||||
|
if (!json.isFromCache) {
|
||||||
|
unawaited(_sembastRepository.cacheComment(comment));
|
||||||
|
}
|
||||||
|
|
||||||
return comment;
|
return comment;
|
||||||
|
}).onError((Object? error, StackTrace stackTrace) {
|
||||||
|
_logger.e(error, stackTrace: stackTrace);
|
||||||
|
return _sembastRepository
|
||||||
|
.getCachedComment(id: id)
|
||||||
|
.then((Comment? value) => value?.copyWith(level: level));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
@ -251,7 +271,17 @@ class StoriesRepository {
|
|||||||
if (json == null) return null;
|
if (json == null) return null;
|
||||||
|
|
||||||
final Comment comment = Comment.fromJson(json, level: level);
|
final Comment comment = Comment.fromJson(json, level: level);
|
||||||
|
|
||||||
|
if (!json.isFromCache) {
|
||||||
|
unawaited(_sembastRepository.cacheComment(comment));
|
||||||
|
}
|
||||||
|
|
||||||
return comment;
|
return comment;
|
||||||
|
}).onError((Object? error, StackTrace stackTrace) {
|
||||||
|
_logger.e(error, stackTrace: stackTrace);
|
||||||
|
return _sembastRepository
|
||||||
|
.getCachedComment(id: id)
|
||||||
|
.then((Comment? value) => value?.copyWith(level: level));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
@ -275,11 +305,10 @@ class StoriesRepository {
|
|||||||
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
|
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
|
||||||
if (json == null) return null;
|
if (json == null) return null;
|
||||||
|
|
||||||
final String type = json['type'] as String;
|
if (json.isStory) {
|
||||||
if (type == 'story' || type == 'job') {
|
|
||||||
final Story story = Story.fromJson(json);
|
final Story story = Story.fromJson(json);
|
||||||
return story;
|
return story;
|
||||||
} else if (type == 'comment') {
|
} else if (json.isComment) {
|
||||||
final Comment comment = Comment.fromJson(json);
|
final Comment comment = Comment.fromJson(json);
|
||||||
return comment;
|
return comment;
|
||||||
}
|
}
|
||||||
@ -343,12 +372,57 @@ class StoriesRepository {
|
|||||||
Map<String, dynamic>? json,
|
Map<String, dynamic>? json,
|
||||||
) async {
|
) async {
|
||||||
if (json == null) return null;
|
if (json == null) return null;
|
||||||
final String text = json['text'] as String? ?? '';
|
final int? itemId = json.itemId;
|
||||||
final String parsedText = await compute<String, String>(
|
|
||||||
HtmlUtil.parseHtml,
|
String? cachedText;
|
||||||
text,
|
if (json.isComment && itemId != null) {
|
||||||
);
|
cachedText =
|
||||||
json['text'] = parsedText;
|
(await locator.get<SembastRepository>().getCachedComment(id: itemId))
|
||||||
|
?.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid(String? text) {
|
||||||
|
return cachedText != null &&
|
||||||
|
cachedText != '[delayed]' &&
|
||||||
|
cachedText != '[flagged]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid(cachedText)) {
|
||||||
|
json
|
||||||
|
..text = cachedText
|
||||||
|
..isFromCache = true;
|
||||||
|
} else {
|
||||||
|
final String? text = json.text;
|
||||||
|
if (text == null || text.isEmpty) return json;
|
||||||
|
final String parsedText = await compute<String, String>(
|
||||||
|
HtmlUtil.parseHtml,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
json.text = parsedText;
|
||||||
|
}
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on Map<String, dynamic> {
|
||||||
|
bool get isFromCache => this['fromCache'] == true;
|
||||||
|
|
||||||
|
set isFromCache(bool value) {
|
||||||
|
this['fromCache'] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get text => this['text'] as String?;
|
||||||
|
|
||||||
|
set text(String? value) {
|
||||||
|
this['text'] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? get itemId => this['id'] as int?;
|
||||||
|
|
||||||
|
String? get type => this['type'] as String?;
|
||||||
|
|
||||||
|
bool get isStory => type == 'story' || type == 'job' || type == 'poll';
|
||||||
|
|
||||||
|
bool get isComment => type == 'comment';
|
||||||
|
}
|
@ -384,7 +384,7 @@ class PreferenceRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateHasRead(int storyId) async {
|
Future<void> addHasRead(int storyId) async {
|
||||||
final String key = _getHasReadKey(storyId);
|
final String key = _getHasReadKey(storyId);
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
await _syncedPrefs.setBool(key: key, val: true);
|
await _syncedPrefs.setBool(key: key, val: true);
|
||||||
@ -398,6 +398,17 @@ class PreferenceRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeHasRead(int storyId) async {
|
||||||
|
final String key = _getHasReadKey(storyId);
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
await _syncedPrefs.remove(key: key);
|
||||||
|
} else {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
|
||||||
|
await prefs.remove(_getHasReadKey(storyId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> clearAllReadStories() async {
|
Future<void> clearAllReadStories() async {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
await _syncedPrefs.clearAll();
|
await _syncedPrefs.clearAll();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export 'auth_repository.dart';
|
export 'auth_repository.dart';
|
||||||
|
export 'hacker_news_repository.dart';
|
||||||
export 'offline_repository.dart';
|
export 'offline_repository.dart';
|
||||||
export 'post_repository.dart';
|
export 'post_repository.dart';
|
||||||
export 'preference_repository.dart';
|
export 'preference_repository.dart';
|
||||||
export 'search_repository.dart';
|
export 'search_repository.dart';
|
||||||
export 'sembast_repository.dart';
|
export 'sembast_repository.dart';
|
||||||
export 'stories_repository.dart';
|
|
||||||
|
@ -60,6 +60,7 @@ class SearchRepository {
|
|||||||
deleted: false,
|
deleted: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
level: 0,
|
level: 0,
|
||||||
|
isFromCache: false,
|
||||||
);
|
);
|
||||||
yield comment;
|
yield comment;
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,12 +196,13 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onStoryTapped(Story story) {
|
void onStoryTapped(Story story) {
|
||||||
final bool useReader = context.read<PreferenceCubit>().state.readerEnabled;
|
final PreferenceState prefState = context.read<PreferenceCubit>().state;
|
||||||
|
final bool useReader = prefState.readerEnabled;
|
||||||
|
final StoryMarkingMode storyMarkingMode = prefState.storyMarkingMode;
|
||||||
final bool offlineReading =
|
final bool offlineReading =
|
||||||
context.read<StoriesBloc>().state.isOfflineReading;
|
context.read<StoriesBloc>().state.isOfflineReading;
|
||||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||||
final StoryMarkingMode storyMarkingMode =
|
final bool markReadStoriesEnabled = prefState.markReadStoriesEnabled;
|
||||||
context.read<PreferenceCubit>().state.storyMarkingMode;
|
|
||||||
|
|
||||||
// If a story is a job story and it has a link to the job posting,
|
// If a story is a job story and it has a link to the job posting,
|
||||||
// it would be better to just navigate to the web page.
|
// it would be better to just navigate to the web page.
|
||||||
@ -210,7 +211,12 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
if (isJobWithLink) {
|
if (isJobWithLink) {
|
||||||
context.read<ReminderCubit>().removeLastReadStoryId();
|
context.read<ReminderCubit>().removeLastReadStoryId();
|
||||||
} else {
|
} else {
|
||||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
final bool shouldMarkNewComment = markReadStoriesEnabled &&
|
||||||
|
context.read<StoriesBloc>().state.readStoriesIds.contains(story.id);
|
||||||
|
final ItemScreenArgs args = ItemScreenArgs(
|
||||||
|
item: story,
|
||||||
|
shouldMarkNewComment: shouldMarkNewComment,
|
||||||
|
);
|
||||||
|
|
||||||
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
|
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
|
||||||
|
|
||||||
@ -230,7 +236,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storyMarkingMode.shouldDetectTapping) {
|
if (markReadStoriesEnabled && storyMarkingMode.shouldDetectTapping) {
|
||||||
context.read<StoriesBloc>().add(StoryRead(story: story));
|
context.read<StoriesBloc>().add(StoryRead(story: story));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +259,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
final int? id = event.itemId;
|
final int? id = event.itemId;
|
||||||
|
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
locator.get<StoriesRepository>().fetchItem(id: id).then((Item? item) {
|
locator.get<HackerNewsRepository>().fetchItem(id: id).then((Item? item) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
goToItemScreen(
|
goToItemScreen(
|
||||||
@ -272,7 +278,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
if (storyId == null) return;
|
if (storyId == null) return;
|
||||||
|
|
||||||
await locator
|
await locator
|
||||||
.get<StoriesRepository>()
|
.get<HackerNewsRepository>()
|
||||||
.fetchStory(id: storyId)
|
.fetchStory(id: storyId)
|
||||||
.then((Story? story) {
|
.then((Story? story) {
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
@ -297,7 +303,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
context.read<NotificationCubit>().markAsRead(commentId);
|
context.read<NotificationCubit>().markAsRead(commentId);
|
||||||
|
|
||||||
await locator
|
await locator
|
||||||
.get<StoriesRepository>()
|
.get<HackerNewsRepository>()
|
||||||
.fetchStory(id: storyId)
|
.fetchStory(id: storyId)
|
||||||
.then((Story? story) {
|
.then((Story? story) {
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
|
@ -24,23 +24,26 @@ class ItemScreenArgs extends Equatable {
|
|||||||
const ItemScreenArgs({
|
const ItemScreenArgs({
|
||||||
required this.item,
|
required this.item,
|
||||||
this.onlyShowTargetComment = false,
|
this.onlyShowTargetComment = false,
|
||||||
|
this.shouldMarkNewComment = false,
|
||||||
this.useCommentCache = false,
|
this.useCommentCache = false,
|
||||||
this.targetComments,
|
this.targetComments,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Item item;
|
final Item item;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
|
final bool shouldMarkNewComment;
|
||||||
final List<Comment>? targetComments;
|
final List<Comment>? targetComments;
|
||||||
|
|
||||||
/// when the user is trying to view a sub-thread from a main thread, we don't
|
/// when the user is trying to view a sub-thread from a main thread, we don't
|
||||||
/// need to fetch comments from [StoriesRepository] since we have some, if not
|
/// need to fetch comments from [HackerNewsRepository] since we have some,
|
||||||
/// all, comments cached in [CommentCache].
|
/// if not all, comments cached in [CommentCache].
|
||||||
final bool useCommentCache;
|
final bool useCommentCache;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
item,
|
item,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
|
shouldMarkNewComment,
|
||||||
targetComments,
|
targetComments,
|
||||||
useCommentCache,
|
useCommentCache,
|
||||||
];
|
];
|
||||||
@ -52,6 +55,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
required this.parentComments,
|
required this.parentComments,
|
||||||
super.key,
|
super.key,
|
||||||
this.splitViewEnabled = false,
|
this.splitViewEnabled = false,
|
||||||
|
this.shouldMarkNewComment = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const String routeName = 'item';
|
static const String routeName = 'item';
|
||||||
@ -81,6 +85,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
child: ItemScreen(
|
child: ItemScreen(
|
||||||
item: args.item,
|
item: args.item,
|
||||||
parentComments: args.targetComments ?? <Comment>[],
|
parentComments: args.targetComments ?? <Comment>[],
|
||||||
|
shouldMarkNewComment: args.shouldMarkNewComment,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -123,6 +128,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
item: args.item,
|
item: args.item,
|
||||||
parentComments: args.targetComments ?? <Comment>[],
|
parentComments: args.targetComments ?? <Comment>[],
|
||||||
splitViewEnabled: true,
|
splitViewEnabled: true,
|
||||||
|
shouldMarkNewComment: args.shouldMarkNewComment,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -130,6 +136,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
|
final bool shouldMarkNewComment;
|
||||||
final Item item;
|
final Item item;
|
||||||
final List<Comment> parentComments;
|
final List<Comment> parentComments;
|
||||||
|
|
||||||
@ -275,6 +282,7 @@ class _ItemScreenState extends State<ItemScreen>
|
|||||||
splitViewEnabled: widget.splitViewEnabled,
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
shouldMarkNewComment: widget.shouldMarkNewComment,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocBuilder<SplitViewCubit, SplitViewState>(
|
BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||||
@ -349,6 +357,7 @@ class _ItemScreenState extends State<ItemScreen>
|
|||||||
splitViewEnabled: widget.splitViewEnabled,
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
shouldMarkNewComment: widget.shouldMarkNewComment,
|
||||||
),
|
),
|
||||||
floatingActionButton: const CustomFloatingActionButton(),
|
floatingActionButton: const CustomFloatingActionButton(),
|
||||||
bottomSheet: ReplyBox(
|
bottomSheet: ReplyBox(
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/context_extension.dart';
|
||||||
import 'package:hacki/models/discoverable_feature.dart';
|
import 'package:hacki/models/discoverable_feature.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
@ -74,7 +75,11 @@ class CustomFloatingActionButton extends StatelessWidget {
|
|||||||
heroTag: UniqueKey().hashCode,
|
heroTag: UniqueKey().hashCode,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedbackUtil.selection();
|
HapticFeedbackUtil.selection();
|
||||||
context.read<CommentsCubit>().scrollToNextRoot();
|
context.read<CommentsCubit>().scrollToNextRoot(
|
||||||
|
onError: () => context.showSnackBar(
|
||||||
|
content: '''No more root level comment below.''',
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.keyboard_arrow_down,
|
Icons.keyboard_arrow_down,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||||
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
@ -87,8 +88,10 @@ class _InThreadSearchViewState extends State<_InThreadSearchView> {
|
|||||||
value: widget.commentsCubit,
|
value: widget.commentsCubit,
|
||||||
child: BlocBuilder<CommentsCubit, CommentsState>(
|
child: BlocBuilder<CommentsCubit, CommentsState>(
|
||||||
buildWhen: (CommentsState previous, CommentsState current) =>
|
buildWhen: (CommentsState previous, CommentsState current) =>
|
||||||
previous.matchedComments != current.matchedComments,
|
previous.matchedComments != current.matchedComments ||
|
||||||
|
previous.inThreadSearchAuthor != current.inThreadSearchAuthor,
|
||||||
builder: (BuildContext context, CommentsState state) {
|
builder: (BuildContext context, CommentsState state) {
|
||||||
|
final AuthState authState = context.read<AuthBloc>().state;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@ -118,7 +121,10 @@ class _InThreadSearchViewState extends State<_InThreadSearchView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: context.read<CommentsCubit>().search,
|
onChanged: (String text) => widget.commentsCubit.search(
|
||||||
|
text,
|
||||||
|
author: state.inThreadSearchAuthor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -136,6 +142,50 @@ class _InThreadSearchViewState extends State<_InThreadSearchView> {
|
|||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt12,
|
||||||
|
),
|
||||||
|
CustomChip(
|
||||||
|
selected: state.inThreadSearchAuthor == state.item.by,
|
||||||
|
label: 'by OP',
|
||||||
|
onSelected: (bool value) {
|
||||||
|
if (value) {
|
||||||
|
widget.commentsCubit.search(
|
||||||
|
state.inThreadSearchQuery,
|
||||||
|
author: state.item.by,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget.commentsCubit.search(
|
||||||
|
state.inThreadSearchQuery,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt12,
|
||||||
|
),
|
||||||
|
if (authState.isLoggedIn)
|
||||||
|
CustomChip(
|
||||||
|
selected:
|
||||||
|
state.inThreadSearchAuthor == authState.username,
|
||||||
|
label: 'by me',
|
||||||
|
onSelected: (bool value) {
|
||||||
|
if (value) {
|
||||||
|
widget.commentsCubit.search(
|
||||||
|
state.inThreadSearchQuery,
|
||||||
|
author: authState.username,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget.commentsCubit.search(
|
||||||
|
state.inThreadSearchQuery,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
for (final int i in state.matchedComments)
|
for (final int i in state.matchedComments)
|
||||||
CommentTile(
|
CommentTile(
|
||||||
index: i,
|
index: i,
|
||||||
|
@ -25,6 +25,7 @@ class MainView extends StatelessWidget {
|
|||||||
required this.splitViewEnabled,
|
required this.splitViewEnabled,
|
||||||
required this.onMoreTapped,
|
required this.onMoreTapped,
|
||||||
required this.onRightMoreTapped,
|
required this.onRightMoreTapped,
|
||||||
|
required this.shouldMarkNewComment,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ class MainView extends StatelessWidget {
|
|||||||
final AuthState authState;
|
final AuthState authState;
|
||||||
final double topPadding;
|
final double topPadding;
|
||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
|
final bool shouldMarkNewComment;
|
||||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||||
final ValueChanged<Comment> onRightMoreTapped;
|
final ValueChanged<Comment> onRightMoreTapped;
|
||||||
|
|
||||||
@ -101,6 +103,7 @@ class MainView extends StatelessWidget {
|
|||||||
|
|
||||||
index = index - 1;
|
index = index - 1;
|
||||||
final Comment comment = state.comments.elementAt(index);
|
final Comment comment = state.comments.elementAt(index);
|
||||||
|
|
||||||
return FadeIn(
|
return FadeIn(
|
||||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||||
child: CommentTile(
|
child: CommentTile(
|
||||||
@ -109,6 +112,8 @@ class MainView extends StatelessWidget {
|
|||||||
level: comment.level,
|
level: comment.level,
|
||||||
opUsername: state.item.by,
|
opUsername: state.item.by,
|
||||||
fetchMode: state.fetchMode,
|
fetchMode: state.fetchMode,
|
||||||
|
isResponse: state.isResponse(comment),
|
||||||
|
isNew: shouldMarkNewComment && !comment.isFromCache,
|
||||||
onReplyTapped: (Comment cmt) {
|
onReplyTapped: (Comment cmt) {
|
||||||
HapticFeedbackUtil.light();
|
HapticFeedbackUtil.light();
|
||||||
if (cmt.deleted || cmt.dead) {
|
if (cmt.deleted || cmt.dead) {
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
@ -77,8 +78,23 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
return Semantics(
|
return Semantics(
|
||||||
excludeSemantics: state.status == Status.inProgress,
|
excludeSemantics: state.status == Status.inProgress,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(
|
leading: Column(
|
||||||
Icons.account_circle,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
AnimatedCrossFade(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
duration: Durations.ms300,
|
||||||
|
crossFadeState: state.status.isLoading
|
||||||
|
? CrossFadeState.showFirst
|
||||||
|
: CrossFadeState.showSecond,
|
||||||
|
firstChild: const Icon(
|
||||||
|
Icons.account_circle_outlined,
|
||||||
|
),
|
||||||
|
secondChild: const Icon(
|
||||||
|
Icons.account_circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
title: Text(item.by),
|
title: Text(item.by),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:math';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/context_extension.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
@ -45,7 +46,12 @@ class PinIconButton extends StatelessWidget {
|
|||||||
if (pinned) {
|
if (pinned) {
|
||||||
context.read<PinCubit>().unpinStory(story);
|
context.read<PinCubit>().unpinStory(story);
|
||||||
} else {
|
} else {
|
||||||
context.read<PinCubit>().pinStory(story);
|
context.read<PinCubit>().pinStory(
|
||||||
|
story,
|
||||||
|
onDone: () => context.showSnackBar(
|
||||||
|
content: 'Pinned to home page.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -378,7 +378,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
||||||
throttle.run(() {
|
throttle.run(() {
|
||||||
locator
|
locator
|
||||||
.get<StoriesRepository>()
|
.get<HackerNewsRepository>()
|
||||||
.fetchParentStoryWithComments(id: comment.parent)
|
.fetchParentStoryWithComments(id: comment.parent)
|
||||||
.then(((Story, List<Comment>)? res) {
|
.then(((Story, List<Comment>)? res) {
|
||||||
if (res != null && mounted) {
|
if (res != null && mounted) {
|
||||||
|
@ -12,14 +12,13 @@ class QrCodeViewScreen extends StatelessWidget {
|
|||||||
|
|
||||||
static const String routeName = 'qr-code-view';
|
static const String routeName = 'qr-code-view';
|
||||||
|
|
||||||
static const int qrCodeVersion = 4;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: Palette.transparent,
|
backgroundColor: Palette.transparent,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@ -35,7 +34,6 @@ class QrCodeViewScreen extends StatelessWidget {
|
|||||||
eyeShape: QrEyeShape.square,
|
eyeShape: QrEyeShape.square,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
version: qrCodeVersion,
|
|
||||||
size: 300,
|
size: 300,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -30,17 +30,29 @@ class SearchScreen extends StatefulWidget {
|
|||||||
class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||||
final RefreshController refreshController = RefreshController();
|
final RefreshController refreshController = RefreshController();
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
final TextEditingController textEditingController = TextEditingController();
|
||||||
|
final FocusNode focusNode = FocusNode();
|
||||||
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
|
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
|
||||||
|
|
||||||
static const Duration chipsAnimationDuration = Durations.ms300;
|
static const Duration chipsAnimationDuration = Durations.ms300;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
scrollController.addListener(onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
refreshController.dispose();
|
refreshController.dispose();
|
||||||
scrollController.dispose();
|
scrollController.dispose();
|
||||||
|
focusNode.dispose();
|
||||||
|
textEditingController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onScroll() => focusNode.unfocus();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||||
@ -59,217 +71,6 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Dimens.pt12,
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
cursorColor: Theme.of(context).primaryColor,
|
|
||||||
autocorrect: false,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Search Hacker News',
|
|
||||||
focusedBorder: UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onChanged: (String val) {
|
|
||||||
if (val.isNotEmpty) {
|
|
||||||
debouncer.run(() {
|
|
||||||
context.read<SearchCubit>().search(val);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: Dimens.pt6,
|
|
||||||
),
|
|
||||||
AnimatedCrossFade(
|
|
||||||
duration: chipsAnimationDuration,
|
|
||||||
crossFadeState: state.showDateRangeShortcutChips
|
|
||||||
? CrossFadeState.showSecond
|
|
||||||
: CrossFadeState.showFirst,
|
|
||||||
firstChild: SizedBox.fromSize(),
|
|
||||||
secondChild: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
DateTimeShortcutChip.dayBefore(
|
|
||||||
onDateTimeRangeUpdated: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
startDate: state.dateFilter?.startTime,
|
|
||||||
endDate: state.dateFilter?.endTime,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
DateTimeShortcutChip.dayAfter(
|
|
||||||
onDateTimeRangeUpdated: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
startDate: state.dateFilter?.startTime,
|
|
||||||
endDate: state.dateFilter?.endTime,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
DateTimeShortcutChip.weekBefore(
|
|
||||||
onDateTimeRangeUpdated: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
startDate: state.dateFilter?.startTime,
|
|
||||||
endDate: state.dateFilter?.endTime,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
DateTimeShortcutChip.weekAfter(
|
|
||||||
onDateTimeRangeUpdated: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
startDate: state.dateFilter?.startTime,
|
|
||||||
endDate: state.dateFilter?.endTime,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
DateTimeShortcutChip.monthBefore(
|
|
||||||
onDateTimeRangeUpdated: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
startDate: state.dateFilter?.startTime,
|
|
||||||
endDate: state.dateFilter?.endTime,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
DateTimeShortcutChip.monthAfter(
|
|
||||||
onDateTimeRangeUpdated: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
startDate: state.dateFilter?.startTime,
|
|
||||||
endDate: state.dateFilter?.endTime,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
DateTimeRangeFilterChip(
|
|
||||||
filter: state.dateFilter,
|
|
||||||
initialStartDate: state.dateFilter?.startTime,
|
|
||||||
initialEndDate: state.dateFilter?.endTime,
|
|
||||||
onDateTimeRangeUpdated: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
onDateTimeRangeRemoved: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.removeFilter<DateTimeRangeFilter>,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
PostedByFilterChip(
|
|
||||||
filter: state.params.get<PostedByFilter>(),
|
|
||||||
onChanged:
|
|
||||||
context.read<SearchCubit>().onPostedByChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
CustomChip(
|
|
||||||
onSelected: (_) =>
|
|
||||||
context.read<SearchCubit>().onSortToggled(),
|
|
||||||
selected: state.params.sorted,
|
|
||||||
label: '''newest first''',
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
for (final CustomDateTimeRange range
|
|
||||||
in CustomDateTimeRange.values) ...<Widget>[
|
|
||||||
CustomRangeFilterChip(
|
|
||||||
range: range,
|
|
||||||
onTap: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onDateTimeRangeUpdated,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
for (final TypeTagFilter filter
|
|
||||||
in TypeTagFilter.all) ...<Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt8,
|
|
||||||
),
|
|
||||||
CustomChip(
|
|
||||||
onSelected: (_) => context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.onToggled(filter),
|
|
||||||
selected: context
|
|
||||||
.read<SearchCubit>()
|
|
||||||
.state
|
|
||||||
.params
|
|
||||||
.get<TypeTagFilter>() ==
|
|
||||||
filter,
|
|
||||||
label: filter.query,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (state.status == SearchStatus.loading &&
|
|
||||||
state.results.isEmpty) ...<Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
height: Dimens.pt100,
|
|
||||||
),
|
|
||||||
const Center(
|
|
||||||
child: CustomCircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (state.status == SearchStatus.loaded &&
|
|
||||||
state.results.isEmpty) ...<Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
height: Dimens.pt100,
|
|
||||||
),
|
|
||||||
const Center(
|
|
||||||
child: Text(
|
|
||||||
'Nothing found...',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Palette.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SmartRefresher(
|
child: SmartRefresher(
|
||||||
enablePullDown: false,
|
enablePullDown: false,
|
||||||
@ -310,6 +111,243 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
|||||||
? const NeverScrollableScrollPhysics()
|
? const NeverScrollableScrollPhysics()
|
||||||
: null,
|
: null,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.pt12,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: textEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
cursorColor: Theme.of(context).primaryColor,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search Hacker News',
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (String val) {
|
||||||
|
if (val.isNotEmpty) {
|
||||||
|
debouncer.run(() {
|
||||||
|
context.read<SearchCubit>().search(val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt6,
|
||||||
|
),
|
||||||
|
AnimatedCrossFade(
|
||||||
|
duration: chipsAnimationDuration,
|
||||||
|
crossFadeState: state.showDateRangeShortcutChips
|
||||||
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
|
firstChild: SizedBox.fromSize(),
|
||||||
|
secondChild: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
DateTimeShortcutChip.dayBefore(
|
||||||
|
onDateTimeRangeUpdated: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
startDate:
|
||||||
|
state.dateFilter?.startTime,
|
||||||
|
endDate: state.dateFilter?.endTime,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
DateTimeShortcutChip.dayAfter(
|
||||||
|
onDateTimeRangeUpdated: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
startDate:
|
||||||
|
state.dateFilter?.startTime,
|
||||||
|
endDate: state.dateFilter?.endTime,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
DateTimeShortcutChip.weekBefore(
|
||||||
|
onDateTimeRangeUpdated: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
startDate:
|
||||||
|
state.dateFilter?.startTime,
|
||||||
|
endDate: state.dateFilter?.endTime,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
DateTimeShortcutChip.weekAfter(
|
||||||
|
onDateTimeRangeUpdated: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
startDate:
|
||||||
|
state.dateFilter?.startTime,
|
||||||
|
endDate: state.dateFilter?.endTime,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
DateTimeShortcutChip.monthBefore(
|
||||||
|
onDateTimeRangeUpdated: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
startDate:
|
||||||
|
state.dateFilter?.startTime,
|
||||||
|
endDate: state.dateFilter?.endTime,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
DateTimeShortcutChip.monthAfter(
|
||||||
|
onDateTimeRangeUpdated: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
startDate:
|
||||||
|
state.dateFilter?.startTime,
|
||||||
|
endDate: state.dateFilter?.endTime,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
DateTimeRangeFilterChip(
|
||||||
|
filter: state.dateFilter,
|
||||||
|
initialStartDate:
|
||||||
|
state.dateFilter?.startTime,
|
||||||
|
initialEndDate: state.dateFilter?.endTime,
|
||||||
|
onDateTimeRangeUpdated: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
onDateTimeRangeRemoved: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.removeFilter<DateTimeRangeFilter>,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
PostedByFilterChip(
|
||||||
|
filter:
|
||||||
|
state.params.get<PostedByFilter>(),
|
||||||
|
onChanged: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onPostedByChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
CustomChip(
|
||||||
|
onSelected: (_) => context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onSortToggled(),
|
||||||
|
selected: state.params.sorted,
|
||||||
|
label: '''newest first''',
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
CustomChip(
|
||||||
|
onSelected: (_) => context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onExactMatchToggled(),
|
||||||
|
selected: state.params.exactMatch,
|
||||||
|
label: '''exact match''',
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
for (final CustomDateTimeRange range
|
||||||
|
in CustomDateTimeRange
|
||||||
|
.values) ...<Widget>[
|
||||||
|
CustomRangeFilterChip(
|
||||||
|
range: range,
|
||||||
|
onTap: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onDateTimeRangeUpdated,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final TypeTagFilter filter
|
||||||
|
in TypeTagFilter.all) ...<Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
CustomChip(
|
||||||
|
onSelected: (_) => context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.onToggled(filter),
|
||||||
|
selected: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.state
|
||||||
|
.params
|
||||||
|
.get<TypeTagFilter>() ==
|
||||||
|
filter,
|
||||||
|
label: filter.query,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.status == SearchStatus.loading &&
|
||||||
|
state.results.isEmpty) ...<Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt100,
|
||||||
|
),
|
||||||
|
const Center(
|
||||||
|
child: CustomCircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (state.status == SearchStatus.loaded &&
|
||||||
|
state.results.isEmpty) ...<Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt100,
|
||||||
|
),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Nothing found...',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
...state.results
|
...state.results
|
||||||
.map(
|
.map(
|
||||||
(Item e) => <Widget>[
|
(Item e) => <Widget>[
|
||||||
|
@ -49,7 +49,7 @@ class DateTimeRangeFilterChip extends StatelessWidget {
|
|||||||
final DateTime? start = filter?.startTime;
|
final DateTime? start = filter?.startTime;
|
||||||
final DateTime? end = filter?.endTime;
|
final DateTime? end = filter?.endTime;
|
||||||
if (start == null && end == null) {
|
if (start == null && end == null) {
|
||||||
return '''from X to Y''';
|
return '''date range''';
|
||||||
} else if (start == end) {
|
} else if (start == end) {
|
||||||
return '''from ${_formatDateTime(start)}''';
|
return '''from ${_formatDateTime(start)}''';
|
||||||
} else {
|
} else {
|
||||||
|
@ -24,6 +24,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
this.actionable = true,
|
this.actionable = true,
|
||||||
this.collapsable = true,
|
this.collapsable = true,
|
||||||
this.selectable = true,
|
this.selectable = true,
|
||||||
|
this.isResponse = false,
|
||||||
|
this.isNew = false,
|
||||||
this.level = 0,
|
this.level = 0,
|
||||||
this.index,
|
this.index,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
@ -36,6 +38,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
final bool actionable;
|
final bool actionable;
|
||||||
final bool collapsable;
|
final bool collapsable;
|
||||||
final bool selectable;
|
final bool selectable;
|
||||||
|
final bool isResponse;
|
||||||
|
final bool isNew;
|
||||||
final FetchMode fetchMode;
|
final FetchMode fetchMode;
|
||||||
|
|
||||||
final void Function(Comment)? onReplyTapped;
|
final void Function(Comment)? onReplyTapped;
|
||||||
@ -171,6 +175,24 @@ class CommentTile extends StatelessWidget {
|
|||||||
textScaleFactor:
|
textScaleFactor:
|
||||||
MediaQuery.of(context).textScaleFactor,
|
MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
|
if (isResponse)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 4),
|
||||||
|
child: Icon(
|
||||||
|
Icons.reply,
|
||||||
|
size: 16,
|
||||||
|
color: Palette.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!comment.dead && isNew)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 4),
|
||||||
|
child: Icon(
|
||||||
|
Icons.sunny_snowing,
|
||||||
|
size: 16,
|
||||||
|
color: Palette.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
comment.timeAgo,
|
comment.timeAgo,
|
||||||
@ -278,7 +300,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final double commentBackgroundColorOpacity =
|
final double commentBackgroundColorOpacity =
|
||||||
Theme.of(context).brightness == Brightness.dark ? 0.03 : 0.15;
|
Theme.of(context).canvasColor != Palette.white ? 0.03 : 0.15;
|
||||||
|
|
||||||
final Color commentColor = prefState.eyeCandyEnabled
|
final Color commentColor = prefState.eyeCandyEnabled
|
||||||
? color.withOpacity(commentBackgroundColorOpacity)
|
? color.withOpacity(commentBackgroundColorOpacity)
|
||||||
|
@ -107,7 +107,7 @@ class _CountDownReminderState extends State<CountdownReminder>
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
if (state.storyId != null) {
|
if (state.storyId != null) {
|
||||||
locator
|
locator
|
||||||
.get<StoriesRepository>()
|
.get<HackerNewsRepository>()
|
||||||
.fetchStory(id: state.storyId!)
|
.fetchStory(id: state.storyId!)
|
||||||
.then((Story? story) {
|
.then((Story? story) {
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
|
@ -27,25 +27,34 @@ class CodeLinkifier extends Linkifier {
|
|||||||
list.add(element);
|
list.add(element);
|
||||||
} else {
|
} else {
|
||||||
final String matchedText = match.group(0)!;
|
final String matchedText = match.group(0)!;
|
||||||
final num pos = element.text.indexOf(matchedText);
|
|
||||||
final List<String> splitTexts = element.text.split(matchedText);
|
final List<String> splitTexts = element.text.split(matchedText);
|
||||||
|
|
||||||
int curPos = 0;
|
final String preceding = splitTexts[0];
|
||||||
bool added = false;
|
|
||||||
|
|
||||||
for (final String text in splitTexts) {
|
list.addAll(
|
||||||
list.addAll(parse(<LinkifyElement>[TextElement(text)], options));
|
parse(
|
||||||
|
<LinkifyElement>[
|
||||||
|
TextElement(preceding == '\n\n' ? '' : preceding),
|
||||||
|
],
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
curPos += text.length;
|
String trimmedText = matchedText
|
||||||
|
.replaceFirst(_openTag, '')
|
||||||
|
.replaceFirst(_closeTag, '');
|
||||||
|
trimmedText = '$trimmedText\n';
|
||||||
|
|
||||||
if (!added && curPos >= pos) {
|
list
|
||||||
added = true;
|
..add(CodeElement(trimmedText))
|
||||||
final String trimmedText = matchedText
|
..addAll(
|
||||||
.replaceFirst(_openTag, '')
|
parse(
|
||||||
.replaceFirst(_closeTag, '');
|
<LinkifyElement>[
|
||||||
list.add(CodeElement(trimmedText));
|
TextElement(splitTexts[1]),
|
||||||
}
|
],
|
||||||
}
|
options,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
list.add(element);
|
list.add(element);
|
||||||
|
@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart' show StringCharacters, immutable;
|
|||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
final RegExp _urlRegex = RegExp(
|
final RegExp _urlRegex = RegExp(
|
||||||
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9()+_.,~-]*)',
|
r'''^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9()+\*_.,'~-]*)''',
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
dotAll: true,
|
dotAll: true,
|
||||||
);
|
);
|
||||||
@ -84,7 +84,7 @@ class UrlLinkifier extends Linkifier {
|
|||||||
|
|
||||||
if (url.endsWith(',')) {
|
if (url.endsWith(',')) {
|
||||||
url = url.substring(0, max(0, url.length - 1));
|
url = url.substring(0, max(0, url.length - 1));
|
||||||
end = '$end,';
|
end = '${end ?? ''},';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options.humanize) || (options.removeWww)) {
|
if ((options.humanize) || (options.removeWww)) {
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
@ -50,7 +51,7 @@ class _StoriesListViewState extends State<StoriesListView>
|
|||||||
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
||||||
previous.complexStoryTileEnabled != current.complexStoryTileEnabled ||
|
previous.complexStoryTileEnabled != current.complexStoryTileEnabled ||
|
||||||
previous.metadataEnabled != current.metadataEnabled ||
|
previous.metadataEnabled != current.metadataEnabled ||
|
||||||
previous.paginationEnabled != current.paginationEnabled,
|
previous.manualPaginationEnabled != current.manualPaginationEnabled,
|
||||||
builder: (BuildContext context, PreferenceState preferenceState) {
|
builder: (BuildContext context, PreferenceState preferenceState) {
|
||||||
return BlocConsumer<StoriesBloc, StoriesState>(
|
return BlocConsumer<StoriesBloc, StoriesState>(
|
||||||
listenWhen: (StoriesState previous, StoriesState current) =>
|
listenWhen: (StoriesState previous, StoriesState current) =>
|
||||||
@ -68,8 +69,18 @@ class _StoriesListViewState extends State<StoriesListView>
|
|||||||
previous.currentPageByType[storyType] == 0) ||
|
previous.currentPageByType[storyType] == 0) ||
|
||||||
(previous.storiesByType[storyType]!.length !=
|
(previous.storiesByType[storyType]!.length !=
|
||||||
current.storiesByType[storyType]!.length) ||
|
current.storiesByType[storyType]!.length) ||
|
||||||
(previous.readStoriesIds.length != current.readStoriesIds.length),
|
(previous.readStoriesIds.length !=
|
||||||
|
current.readStoriesIds.length) ||
|
||||||
|
(previous.statusByType[widget.storyType] !=
|
||||||
|
current.statusByType[widget.storyType]),
|
||||||
builder: (BuildContext context, StoriesState state) {
|
builder: (BuildContext context, StoriesState state) {
|
||||||
|
bool shouldShowLoadButton() {
|
||||||
|
return preferenceState.manualPaginationEnabled &&
|
||||||
|
state.statusByType[widget.storyType] == Status.success &&
|
||||||
|
(state.storiesByType[widget.storyType]?.length ?? 0) <
|
||||||
|
(state.storyIdsByType[widget.storyType]?.length ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
return ItemsListView<Story>(
|
return ItemsListView<Story>(
|
||||||
showOfflineBanner: true,
|
showOfflineBanner: true,
|
||||||
markReadStories: preferenceState.markReadStoriesEnabled,
|
markReadStories: preferenceState.markReadStoriesEnabled,
|
||||||
@ -88,7 +99,7 @@ class _StoriesListViewState extends State<StoriesListView>
|
|||||||
context.read<PinCubit>().refresh();
|
context.read<PinCubit>().refresh();
|
||||||
},
|
},
|
||||||
onLoadMore: () {
|
onLoadMore: () {
|
||||||
if (preferenceState.paginationEnabled) {
|
if (preferenceState.manualPaginationEnabled) {
|
||||||
refreshController
|
refreshController
|
||||||
..refreshCompleted(resetFooterState: true)
|
..refreshCompleted(resetFooterState: true)
|
||||||
..loadComplete();
|
..loadComplete();
|
||||||
@ -100,33 +111,42 @@ class _StoriesListViewState extends State<StoriesListView>
|
|||||||
onPinned: context.read<PinCubit>().pinStory,
|
onPinned: context.read<PinCubit>().pinStory,
|
||||||
header: state.isOfflineReading ? null : header,
|
header: state.isOfflineReading ? null : header,
|
||||||
loadStyle: LoadStyle.HideAlways,
|
loadStyle: LoadStyle.HideAlways,
|
||||||
footer: preferenceState.paginationEnabled &&
|
footer: Center(
|
||||||
state.statusByType[widget.storyType] == Status.success &&
|
child: AnimatedCrossFade(
|
||||||
(state.storiesByType[widget.storyType]?.length ?? 0) <
|
alignment: Alignment.center,
|
||||||
(state.storyIdsByType[widget.storyType]?.length ?? 0)
|
crossFadeState: shouldShowLoadButton()
|
||||||
? Padding(
|
? CrossFadeState.showFirst
|
||||||
padding: const EdgeInsets.only(
|
: CrossFadeState.showSecond,
|
||||||
left: Dimens.pt48,
|
duration: Durations.ms300,
|
||||||
right: Dimens.pt48,
|
firstChild: Padding(
|
||||||
top: Dimens.pt36,
|
padding: const EdgeInsets.only(
|
||||||
bottom: Dimens.pt12,
|
left: Dimens.pt48,
|
||||||
),
|
right: Dimens.pt48,
|
||||||
child: OutlinedButton(
|
top: Dimens.pt36,
|
||||||
onPressed: loadMoreStories,
|
bottom: Dimens.pt12,
|
||||||
style: ButtonStyle(
|
),
|
||||||
foregroundColor: MaterialStateColor.resolveWith(
|
child: OutlinedButton(
|
||||||
(_) => Theme.of(context).colorScheme.onSurface,
|
onPressed: loadMoreStories,
|
||||||
),
|
style: ButtonStyle(
|
||||||
|
minimumSize: MaterialStateProperty.all(
|
||||||
|
const Size(double.infinity, Dimens.pt48),
|
||||||
),
|
),
|
||||||
child: Text(
|
foregroundColor: MaterialStateColor.resolveWith(
|
||||||
'''Load Page ${(state.currentPageByType[widget.storyType] ?? 0) + 2}''',
|
(_) => Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
child: Text(
|
||||||
: null,
|
'''Load Page ${(state.currentPageByType[widget.storyType] ?? 0) + 2}''',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
secondChild: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
itemBuilder: (Widget child, Story story) {
|
itemBuilder: (Widget child, Story story) {
|
||||||
return Slidable(
|
return Slidable(
|
||||||
|
key: ValueKey<Story>(story),
|
||||||
enabled: !preferenceState.swipeGestureEnabled,
|
enabled: !preferenceState.swipeGestureEnabled,
|
||||||
startActionPane: ActionPane(
|
startActionPane: ActionPane(
|
||||||
motion: const BehindMotion(),
|
motion: const BehindMotion(),
|
||||||
@ -160,6 +180,30 @@ class _StoriesListViewState extends State<StoriesListView>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
endActionPane: ActionPane(
|
||||||
|
motion: const BehindMotion(),
|
||||||
|
dismissible: DismissiblePane(
|
||||||
|
closeOnCancel: true,
|
||||||
|
confirmDismiss: () async {
|
||||||
|
mark(story);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onDismissed: () {},
|
||||||
|
),
|
||||||
|
children: <Widget>[
|
||||||
|
SlidableAction(
|
||||||
|
onPressed: (_) => mark(story),
|
||||||
|
backgroundColor: preferenceState.markReadStoriesEnabled
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: Palette.grey,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
|
icon: state.readStoriesIds.contains(story.id)
|
||||||
|
? Icons.visibility_off
|
||||||
|
: Icons.visibility,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: OptionalWrapper(
|
child: OptionalWrapper(
|
||||||
enabled: context
|
enabled: context
|
||||||
.read<PreferenceCubit>()
|
.read<PreferenceCubit>()
|
||||||
@ -193,6 +237,22 @@ class _StoriesListViewState extends State<StoriesListView>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void mark(Story story) {
|
||||||
|
HapticFeedbackUtil.light();
|
||||||
|
final StoriesBloc storiesBloc = context.read<StoriesBloc>();
|
||||||
|
final bool markReadStoriesEnabled =
|
||||||
|
context.read<PreferenceCubit>().state.markReadStoriesEnabled;
|
||||||
|
if (markReadStoriesEnabled) {
|
||||||
|
if (storiesBloc.state.readStoriesIds.contains(story.id)) {
|
||||||
|
storiesBloc.add(StoryUnread(story: story));
|
||||||
|
} else {
|
||||||
|
storiesBloc.add(StoryRead(story: story));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.showSnackBar(content: 'Read story marking is disabled.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void loadMoreStories() =>
|
void loadMoreStories() =>
|
||||||
context.read<StoriesBloc>().add(StoriesLoadMore(type: widget.storyType));
|
context.read<StoriesBloc>().add(StoriesLoadMore(type: widget.storyType));
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ abstract class Fetcher {
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
);
|
);
|
||||||
|
|
||||||
final StoriesRepository storiesRepository = StoriesRepository();
|
final HackerNewsRepository hackerNewsRepository = HackerNewsRepository();
|
||||||
final SembastRepository sembastRepository = SembastRepository();
|
final SembastRepository sembastRepository = SembastRepository();
|
||||||
|
|
||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
@ -57,7 +57,7 @@ abstract class Fetcher {
|
|||||||
|
|
||||||
Comment? newReply;
|
Comment? newReply;
|
||||||
|
|
||||||
await storiesRepository
|
await hackerNewsRepository
|
||||||
.fetchSubmitted(userId: username)
|
.fetchSubmitted(userId: username)
|
||||||
.then((List<int>? submittedItems) async {
|
.then((List<int>? submittedItems) async {
|
||||||
if (submittedItems != null) {
|
if (submittedItems != null) {
|
||||||
@ -67,7 +67,9 @@ abstract class Fetcher {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (final int id in subscribedItems) {
|
for (final int id in subscribedItems) {
|
||||||
await storiesRepository.fetchRawItem(id: id).then((Item? item) async {
|
await hackerNewsRepository
|
||||||
|
.fetchRawItem(id: id)
|
||||||
|
.then((Item? item) async {
|
||||||
final List<int> kids = item?.kids ?? <int>[];
|
final List<int> kids = item?.kids ?? <int>[];
|
||||||
final List<int> previousKids =
|
final List<int> previousKids =
|
||||||
(await sembastRepository.kids(of: id)) ?? <int>[];
|
(await sembastRepository.kids(of: id)) ?? <int>[];
|
||||||
@ -81,7 +83,7 @@ abstract class Fetcher {
|
|||||||
for (final int newCommentId in diff) {
|
for (final int newCommentId in diff) {
|
||||||
if (unreadIds.contains(newCommentId)) continue;
|
if (unreadIds.contains(newCommentId)) continue;
|
||||||
|
|
||||||
await storiesRepository
|
await hackerNewsRepository
|
||||||
.fetchRawComment(id: newCommentId)
|
.fetchRawComment(id: newCommentId)
|
||||||
.then((Comment? comment) async {
|
.then((Comment? comment) async {
|
||||||
final bool hasPushedBefore =
|
final bool hasPushedBefore =
|
||||||
@ -113,7 +115,7 @@ abstract class Fetcher {
|
|||||||
// pushed before.
|
// pushed before.
|
||||||
if (newReply != null) {
|
if (newReply != null) {
|
||||||
final Story? story =
|
final Story? story =
|
||||||
await storiesRepository.fetchRawParentStory(id: newReply!.id);
|
await hackerNewsRepository.fetchRawParentStory(id: newReply!.id);
|
||||||
final String text = HtmlUtil.parseHtml(newReply!.text);
|
final String text = HtmlUtil.parseHtml(newReply!.text);
|
||||||
|
|
||||||
if (story != null) {
|
if (story != null) {
|
||||||
|
@ -219,10 +219,8 @@ class WebAnalyzer {
|
|||||||
String? fallbackDescription;
|
String? fallbackDescription;
|
||||||
|
|
||||||
if (res == null || isEmpty(res[2] as String?)) {
|
if (res == null || isEmpty(res[2] as String?)) {
|
||||||
final String? commentText = await compute(
|
final List<int> ids = <int>[story.id, ...story.kids];
|
||||||
_fetchInfoFromStory,
|
final String? commentText = await _fetchInfoFromStory(ids);
|
||||||
<int>[story.id, ...story.kids],
|
|
||||||
);
|
|
||||||
|
|
||||||
shouldRetry = commentText == null;
|
shouldRetry = commentText == null;
|
||||||
fallbackDescription = commentText ?? 'no comment yet';
|
fallbackDescription = commentText ?? 'no comment yet';
|
||||||
@ -288,14 +286,14 @@ class WebAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> _fetchInfoFromStory(List<int> meta) async {
|
static Future<String?> _fetchInfoFromStory(List<int> meta) async {
|
||||||
final StoriesRepository storiesRepository = StoriesRepository();
|
final HackerNewsRepository hackerNewsRepository = HackerNewsRepository();
|
||||||
final int storyId = meta.first;
|
final int storyId = meta.first;
|
||||||
List<int> kids = meta.sublist(1, meta.length);
|
List<int> kids = meta.sublist(1, meta.length);
|
||||||
|
|
||||||
// Kids of stories from search results are always empty, so here we try
|
// Kids of stories from search results are always empty, so here we try
|
||||||
// to fetch the story itself first and see if the kids are still empty.
|
// to fetch the story itself first and see if the kids are still empty.
|
||||||
if (kids.isEmpty) {
|
if (kids.isEmpty) {
|
||||||
final Story? story = await storiesRepository.fetchStory(id: storyId);
|
final Story? story = await hackerNewsRepository.fetchStory(id: storyId);
|
||||||
|
|
||||||
if (story == null) return null;
|
if (story == null) return null;
|
||||||
|
|
||||||
@ -305,7 +303,7 @@ class WebAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Comment? comment =
|
final Comment? comment =
|
||||||
await storiesRepository.fetchComment(id: kids.first);
|
await hackerNewsRepository.fetchComment(id: kids.first);
|
||||||
|
|
||||||
return comment != null ? '${comment.by}: ${comment.text}' : null;
|
return comment != null ? '${comment.by}: ${comment.text}' : null;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
abstract class HapticFeedbackUtil {
|
abstract class HapticFeedbackUtil {
|
||||||
static void selection() => HapticFeedback.selectionClick();
|
static bool enabled = true;
|
||||||
|
|
||||||
static void light() => HapticFeedback.lightImpact();
|
static void selection() {
|
||||||
|
if (enabled) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void light() {
|
||||||
|
if (enabled) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,16 @@ abstract class HtmlUtil {
|
|||||||
static String parseHtml(String text) {
|
static String parseHtml(String text) {
|
||||||
return HtmlUnescape()
|
return HtmlUnescape()
|
||||||
.convert(text)
|
.convert(text)
|
||||||
.replaceAll('<p>', '\n')
|
.replaceAllMapped(
|
||||||
|
RegExp(r'\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>', dotAll: true),
|
||||||
|
(Match match) =>
|
||||||
|
'<pre><code>${match[1]?.replaceAll('\n', '[break]')}</code></pre>',
|
||||||
|
)
|
||||||
|
.replaceAll('\n', '')
|
||||||
|
.replaceAllMapped(
|
||||||
|
RegExp(r'\<p\>(.*?)\<p\>', dotAll: true),
|
||||||
|
(Match match) => '\n${match[1]?.replaceAll('\n', ' ')}\n',
|
||||||
|
)
|
||||||
.replaceAllMapped(
|
.replaceAllMapped(
|
||||||
RegExp(r'\<i\>(.*?)\<\/i\>'),
|
RegExp(r'\<i\>(.*?)\<\/i\>'),
|
||||||
(Match match) => '*${match[1]}*',
|
(Match match) => '*${match[1]}*',
|
||||||
@ -43,6 +52,8 @@ abstract class HtmlUtil {
|
|||||||
RegExp(r'\<a href=\"(.*?)\".*?\>.*?\<\/a\>'),
|
RegExp(r'\<a href=\"(.*?)\".*?\>.*?\<\/a\>'),
|
||||||
(Match match) => match[1] ?? '',
|
(Match match) => match[1] ?? '',
|
||||||
)
|
)
|
||||||
.replaceAll('\n', '\n\n');
|
.replaceAll('\n', '\n\n')
|
||||||
|
.replaceAll('<p>', '\n\n')
|
||||||
|
.replaceAll('[break]', '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,10 @@ abstract class LinkUtil {
|
|||||||
if (useHackiForHnLink && link.isStoryLink) {
|
if (useHackiForHnLink && link.isStoryLink) {
|
||||||
final int? id = link.itemId;
|
final int? id = link.itemId;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
locator.get<StoriesRepository>().fetchItem(id: id).then((Item? item) {
|
locator
|
||||||
|
.get<HackerNewsRepository>()
|
||||||
|
.fetchItem(id: id)
|
||||||
|
.then((Item? item) {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
router.push(
|
router.push(
|
||||||
'/${ItemScreen.routeName}',
|
'/${ItemScreen.routeName}',
|
||||||
|
46
pubspec.lock
46
pubspec.lock
@ -467,14 +467,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
gbk_codec:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: gbk_codec
|
|
||||||
sha256: "3af5311fc9393115e3650ae6023862adf998051a804a08fb804f042724999f61"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.0"
|
|
||||||
get_it:
|
get_it:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -495,10 +487,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: a206cc4621a644531a2e05e7774616ab4d9d85eab1f3b0e255f3102937fccab1
|
sha256: c247a4f76071c3b97bb5ae8912968870d5565644801c5e09f3bc961b4d874895
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.0"
|
version: "12.1.1"
|
||||||
hive:
|
hive:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1176,66 +1168,66 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
|
sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.14"
|
version: "6.2.1"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
|
sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.2.0"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
|
sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5"
|
version: "6.2.0"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_linux
|
name: url_launcher_linux
|
||||||
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
|
sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.1.0"
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
|
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.1.0"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_platform_interface
|
name: url_launcher_platform_interface
|
||||||
sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
|
sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.2.0"
|
||||||
url_launcher_web:
|
url_launcher_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
|
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.20"
|
version: "2.2.0"
|
||||||
url_launcher_windows:
|
url_launcher_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_windows
|
name: url_launcher_windows
|
||||||
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
|
sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.8"
|
version: "3.1.0"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1438,4 +1430,4 @@ packages:
|
|||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.1.0 <4.0.0"
|
dart: ">=3.1.0 <4.0.0"
|
||||||
flutter: ">=3.13.8"
|
flutter: ">=3.13.9"
|
||||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -1,11 +1,11 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 2.2.0+128
|
version: 2.4.2+133
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
flutter: "3.13.8"
|
flutter: "3.13.9"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.2.0
|
adaptive_theme: ^3.2.0
|
||||||
@ -38,9 +38,8 @@ dependencies:
|
|||||||
flutter_siri_suggestions: ^2.1.0
|
flutter_siri_suggestions: ^2.1.0
|
||||||
flutter_slidable: ^3.0.0
|
flutter_slidable: ^3.0.0
|
||||||
font_awesome_flutter: ^10.3.0
|
font_awesome_flutter: ^10.3.0
|
||||||
gbk_codec: ^0.4.0
|
|
||||||
get_it: ^7.2.0
|
get_it: ^7.2.0
|
||||||
go_router: ^12.0.0
|
go_router: ^12.1.1
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
html: ^0.15.1
|
html: ^0.15.1
|
||||||
html_unescape: ^2.0.0
|
html_unescape: ^2.0.0
|
||||||
@ -67,7 +66,7 @@ dependencies:
|
|||||||
responsive_builder: ^0.7.0
|
responsive_builder: ^0.7.0
|
||||||
rxdart: ^0.27.7
|
rxdart: ^0.27.7
|
||||||
scrollable_positioned_list: ^0.3.5
|
scrollable_positioned_list: ^0.3.5
|
||||||
sembast: ^3.4.0+6
|
sembast: ^3.5.0+1
|
||||||
share_plus: ^7.2.1
|
share_plus: ^7.2.1
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
shared_preferences_android: ^2.2.1
|
shared_preferences_android: ^2.2.1
|
||||||
@ -76,7 +75,7 @@ dependencies:
|
|||||||
synced_shared_preferences:
|
synced_shared_preferences:
|
||||||
path: components/synced_shared_preferences
|
path: components/synced_shared_preferences
|
||||||
universal_platform: ^1.0.0+1
|
universal_platform: ^1.0.0+1
|
||||||
url_launcher: ^6.1.9
|
url_launcher: ^6.2.1
|
||||||
visibility_detector: ^0.4.0+2
|
visibility_detector: ^0.4.0+2
|
||||||
wakelock: ^0.6.2
|
wakelock: ^0.6.2
|
||||||
webview_flutter: ^4.4.1
|
webview_flutter: ^4.4.1
|
||||||
|
Submodule submodules/flutter updated: 6c4930c4ac...d211f42860
@ -9,7 +9,7 @@ class MockAuthRepository extends Mock implements AuthRepository {}
|
|||||||
|
|
||||||
class MockPreferenceRepository extends Mock implements PreferenceRepository {}
|
class MockPreferenceRepository extends Mock implements PreferenceRepository {}
|
||||||
|
|
||||||
class MockStoriesRepository extends Mock implements StoriesRepository {}
|
class MockHackerNewsRepository extends Mock implements HackerNewsRepository {}
|
||||||
|
|
||||||
class MockSembastRepository extends Mock implements SembastRepository {}
|
class MockSembastRepository extends Mock implements SembastRepository {}
|
||||||
|
|
||||||
@ -17,7 +17,8 @@ void main() {
|
|||||||
final MockAuthRepository mockAuthRepository = MockAuthRepository();
|
final MockAuthRepository mockAuthRepository = MockAuthRepository();
|
||||||
final MockPreferenceRepository mockPreferenceRepository =
|
final MockPreferenceRepository mockPreferenceRepository =
|
||||||
MockPreferenceRepository();
|
MockPreferenceRepository();
|
||||||
final MockStoriesRepository mockStoriesRepository = MockStoriesRepository();
|
final MockHackerNewsRepository mockHackerNewsRepository =
|
||||||
|
MockHackerNewsRepository();
|
||||||
final MockSembastRepository mockSembastRepository = MockSembastRepository();
|
final MockSembastRepository mockSembastRepository = MockSembastRepository();
|
||||||
|
|
||||||
const int created = 0;
|
const int created = 0;
|
||||||
@ -49,7 +50,7 @@ void main() {
|
|||||||
AuthBloc(
|
AuthBloc(
|
||||||
authRepository: mockAuthRepository,
|
authRepository: mockAuthRepository,
|
||||||
preferenceRepository: mockPreferenceRepository,
|
preferenceRepository: mockPreferenceRepository,
|
||||||
storiesRepository: mockStoriesRepository,
|
hackerNewsRepository: mockHackerNewsRepository,
|
||||||
sembastRepository: mockSembastRepository,
|
sembastRepository: mockSembastRepository,
|
||||||
).state,
|
).state,
|
||||||
equals(const AuthState.init()),
|
equals(const AuthState.init()),
|
||||||
@ -67,7 +68,7 @@ void main() {
|
|||||||
.thenAnswer((_) => Future<String?>.value(username));
|
.thenAnswer((_) => Future<String?>.value(username));
|
||||||
when(() => mockAuthRepository.password)
|
when(() => mockAuthRepository.password)
|
||||||
.thenAnswer((_) => Future<String>.value(password));
|
.thenAnswer((_) => Future<String>.value(password));
|
||||||
when(() => mockStoriesRepository.fetchUser(id: username))
|
when(() => mockHackerNewsRepository.fetchUser(id: username))
|
||||||
.thenAnswer((_) => Future<User>.value(tUser));
|
.thenAnswer((_) => Future<User>.value(tUser));
|
||||||
when(() => mockAuthRepository.loggedIn)
|
when(() => mockAuthRepository.loggedIn)
|
||||||
.thenAnswer((_) => Future<bool>.value(false));
|
.thenAnswer((_) => Future<bool>.value(false));
|
||||||
@ -79,7 +80,7 @@ void main() {
|
|||||||
return AuthBloc(
|
return AuthBloc(
|
||||||
authRepository: mockAuthRepository,
|
authRepository: mockAuthRepository,
|
||||||
preferenceRepository: mockPreferenceRepository,
|
preferenceRepository: mockPreferenceRepository,
|
||||||
storiesRepository: mockStoriesRepository,
|
hackerNewsRepository: mockHackerNewsRepository,
|
||||||
sembastRepository: mockSembastRepository,
|
sembastRepository: mockSembastRepository,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -91,7 +92,7 @@ void main() {
|
|||||||
verify: (_) {
|
verify: (_) {
|
||||||
verify(() => mockAuthRepository.loggedIn).called(2);
|
verify(() => mockAuthRepository.loggedIn).called(2);
|
||||||
verifyNever(() => mockAuthRepository.username);
|
verifyNever(() => mockAuthRepository.username);
|
||||||
verifyNever(() => mockStoriesRepository.fetchUser(id: username));
|
verifyNever(() => mockHackerNewsRepository.fetchUser(id: username));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ void main() {
|
|||||||
return AuthBloc(
|
return AuthBloc(
|
||||||
authRepository: mockAuthRepository,
|
authRepository: mockAuthRepository,
|
||||||
preferenceRepository: mockPreferenceRepository,
|
preferenceRepository: mockPreferenceRepository,
|
||||||
storiesRepository: mockStoriesRepository,
|
hackerNewsRepository: mockHackerNewsRepository,
|
||||||
sembastRepository: mockSembastRepository,
|
sembastRepository: mockSembastRepository,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -154,7 +155,8 @@ void main() {
|
|||||||
password: password,
|
password: password,
|
||||||
),
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
verify(() => mockStoriesRepository.fetchUser(id: username)).called(1);
|
verify(() => mockHackerNewsRepository.fetchUser(id: username))
|
||||||
|
.called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user