mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
bfbc80b56b |
4
fastlane/metadata/android/en-US/changelogs/38.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/38.txt
Normal 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.
|
@ -13,4 +13,4 @@ Features:
|
|||||||
- Get in-app notification when there is new reply to your stories or comments.
|
- Get in-app notification when there is new reply to your stories or comments.
|
||||||
- Download stories for offline reading.
|
- Download stories for offline reading.
|
||||||
- Pick up where you left off.
|
- Pick up where you left off.
|
||||||
- And more...
|
- And more...
|
||||||
|
@ -1 +1 @@
|
|||||||
Hacki is a simple noiseless Hacker News reader.
|
Hacki is a simple noiseless Hacker News reader that is just enough.
|
@ -10,6 +10,8 @@ PODS:
|
|||||||
- flutter_inappwebview/Core (0.0.1):
|
- flutter_inappwebview/Core (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- OrderedSet (~> 5.0)
|
- OrderedSet (~> 5.0)
|
||||||
|
- flutter_local_notifications (0.0.1):
|
||||||
|
- Flutter
|
||||||
- flutter_secure_storage (3.3.1):
|
- flutter_secure_storage (3.3.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
@ -32,11 +34,14 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- webview_flutter_wkwebview (0.0.1):
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- workmanager (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
- 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`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_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`)
|
- video_player (from `.symlinks/plugins/video_player/ios`)
|
||||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||||
|
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@ -59,6 +65,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_inappwebview:
|
flutter_inappwebview:
|
||||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
||||||
|
flutter_local_notifications:
|
||||||
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
@ -75,22 +83,26 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/wakelock/ios"
|
:path: ".symlinks/plugins/wakelock/ios"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||||
|
workmanager:
|
||||||
|
:path: ".symlinks/plugins/workmanager/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||||
|
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32
|
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
|
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
|
||||||
video_player: ecd305f42e9044793efd34846e1ce64c31ea6fcb
|
video_player: ecd305f42e9044793efd34846e1ce64c31ea6fcb
|
||||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162
|
webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162
|
||||||
|
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||||
|
|
||||||
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b
|
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b
|
||||||
|
|
||||||
|
@ -363,7 +363,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -372,7 +372,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.3;
|
MARKETING_VERSION = 0.2.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -499,7 +499,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -508,7 +508,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.3;
|
MARKETING_VERSION = 0.2.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -529,7 +529,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -538,7 +538,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.3;
|
MARKETING_VERSION = 0.2.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -1,13 +1,34 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
import workmanager
|
||||||
|
import shared_preferences_ios
|
||||||
|
import flutter_secure_storage
|
||||||
|
import path_provider_ios
|
||||||
|
import flutter_local_notifications
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
||||||
}
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>workmanager.background.task</string>
|
||||||
|
</array>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@ -25,7 +29,9 @@
|
|||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array/>
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
@ -10,12 +10,12 @@ part 'auth_state.dart';
|
|||||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
AuthBloc({
|
AuthBloc({
|
||||||
AuthRepository? authRepository,
|
AuthRepository? authRepository,
|
||||||
PreferenceRepository? storageRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
SembastRepository? sembastRepository,
|
SembastRepository? sembastRepository,
|
||||||
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
|
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||||
_storageRepository =
|
_preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
@ -30,7 +30,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
final SembastRepository _sembastRepository;
|
final SembastRepository _sembastRepository;
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _authRepository.logout();
|
await _authRepository.logout();
|
||||||
await _storageRepository.updateUnreadCommentsIds(<int>[]);
|
await _preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||||
await _sembastRepository.deleteAll();
|
await _sembastRepository.deleteAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
/// Global [GetIt.instance].
|
/// Global [GetIt.instance].
|
||||||
final GetIt locator = GetIt.instance;
|
final GetIt locator = GetIt.instance;
|
||||||
@ -15,5 +16,7 @@ Future<void> setUpLocator() async {
|
|||||||
..registerSingleton<PostRepository>(PostRepository())
|
..registerSingleton<PostRepository>(PostRepository())
|
||||||
..registerSingleton<SembastRepository>(SembastRepository())
|
..registerSingleton<SembastRepository>(SembastRepository())
|
||||||
..registerSingleton<CacheRepository>(CacheRepository())
|
..registerSingleton<CacheRepository>(CacheRepository())
|
||||||
..registerSingleton<CacheService>(CacheService());
|
..registerSingleton<CacheService>(CacheService())
|
||||||
|
..registerSingleton<LocalNotification>(LocalNotification())
|
||||||
|
..registerSingleton<Logger>(Logger());
|
||||||
}
|
}
|
||||||
|
@ -7,16 +7,16 @@ part 'blocklist_state.dart';
|
|||||||
|
|
||||||
class BlocklistCubit extends Cubit<BlocklistState> {
|
class BlocklistCubit extends Cubit<BlocklistState> {
|
||||||
BlocklistCubit({PreferenceRepository? storageRepository})
|
BlocklistCubit({PreferenceRepository? storageRepository})
|
||||||
: _storageRepository =
|
: _preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
storageRepository ?? locator.get<PreferenceRepository>(),
|
||||||
super(BlocklistState.init()) {
|
super(BlocklistState.init()) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
_storageRepository.blocklist.then(
|
_preferenceRepository.blocklist.then(
|
||||||
(List<String> blocklist) => emit(state.copyWith(blocklist: blocklist)),
|
(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)
|
final List<String> updated = List<String>.from(state.blocklist)
|
||||||
..add(username);
|
..add(username);
|
||||||
emit(state.copyWith(blocklist: updated));
|
emit(state.copyWith(blocklist: updated));
|
||||||
_storageRepository.updateBlocklist(updated);
|
_preferenceRepository.updateBlocklist(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeFromBlocklist(String username) {
|
void removeFromBlocklist(String username) {
|
||||||
final List<String> updated = List<String>.from(state.blocklist)
|
final List<String> updated = List<String>.from(state.blocklist)
|
||||||
..remove(username);
|
..remove(username);
|
||||||
emit(state.copyWith(blocklist: updated));
|
emit(state.copyWith(blocklist: updated));
|
||||||
_storageRepository.updateBlocklist(updated);
|
_preferenceRepository.updateBlocklist(updated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,21 +110,15 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await _streamSubscription?.cancel();
|
||||||
|
|
||||||
final Story story = (state.item as Story?)!;
|
final Story story = (state.item as Story?)!;
|
||||||
final Story updatedStory =
|
final Story updatedStory =
|
||||||
await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
if (state.offlineReading) {
|
.fetchCommentsStream(ids: updatedStory.kids)
|
||||||
_streamSubscription = _cacheRepository
|
.listen(_onCommentFetched)
|
||||||
.getCachedCommentsStream(ids: updatedStory.kids)
|
..onDone(_onDone);
|
||||||
.listen(_onCommentFetched)
|
|
||||||
..onDone(_onDone);
|
|
||||||
} else {
|
|
||||||
_streamSubscription = _storiesRepository
|
|
||||||
.fetchCommentsStream(ids: updatedStory.kids)
|
|
||||||
.listen(_onCommentFetched)
|
|
||||||
..onDone(_onDone);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
@ -11,12 +11,12 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
FavCubit({
|
FavCubit({
|
||||||
required AuthBloc authBloc,
|
required AuthBloc authBloc,
|
||||||
AuthRepository? authRepository,
|
AuthRepository? authRepository,
|
||||||
PreferenceRepository? storageRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
}) : _authBloc = authBloc,
|
}) : _authBloc = authBloc,
|
||||||
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||||
_storageRepository =
|
_preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
super(FavState.init()) {
|
super(FavState.init()) {
|
||||||
@ -25,7 +25,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
|
|
||||||
final AuthBloc _authBloc;
|
final AuthBloc _authBloc;
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
String? _username;
|
String? _username;
|
||||||
@ -33,7 +33,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_authBloc.stream.listen((AuthState authState) {
|
_authBloc.stream.listen((AuthState authState) {
|
||||||
if (authState.username != _username) {
|
if (authState.username != _username) {
|
||||||
_storageRepository
|
_preferenceRepository
|
||||||
.favList(of: authState.username)
|
.favList(of: authState.username)
|
||||||
.then((List<int> favIds) {
|
.then((List<int> favIds) {
|
||||||
emit(
|
emit(
|
||||||
@ -65,7 +65,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
Future<void> addFav(int id) async {
|
Future<void> addFav(int id) async {
|
||||||
final String username = _authBloc.state.username;
|
final String username = _authBloc.state.username;
|
||||||
|
|
||||||
await _storageRepository.addFav(username: username, id: id);
|
await _preferenceRepository.addFav(username: username, id: id);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -91,7 +91,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
void removeFav(int id) {
|
void removeFav(int id) {
|
||||||
final String username = _authBloc.state.username;
|
final String username = _authBloc.state.username;
|
||||||
|
|
||||||
_storageRepository.removeFav(username: username, id: id);
|
_preferenceRepository.removeFav(username: username, id: id);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
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));
|
emit(state.copyWith(favIds: favIds));
|
||||||
_storiesRepository
|
_storiesRepository
|
||||||
.fetchStoriesStream(
|
.fetchStoriesStream(
|
||||||
|
@ -16,21 +16,22 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
required AuthBloc authBloc,
|
required AuthBloc authBloc,
|
||||||
required PreferenceCubit preferenceCubit,
|
required PreferenceCubit preferenceCubit,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
PreferenceRepository? storageRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
SembastRepository? sembastRepository,
|
SembastRepository? sembastRepository,
|
||||||
}) : _authBloc = authBloc,
|
}) : _authBloc = authBloc,
|
||||||
_preferenceCubit = preferenceCubit,
|
_preferenceCubit = preferenceCubit,
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
_storageRepository =
|
_preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
super(NotificationState.init()) {
|
super(NotificationState.init()) {
|
||||||
_authBloc.stream.listen((AuthState authState) {
|
_authBloc.stream.listen((AuthState authState) {
|
||||||
if (authState.isLoggedIn && authState.username != _username) {
|
if (authState.isLoggedIn && authState.username != _username) {
|
||||||
// Get the user setting.
|
// Get the user setting.
|
||||||
_storageRepository.shouldShowNotification.then((bool showNotification) {
|
_preferenceRepository.shouldShowNotification
|
||||||
|
.then((bool showNotification) {
|
||||||
if (showNotification) {
|
if (showNotification) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
@ -56,12 +57,12 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
final AuthBloc _authBloc;
|
final AuthBloc _authBloc;
|
||||||
final PreferenceCubit _preferenceCubit;
|
final PreferenceCubit _preferenceCubit;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final SembastRepository _sembastRepository;
|
final SembastRepository _sembastRepository;
|
||||||
String? _username;
|
String? _username;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
static const Duration _refreshDuration = Duration(minutes: 1);
|
static const Duration _refreshInterval = Duration(minutes: 5);
|
||||||
static const int _subscriptionUpperLimit = 15;
|
static const int _subscriptionUpperLimit = 15;
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
emit(state.copyWith(allCommentsIds: commentIds));
|
emit(state.copyWith(allCommentsIds: commentIds));
|
||||||
});
|
});
|
||||||
|
|
||||||
await _storageRepository.unreadCommentsIds.then((List<int> unreadIds) {
|
await _preferenceRepository.unreadCommentsIds.then((List<int> unreadIds) {
|
||||||
emit(state.copyWith(unreadCommentsIds: unreadIds));
|
emit(state.copyWith(unreadCommentsIds: unreadIds));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -103,21 +104,24 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
_initializeTimer();
|
_initializeTimer();
|
||||||
}).onError((Object? error, StackTrace stackTrace) => _initializeTimer());
|
}).onError((Object? error, StackTrace stackTrace) {
|
||||||
|
_initializeTimer();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void markAsRead(Comment comment) {
|
void markAsRead(int id) {
|
||||||
if (state.unreadCommentsIds.contains(comment.id)) {
|
if (state.unreadCommentsIds.contains(id)) {
|
||||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||||
..remove(comment.id);
|
..remove(id);
|
||||||
_storageRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
||||||
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void markAllAsRead() {
|
void markAllAsRead() {
|
||||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||||
_storageRepository.updateUnreadCommentsIds(<int>[]);
|
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
@ -144,6 +148,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
_initializeTimer();
|
_initializeTimer();
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
emit(
|
emit(
|
||||||
@ -184,7 +189,10 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
|
|
||||||
void _initializeTimer() {
|
void _initializeTimer() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = Timer.periodic(_refreshDuration, (Timer timer) => _fetchReplies());
|
_timer = Timer.periodic(
|
||||||
|
_refreshInterval,
|
||||||
|
(Timer timer) => _fetchReplies(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchReplies() {
|
Future<void> _fetchReplies() {
|
||||||
@ -210,25 +218,30 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
|
|
||||||
if (diff.isNotEmpty) {
|
if (diff.isNotEmpty) {
|
||||||
for (final int newCommentId in diff) {
|
for (final int newCommentId in diff) {
|
||||||
await _storageRepository.updateUnreadCommentsIds(
|
final bool hasPushed =
|
||||||
<int>[
|
await _preferenceRepository.hasPushed(newCommentId);
|
||||||
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
|
if (!hasPushed) {
|
||||||
// and its id to unreadCommentsIds and allCommentsIds,
|
await _preferenceRepository.updateUnreadCommentsIds(
|
||||||
emit(state.copyWithNewUnreadComment(comment: comment));
|
<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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -8,22 +8,22 @@ part 'pin_state.dart';
|
|||||||
|
|
||||||
class PinCubit extends Cubit<PinState> {
|
class PinCubit extends Cubit<PinState> {
|
||||||
PinCubit({
|
PinCubit({
|
||||||
PreferenceRepository? storageRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
}) : _storageRepository =
|
}) : _preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
super(PinState.init()) {
|
super(PinState.init()) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
emit(PinState.init());
|
emit(PinState.init());
|
||||||
_storageRepository.pinnedStoriesIds.then((List<int> ids) {
|
_preferenceRepository.pinnedStoriesIds.then((List<int> ids) {
|
||||||
emit(state.copyWith(pinnedStoriesIds: ids));
|
emit(state.copyWith(pinnedStoriesIds: ids));
|
||||||
|
|
||||||
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
|
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
|
||||||
@ -38,7 +38,7 @@ class PinCubit extends Cubit<PinState> {
|
|||||||
pinnedStories: <Story>{story, ...state.pinnedStories}.toList(),
|
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),
|
pinnedStories: <Story>[...state.pinnedStories]..remove(story),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_storageRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStoryFetched(Story story) {
|
void _onStoryFetched(Story story) {
|
||||||
|
@ -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) {
|
if (pollOptionsIds.isEmpty) {
|
||||||
emit(state.copyWith(status: PollStatus.loaded));
|
emit(state.copyWith(status: PollStatus.loaded));
|
||||||
return;
|
return;
|
||||||
|
@ -7,64 +7,64 @@ part 'preference_state.dart';
|
|||||||
|
|
||||||
class PreferenceCubit extends Cubit<PreferenceState> {
|
class PreferenceCubit extends Cubit<PreferenceState> {
|
||||||
PreferenceCubit({PreferenceRepository? storageRepository})
|
PreferenceCubit({PreferenceRepository? storageRepository})
|
||||||
: _storageRepository =
|
: _preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
storageRepository ?? locator.get<PreferenceRepository>(),
|
||||||
super(const PreferenceState.init()) {
|
super(const PreferenceState.init()) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
_storageRepository.shouldShowNotification
|
_preferenceRepository.shouldShowNotification
|
||||||
.then((bool value) => emit(state.copyWith(showNotification: value)));
|
.then((bool value) => emit(state.copyWith(showNotification: value)));
|
||||||
_storageRepository.shouldShowComplexStoryTile.then(
|
_preferenceRepository.shouldShowComplexStoryTile.then(
|
||||||
(bool value) => emit(state.copyWith(showComplexStoryTile: value)),
|
(bool value) => emit(state.copyWith(showComplexStoryTile: value)),
|
||||||
);
|
);
|
||||||
_storageRepository.shouldShowWebFirst
|
_preferenceRepository.shouldShowWebFirst
|
||||||
.then((bool value) => emit(state.copyWith(showWebFirst: value)));
|
.then((bool value) => emit(state.copyWith(showWebFirst: value)));
|
||||||
_storageRepository.shouldShowEyeCandy
|
_preferenceRepository.shouldShowEyeCandy
|
||||||
.then((bool value) => emit(state.copyWith(showEyeCandy: value)));
|
.then((bool value) => emit(state.copyWith(showEyeCandy: value)));
|
||||||
_storageRepository.trueDarkMode
|
_preferenceRepository.trueDarkMode
|
||||||
.then((bool value) => emit(state.copyWith(useTrueDark: value)));
|
.then((bool value) => emit(state.copyWith(useTrueDark: value)));
|
||||||
_storageRepository.readerMode
|
_preferenceRepository.readerMode
|
||||||
.then((bool value) => emit(state.copyWith(useReader: value)));
|
.then((bool value) => emit(state.copyWith(useReader: value)));
|
||||||
_storageRepository.markReadStories
|
_preferenceRepository.markReadStories
|
||||||
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleNotificationMode() {
|
void toggleNotificationMode() {
|
||||||
emit(state.copyWith(showNotification: !state.showNotification));
|
emit(state.copyWith(showNotification: !state.showNotification));
|
||||||
_storageRepository.toggleNotificationMode();
|
_preferenceRepository.toggleNotificationMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleDisplayMode() {
|
void toggleDisplayMode() {
|
||||||
emit(state.copyWith(showComplexStoryTile: !state.showComplexStoryTile));
|
emit(state.copyWith(showComplexStoryTile: !state.showComplexStoryTile));
|
||||||
_storageRepository.toggleDisplayMode();
|
_preferenceRepository.toggleDisplayMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleNavigationMode() {
|
void toggleNavigationMode() {
|
||||||
emit(state.copyWith(showWebFirst: !state.showWebFirst));
|
emit(state.copyWith(showWebFirst: !state.showWebFirst));
|
||||||
_storageRepository.toggleNavigationMode();
|
_preferenceRepository.toggleNavigationMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleEyeCandyMode() {
|
void toggleEyeCandyMode() {
|
||||||
emit(state.copyWith(showEyeCandy: !state.showEyeCandy));
|
emit(state.copyWith(showEyeCandy: !state.showEyeCandy));
|
||||||
_storageRepository.toggleEyeCandyMode();
|
_preferenceRepository.toggleEyeCandyMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleTrueDarkMode() {
|
void toggleTrueDarkMode() {
|
||||||
emit(state.copyWith(useTrueDark: !state.useTrueDark));
|
emit(state.copyWith(useTrueDark: !state.useTrueDark));
|
||||||
_storageRepository.toggleTrueDarkMode();
|
_preferenceRepository.toggleTrueDarkMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleReaderMode() {
|
void toggleReaderMode() {
|
||||||
emit(state.copyWith(useReader: !state.useReader));
|
emit(state.copyWith(useReader: !state.useReader));
|
||||||
_storageRepository.toggleReaderMode();
|
_preferenceRepository.toggleReaderMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleMarkReadStoriesMode() {
|
void toggleMarkReadStoriesMode() {
|
||||||
emit(state.copyWith(markReadStories: !state.markReadStories));
|
emit(state.copyWith(markReadStories: !state.markReadStories));
|
||||||
_storageRepository.toggleMarkReadStoriesMode();
|
_preferenceRepository.toggleMarkReadStoriesMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,22 +14,22 @@ class VoteCubit extends Cubit<VoteState> {
|
|||||||
required Item item,
|
required Item item,
|
||||||
required AuthBloc authBloc,
|
required AuthBloc authBloc,
|
||||||
AuthRepository? authRepository,
|
AuthRepository? authRepository,
|
||||||
PreferenceRepository? storageRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
}) : _authBloc = authBloc,
|
}) : _authBloc = authBloc,
|
||||||
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||||
_storageRepository =
|
_preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
super(VoteState.init(item: item)) {
|
super(VoteState.init(item: item)) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final AuthBloc _authBloc;
|
final AuthBloc _authBloc;
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
static const int _karmaThreshold = 501;
|
static const int _karmaThreshold = 501;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
final bool? vote = await _storageRepository.vote(
|
final bool? vote = await _preferenceRepository.vote(
|
||||||
submittedTo: state.item.id,
|
submittedTo: state.item.id,
|
||||||
from: _authBloc.state.username,
|
from: _authBloc.state.username,
|
||||||
);
|
);
|
||||||
@ -73,7 +73,7 @@ class VoteCubit extends Cubit<VoteState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
_storageRepository.addVote(
|
_preferenceRepository.addVote(
|
||||||
username: _authBloc.state.username,
|
username: _authBloc.state.username,
|
||||||
id: state.item.id,
|
id: state.item.id,
|
||||||
vote: true,
|
vote: true,
|
||||||
@ -88,7 +88,7 @@ class VoteCubit extends Cubit<VoteState> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await _authRepository.upvote(id: state.item.id, upvote: false);
|
await _authRepository.upvote(id: state.item.id, upvote: false);
|
||||||
await _storageRepository.removeVote(
|
await _preferenceRepository.removeVote(
|
||||||
username: _authBloc.state.username,
|
username: _authBloc.state.username,
|
||||||
id: state.item.id,
|
id: state.item.id,
|
||||||
);
|
);
|
||||||
@ -118,7 +118,7 @@ class VoteCubit extends Cubit<VoteState> {
|
|||||||
await _authRepository.downvote(id: state.item.id, downvote: true);
|
await _authRepository.downvote(id: state.item.id, downvote: true);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await _storageRepository.addVote(
|
await _preferenceRepository.addVote(
|
||||||
username: _authBloc.state.username,
|
username: _authBloc.state.username,
|
||||||
id: state.item.id,
|
id: state.item.id,
|
||||||
vote: false,
|
vote: false,
|
||||||
@ -133,7 +133,7 @@ class VoteCubit extends Cubit<VoteState> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await _authRepository.downvote(id: state.item.id, downvote: false);
|
await _authRepository.downvote(id: state.item.id, downvote: false);
|
||||||
await _storageRepository.removeVote(
|
await _preferenceRepository.removeVote(
|
||||||
username: _authBloc.state.username,
|
username: _authBloc.state.username,
|
||||||
id: state.item.id,
|
id: state.item.id,
|
||||||
);
|
);
|
||||||
|
@ -1,20 +1,61 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
import 'package:hacki/config/custom_router.dart';
|
import 'package:hacki/config/custom_router.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
|
import 'package:hacki/services/fetcher.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:path_provider/path_provider.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 {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
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 Directory tempDir = await getTemporaryDirectory();
|
||||||
final String tempPath = tempDir.path;
|
final String tempPath = tempDir.path;
|
||||||
Hive.init(tempPath);
|
Hive.init(tempPath);
|
||||||
|
@ -10,21 +10,21 @@ import 'package:hacki/utils/service_exception.dart';
|
|||||||
class AuthRepository {
|
class AuthRepository {
|
||||||
AuthRepository({
|
AuthRepository({
|
||||||
Dio? dio,
|
Dio? dio,
|
||||||
PreferenceRepository? storageRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
}) : _dio = dio ?? Dio(),
|
}) : _dio = dio ?? Dio(),
|
||||||
_storageRepository =
|
_preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>();
|
preferenceRepository ?? locator.get<PreferenceRepository>();
|
||||||
|
|
||||||
static const String _authority = 'news.ycombinator.com';
|
static const String _authority = 'news.ycombinator.com';
|
||||||
|
|
||||||
final Dio _dio;
|
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({
|
Future<bool> login({
|
||||||
required String username,
|
required String username,
|
||||||
@ -40,16 +40,19 @@ class AuthRepository {
|
|||||||
final bool success = await _performDefaultPost(uri, data);
|
final bool success = await _performDefaultPost(uri, data);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await _storageRepository.setAuth(username: username, password: password);
|
await _preferenceRepository.setAuth(
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasLoggedIn() => _storageRepository.loggedIn;
|
Future<bool> hasLoggedIn() => _preferenceRepository.loggedIn;
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await _storageRepository.removeAuth();
|
await _preferenceRepository.removeAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> flag({
|
Future<bool> flag({
|
||||||
@ -57,8 +60,8 @@ class AuthRepository {
|
|||||||
required bool flag,
|
required bool flag,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'flag');
|
final Uri uri = Uri.https(_authority, 'flag');
|
||||||
final String? username = await _storageRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _storageRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = FlagPostData(
|
final PostDataMixin data = FlagPostData(
|
||||||
acct: username!,
|
acct: username!,
|
||||||
pw: password!,
|
pw: password!,
|
||||||
@ -74,8 +77,8 @@ class AuthRepository {
|
|||||||
required bool favorite,
|
required bool favorite,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'fave');
|
final Uri uri = Uri.https(_authority, 'fave');
|
||||||
final String? username = await _storageRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _storageRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = FavoritePostData(
|
final PostDataMixin data = FavoritePostData(
|
||||||
acct: username!,
|
acct: username!,
|
||||||
pw: password!,
|
pw: password!,
|
||||||
@ -91,8 +94,8 @@ class AuthRepository {
|
|||||||
required bool upvote,
|
required bool upvote,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'vote');
|
final Uri uri = Uri.https(_authority, 'vote');
|
||||||
final String? username = await _storageRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _storageRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = VotePostData(
|
final PostDataMixin data = VotePostData(
|
||||||
acct: username!,
|
acct: username!,
|
||||||
pw: password!,
|
pw: password!,
|
||||||
@ -108,8 +111,8 @@ class AuthRepository {
|
|||||||
required bool downvote,
|
required bool downvote,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'vote');
|
final Uri uri = Uri.https(_authority, 'vote');
|
||||||
final String? username = await _storageRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _storageRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = VotePostData(
|
final PostDataMixin data = VotePostData(
|
||||||
acct: username!,
|
acct: username!,
|
||||||
pw: password!,
|
pw: password!,
|
||||||
|
@ -9,20 +9,20 @@ import 'package:hacki/utils/utils.dart';
|
|||||||
class PostRepository {
|
class PostRepository {
|
||||||
PostRepository({Dio? dio, PreferenceRepository? storageRepository})
|
PostRepository({Dio? dio, PreferenceRepository? storageRepository})
|
||||||
: _dio = dio ?? Dio(),
|
: _dio = dio ?? Dio(),
|
||||||
_storageRepository =
|
_preferenceRepository =
|
||||||
storageRepository ?? locator.get<PreferenceRepository>();
|
storageRepository ?? locator.get<PreferenceRepository>();
|
||||||
|
|
||||||
static const String _authority = 'news.ycombinator.com';
|
static const String _authority = 'news.ycombinator.com';
|
||||||
|
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
final PreferenceRepository _storageRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
|
|
||||||
Future<bool> comment({
|
Future<bool> comment({
|
||||||
required int parentId,
|
required int parentId,
|
||||||
required String text,
|
required String text,
|
||||||
}) async {
|
}) async {
|
||||||
final String? username = await _storageRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _storageRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final Uri uri = Uri.https(_authority, 'comment');
|
final Uri uri = Uri.https(_authority, 'comment');
|
||||||
|
|
||||||
if (username == null || password == null) {
|
if (username == null || password == null) {
|
||||||
@ -48,8 +48,8 @@ class PostRepository {
|
|||||||
String? url,
|
String? url,
|
||||||
String? text,
|
String? text,
|
||||||
}) async {
|
}) async {
|
||||||
final String? username = await _storageRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _storageRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
|
|
||||||
if (username == null || password == null) {
|
if (username == null || password == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -91,8 +91,8 @@ class PostRepository {
|
|||||||
required int id,
|
required int id,
|
||||||
String? text,
|
String? text,
|
||||||
}) async {
|
}) async {
|
||||||
final String? username = await _storageRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _storageRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
|
|
||||||
if (username == null || password == null) {
|
if (username == null || password == null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -115,6 +115,15 @@ class PreferenceRepository {
|
|||||||
return int.tryParse(val);
|
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(
|
Future<List<int>> favList({required String of}) => _prefs.then(
|
||||||
(SharedPreferences prefs) =>
|
(SharedPreferences prefs) =>
|
||||||
((prefs.getStringList(_getFavKey('')) ?? <String>[])
|
((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 _getFavKey(String username) => 'fav_$username';
|
||||||
|
|
||||||
String _getVoteKey(String username, int id) => 'vote_$username-$id';
|
String _getVoteKey(String username, int id) => 'vote_$username-$id';
|
||||||
|
@ -69,7 +69,7 @@ class SembastRepository {
|
|||||||
if (_idsOfCommentsRepliedToMe == null) {
|
if (_idsOfCommentsRepliedToMe == null) {
|
||||||
final RecordSnapshot<dynamic, dynamic>? snapshot =
|
final RecordSnapshot<dynamic, dynamic>? snapshot =
|
||||||
await store.record(_idsOfCommentsRepliedToMeKey).getSnapshot(db);
|
await store.record(_idsOfCommentsRepliedToMeKey).getSnapshot(db);
|
||||||
list = snapshot?.value as List<int>? ?? <int>[];
|
list = (snapshot?.value as List<dynamic>? ?? <int>[]).cast<int>();
|
||||||
_idsOfCommentsRepliedToMe = list;
|
_idsOfCommentsRepliedToMe = list;
|
||||||
} else {
|
} else {
|
||||||
list = _idsOfCommentsRepliedToMe!;
|
list = _idsOfCommentsRepliedToMe!;
|
||||||
|
@ -161,6 +161,20 @@ class StoriesRepository {
|
|||||||
return comment;
|
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 {
|
Future<Item?> fetchItemBy({required int id}) async {
|
||||||
final Item? item = await _firebaseClient
|
final Item? item = await _firebaseClient
|
||||||
.get('${_baseUrl}item/$id.json')
|
.get('${_baseUrl}item/$id.json')
|
||||||
@ -182,6 +196,28 @@ class StoriesRepository {
|
|||||||
return item;
|
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 {
|
Future<List<int>?> fetchSubmitted({required String of}) async {
|
||||||
final List<int>? submitted = await _firebaseClient
|
final List<int>? submitted = await _firebaseClient
|
||||||
.get('${_baseUrl}user/$of.json')
|
.get('${_baseUrl}user/$of.json')
|
||||||
@ -209,6 +245,17 @@ class StoriesRepository {
|
|||||||
return item as Story;
|
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({
|
Future<Tuple2<Story, List<Comment>>?> fetchParentStoryWithComments({
|
||||||
required int id,
|
required int id,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// ignore_for_file: lines_longer_than_80_chars
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:badges/badges.dart';
|
import 'package:badges/badges.dart';
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
import 'package:flutter/material.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/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
@ -54,6 +58,34 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
// Constants.featurePinToTop,
|
// 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((_) {
|
SchedulerBinding.instance?.addPostFrameCallback((_) {
|
||||||
FeatureDiscovery.discoverFeatures(
|
FeatureDiscovery.discoverFeatures(
|
||||||
context,
|
context,
|
||||||
|
@ -216,7 +216,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
then: () {
|
then: () {
|
||||||
context
|
context
|
||||||
.read<NotificationCubit>()
|
.read<NotificationCubit>()
|
||||||
.markAsRead(cmt);
|
.markAsRead(cmt.id);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -265,7 +265,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'Hacki scans for new replies to your 15 '
|
'Hacki scans for new replies to your 15 '
|
||||||
'most recent comments or stories '
|
'most recent comments or stories '
|
||||||
'every 1 minute while the app is '
|
'every 5 minutes while the app is '
|
||||||
'running in the foreground.',
|
'running in the foreground.',
|
||||||
),
|
),
|
||||||
value: preferenceState.showNotification,
|
value: preferenceState.showNotification,
|
||||||
@ -388,7 +388,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Hacki',
|
applicationName: 'Hacki',
|
||||||
applicationVersion: 'v0.2.3',
|
applicationVersion: 'v0.2.4',
|
||||||
applicationIcon: Image.asset(
|
applicationIcon: Image.asset(
|
||||||
Constants.hackiIconPath,
|
Constants.hackiIconPath,
|
||||||
height: 50,
|
height: 50,
|
||||||
|
@ -1,34 +1,24 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
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 {
|
class CustomBlocObserver extends BlocObserver {
|
||||||
@override
|
@override
|
||||||
void onCreate(BlocBase<dynamic> bloc) {
|
void onEvent(
|
||||||
super.onCreate(bloc);
|
Bloc<dynamic, dynamic> bloc,
|
||||||
bloc.log(identifier: 'Bloc Created:');
|
Object? event,
|
||||||
}
|
) {
|
||||||
|
locator.get<Logger>().d(event);
|
||||||
@override
|
|
||||||
void onEvent(Bloc<dynamic, dynamic> bloc, Object? event) {
|
|
||||||
super.onEvent(bloc, event);
|
super.onEvent(bloc, event);
|
||||||
event?.log(identifier: 'Bloc Event:');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
|
void onError(
|
||||||
super.onChange(bloc, change);
|
BlocBase<dynamic> bloc,
|
||||||
change.log(identifier: 'Bloc Changed:');
|
Object error,
|
||||||
}
|
StackTrace stackTrace,
|
||||||
|
) {
|
||||||
@override
|
locator.get<Logger>().e(error);
|
||||||
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
|
|
||||||
super.onError(bloc, error, stackTrace);
|
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
140
lib/services/fetcher.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
lib/services/local_notification.dart
Normal file
33
lib/services/local_notification.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
export 'cache_service.dart';
|
export 'cache_service.dart';
|
||||||
export 'custom_bloc_observer.dart';
|
export 'custom_bloc_observer.dart';
|
||||||
|
export 'fetcher.dart';
|
||||||
export 'firebase_client.dart';
|
export 'firebase_client.dart';
|
||||||
|
export 'local_notification.dart';
|
||||||
|
60
pubspec.lock
60
pubspec.lock
@ -334,7 +334,7 @@ packages:
|
|||||||
name: flutter_inappwebview
|
name: flutter_inappwebview
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.3.2"
|
version: "5.4.3+4"
|
||||||
flutter_layout_grid:
|
flutter_layout_grid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -349,6 +349,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
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:
|
flutter_math_fork:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -527,6 +548,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
|
logger:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: logger
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -640,14 +668,14 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.9"
|
version: "2.0.9"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.12"
|
version: "2.0.12"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider_ios
|
name: path_provider_ios
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@ -759,7 +787,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.2"
|
version: "0.4.2"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rxdart
|
name: rxdart
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@ -780,19 +808,19 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.11"
|
version: "2.0.11"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.9"
|
version: "2.0.11"
|
||||||
shared_preferences_ios:
|
shared_preferences_ios:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_ios
|
name: shared_preferences_ios
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.1.0"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -959,6 +987,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.9"
|
version: "0.4.9"
|
||||||
|
timezone:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.0"
|
||||||
tuple:
|
tuple:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1176,13 +1211,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.3"
|
version: "2.3.3"
|
||||||
|
workmanager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: workmanager
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xdg_directories
|
name: xdg_directories
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0+1"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
12
pubspec.yaml
12
pubspec.yaml
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 0.2.3+37
|
version: 0.2.4+38
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@ -24,8 +24,9 @@ dependencies:
|
|||||||
flutter_fadein: ^2.0.0
|
flutter_fadein: ^2.0.0
|
||||||
flutter_feather_icons: 2.0.0+1
|
flutter_feather_icons: 2.0.0+1
|
||||||
flutter_html: ^2.2.1
|
flutter_html: ^2.2.1
|
||||||
flutter_inappwebview: ^5.3.2
|
flutter_inappwebview: ^5.4.3+4
|
||||||
flutter_linkify: ^5.0.2
|
flutter_linkify: ^5.0.2
|
||||||
|
flutter_local_notifications: ^9.5.0
|
||||||
flutter_secure_storage: ^5.0.2
|
flutter_secure_storage: ^5.0.2
|
||||||
flutter_slidable: ^1.2.0
|
flutter_slidable: ^1.2.0
|
||||||
font_awesome_flutter: ^9.2.0
|
font_awesome_flutter: ^9.2.0
|
||||||
@ -36,17 +37,24 @@ dependencies:
|
|||||||
html_unescape: ^2.0.0
|
html_unescape: ^2.0.0
|
||||||
http: ^0.13.3
|
http: ^0.13.3
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
|
logger: ^1.1.0
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.8
|
path_provider: ^2.0.8
|
||||||
|
path_provider_android: ^2.0.8
|
||||||
|
path_provider_ios: ^2.0.8
|
||||||
pull_to_refresh: ^2.0.0
|
pull_to_refresh: ^2.0.0
|
||||||
responsive_builder: ^0.4.2
|
responsive_builder: ^0.4.2
|
||||||
|
rxdart: ^0.27.3
|
||||||
sembast: ^3.1.1+1
|
sembast: ^3.1.1+1
|
||||||
shared_preferences: ^2.0.11
|
shared_preferences: ^2.0.11
|
||||||
|
shared_preferences_android: ^2.0.11
|
||||||
|
shared_preferences_ios: ^2.0.11
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
tuple: ^2.0.0
|
tuple: ^2.0.0
|
||||||
universal_platform: ^1.0.0+1
|
universal_platform: ^1.0.0+1
|
||||||
url_launcher: ^6.0.10
|
url_launcher: ^6.0.10
|
||||||
wakelock: ^0.6.1+2
|
wakelock: ^0.6.1+2
|
||||||
|
workmanager: ^0.4.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
bloc_test: ^9.0.3
|
bloc_test: ^9.0.3
|
||||||
|
Reference in New Issue
Block a user