Compare commits

...

1 Commits

Author SHA1 Message Date
bfbc80b56b v0.2.4 (#15)
* fixed naming.

* add local notification when there is new reply.

* updated fastlane.

* removed debug flag.

* bumped version.

* add local push notification.

* removed mock.

* fix notification.

* updated notification msg.

* fixed fetcher.

* not using work manager anymore.

* revert interval.

* not sending notification on launch.

* bumped version.

* added logger.

* bumped inappwebview version.

* updated background task info.

* updated fastlane
2022-05-05 23:48:41 -07:00
31 changed files with 587 additions and 177 deletions

View File

@ -0,0 +1,4 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -13,4 +13,4 @@ Features:
- Get in-app notification when there is new reply to your stories or comments.
- Download stories for offline reading.
- Pick up where you left off.
- And more...
- And more...

View File

@ -1 +1 @@
Hacki is a simple noiseless Hacker News reader.
Hacki is a simple noiseless Hacker News reader that is just enough.

View File

@ -10,6 +10,8 @@ PODS:
- flutter_inappwebview/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (3.3.1):
- Flutter
- FMDB (2.7.5):
@ -32,11 +34,14 @@ PODS:
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- workmanager (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
@ -45,6 +50,7 @@ DEPENDENCIES:
- video_player (from `.symlinks/plugins/video_player/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
trunk:
@ -59,6 +65,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
path_provider_ios:
@ -75,22 +83,26 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
workmanager:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
video_player: ecd305f42e9044793efd34846e1ce64c31ea6fcb
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b

View File

@ -363,7 +363,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -372,7 +372,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.3;
MARKETING_VERSION = 0.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -499,7 +499,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -508,7 +508,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.3;
MARKETING_VERSION = 0.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -529,7 +529,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -538,7 +538,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.3;
MARKETING_VERSION = 0.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -1,13 +1,34 @@
import UIKit
import Flutter
import workmanager
import shared_preferences_ios
import flutter_secure_storage
import path_provider_ios
import flutter_local_notifications
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60*15))
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
FLTSharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.pathprovider.PathProviderPlugin")!)
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin")!)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>workmanager.background.task</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -25,7 +29,9 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIBackgroundModes</key>
<array/>
<array>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>

View File

@ -10,12 +10,12 @@ part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc({
AuthRepository? authRepository,
PreferenceRepository? storageRepository,
PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository,
SembastRepository? sembastRepository,
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
_storageRepository =
storageRepository ?? locator.get<PreferenceRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository =
@ -30,7 +30,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
final AuthRepository _authRepository;
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository;
final SembastRepository _sembastRepository;
@ -108,7 +108,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
);
await _authRepository.logout();
await _storageRepository.updateUnreadCommentsIds(<int>[]);
await _preferenceRepository.updateUnreadCommentsIds(<int>[]);
await _sembastRepository.deleteAll();
}
}

View File

@ -1,6 +1,7 @@
import 'package:get_it/get_it.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
/// Global [GetIt.instance].
final GetIt locator = GetIt.instance;
@ -15,5 +16,7 @@ Future<void> setUpLocator() async {
..registerSingleton<PostRepository>(PostRepository())
..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<CacheRepository>(CacheRepository())
..registerSingleton<CacheService>(CacheService());
..registerSingleton<CacheService>(CacheService())
..registerSingleton<LocalNotification>(LocalNotification())
..registerSingleton<Logger>(Logger());
}

View File

@ -7,16 +7,16 @@ part 'blocklist_state.dart';
class BlocklistCubit extends Cubit<BlocklistState> {
BlocklistCubit({PreferenceRepository? storageRepository})
: _storageRepository =
: _preferenceRepository =
storageRepository ?? locator.get<PreferenceRepository>(),
super(BlocklistState.init()) {
init();
}
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
void init() {
_storageRepository.blocklist.then(
_preferenceRepository.blocklist.then(
(List<String> blocklist) => emit(state.copyWith(blocklist: blocklist)),
);
}
@ -25,13 +25,13 @@ class BlocklistCubit extends Cubit<BlocklistState> {
final List<String> updated = List<String>.from(state.blocklist)
..add(username);
emit(state.copyWith(blocklist: updated));
_storageRepository.updateBlocklist(updated);
_preferenceRepository.updateBlocklist(updated);
}
void removeFromBlocklist(String username) {
final List<String> updated = List<String>.from(state.blocklist)
..remove(username);
emit(state.copyWith(blocklist: updated));
_storageRepository.updateBlocklist(updated);
_preferenceRepository.updateBlocklist(updated);
}
}

View File

@ -110,21 +110,15 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
),
);
await _streamSubscription?.cancel();
final Story story = (state.item as Story?)!;
final Story updatedStory =
await _storiesRepository.fetchStoryBy(story.id) ?? story;
if (state.offlineReading) {
_streamSubscription = _cacheRepository
.getCachedCommentsStream(ids: updatedStory.kids)
.listen(_onCommentFetched)
..onDone(_onDone);
} else {
_streamSubscription = _storiesRepository
.fetchCommentsStream(ids: updatedStory.kids)
.listen(_onCommentFetched)
..onDone(_onDone);
}
_streamSubscription = _storiesRepository
.fetchCommentsStream(ids: updatedStory.kids)
.listen(_onCommentFetched)
..onDone(_onDone);
emit(
state.copyWith(

View File

@ -11,12 +11,12 @@ class FavCubit extends Cubit<FavState> {
FavCubit({
required AuthBloc authBloc,
AuthRepository? authRepository,
PreferenceRepository? storageRepository,
PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository,
}) : _authBloc = authBloc,
_authRepository = authRepository ?? locator.get<AuthRepository>(),
_storageRepository =
storageRepository ?? locator.get<PreferenceRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
super(FavState.init()) {
@ -25,7 +25,7 @@ class FavCubit extends Cubit<FavState> {
final AuthBloc _authBloc;
final AuthRepository _authRepository;
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository;
static const int _pageSize = 20;
String? _username;
@ -33,7 +33,7 @@ class FavCubit extends Cubit<FavState> {
Future<void> init() async {
_authBloc.stream.listen((AuthState authState) {
if (authState.username != _username) {
_storageRepository
_preferenceRepository
.favList(of: authState.username)
.then((List<int> favIds) {
emit(
@ -65,7 +65,7 @@ class FavCubit extends Cubit<FavState> {
Future<void> addFav(int id) async {
final String username = _authBloc.state.username;
await _storageRepository.addFav(username: username, id: id);
await _preferenceRepository.addFav(username: username, id: id);
emit(
state.copyWith(
@ -91,7 +91,7 @@ class FavCubit extends Cubit<FavState> {
void removeFav(int id) {
final String username = _authBloc.state.username;
_storageRepository.removeFav(username: username, id: id);
_preferenceRepository.removeFav(username: username, id: id);
emit(
state.copyWith(
@ -147,7 +147,7 @@ class FavCubit extends Cubit<FavState> {
),
);
_storageRepository.favList(of: username).then((List<int> favIds) {
_preferenceRepository.favList(of: username).then((List<int> favIds) {
emit(state.copyWith(favIds: favIds));
_storiesRepository
.fetchStoriesStream(

View File

@ -16,21 +16,22 @@ class NotificationCubit extends Cubit<NotificationState> {
required AuthBloc authBloc,
required PreferenceCubit preferenceCubit,
StoriesRepository? storiesRepository,
PreferenceRepository? storageRepository,
PreferenceRepository? preferenceRepository,
SembastRepository? sembastRepository,
}) : _authBloc = authBloc,
_preferenceCubit = preferenceCubit,
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_storageRepository =
storageRepository ?? locator.get<PreferenceRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(),
super(NotificationState.init()) {
_authBloc.stream.listen((AuthState authState) {
if (authState.isLoggedIn && authState.username != _username) {
// Get the user setting.
_storageRepository.shouldShowNotification.then((bool showNotification) {
_preferenceRepository.shouldShowNotification
.then((bool showNotification) {
if (showNotification) {
init();
}
@ -56,12 +57,12 @@ class NotificationCubit extends Cubit<NotificationState> {
final AuthBloc _authBloc;
final PreferenceCubit _preferenceCubit;
final StoriesRepository _storiesRepository;
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
final SembastRepository _sembastRepository;
String? _username;
Timer? _timer;
static const Duration _refreshDuration = Duration(minutes: 1);
static const Duration _refreshInterval = Duration(minutes: 5);
static const int _subscriptionUpperLimit = 15;
static const int _pageSize = 20;
@ -74,7 +75,7 @@ class NotificationCubit extends Cubit<NotificationState> {
emit(state.copyWith(allCommentsIds: commentIds));
});
await _storageRepository.unreadCommentsIds.then((List<int> unreadIds) {
await _preferenceRepository.unreadCommentsIds.then((List<int> unreadIds) {
emit(state.copyWith(unreadCommentsIds: unreadIds));
});
@ -103,21 +104,24 @@ class NotificationCubit extends Cubit<NotificationState> {
),
);
_initializeTimer();
}).onError((Object? error, StackTrace stackTrace) => _initializeTimer());
}).onError((Object? error, StackTrace stackTrace) {
_initializeTimer();
return null;
});
}
void markAsRead(Comment comment) {
if (state.unreadCommentsIds.contains(comment.id)) {
void markAsRead(int id) {
if (state.unreadCommentsIds.contains(id)) {
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
..remove(comment.id);
_storageRepository.updateUnreadCommentsIds(updatedUnreadIds);
..remove(id);
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
}
}
void markAllAsRead() {
emit(state.copyWith(unreadCommentsIds: <int>[]));
_storageRepository.updateUnreadCommentsIds(<int>[]);
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
}
Future<void> refresh() async {
@ -144,6 +148,7 @@ class NotificationCubit extends Cubit<NotificationState> {
),
);
_initializeTimer();
return null;
});
} else {
emit(
@ -184,7 +189,10 @@ class NotificationCubit extends Cubit<NotificationState> {
void _initializeTimer() {
_timer?.cancel();
_timer = Timer.periodic(_refreshDuration, (Timer timer) => _fetchReplies());
_timer = Timer.periodic(
_refreshInterval,
(Timer timer) => _fetchReplies(),
);
}
Future<void> _fetchReplies() {
@ -210,25 +218,30 @@ class NotificationCubit extends Cubit<NotificationState> {
if (diff.isNotEmpty) {
for (final int newCommentId in diff) {
await _storageRepository.updateUnreadCommentsIds(
<int>[
newCommentId,
...state.unreadCommentsIds,
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
);
await _storiesRepository
.fetchCommentBy(id: newCommentId)
.then((Comment? comment) {
if (comment != null && !comment.dead && !comment.deleted) {
_sembastRepository
..saveComment(comment)
..updateIdsOfCommentsRepliedToMe(comment.id);
final bool hasPushed =
await _preferenceRepository.hasPushed(newCommentId);
// Add comment fetched to comments
// and its id to unreadCommentsIds and allCommentsIds,
emit(state.copyWithNewUnreadComment(comment: comment));
}
});
if (!hasPushed) {
await _preferenceRepository.updateUnreadCommentsIds(
<int>[
newCommentId,
...state.unreadCommentsIds,
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
);
await _storiesRepository
.fetchCommentBy(id: newCommentId)
.then((Comment? comment) {
if (comment != null && !comment.dead && !comment.deleted) {
_sembastRepository
..saveComment(comment)
..updateIdsOfCommentsRepliedToMe(comment.id);
// Add comment fetched to comments
// and its id to unreadCommentsIds and allCommentsIds,
emit(state.copyWithNewUnreadComment(comment: comment));
}
});
}
}
}
});

View File

@ -8,22 +8,22 @@ part 'pin_state.dart';
class PinCubit extends Cubit<PinState> {
PinCubit({
PreferenceRepository? storageRepository,
PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository,
}) : _storageRepository =
storageRepository ?? locator.get<PreferenceRepository>(),
}) : _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
super(PinState.init()) {
init();
}
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository;
void init() {
emit(PinState.init());
_storageRepository.pinnedStoriesIds.then((List<int> ids) {
_preferenceRepository.pinnedStoriesIds.then((List<int> ids) {
emit(state.copyWith(pinnedStoriesIds: ids));
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
@ -38,7 +38,7 @@ class PinCubit extends Cubit<PinState> {
pinnedStories: <Story>{story, ...state.pinnedStories}.toList(),
),
);
_storageRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
}
}
@ -49,7 +49,7 @@ class PinCubit extends Cubit<PinState> {
pinnedStories: <Story>[...state.pinnedStories]..remove(story),
),
);
_storageRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
}
void _onStoryFetched(Story story) {

View File

@ -33,7 +33,7 @@ class PollCubit extends Cubit<PollState> {
}
}
// if pollOptionsIds is still empty, exit loading state.
// If pollOptionsIds is still empty, exit loading state.
if (pollOptionsIds.isEmpty) {
emit(state.copyWith(status: PollStatus.loaded));
return;

View File

@ -7,64 +7,64 @@ part 'preference_state.dart';
class PreferenceCubit extends Cubit<PreferenceState> {
PreferenceCubit({PreferenceRepository? storageRepository})
: _storageRepository =
: _preferenceRepository =
storageRepository ?? locator.get<PreferenceRepository>(),
super(const PreferenceState.init()) {
init();
}
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
void init() {
_storageRepository.shouldShowNotification
_preferenceRepository.shouldShowNotification
.then((bool value) => emit(state.copyWith(showNotification: value)));
_storageRepository.shouldShowComplexStoryTile.then(
_preferenceRepository.shouldShowComplexStoryTile.then(
(bool value) => emit(state.copyWith(showComplexStoryTile: value)),
);
_storageRepository.shouldShowWebFirst
_preferenceRepository.shouldShowWebFirst
.then((bool value) => emit(state.copyWith(showWebFirst: value)));
_storageRepository.shouldShowEyeCandy
_preferenceRepository.shouldShowEyeCandy
.then((bool value) => emit(state.copyWith(showEyeCandy: value)));
_storageRepository.trueDarkMode
_preferenceRepository.trueDarkMode
.then((bool value) => emit(state.copyWith(useTrueDark: value)));
_storageRepository.readerMode
_preferenceRepository.readerMode
.then((bool value) => emit(state.copyWith(useReader: value)));
_storageRepository.markReadStories
_preferenceRepository.markReadStories
.then((bool value) => emit(state.copyWith(markReadStories: value)));
}
void toggleNotificationMode() {
emit(state.copyWith(showNotification: !state.showNotification));
_storageRepository.toggleNotificationMode();
_preferenceRepository.toggleNotificationMode();
}
void toggleDisplayMode() {
emit(state.copyWith(showComplexStoryTile: !state.showComplexStoryTile));
_storageRepository.toggleDisplayMode();
_preferenceRepository.toggleDisplayMode();
}
void toggleNavigationMode() {
emit(state.copyWith(showWebFirst: !state.showWebFirst));
_storageRepository.toggleNavigationMode();
_preferenceRepository.toggleNavigationMode();
}
void toggleEyeCandyMode() {
emit(state.copyWith(showEyeCandy: !state.showEyeCandy));
_storageRepository.toggleEyeCandyMode();
_preferenceRepository.toggleEyeCandyMode();
}
void toggleTrueDarkMode() {
emit(state.copyWith(useTrueDark: !state.useTrueDark));
_storageRepository.toggleTrueDarkMode();
_preferenceRepository.toggleTrueDarkMode();
}
void toggleReaderMode() {
emit(state.copyWith(useReader: !state.useReader));
_storageRepository.toggleReaderMode();
_preferenceRepository.toggleReaderMode();
}
void toggleMarkReadStoriesMode() {
emit(state.copyWith(markReadStories: !state.markReadStories));
_storageRepository.toggleMarkReadStoriesMode();
_preferenceRepository.toggleMarkReadStoriesMode();
}
}

View File

@ -14,22 +14,22 @@ class VoteCubit extends Cubit<VoteState> {
required Item item,
required AuthBloc authBloc,
AuthRepository? authRepository,
PreferenceRepository? storageRepository,
PreferenceRepository? preferenceRepository,
}) : _authBloc = authBloc,
_authRepository = authRepository ?? locator.get<AuthRepository>(),
_storageRepository =
storageRepository ?? locator.get<PreferenceRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
super(VoteState.init(item: item)) {
init();
}
final AuthBloc _authBloc;
final AuthRepository _authRepository;
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
static const int _karmaThreshold = 501;
Future<void> init() async {
final bool? vote = await _storageRepository.vote(
final bool? vote = await _preferenceRepository.vote(
submittedTo: state.item.id,
from: _authBloc.state.username,
);
@ -73,7 +73,7 @@ class VoteCubit extends Cubit<VoteState> {
);
unawaited(
_storageRepository.addVote(
_preferenceRepository.addVote(
username: _authBloc.state.username,
id: state.item.id,
vote: true,
@ -88,7 +88,7 @@ class VoteCubit extends Cubit<VoteState> {
}
} else {
await _authRepository.upvote(id: state.item.id, upvote: false);
await _storageRepository.removeVote(
await _preferenceRepository.removeVote(
username: _authBloc.state.username,
id: state.item.id,
);
@ -118,7 +118,7 @@ class VoteCubit extends Cubit<VoteState> {
await _authRepository.downvote(id: state.item.id, downvote: true);
if (success) {
await _storageRepository.addVote(
await _preferenceRepository.addVote(
username: _authBloc.state.username,
id: state.item.id,
vote: false,
@ -133,7 +133,7 @@ class VoteCubit extends Cubit<VoteState> {
}
} else {
await _authRepository.downvote(id: state.item.id, downvote: false);
await _storageRepository.removeVote(
await _preferenceRepository.removeVote(
username: _authBloc.state.username,
id: state.item.id,
);

View File

@ -1,20 +1,61 @@
import 'dart:async';
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/fetcher.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
import 'package:workmanager/workmanager.dart';
// For receiving payload event from local notifications.
final BehaviorSubject<String?> selectNotificationSubject =
BehaviorSubject<String?>();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isIOS) {
unawaited(
Workmanager().initialize(
fetcherCallbackDispatcher,
),
);
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const IOSInitializationSettings initializationSettingsIOS =
IOSInitializationSettings();
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onSelectNotification: selectNotificationSubject.add,
);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
final Directory tempDir = await getTemporaryDirectory();
final String tempPath = tempDir.path;
Hive.init(tempPath);

View File

@ -10,21 +10,21 @@ import 'package:hacki/utils/service_exception.dart';
class AuthRepository {
AuthRepository({
Dio? dio,
PreferenceRepository? storageRepository,
PreferenceRepository? preferenceRepository,
}) : _dio = dio ?? Dio(),
_storageRepository =
storageRepository ?? locator.get<PreferenceRepository>();
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>();
static const String _authority = 'news.ycombinator.com';
final Dio _dio;
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
Future<bool> get loggedIn async => _storageRepository.loggedIn;
Future<bool> get loggedIn async => _preferenceRepository.loggedIn;
Future<String?> get username async => _storageRepository.username;
Future<String?> get username async => _preferenceRepository.username;
Future<String?> get password async => _storageRepository.password;
Future<String?> get password async => _preferenceRepository.password;
Future<bool> login({
required String username,
@ -40,16 +40,19 @@ class AuthRepository {
final bool success = await _performDefaultPost(uri, data);
if (success) {
await _storageRepository.setAuth(username: username, password: password);
await _preferenceRepository.setAuth(
username: username,
password: password,
);
}
return success;
}
Future<bool> hasLoggedIn() => _storageRepository.loggedIn;
Future<bool> hasLoggedIn() => _preferenceRepository.loggedIn;
Future<void> logout() async {
await _storageRepository.removeAuth();
await _preferenceRepository.removeAuth();
}
Future<bool> flag({
@ -57,8 +60,8 @@ class AuthRepository {
required bool flag,
}) async {
final Uri uri = Uri.https(_authority, 'flag');
final String? username = await _storageRepository.username;
final String? password = await _storageRepository.password;
final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password;
final PostDataMixin data = FlagPostData(
acct: username!,
pw: password!,
@ -74,8 +77,8 @@ class AuthRepository {
required bool favorite,
}) async {
final Uri uri = Uri.https(_authority, 'fave');
final String? username = await _storageRepository.username;
final String? password = await _storageRepository.password;
final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password;
final PostDataMixin data = FavoritePostData(
acct: username!,
pw: password!,
@ -91,8 +94,8 @@ class AuthRepository {
required bool upvote,
}) async {
final Uri uri = Uri.https(_authority, 'vote');
final String? username = await _storageRepository.username;
final String? password = await _storageRepository.password;
final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password;
final PostDataMixin data = VotePostData(
acct: username!,
pw: password!,
@ -108,8 +111,8 @@ class AuthRepository {
required bool downvote,
}) async {
final Uri uri = Uri.https(_authority, 'vote');
final String? username = await _storageRepository.username;
final String? password = await _storageRepository.password;
final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password;
final PostDataMixin data = VotePostData(
acct: username!,
pw: password!,

View File

@ -9,20 +9,20 @@ import 'package:hacki/utils/utils.dart';
class PostRepository {
PostRepository({Dio? dio, PreferenceRepository? storageRepository})
: _dio = dio ?? Dio(),
_storageRepository =
_preferenceRepository =
storageRepository ?? locator.get<PreferenceRepository>();
static const String _authority = 'news.ycombinator.com';
final Dio _dio;
final PreferenceRepository _storageRepository;
final PreferenceRepository _preferenceRepository;
Future<bool> comment({
required int parentId,
required String text,
}) async {
final String? username = await _storageRepository.username;
final String? password = await _storageRepository.password;
final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password;
final Uri uri = Uri.https(_authority, 'comment');
if (username == null || password == null) {
@ -48,8 +48,8 @@ class PostRepository {
String? url,
String? text,
}) async {
final String? username = await _storageRepository.username;
final String? password = await _storageRepository.password;
final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password;
if (username == null || password == null) {
return false;
@ -91,8 +91,8 @@ class PostRepository {
required int id,
String? text,
}) async {
final String? username = await _storageRepository.username;
final String? password = await _storageRepository.password;
final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password;
if (username == null || password == null) {
return false;

View File

@ -115,6 +115,15 @@ class PreferenceRepository {
return int.tryParse(val);
});
Future<bool> hasPushed(int commentId) async =>
_prefs.then((SharedPreferences prefs) {
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
if (val == null) return false;
return true;
});
Future<List<int>> favList({required String of}) => _prefs.then(
(SharedPreferences prefs) =>
((prefs.getStringList(_getFavKey('')) ?? <String>[])
@ -266,6 +275,16 @@ class PreferenceRepository {
);
}
Future<void> updateHasPushed(int commentId) async {
final SharedPreferences prefs = await _prefs;
await prefs.setBool(
_getPushNotificationKey(commentId),
true,
);
}
String _getPushNotificationKey(int commentId) => 'pushed_$commentId';
String _getFavKey(String username) => 'fav_$username';
String _getVoteKey(String username, int id) => 'vote_$username-$id';

View File

@ -69,7 +69,7 @@ class SembastRepository {
if (_idsOfCommentsRepliedToMe == null) {
final RecordSnapshot<dynamic, dynamic>? snapshot =
await store.record(_idsOfCommentsRepliedToMeKey).getSnapshot(db);
list = snapshot?.value as List<int>? ?? <int>[];
list = (snapshot?.value as List<dynamic>? ?? <int>[]).cast<int>();
_idsOfCommentsRepliedToMe = list;
} else {
list = _idsOfCommentsRepliedToMe!;

View File

@ -161,6 +161,20 @@ class StoriesRepository {
return comment;
}
Future<Comment?> fetchRawCommentBy({required int id}) async {
final Comment? comment = await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic val) async {
if (val == null) return null;
final Map<String, dynamic> json = val as Map<String, dynamic>;
final Comment comment = Comment.fromJson(json);
return comment;
});
return comment;
}
Future<Item?> fetchItemBy({required int id}) async {
final Item? item = await _firebaseClient
.get('${_baseUrl}item/$id.json')
@ -182,6 +196,28 @@ class StoriesRepository {
return item;
}
Future<Item?> fetchRawItemBy({required int id}) async {
final Item? item = await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic val) {
if (val == null) return null;
final Map<String, dynamic> json = val as Map<String, dynamic>;
final String type = json['type'] as String;
if (type == 'story' || type == 'job') {
final Story story = Story.fromJson(json);
return story;
} else if (json['type'] == 'comment') {
final Comment comment = Comment.fromJson(json);
return comment;
}
return null;
});
return item;
}
Future<List<int>?> fetchSubmitted({required String of}) async {
final List<int>? submitted = await _firebaseClient
.get('${_baseUrl}user/$of.json')
@ -209,6 +245,17 @@ class StoriesRepository {
return item as Story;
}
Future<Story?> fetchRawParentStory({required int id}) async {
Item? item;
do {
item = await fetchRawItemBy(id: item?.parent ?? id);
if (item == null) return null;
} while (item is Comment);
return item as Story;
}
Future<Tuple2<Story, List<Comment>>?> fetchParentStoryWithComments({
required int id,
}) async {

View File

@ -1,5 +1,7 @@
// ignore_for_file: lines_longer_than_80_chars
import 'dart:convert';
import 'package:badges/badges.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
@ -12,8 +14,10 @@ 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';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart';
@ -54,6 +58,34 @@ class _HomeScreenState extends State<HomeScreen>
// Constants.featurePinToTop,
// ]);
if (!selectNotificationSubject.hasListener) {
selectNotificationSubject.stream.listen((String? payload) async {
if (payload == null) return;
final Map<String, dynamic> payloadJson =
jsonDecode(payload) as Map<String, dynamic>;
final int? storyId = payloadJson['storyId'] as int?;
final int? commentId = payloadJson['commentId'] as int?;
if (storyId != null && commentId != null) {
context.read<NotificationCubit>().markAsRead(commentId);
await locator
.get<StoriesRepository>()
.fetchStoryBy(storyId)
.then((Story? story) {
if (story == null) {
showSnackBar(content: 'Something went wrong...');
return;
}
final StoryScreenArgs args = StoryScreenArgs(story: story);
goToStoryScreen(args: args);
});
}
});
}
SchedulerBinding.instance?.addPostFrameCallback((_) {
FeatureDiscovery.discoverFeatures(
context,

View File

@ -216,7 +216,7 @@ class _ProfileScreenState extends State<ProfileScreen>
then: () {
context
.read<NotificationCubit>()
.markAsRead(cmt);
.markAsRead(cmt.id);
},
);
},
@ -265,7 +265,7 @@ class _ProfileScreenState extends State<ProfileScreen>
subtitle: const Text(
'Hacki scans for new replies to your 15 '
'most recent comments or stories '
'every 1 minute while the app is '
'every 5 minutes while the app is '
'running in the foreground.',
),
value: preferenceState.showNotification,
@ -388,7 +388,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog(
context: context,
applicationName: 'Hacki',
applicationVersion: 'v0.2.3',
applicationVersion: 'v0.2.4',
applicationIcon: Image.asset(
Constants.hackiIconPath,
height: 50,

View File

@ -1,34 +1,24 @@
import 'package:bloc/bloc.dart';
import 'package:hacki/extensions/extensions.dart' show ObjectExtension;
import 'package:hacki/config/locator.dart';
import 'package:logger/logger.dart';
class CustomBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase<dynamic> bloc) {
super.onCreate(bloc);
bloc.log(identifier: 'Bloc Created:');
}
@override
void onEvent(Bloc<dynamic, dynamic> bloc, Object? event) {
void onEvent(
Bloc<dynamic, dynamic> bloc,
Object? event,
) {
locator.get<Logger>().d(event);
super.onEvent(bloc, event);
event?.log(identifier: 'Bloc Event:');
}
@override
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change);
change.log(identifier: 'Bloc Changed:');
}
@override
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
void onError(
BlocBase<dynamic> bloc,
Object error,
StackTrace stackTrace,
) {
locator.get<Logger>().e(error);
super.onError(bloc, error, stackTrace);
error.log(identifier: 'Bloc Error:');
}
@override
void onClose(BlocBase<dynamic> bloc) {
super.onClose(bloc);
bloc.log(identifier: 'Bloc Closed:');
}
}

140
lib/services/fetcher.dart Normal file
View File

@ -0,0 +1,140 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/utils/html_util.dart';
import 'package:path_provider_android/path_provider_android.dart';
import 'package:path_provider_ios/path_provider_ios.dart';
import 'package:shared_preferences_android/shared_preferences_android.dart';
import 'package:shared_preferences_ios/shared_preferences_ios.dart';
import 'package:workmanager/workmanager.dart';
void fetcherCallbackDispatcher() {
Workmanager()
.executeTask((String task, Map<String, dynamic>? inputData) async {
if (Platform.isAndroid) {
PathProviderAndroid.registerWith();
SharedPreferencesAndroid.registerWith();
}
if (Platform.isIOS) {
PathProviderIOS.registerWith();
SharedPreferencesIOS.registerWith();
}
await Fetcher.fetchReplies();
return Future<bool>.value(true);
});
}
abstract class Fetcher {
static const int _subscriptionUpperLimit = 15;
static Future<void> fetchReplies() async {
final PreferenceRepository preferenceRepository = PreferenceRepository();
final AuthRepository authRepository = AuthRepository(
preferenceRepository: preferenceRepository,
);
final StoriesRepository storiesRepository = StoriesRepository();
final SembastRepository sembastRepository = SembastRepository();
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
final String happyFace = Constants.happyFaces.pickRandomly()!;
final String? username = await authRepository.username;
final List<int> unreadIds = await preferenceRepository.unreadCommentsIds;
if (username == null || username.isEmpty) return;
Comment? newReply;
await storiesRepository
.fetchSubmitted(of: username)
.then((List<int>? submittedItems) async {
if (submittedItems != null) {
final List<int> subscribedItems = submittedItems.sublist(
0,
min(_subscriptionUpperLimit, submittedItems.length),
);
for (final int id in subscribedItems) {
await storiesRepository
.fetchRawItemBy(id: id)
.then((Item? item) async {
final List<int> kids = item?.kids ?? <int>[];
final List<int> previousKids =
(await sembastRepository.kids(of: id)) ?? <int>[];
await sembastRepository.updateKidsOf(id: id, kids: kids);
final Set<int> diff =
<int>{...kids}.difference(<int>{...previousKids});
if (diff.isNotEmpty) {
for (final int newCommentId in diff) {
if (unreadIds.contains(newCommentId)) continue;
await storiesRepository
.fetchRawCommentBy(id: newCommentId)
.then((Comment? comment) async {
final bool hasPushedBefore =
await preferenceRepository.hasPushed(newReply!.id);
if (comment != null && !comment.dead && !comment.deleted) {
await sembastRepository.saveComment(comment);
await sembastRepository.updateIdsOfCommentsRepliedToMe(
comment.id,
);
if (!hasPushedBefore) {
newReply = comment;
}
}
});
if (newReply != null) break;
}
}
});
if (newReply != null) break;
}
}
});
// Push notification for new unread reply that has not been
// pushed before.
if (newReply != null) {
final Story? story =
await storiesRepository.fetchRawParentStory(id: newReply!.id);
final String text = HtmlUtil.parseHtml(newReply!.text);
if (story != null) {
final Map<String, int> payloadJson = <String, int>{
'commentId': newReply!.id,
'storyId': story.id,
};
final String payload = jsonEncode(payloadJson);
await preferenceRepository.updateHasPushed(newReply!.id);
await flutterLocalNotificationsPlugin.show(
newReply?.id ?? 0,
'You have a new reply! $happyFace',
'${newReply?.by}: $text',
const NotificationDetails(
iOS: IOSNotificationDetails(
presentBadge: false,
threadIdentifier: 'hacki',
),
),
payload: payload,
);
}
}
}
}

View File

@ -0,0 +1,33 @@
import 'dart:convert';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
class LocalNotification {
Future<void> pushForNewReply(Comment newReply, int storyId) async {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
final String happyFace = Constants.happyFaces.pickRandomly()!;
final Map<String, int> payloadJson = <String, int>{
'commentId': newReply.id,
'storyId': storyId,
};
final String payload = jsonEncode(payloadJson);
return flutterLocalNotificationsPlugin.show(
newReply.id,
'You have a new reply! $happyFace',
'${newReply.by}: ${newReply.text}',
const NotificationDetails(
iOS: IOSNotificationDetails(
presentBadge: false,
threadIdentifier: 'hacki',
),
),
payload: payload,
);
}
}

View File

@ -1,3 +1,5 @@
export 'cache_service.dart';
export 'custom_bloc_observer.dart';
export 'fetcher.dart';
export 'firebase_client.dart';
export 'local_notification.dart';

View File

@ -334,7 +334,7 @@ packages:
name: flutter_inappwebview
url: "https://pub.dartlang.org"
source: hosted
version: "5.3.2"
version: "5.4.3+4"
flutter_layout_grid:
dependency: transitive
description:
@ -349,6 +349,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "9.5.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
flutter_math_fork:
dependency: transitive
description:
@ -527,6 +548,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
logger:
dependency: "direct main"
description:
name: logger
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
logging:
dependency: transitive
description:
@ -640,14 +668,14 @@ packages:
source: hosted
version: "2.0.9"
path_provider_android:
dependency: transitive
dependency: "direct main"
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
path_provider_ios:
dependency: transitive
dependency: "direct main"
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
@ -759,7 +787,7 @@ packages:
source: hosted
version: "0.4.2"
rxdart:
dependency: transitive
dependency: "direct main"
description:
name: rxdart
url: "https://pub.dartlang.org"
@ -780,19 +808,19 @@ packages:
source: hosted
version: "2.0.11"
shared_preferences_android:
dependency: transitive
dependency: "direct main"
description:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
version: "2.0.11"
shared_preferences_ios:
dependency: transitive
dependency: "direct main"
description:
name: shared_preferences_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.1.0"
shared_preferences_linux:
dependency: transitive
description:
@ -959,6 +987,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
timezone:
dependency: transitive
description:
name: timezone
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.0"
tuple:
dependency: "direct main"
description:
@ -1176,13 +1211,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.3"
workmanager:
dependency: "direct main"
description:
name: workmanager
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.0+1"
xml:
dependency: transitive
description:

View File

@ -1,6 +1,6 @@
name: hacki
description: A Hacker News reader.
version: 0.2.3+37
version: 0.2.4+38
publish_to: none
environment:
@ -24,8 +24,9 @@ dependencies:
flutter_fadein: ^2.0.0
flutter_feather_icons: 2.0.0+1
flutter_html: ^2.2.1
flutter_inappwebview: ^5.3.2
flutter_inappwebview: ^5.4.3+4
flutter_linkify: ^5.0.2
flutter_local_notifications: ^9.5.0
flutter_secure_storage: ^5.0.2
flutter_slidable: ^1.2.0
font_awesome_flutter: ^9.2.0
@ -36,17 +37,24 @@ dependencies:
html_unescape: ^2.0.0
http: ^0.13.3
intl: ^0.17.0
logger: ^1.1.0
path: ^1.8.0
path_provider: ^2.0.8
path_provider_android: ^2.0.8
path_provider_ios: ^2.0.8
pull_to_refresh: ^2.0.0
responsive_builder: ^0.4.2
rxdart: ^0.27.3
sembast: ^3.1.1+1
shared_preferences: ^2.0.11
shared_preferences_android: ^2.0.11
shared_preferences_ios: ^2.0.11
shimmer: ^2.0.0
tuple: ^2.0.0
universal_platform: ^1.0.0+1
url_launcher: ^6.0.10
wakelock: ^0.6.1+2
workmanager: ^0.4.1
dev_dependencies:
bloc_test: ^9.0.3