Compare commits

...

4 Commits

Author SHA1 Message Date
f39408fbcc fix time machine. (#148) 2023-02-11 04:17:43 -08:00
ca2f063297 update comment tile. (#146) 2023-02-11 02:30:51 -08:00
1ad231adbb fix offline mode. (#145) 2023-02-11 01:33:45 -08:00
60b09fd81e cleanup. (#143) 2023-02-11 00:39:30 -08:00
34 changed files with 391 additions and 487 deletions

View File

@ -137,7 +137,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6

View File

@ -41,8 +41,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
await _authRepository.loggedIn.then((bool loggedIn) async {
if (loggedIn) {
final String? username = await _authRepository.username;
final User user =
await _storiesRepository.fetchUserBy(userId: username!);
final User user = await _storiesRepository.fetchUser(id: username!);
emit(
state.copyWith(
@ -84,8 +83,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
);
if (successful) {
final User user =
await _storiesRepository.fetchUserBy(userId: event.username);
final User user = await _storiesRepository.fetchUser(id: event.username);
emit(
state.copyWith(
user: user,

View File

@ -56,15 +56,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
static const int _tabletSmallPageSize = 15;
static const int _tabletLargePageSize = 25;
/// Types of story to be shown in the tab bar.
static const Set<StoryType> types = <StoryType>{
StoryType.top,
StoryType.best,
StoryType.latest,
StoryType.ask,
StoryType.show,
};
Future<void> onInitialize(
StoriesInitialize event,
Emitter<StoriesState> emit,
@ -72,7 +63,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
_streamSubscription ??=
_preferenceCubit.stream.listen((PreferenceState event) {
final bool isComplexTile = event.complexStoryTileEnabled;
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
final int pageSize = getPageSize(isComplexTile: isComplexTile);
if (pageSize != state.currentPageSize) {
add(StoriesPageSizeChanged(pageSize: pageSize));
@ -80,7 +71,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
});
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
final bool isComplexTile = _preferenceCubit.state.complexStoryTileEnabled;
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
final int pageSize = getPageSize(isComplexTile: isComplexTile);
emit(
const StoriesState.init().copyWith(
offlineReading: hasCachedStories &&
@ -92,44 +83,45 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
storiesToBeDownloaded: state.storiesToBeDownloaded,
),
);
for (final StoryType type in types) {
await loadStories(of: type, emit: emit);
for (final StoryType type in StoryType.values) {
await loadStories(type: type, emit: emit);
}
}
Future<void> loadStories({
required StoryType of,
required StoryType type,
required Emitter<StoriesState> emit,
}) async {
if (state.offlineReading) {
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
final List<int> ids =
await _offlineRepository.getCachedStoryIds(type: type);
emit(
state
.copyWithStoryIdsUpdated(of: of, to: ids)
.copyWithCurrentPageUpdated(of: of, to: 0),
.copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0),
);
_offlineRepository
.getCachedStoriesStream(
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
)
.listen((Story story) {
add(StoryLoaded(story: story, type: of));
add(StoryLoaded(story: story, type: type));
}).onDone(() {
add(StoriesLoaded(type: of));
add(StoriesLoaded(type: type));
});
} else {
final List<int> ids = await _storiesRepository.fetchStoryIds(of: of);
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
emit(
state
.copyWithStoryIdsUpdated(of: of, to: ids)
.copyWithCurrentPageUpdated(of: of, to: 0),
.copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0),
);
_storiesRepository
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
.listen((Story story) {
add(StoryLoaded(story: story, type: of));
add(StoryLoaded(story: story, type: type));
}).onDone(() {
add(StoriesLoaded(type: of));
add(StoriesLoaded(type: type));
});
}
}
@ -140,7 +132,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
) async {
emit(
state.copyWithStatusUpdated(
of: event.type,
type: event.type,
to: StoriesStatus.loading,
),
);
@ -148,27 +140,29 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
if (state.offlineReading) {
emit(
state.copyWithStatusUpdated(
of: event.type,
type: event.type,
to: StoriesStatus.loaded,
),
);
} else {
emit(state.copyWithRefreshed(of: event.type));
await loadStories(of: event.type, emit: emit);
emit(state.copyWithRefreshed(type: event.type));
await loadStories(type: event.type, emit: emit);
}
}
void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
emit(
state.copyWithStatusUpdated(
of: event.type,
type: event.type,
to: StoriesStatus.loading,
),
);
final int currentPage = state.currentPageByType[event.type]!;
final int len = state.storyIdsByType[event.type]!.length;
emit(state.copyWithCurrentPageUpdated(of: event.type, to: currentPage + 1));
emit(
state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1),
);
final int currentPageSize = state.currentPageSize;
final int lower = currentPageSize * (currentPage + 1);
int upper = currentPageSize + lower;
@ -218,7 +212,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
} else {
emit(
state.copyWithStatusUpdated(
of: event.type,
type: event.type,
to: StoriesStatus.loaded,
),
);
@ -232,7 +226,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
emit(
state.copyWithStoryAdded(
of: event.type,
type: event.type,
story: event.story,
hasRead: hasRead,
),
@ -240,7 +234,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
}
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
emit(state.copyWithStatusUpdated(of: event.type, to: StoriesStatus.loaded));
emit(
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded),
);
}
Future<void> onDownload(
@ -258,12 +254,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _offlineRepository.deleteAllComments();
final Set<int> prioritizedIds = <int>{};
final List<StoryType> prioritizedTypes = <StoryType>[...types]
/// Prioritizing all types of stories except StoryType.latest since
/// new stories tend to have less or no comment at all.
final List<StoryType> prioritizedTypes = <StoryType>[...StoryType.values]
..remove(StoryType.latest);
for (final StoryType type in prioritizedTypes) {
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
await _offlineRepository.cacheStoryIds(of: type, ids: ids);
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
await _offlineRepository.cacheStoryIds(type: type, ids: ids);
prioritizedIds.addAll(ids);
}
@ -283,9 +282,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final Set<int> latestIds = <int>{};
final List<int> ids = await _storiesRepository.fetchStoryIds(
of: StoryType.latest,
type: StoryType.latest,
);
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
latestIds.addAll(ids);
await fetchAndCacheStories(
@ -311,10 +310,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
downloadStatus: StoriesDownloadStatus.canceled,
),
);
await _offlineRepository.deleteAllStoryIds();
await _offlineRepository.deleteAllStories();
await _offlineRepository.deleteAllComments();
}
Future<void> fetchAndCacheStories(
@ -322,11 +317,25 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
required bool includingWebPage,
required bool isPrioritized,
}) async {
final List<StreamSubscription<Comment>> downloadStreams =
<StreamSubscription<Comment>>[];
for (final int id in ids) {
if (state.downloadStatus == StoriesDownloadStatus.canceled) break;
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
_logger.d('aborting downloading');
for (final StreamSubscription<Comment> stream in downloadStreams) {
await stream.cancel();
}
_logger.d('deleting downloaded contents');
await _offlineRepository.deleteAllStoryIds();
await _offlineRepository.deleteAllStories();
await _offlineRepository.deleteAllComments();
break;
}
_logger.d('fetching story $id');
final Story? story = await _storiesRepository.fetchStoryBy(id);
final Story? story = await _storiesRepository.fetchStory(id: id);
if (story == null) {
if (isPrioritized) {
@ -349,17 +358,37 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _offlineRepository.cacheUrl(url: story.url);
}
_storiesRepository
/// Not awaiting the completion of comments stream because otherwise
/// it's going to take forever to finish downloading all the stories
/// since we need to make a single http call for each comment.
///
/// In other words, we are prioritizing the story itself instead of
/// the comments in the story.
late final StreamSubscription<Comment>? downloadStream;
downloadStream = _storiesRepository
.fetchAllChildrenComments(ids: story.kids)
.whereType<Comment>()
.listen(
(Comment comment) {
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
_logger.d('aborting downloading from comments stream');
downloadStream?.cancel();
return;
}
_logger.d('fetched comment ${comment.id}');
unawaited(
_offlineRepository.cacheComment(comment: comment),
);
},
).onDone(() => add(StoryDownloaded(skipped: false)));
)..onDone(() {
_logger.d(
'''finished downloading story ${story.id} with ${story.descendants} comments''',
);
add(StoryDownloaded(skipped: false));
});
downloadStreams.add(downloadStream);
}
}
@ -443,7 +472,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
bool hasRead(Story story) => state.readStoriesIds.contains(story.id);
int _getPageSize({required bool isComplexTile}) {
int getPageSize({required bool isComplexTile}) {
int pageSize = isComplexTile ? _smallPageSize : _largePageSize;
if (deviceScreenType != DeviceScreenType.mobile) {

View File

@ -103,13 +103,13 @@ class StoriesState extends Equatable {
}
StoriesState copyWithStoryAdded({
required StoryType of,
required StoryType type,
required Story story,
required bool hasRead,
}) {
final Map<StoryType, List<Story>> newMap =
Map<StoryType, List<Story>>.from(storiesByType);
newMap[of] = List<Story>.from(newMap[of]!)..add(story);
newMap[type] = List<Story>.from(newMap[type]!)..add(story);
return copyWith(
storiesByType: newMap,
readStoriesIds: <int>{
@ -120,54 +120,54 @@ class StoriesState extends Equatable {
}
StoriesState copyWithStoryIdsUpdated({
required StoryType of,
required StoryType type,
required List<int> to,
}) {
final Map<StoryType, List<int>> newMap =
Map<StoryType, List<int>>.from(storyIdsByType);
newMap[of] = to;
newMap[type] = to;
return copyWith(
storyIdsByType: newMap,
);
}
StoriesState copyWithStatusUpdated({
required StoryType of,
required StoryType type,
required StoriesStatus to,
}) {
final Map<StoryType, StoriesStatus> newMap =
Map<StoryType, StoriesStatus>.from(statusByType);
newMap[of] = to;
newMap[type] = to;
return copyWith(
statusByType: newMap,
);
}
StoriesState copyWithCurrentPageUpdated({
required StoryType of,
required StoryType type,
required int to,
}) {
final Map<StoryType, int> newMap =
Map<StoryType, int>.from(currentPageByType);
newMap[of] = to;
newMap[type] = to;
return copyWith(
currentPageByType: newMap,
);
}
StoriesState copyWithRefreshed({required StoryType of}) {
StoriesState copyWithRefreshed({required StoryType type}) {
final Map<StoryType, List<Story>> newStoriesMap =
Map<StoryType, List<Story>>.from(storiesByType);
newStoriesMap[of] = <Story>[];
newStoriesMap[type] = <Story>[];
final Map<StoryType, List<int>> newStoryIdsMap =
Map<StoryType, List<int>>.from(storyIdsByType);
newStoryIdsMap[of] = <int>[];
newStoryIdsMap[type] = <int>[];
final Map<StoryType, StoriesStatus> newStatusMap =
Map<StoryType, StoriesStatus>.from(statusByType);
newStatusMap[of] = StoriesStatus.loading;
newStatusMap[type] = StoriesStatus.loading;
final Map<StoryType, int> newCurrentPageMap =
Map<StoryType, int>.from(currentPageByType);
newCurrentPageMap[of] = 0;
newCurrentPageMap[type] = 0;
return copyWith(
storiesByType: newStoriesMap,
storyIdsByType: newStoryIdsMap,

View File

@ -61,6 +61,6 @@ abstract class Constants {
}
abstract class RegExpConstants {
static const String linkSuffix = r'(\)|])(.)*$';
static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
static const String number = '[0-9]+';
}

View File

@ -106,7 +106,7 @@ class CommentsCubit extends Cubit<CommentsState> {
final Item item = state.item;
final Item updatedItem = state.offlineReading
? item
: await _storiesRepository.fetchItemBy(id: item.id) ?? item;
: await _storiesRepository.fetchItem(id: item.id) ?? item;
final List<int> kids = sortKids(updatedItem.kids);
emit(state.copyWith(item: updatedItem));
@ -173,7 +173,7 @@ class CommentsCubit extends Cubit<CommentsState> {
final Item item = state.item;
final Item updatedItem =
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
await _storiesRepository.fetchItem(id: item.id) ?? item;
final List<int> kids = sortKids(updatedItem.kids);
if (state.fetchMode == FetchMode.lazy) {

View File

@ -73,7 +73,7 @@ class FavCubit extends Cubit<FavState> {
),
);
final Item? item = await _storiesRepository.fetchItemBy(id: id);
final Item? item = await _storiesRepository.fetchItem(id: id);
if (item == null) return;

View File

@ -28,7 +28,7 @@ class HistoryCubit extends Cubit<HistoryState> {
final String username = authState.username;
_storiesRepository
.fetchSubmitted(of: username)
.fetchSubmitted(userId: username)
.then((List<int>? submittedIds) {
emit(
state.copyWith(
@ -94,7 +94,7 @@ class HistoryCubit extends Cubit<HistoryState> {
);
_storiesRepository
.fetchSubmitted(of: username)
.fetchSubmitted(userId: username)
.then((List<int>? submittedIds) {
emit(state.copyWith(submittedIds: submittedIds));
if (submittedIds != null) {

View File

@ -81,7 +81,7 @@ class NotificationCubit extends Cubit<NotificationState> {
for (final int id in commentsToBeLoaded) {
Comment? comment = await _sembastRepository.getComment(id: id);
comment ??= await _storiesRepository.fetchCommentBy(id: id);
comment ??= await _storiesRepository.fetchComment(id: id);
if (comment != null) {
emit(
state.copyWith(
@ -159,7 +159,7 @@ class NotificationCubit extends Cubit<NotificationState> {
for (final int id in commentsToBeLoaded) {
Comment? comment = await _sembastRepository.getComment(id: id);
comment ??= await _storiesRepository.fetchCommentBy(id: id);
comment ??= await _storiesRepository.fetchComment(id: id);
if (comment != null) {
emit(state.copyWith(comments: <Comment>[...state.comments, comment]));
}
@ -184,7 +184,7 @@ class NotificationCubit extends Cubit<NotificationState> {
Future<void> _fetchReplies() {
return _storiesRepository
.fetchSubmitted(of: _authBloc.state.username)
.fetchSubmitted(userId: _authBloc.state.username)
.then((List<int>? submittedItems) async {
if (submittedItems != null) {
final List<int> subscribedItems = submittedItems.sublist(
@ -193,7 +193,7 @@ class NotificationCubit extends Cubit<NotificationState> {
);
for (final int id in subscribedItems) {
await _storiesRepository.fetchItemBy(id: id).then((Item? item) async {
await _storiesRepository.fetchItem(id: id).then((Item? item) async {
final List<int> kids = item?.kids ?? <int>[];
final List<int> previousKids =
(await _sembastRepository.kids(of: id)) ?? <int>[];
@ -216,7 +216,7 @@ class NotificationCubit extends Cubit<NotificationState> {
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
);
await _storiesRepository
.fetchCommentBy(id: newCommentId)
.fetchComment(id: newCommentId)
.then((Comment? comment) {
if (comment != null && !comment.dead && !comment.deleted) {
_sembastRepository

View File

@ -33,7 +33,7 @@ class PollCubit extends Cubit<PollState> {
if (pollOptionsIds.isEmpty || refresh) {
final Story? updatedStory =
await _storiesRepository.fetchStoryBy(_story.id);
await _storiesRepository.fetchStory(id: _story.id);
if (updatedStory != null) {
pollOptionsIds = updatedStory.parts;

View File

@ -16,7 +16,7 @@ class UserCubit extends Cubit<UserState> {
void init({required String userId}) {
emit(state.copyWith(status: UserStatus.loading));
_storiesRepository.fetchUserBy(userId: userId).then((User user) {
_storiesRepository.fetchUser(id: userId).then((User user) {
emit(state.copyWith(user: user, status: UserStatus.loaded));
}).onError((_, __) {
emit(state.copyWith(status: UserStatus.failure));

View File

@ -95,7 +95,7 @@ extension StateExtension on State {
if (id != null) {
await locator
.get<StoriesRepository>()
.fetchItemBy(id: id)
.fetchItem(id: id)
.then((Item? item) {
if (mounted) {
if (item != null) {

View File

@ -2,6 +2,8 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:hacki/models/comment.dart';
import 'package:hacki/models/models.dart';
/// [BuildableComment] is a subtype of [Comment] which stores
/// the corresponding [LinkifyElement] for faster widget building.
class BuildableComment extends Comment {
BuildableComment({
required super.id,

View File

@ -1,6 +1,10 @@
import 'package:equatable/equatable.dart';
import 'package:hacki/extensions/date_time_extension.dart';
import 'package:hacki/models/comment.dart';
import 'package:hacki/models/poll_option.dart';
import 'package:hacki/models/story.dart';
/// [Item] is the base type of [Story], [Comment] and [PollOption].
class Item extends Equatable {
const Item({
required this.id,

View File

@ -2,10 +2,16 @@ import 'dart:async';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/post_repository.dart';
import 'package:hacki/repositories/postable_repository.dart';
import 'package:hacki/repositories/preference_repository.dart';
import 'package:logger/logger.dart';
/// [AuthRepository] if for logging user in/out and performing actions
/// that require a logged in user such as [flag], [favorite], [upvote],
/// and [downvote].
///
/// For posting actions such as posting a comment, see [PostRepository].
class AuthRepository extends PostableRepository {
AuthRepository({
super.dio,

View File

@ -4,9 +4,14 @@ import 'package:hacki/models/models.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
/// [OfflineRepository] is for storing stories and comments for offline reading.
/// It's using [Hive] as its database which is being stored in temp directory.
/// [OfflineRepository] is for storing [Story] and [Comment] for
/// offline reading.
///
/// [Hive] is used as its database and is being stored in the temporary
/// directory assigned by host system which you can retrieve
/// by calling [getTemporaryDirectory].
class OfflineRepository {
OfflineRepository({
Future<Box<List<int>>>? storyIdBox,
@ -36,7 +41,7 @@ class OfflineRepository {
_storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty);
Future<void> cacheStoryIds({
required StoryType of,
required StoryType type,
required List<int> ids,
}) async {
late final Box<List<int>> box;
@ -49,7 +54,7 @@ class OfflineRepository {
box = await _storyIdBox;
}
return box.put(of.name, ids);
return box.put(type.name, ids);
}
Future<void> cacheStory({required Story story}) async {
@ -103,10 +108,10 @@ class OfflineRepository {
}
}
Future<List<int>> getCachedStoryIds({required StoryType of}) async {
Future<List<int>> getCachedStoryIds({required StoryType type}) async {
try {
final Box<List<int>> box = await _storyIdBox;
final List<int>? ids = box.get(of.name);
final List<int>? ids = box.get(type.name);
return ids ?? <int>[];
} catch (_) {
_logger.e(_);

View File

@ -7,6 +7,7 @@ import 'package:hacki/repositories/postable_repository.dart';
import 'package:hacki/repositories/preference_repository.dart';
import 'package:hacki/utils/utils.dart';
/// [PostRepository] is for posting contents to Hacker News.
class PostRepository extends PostableRepository {
PostRepository({super.dio, PreferenceRepository? storageRepository})
: _preferenceRepository =

View File

@ -3,8 +3,12 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/auth_repository.dart';
import 'package:hacki/repositories/post_repository.dart';
import 'package:hacki/utils/service_exception.dart';
/// [PostableRepository] is solely for hosting functionalities shared between
/// [AuthRepository] and [PostRepository].
class PostableRepository {
PostableRepository({
Dio? dio,

View File

@ -7,6 +7,7 @@ import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
/// [PreferenceRepository] is for storing user preferences.
class PreferenceRepository {
PreferenceRepository({
SyncedSharedPreferences? syncedPrefs,

View File

@ -3,6 +3,9 @@ import 'package:flutter/foundation.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/utils/utils.dart';
/// [SearchRepository] is for searching contents on Hacker News.
///
/// You can learn about the search API at https://hn.algolia.com/api.
class SearchRepository {
SearchRepository({Dio? dio}) : _dio = dio ?? Dio();

View File

@ -7,7 +7,10 @@ import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
/// [SembastRepository] is for storing stories and comments for faster loading.
/// It's using Sembast as its database which is being stored in doc directory.
///
/// Sembast [Database] is used as its database and is being stored in the
/// documents directory assigned by host system which you can retrieve
/// by calling [getApplicationDocumentsDirectory].
class SembastRepository {
SembastRepository({Database? database}) {
if (database == null) {

View File

@ -4,6 +4,11 @@ import 'package:hacki/services/services.dart';
import 'package:hacki/utils/utils.dart';
import 'package:tuple/tuple.dart';
/// [StoriesRepository] is for fetching
/// [Item] such as [Story], [PollOption], [Comment] or [User].
///
/// You can learn more about the Hacker News API at
/// https://github.com/HackerNews/API.
class StoriesRepository {
StoriesRepository({
FirebaseClient? firebaseClient,
@ -12,9 +17,11 @@ class StoriesRepository {
final FirebaseClient _firebaseClient;
static const String _baseUrl = 'https://hacker-news.firebaseio.com/v0/';
Future<User> fetchUserBy({required String userId}) async {
/// Fetch a [User] by its [id].
/// Hacker News uses user's username as [id].
Future<User> fetchUser({required String id}) async {
final User user = await _firebaseClient
.get('${_baseUrl}user/$userId.json')
.get('${_baseUrl}user/$id.json')
.then((dynamic val) {
final Map<String, dynamic> json = val as Map<String, dynamic>;
final User user = User.fromJson(json);
@ -24,9 +31,10 @@ class StoriesRepository {
return user;
}
Future<List<int>> fetchStoryIds({required StoryType of}) async {
/// Fetch ids of stories of a certain [StoryType].
Future<List<int>> fetchStoryIds({required StoryType type}) async {
final List<int> ids = await _firebaseClient
.get('$_baseUrl${of.path}.json')
.get('$_baseUrl${type.path}.json')
.then((dynamic val) {
final List<int> ids = (val as List<dynamic>).cast<int>();
return ids;
@ -35,7 +43,8 @@ class StoriesRepository {
return ids;
}
Future<Story?> fetchStoryBy(int id) async {
/// Fetch a [Story] based on its id.
Future<Story?> fetchStory({required int id}) async {
final Story? story = await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
@ -48,6 +57,8 @@ class StoriesRepository {
return story;
}
/// Fetch a list of [Comment] based on ids and return results
/// using a stream.
Stream<Comment> fetchCommentsStream({
required List<int> ids,
int level = 0,
@ -73,6 +84,8 @@ class StoriesRepository {
return;
}
/// Fetch a list of [Comment] based on ids recursively and
/// return results using a stream.
Stream<Comment> fetchAllCommentsRecursivelyStream({
required List<int> ids,
int level = 0,
@ -104,6 +117,8 @@ class StoriesRepository {
return;
}
/// Fetch a list of [Item] based on ids and return results
/// using a stream.
Stream<Item> fetchItemsStream({required List<int> ids}) async* {
for (final int id in ids) {
final Item? item = await _firebaseClient
@ -129,6 +144,8 @@ class StoriesRepository {
}
}
/// Fetch a list of [Story] based on ids and return results
/// using a stream.
Stream<Story> fetchStoriesStream({required List<int> ids}) async* {
for (final int id in ids) {
final Story? story = await _firebaseClient
@ -146,6 +163,8 @@ class StoriesRepository {
}
}
/// Fetch a list of [PollOption] based on ids and return results
/// using a stream.
Stream<PollOption> fetchPollOptionsStream({required List<int> ids}) async* {
for (final int id in ids) {
final PollOption? option = await _firebaseClient
@ -163,7 +182,8 @@ class StoriesRepository {
}
}
Future<Comment?> fetchCommentBy({required int id}) async {
/// Fetch a [Comment] based on its id.
Future<Comment?> fetchComment({required int id}) async {
final Comment? comment = await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
@ -177,7 +197,10 @@ class StoriesRepository {
return comment;
}
Future<Comment?> fetchRawCommentBy({required int id}) async {
/// Fetch a raw [Comment] based on its id.
/// The content of [Comment] will not be parsed, use this function only if
/// the format of content doesn't matter, otherwise, use [fetchComment].
Future<Comment?> fetchRawComment({required int id}) async {
final Comment? comment = await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic val) async {
@ -191,7 +214,8 @@ class StoriesRepository {
return comment;
}
Future<Item?> fetchItemBy({required int id}) async {
/// Fetch a [Item] based on its id.
Future<Item?> fetchItem({required int id}) async {
final Item? item = await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
@ -212,7 +236,10 @@ class StoriesRepository {
return item;
}
Future<Item?> fetchRawItemBy({required int id}) async {
/// Fetch a raw [Item] based on its id.
/// The content of [Item] will not be parsed, use this function only if
/// the format of content doesn't matter, otherwise, use [fetchItem].
Future<Item?> fetchRawItem({required int id}) async {
final Item? item = await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic val) {
@ -234,9 +261,10 @@ class StoriesRepository {
return item;
}
Future<List<int>?> fetchSubmitted({required String of}) async {
/// Fetch a list of ids of [Story] or [Comment] submitted by the user.
Future<List<int>?> fetchSubmitted({required String userId}) async {
final List<int>? submitted = await _firebaseClient
.get('${_baseUrl}user/$of.json')
.get('${_baseUrl}user/$userId.json')
.then((dynamic val) {
if (val == null) {
return null;
@ -250,28 +278,34 @@ class StoriesRepository {
return submitted;
}
/// Fetch the parent [Story] of a [Comment].
Future<Story?> fetchParentStory({required int id}) async {
Item? item;
do {
item = await fetchItemBy(id: item?.parent ?? id);
item = await fetchItem(id: item?.parent ?? id);
if (item == null) return null;
} while (item is Comment);
return item as Story;
}
/// Fetch the raw parent [Story] of a [Comment].
/// The content of [Story] will not be parsed, use this function only if
/// the format of content doesn't matter, otherwise, use [fetchParentStory].
Future<Story?> fetchRawParentStory({required int id}) async {
Item? item;
do {
item = await fetchRawItemBy(id: item?.parent ?? id);
item = await fetchRawItem(id: item?.parent ?? id);
if (item == null) return null;
} while (item is Comment);
return item as Story;
}
/// Fetch the parent [Story] of a [Comment] as well as
/// the list of [Comment] traversed in order to reach the parent.
Future<Tuple2<Story, List<Comment>>?> fetchParentStoryWithComments({
required int id,
}) async {
@ -279,7 +313,7 @@ class StoriesRepository {
final List<Comment> parentComments = <Comment>[];
do {
item = await fetchItemBy(id: item?.parent ?? id);
item = await fetchItem(id: item?.parent ?? id);
if (item is Comment) {
parentComments.add(item);
}
@ -297,9 +331,10 @@ class StoriesRepository {
);
}
/// Fetch a list of [Comment] based on ids recursively.
Stream<Comment?> fetchAllChildrenComments({required List<int> ids}) async* {
for (final int id in ids) {
final Comment? comment = await fetchCommentBy(id: id);
final Comment? comment = await fetchComment(id: id);
if (comment != null) {
yield comment;
yield* fetchAllChildrenComments(ids: comment.kids);
@ -307,6 +342,7 @@ class StoriesRepository {
}
}
/// Parse the json of an [Item] by removing useless HTML tags.
Future<Map<String, dynamic>?> _parseJson(Map<String, dynamic>? json) async {
if (json == null) return null;
final String text = json['text'] as String? ?? '';

View File

@ -278,7 +278,7 @@ class _HomeScreenState extends State<HomeScreen>
final int? id = event.itemId;
if (id != null) {
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
locator.get<StoriesRepository>().fetchItem(id: id).then((Item? item) {
if (mounted) {
if (item != null) {
goToItemScreen(
@ -298,7 +298,7 @@ class _HomeScreenState extends State<HomeScreen>
await locator
.get<StoriesRepository>()
.fetchStoryBy(storyId)
.fetchStory(id: storyId)
.then((Story? story) {
if (story == null) {
showErrorSnackBar();
@ -323,7 +323,7 @@ class _HomeScreenState extends State<HomeScreen>
await locator
.get<StoriesRepository>()
.fetchStoryBy(storyId)
.fetchStory(id: storyId)
.then((Story? story) {
if (story == null) {
showErrorSnackBar();

View File

@ -1,5 +1,3 @@
// ignore_for_file: comment_references
import 'package:equatable/equatable.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
@ -13,6 +11,7 @@ import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/stories_repository.dart';
import 'package:hacki/screens/item/widgets/widgets.dart';
import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart';
@ -33,7 +32,7 @@ class ItemScreenArgs extends Equatable {
final List<Comment>? targetComments;
/// when a user is trying to view a sub-thread from a main thread, we don't
/// need to fetch comments from [StoryRepository] since we have some, if not
/// need to fetch comments from [StoriesRepository] since we have some, if not
/// all, comments cached in [CommentCache].
final bool useCommentCache;

View File

@ -342,13 +342,13 @@ class _ParentItemSection extends StatelessWidget {
),
child: RichText(
textAlign: TextAlign.center,
textScaleFactor: MediaQuery.of(
context,
).textScaleFactor,
text: TextSpan(
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
fontSize: prefState.fontSize.fontSize,
color: Theme.of(context)
.textTheme
.bodyLarge
@ -359,10 +359,7 @@ class _ParentItemSection extends StatelessWidget {
text: state.item.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
fontSize: prefState.fontSize.fontSize,
color: state.item.url.isNotEmpty
? Palette.orange
: null,
@ -374,10 +371,8 @@ class _ParentItemSection extends StatelessWidget {
''' (${(state.item as Story).readableUrl})''',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
(prefState.fontSize.fontSize - 4),
fontSize:
prefState.fontSize.fontSize - 4,
color: Palette.orange,
),
),
@ -391,36 +386,39 @@ class _ParentItemSection extends StatelessWidget {
height: Dimens.pt6,
),
if (state.item.text.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt10,
),
child: SelectableLinkify(
text: state.item.text,
style: TextStyle(
fontSize: MediaQuery.of(context).textScaleFactor *
context
.read<PreferenceCubit>()
.state
.fontSize
.fontSize,
SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt10,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context).textScaleFactor *
context
.read<PreferenceCubit>()
.state
.fontSize
.fontSize,
color: Palette.orange,
child: SelectableLinkify(
text: state.item.text,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
style: TextStyle(
fontSize: context
.read<PreferenceCubit>()
.state
.fontSize
.fontSize,
),
linkStyle: TextStyle(
fontSize: context
.read<PreferenceCubit>()
.state
.fontSize
.fontSize,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
),
],

View File

@ -336,9 +336,10 @@ class _ReplyBoxState extends State<ReplyBox> {
child: SingleChildScrollView(
child: SelectableLinkify(
scrollPhysics: const NeverScrollableScrollPhysics(),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context).textScaleFactor *
TextDimens.pt15,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
linkStyle: const TextStyle(
fontSize: TextDimens.pt15,
color: Palette.orange,
),
onOpen: (LinkableElement link) =>

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
@ -384,193 +383,6 @@ class _ProfileScreenState extends State<ProfileScreen>
});
}
void onLoginTapped() {
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return BlocConsumer<AuthBloc, AuthState>(
listener: (BuildContext context, AuthState state) {
if (state.isLoggedIn) {
Navigator.pop(context);
showSnackBar(content: 'Logged in successfully!');
}
},
builder: (BuildContext context, AuthState state) {
return SimpleDialog(
children: <Widget>[
if (state.status == AuthStatus.loading)
const SizedBox(
height: Dimens.pt36,
width: Dimens.pt36,
child: Center(
child: CircularProgressIndicator(
color: Palette.orange,
),
),
)
else if (!state.isLoggedIn) ...<Widget>[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt18,
),
child: TextField(
controller: usernameController,
cursorColor: Palette.orange,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'Username',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
),
),
),
),
const SizedBox(
height: Dimens.pt16,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt18,
),
child: TextField(
controller: passwordController,
cursorColor: Palette.orange,
obscureText: true,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'Password',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
),
),
),
),
const SizedBox(
height: Dimens.pt16,
),
if (state.status == AuthStatus.failure)
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt18,
),
child: Text(
Constants.errorMessage,
style: const TextStyle(
color: Palette.grey,
fontSize: TextDimens.pt12,
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(
state.agreedToEULA
? Icons.check_box
: Icons.check_box_outline_blank,
color: state.agreedToEULA
? Palette.deepOrange
: Palette.grey,
),
onPressed: () => context
.read<AuthBloc>()
.add(AuthToggleAgreeToEULA()),
),
Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(
text: 'I agree to ',
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
WidgetSpan(
child: Transform.translate(
offset: const Offset(0, 1),
child: TapDownWrapper(
onTap: () => LinkUtil.launch(
Constants.endUserAgreementLink,
),
child: const Text(
'End User Agreement',
style: TextStyle(
color: Palette.deepOrange,
decoration: TextDecoration.underline,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
)
],
),
Padding(
padding: const EdgeInsets.only(
right: Dimens.pt12,
),
child: ButtonBar(
children: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
context.read<AuthBloc>().add(AuthInitialize());
},
child: const Text(
'Cancel',
),
),
ElevatedButton(
onPressed: () {
if (state.agreedToEULA) {
final String username = usernameController.text;
final String password = passwordController.text;
if (username.isNotEmpty && password.isNotEmpty) {
context.read<AuthBloc>().add(
AuthLogin(
username: username,
password: password,
),
);
}
}
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
state.agreedToEULA
? Palette.deepOrange
: Palette.grey,
),
),
child: const Text(
'Log in',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Palette.white,
),
),
),
],
),
),
],
],
);
},
);
},
);
}
@override
bool get wantKeepAlive => true;
}

View File

@ -186,61 +186,14 @@ class CommentTile extends StatelessWidget {
top: Dimens.pt6,
bottom: Dimens.pt12,
),
child: comment is BuildableComment
? SelectableText.rich(
key: ValueKey<int>(comment.id),
buildTextSpan(
(comment as BuildableComment)
.elements,
style: TextStyle(
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
decoration:
TextDecoration.underline,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped
.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
onTap: () => onTextTapped(context),
)
: SelectableLinkify(
key: ValueKey<int>(comment.id),
text: comment.text,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
prefState.fontSize.fontSize,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
onTap: () => onTextTapped(context),
),
child: SizedBox(
width: double.infinity,
child: _CommentText(
key: ValueKey<int>(comment.id),
comment: comment,
onLinkTapped: _onLinkTapped,
),
),
),
],
),
@ -335,13 +288,6 @@ class CommentTile extends StatelessWidget {
);
}
void onTextTapped(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse();
}
}
Color _getColor(int level) {
final int initialLevel = level;
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
@ -372,11 +318,70 @@ class CommentTile extends StatelessWidget {
bool _shouldShowLoadButton(BuildContext context) {
final CollapseState collapseState = context.read<CollapseCubit>().state;
final CommentsState commentsState = context.read<CommentsCubit>().state;
final CommentsState? commentsState =
context.tryRead<CommentsCubit>()?.state;
return fetchMode == FetchMode.lazy &&
comment.kids.isNotEmpty &&
collapseState.collapsed == false &&
commentsState.commentIds.contains(comment.kids.first) == false &&
commentsState.onlyShowTargetComment == false;
commentsState?.commentIds.contains(comment.kids.first) == false &&
commentsState?.onlyShowTargetComment == false;
}
void _onLinkTapped(LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped.call(link.url);
} else {
LinkUtil.launch(link.url);
}
}
}
class _CommentText extends StatelessWidget {
const _CommentText({
super.key,
required this.comment,
required this.onLinkTapped,
});
final Comment comment;
final void Function(LinkableElement) onLinkTapped;
@override
Widget build(BuildContext context) {
final PreferenceState prefState = context.read<PreferenceCubit>().state;
final TextStyle style = TextStyle(
fontSize: prefState.fontSize.fontSize,
);
final TextStyle linkStyle = TextStyle(
fontSize: prefState.fontSize.fontSize,
decoration: TextDecoration.underline,
color: Palette.orange,
);
if (comment is BuildableComment) {
return SelectableText.rich(
buildTextSpan(
(comment as BuildableComment).elements,
style: style,
linkStyle: linkStyle,
onOpen: onLinkTapped,
),
onTap: () => onTextTapped(context),
);
} else {
return SelectableLinkify(
text: comment.text,
style: style,
linkStyle: linkStyle,
onOpen: onLinkTapped,
onTap: () => onTextTapped(context),
);
}
}
void onTextTapped(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse();
}
}
}

View File

@ -108,7 +108,7 @@ class _CountDownReminderState extends State<CountdownReminder>
if (state.storyId != null) {
locator
.get<StoriesRepository>()
.fetchStoryBy(state.storyId!)
.fetchStory(id: state.storyId!)
.then((Story? story) {
if (story == null) {
showErrorSnackBar();

View File

@ -50,7 +50,7 @@ abstract class Fetcher {
Comment? newReply;
await storiesRepository
.fetchSubmitted(of: username)
.fetchSubmitted(userId: username)
.then((List<int>? submittedItems) async {
if (submittedItems != null) {
final List<int> subscribedItems = submittedItems.sublist(
@ -59,9 +59,7 @@ abstract class Fetcher {
);
for (final int id in subscribedItems) {
await storiesRepository
.fetchRawItemBy(id: id)
.then((Item? item) async {
await storiesRepository.fetchRawItem(id: id).then((Item? item) async {
final List<int> kids = item?.kids ?? <int>[];
final List<int> previousKids =
(await sembastRepository.kids(of: id)) ?? <int>[];
@ -76,7 +74,7 @@ abstract class Fetcher {
if (unreadIds.contains(newCommentId)) continue;
await storiesRepository
.fetchRawCommentBy(id: newCommentId)
.fetchRawComment(id: newCommentId)
.then((Comment? comment) async {
final bool hasPushedBefore =
await preferenceRepository.hasPushed(newReply!.id);

View File

@ -294,7 +294,7 @@ class WebAnalyzer {
// 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.
if (kids.isEmpty) {
final Story? story = await storiesRepository.fetchStoryBy(storyId);
final Story? story = await storiesRepository.fetchStory(id: storyId);
if (story == null) return null;
@ -304,7 +304,7 @@ class WebAnalyzer {
}
final Comment? comment =
await storiesRepository.fetchCommentBy(id: kids.first);
await storiesRepository.fetchComment(id: kids.first);
return comment != null ? '${comment.by}: ${comment.text}' : null;
}

View File

@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: args
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
async:
dependency: transitive
description:
@ -61,18 +61,18 @@ packages:
dependency: "direct main"
description:
name: bloc
sha256: bd4f8027bfa60d96c8046dec5ce74c463b2c918dce1b0d36593575995344534a
sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80"
url: "https://pub.dev"
source: hosted
version: "8.1.0"
version: "8.1.1"
bloc_test:
dependency: "direct dev"
description:
name: bloc_test
sha256: "622b97678bf8c06a94f4c26a89ee9ebf7319bf775383dee2233e86e1f94ee28d"
sha256: ffbb60c17ee3d8e3784cb78071088e353199057233665541e8ac6cd438dca8ad
url: "https://pub.dev"
source: hosted
version: "9.1.0"
version: "9.1.1"
boolean_selector:
dependency: transitive
description:
@ -141,18 +141,18 @@ packages:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "745ebcccb1ef73768386154428a55250bc8d44059c19fd27aecda2a6dc013a22"
sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: b8795b9238bf83b64375f63492034cb3d8e222af4d9ce59dda085edf038fa06f
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
url: "https://pub.dev"
source: hosted
version: "1.2.3"
version: "1.2.4"
convert:
dependency: transitive
description:
@ -165,18 +165,18 @@ packages:
dependency: transitive
description:
name: coverage
sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040"
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
url: "https://pub.dev"
source: hosted
version: "1.6.2"
version: "1.6.3"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
url: "https://pub.dev"
source: hosted
version: "0.3.3+2"
version: "0.3.3+4"
crypto:
dependency: transitive
description:
@ -275,10 +275,10 @@ packages:
dependency: "direct main"
description:
name: flutter_bloc
sha256: "890c51c8007f0182360e523518a0c732efb89876cb4669307af7efada5b55557"
sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775"
url: "https://pub.dev"
source: hosted
version: "8.1.1"
version: "8.1.2"
flutter_blurhash:
dependency: transitive
description:
@ -368,26 +368,26 @@ packages:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: f2afec1f1762c040a349ea2a588e32f442da5d0db3494a52a929a97c9e550bc5
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
version: "8.0.0"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "736436adaf91552433823f51ce22e098c2f0551db06b6596f58597a25b8ea797"
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: ff0768a6700ea1d9620e03518e2e25eac86a8bd07ca3556e9617bfa5ace4bd00
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.0"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
@ -408,10 +408,10 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: ca89c8059cf439985aa83c59619b3674c7ef6cc2e86943d169a7369d6a69cab5
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "2.0.0"
flutter_siri_suggestions:
dependency: "direct main"
description:
@ -442,10 +442,10 @@ packages:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: "875dbb9ec1ad30d68102019ceb682760d06c72747c1c5b7885781b95f88569cc"
sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206"
url: "https://pub.dev"
source: hosted
version: "10.3.0"
version: "10.4.0"
frontend_server_client:
dependency: transitive
description:
@ -535,10 +535,10 @@ packages:
dependency: "direct main"
description:
name: hydrated_bloc
sha256: "5871204f14b24638dc9d18d5b94cf22a66fc4be40756925cafff3a7553c7d7b7"
sha256: eb92d88061b6b911c48779b08a91c8a9f3a3aa8475f80d9380045375d9876536
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "9.1.0"
integration_test:
dependency: "direct dev"
description: flutter
@ -676,10 +676,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: f619162573096d428ccde2e33f92e05b5a179cd6f0e3120c1005f181bee8ed16
sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
package_info_plus_platform_interface:
dependency: transitive
description:
@ -724,10 +724,10 @@ packages:
dependency: transitive
description:
name: path_provider_linux
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
path_provider_platform_interface:
dependency: transitive
description:
@ -829,10 +829,10 @@ packages:
dependency: "direct main"
description:
name: responsive_builder
sha256: "0f082dff291f5ee4b4ef713d7d1e2a242b126204559024de07039aa7d9012aa5"
sha256: "8eed603781a53fe1804a9ba50089ceb4882887f9c5b84ff139b03d8583a12fc9"
url: "https://pub.dev"
source: hosted
version: "0.5.0+1"
version: "0.5.1"
rxdart:
dependency: "direct main"
description:
@ -853,10 +853,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: e387077716f80609bb979cd199331033326033ecd1c8f200a90c5f57b1c9f55e
sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
share_plus_platform_interface:
dependency: transitive
description:
@ -885,10 +885,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences_foundation
sha256: "1ffa239043ab8baf881ec3094a3c767af9d10399b2839020b9e4d44c0bb23951"
sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
shared_preferences_linux:
dependency: transitive
description:
@ -1121,10 +1121,10 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809"
sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b
url: "https://pub.dev"
source: hosted
version: "6.1.8"
version: "6.1.9"
url_launcher_android:
dependency: transitive
description:
@ -1137,10 +1137,10 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3
sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815"
url: "https://pub.dev"
source: hosted
version: "6.0.18"
version: "6.1.0"
url_launcher_linux:
dependency: transitive
description:
@ -1201,10 +1201,10 @@ packages:
dependency: "direct dev"
description:
name: very_good_analysis
sha256: "4815adc7ded57657038d2bb2a7f332c50e3c8152f7d3c6acf8f6b7c0cc81e5e2"
sha256: ebc48c51db35beeeec8c414e32f7bd78e612bd7f5992ccb0d46e19edaeb40b08
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "4.0.0+1"
vm_service:
dependency: transitive
description:
@ -1297,10 +1297,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: "9d97fa2bae0f1900553c48a2ef0aaa3864367fd7bb625d683c460754b691312c"
sha256: "5f49a6e5fc59e21fcec5e1bbcd401afbee9792a24a4f3d9cef9b5bb0cd1e3767"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.4"
webview_flutter_platform_interface:
dependency: transitive
description:
@ -1313,10 +1313,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "523aff9168af9bb2170e4809e0499d7dee065c3919799fd3341d3e616c137960"
sha256: "92e7e7fa468f1df597fb9d37bcf1f303175cbe147c4dbdf06ecc323d950116eb"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.5"
win32:
dependency: transitive
description:
@ -1358,5 +1358,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.0 <3.0.0"
dart: ">=2.19.0 <3.0.0"
flutter: ">=3.7.3"

View File

@ -1,6 +1,6 @@
name: hacki
description: A Hacker News reader.
version: 1.0.10+88
version: 1.0.11+89
publish_to: none
environment:
@ -10,12 +10,12 @@ environment:
dependencies:
adaptive_theme: ^3.0.0
badges: ^3.0.2
bloc: ^8.1.0
cached_network_image: ^3.2.1
bloc: ^8.1.1
cached_network_image: ^3.2.3
clipboard: ^0.1.3
collection: ^1.17.0
connectivity_plus: ^3.0.2
dio: ^4.0.4
dio: ^4.0.6
equatable: ^2.0.5
fast_gbk: ^1.0.0
feature_discovery:
@ -24,7 +24,7 @@ dependencies:
ref: flutter3_compatibility
flutter:
sdk: flutter
flutter_bloc: ^8.1.1
flutter_bloc: ^8.1.2
flutter_cache_manager: ^3.3.0
flutter_email_sender: ^5.2.0
flutter_fadein: ^2.0.0
@ -32,45 +32,45 @@ dependencies:
flutter_inappwebview: ^5.7.2+3
flutter_linkify: ^5.0.2
flutter_local_notifications: ^13.0.0
flutter_secure_storage: ^7.0.1
flutter_secure_storage: ^8.0.0
flutter_siri_suggestions: ^2.1.0
flutter_slidable: ^2.0.0
font_awesome_flutter: ^10.3.0
gbk_codec: ^0.4.0
get_it: 7.2.0
hive: ^2.0.6
html: ^0.15.0
get_it: ^7.2.0
hive: ^2.2.3
html: ^0.15.1
html_unescape: ^2.0.0
http: ^0.13.3
hydrated_bloc: ^9.0.0-dev.3
http: ^0.13.5
hydrated_bloc: ^9.1.0
intl: ^0.18.0
logger: ^1.1.0
package_info_plus: ^3.0.2
package_info_plus: ^3.0.3
path: ^1.8.2
path_provider: ^2.0.8
path_provider_android: ^2.0.8
path_provider: ^2.0.12
path_provider_android: ^2.0.22
path_provider_foundation: ^2.1.1
pull_to_refresh:
git:
url: https://github.com/livinglist/flutter_pulltorefresh
ref: master
receive_sharing_intent: ^1.4.5
responsive_builder: ^0.5.0+1
rxdart: ^0.27.3
sembast: ^3.1.1+1
share_plus: ^6.3.0
responsive_builder: ^0.5.1
rxdart: ^0.27.7
sembast: ^3.4.0+6
share_plus: ^6.3.1
shared_preferences: ^2.0.17
shared_preferences_android: ^2.0.15
shared_preferences_foundation: ^2.1.2
shared_preferences_foundation: ^2.1.3
shimmer: ^2.0.0
synced_shared_preferences:
path: components/synced_shared_preferences
tuple: ^2.0.0
tuple: ^2.0.1
universal_platform: ^1.0.0+1
url_launcher: ^6.1.3
url_launcher: ^6.1.9
wakelock: ^0.6.1+2
webview_flutter: ^4.0.2
workmanager: ^0.5.0
workmanager: ^0.5.1
dev_dependencies:
bloc_test: ^9.1.0
@ -81,7 +81,7 @@ dev_dependencies:
integration_test:
sdk: flutter
mocktail: ^0.3.0
very_good_analysis: ^3.1.0
very_good_analysis: ^4.0.0+1
flutter:
uses-material-design: true

View File

@ -67,7 +67,7 @@ void main() {
.thenAnswer((_) => Future<String?>.value(username));
when(() => mockAuthRepository.password)
.thenAnswer((_) => Future<String>.value(password));
when(() => mockStoriesRepository.fetchUserBy(userId: username))
when(() => mockStoriesRepository.fetchUser(id: username))
.thenAnswer((_) => Future<User>.value(tUser));
when(() => mockAuthRepository.loggedIn)
.thenAnswer((_) => Future<bool>.value(false));
@ -91,7 +91,7 @@ void main() {
verify: (_) {
verify(() => mockAuthRepository.loggedIn).called(2);
verifyNever(() => mockAuthRepository.username);
verifyNever(() => mockStoriesRepository.fetchUserBy(userId: username));
verifyNever(() => mockStoriesRepository.fetchUser(id: username));
},
);
@ -154,8 +154,7 @@ void main() {
password: password,
),
).called(1);
verify(() => mockStoriesRepository.fetchUserBy(userId: username))
.called(1);
verify(() => mockStoriesRepository.fetchUser(id: username)).called(1);
},
);
});