Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
268f4054a3 | |||
988c5d9881 | |||
e748e2f818 | |||
1b0a0dbda9 | |||
64d68389ba | |||
381c99b353 | |||
39ee3137f8 | |||
0d76be8634 | |||
9986f72e11 | |||
ef557e7b84 | |||
ec065c0122 | |||
2960c6e59e | |||
92dac6b932 | |||
20365393a3 | |||
8d238744c7 | |||
e33ff417fb | |||
d8922c2641 | |||
c6e0461857 | |||
30ca356dc8 | |||
7d11398e6d | |||
a4f52284ef |
3
.github/workflows/commit_check.yml
vendored
@ -9,13 +9,14 @@ on:
|
||||
jobs:
|
||||
releases:
|
||||
name: Check commit
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: checkout all the submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
|
1
.github/workflows/publish_ios.yml
vendored
@ -23,6 +23,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
|
@ -29,6 +29,7 @@ Features:
|
||||
- Download stories and comments for offline reading.
|
||||
- Pick up where you left off.
|
||||
- Synced favorites and pins across devices. (iOS only)
|
||||
- Export or import your favorites.
|
||||
- Launch from system share sheet.
|
||||
- And more...
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
include: package:very_good_analysis/analysis_options.3.1.0.yaml
|
||||
include: package:very_good_analysis/analysis_options.5.0.0.yaml
|
||||
linter:
|
||||
rules:
|
||||
parameter_assignments: false
|
||||
|
@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 940 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.4 KiB |
@ -24,6 +24,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
BIN
assets/hacki-github.xcf
Normal file
BIN
assets/hacki.xcf
Normal file
BIN
assets/screenshots/hacki-1.png
Normal file
After Width: | Height: | Size: 890 KiB |
BIN
assets/screenshots/hacki-2.png
Normal file
After Width: | Height: | Size: 873 KiB |
BIN
assets/screenshots/hacki-3.png
Normal file
After Width: | Height: | Size: 770 KiB |
BIN
assets/screenshots/hacki-4.png
Normal file
After Width: | Height: | Size: 517 KiB |
@ -1,108 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:in_app_review_platform_interface/in_app_review_platform_interface.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final inAppReview = InAppReview.instance;
|
||||
late MockInAppReviewPlatform platform;
|
||||
|
||||
setUp(() {
|
||||
platform = MockInAppReviewPlatform();
|
||||
InAppReviewPlatform.instance = platform;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
verifyNoMoreInteractions(platform);
|
||||
});
|
||||
|
||||
group('isAvailable', () {
|
||||
test(
|
||||
'should call InAppReviewPlatform.isAvailable()',
|
||||
() async {
|
||||
// ARRANGE
|
||||
when(platform.isAvailable()).thenAnswer((_) async => true);
|
||||
|
||||
// ACT
|
||||
final result = await inAppReview.isAvailable();
|
||||
|
||||
// ASSERT
|
||||
verify(platform.isAvailable());
|
||||
expect(result, isTrue);
|
||||
},
|
||||
);
|
||||
});
|
||||
group('requestReview', () {
|
||||
test(
|
||||
'should call InAppReviewPlatform.requestReview()',
|
||||
() async {
|
||||
// ARRANGE
|
||||
when(platform.requestReview()).thenAnswer((_) async {});
|
||||
|
||||
// ACT
|
||||
await inAppReview.requestReview();
|
||||
|
||||
// ASSERT
|
||||
verify(platform.requestReview());
|
||||
},
|
||||
);
|
||||
});
|
||||
group('openStoreListing', () {
|
||||
test(
|
||||
'should call InAppReviewPlatform.openStoreListing()',
|
||||
() async {
|
||||
// ARRANGE
|
||||
const appStoreId = 'app_store_id';
|
||||
const microsoftStoreId = 'microsoft_store_id';
|
||||
when(platform.openStoreListing(
|
||||
appStoreId: appStoreId,
|
||||
microsoftStoreId: microsoftStoreId,
|
||||
)).thenAnswer((_) async {});
|
||||
|
||||
// ACT
|
||||
await inAppReview.openStoreListing(
|
||||
appStoreId: appStoreId,
|
||||
microsoftStoreId: microsoftStoreId,
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
verify(platform.openStoreListing(
|
||||
appStoreId: appStoreId,
|
||||
microsoftStoreId: microsoftStoreId,
|
||||
));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class MockInAppReviewPlatform extends Mock
|
||||
with MockPlatformInterfaceMixin
|
||||
implements InAppReviewPlatform {
|
||||
@override
|
||||
Future<bool> isAvailable() => super.noSuchMethod(
|
||||
Invocation.method(#isAvailable, null),
|
||||
returnValue: Future.value(true),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> requestReview() => super.noSuchMethod(
|
||||
Invocation.method(#requestReview, null),
|
||||
returnValue: Future<void>.value(),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> openStoreListing({
|
||||
String? appStoreId,
|
||||
String? microsoftStoreId,
|
||||
}) =>
|
||||
super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#openStoreListing,
|
||||
null,
|
||||
{#appStoreId: appStoreId, #microsoftStoreId: microsoftStoreId},
|
||||
),
|
||||
returnValue: Future<void>.value(),
|
||||
);
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:in_app_review_platform_interface/method_channel_in_app_review.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
late MethodChannelInAppReview methodChannelInAppReview;
|
||||
late List<MethodCall> log = <MethodCall>[];
|
||||
const MethodChannel channel = MethodChannel('dev.britannio.in_app_review');
|
||||
|
||||
setUp(() {
|
||||
methodChannelInAppReview = MethodChannelInAppReview();
|
||||
methodChannelInAppReview.channel = channel;
|
||||
log = <MethodCall>[];
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
log.clear();
|
||||
});
|
||||
|
||||
channel.setMockMethodCallHandler((MethodCall call) async {
|
||||
log.add(call);
|
||||
|
||||
switch (call.method) {
|
||||
case 'isAvailable':
|
||||
return true;
|
||||
case 'requestReview':
|
||||
case 'openStoreListing':
|
||||
return null;
|
||||
default:
|
||||
assert(false);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
group('isAvailable', () {
|
||||
test(
|
||||
'should invoke the isAvailable method channel',
|
||||
() async {
|
||||
// ACT
|
||||
final bool result = await methodChannelInAppReview.isAvailable();
|
||||
|
||||
// ASSERT
|
||||
expect(log, <Matcher>[isMethodCall('isAvailable', arguments: null)]);
|
||||
expect(result, isTrue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('requestReview', () {
|
||||
test(
|
||||
'should invoke the requestReview method channel',
|
||||
() async {
|
||||
// ACT
|
||||
await methodChannelInAppReview.requestReview();
|
||||
|
||||
// ASSERT
|
||||
expect(log, <Matcher>[isMethodCall('requestReview', arguments: null)]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('openStoreListing', () {
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on Android',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'android');
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing();
|
||||
|
||||
// ASSERT
|
||||
expect(
|
||||
log,
|
||||
<Matcher>[isMethodCall('openStoreListing', arguments: null)],
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on iOS',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'ios');
|
||||
final String appStoreId = "store_id";
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
|
||||
|
||||
// ASSERT
|
||||
expect(log,
|
||||
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on MacOS',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'macos');
|
||||
final String appStoreId = "store_id";
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
|
||||
|
||||
// ASSERT
|
||||
expect(log,
|
||||
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on Windows',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'windows');
|
||||
final String microsoftStoreId = 'store_id';
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing(
|
||||
microsoftStoreId: microsoftStoreId,
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(log, <Matcher>[
|
||||
isMethodCall('openStoreListing', arguments: microsoftStoreId)
|
||||
]);
|
||||
},
|
||||
skip:
|
||||
'The windows uwp implementation still uses the url_launcher package',
|
||||
);
|
||||
});
|
||||
}
|
1
fastlane/metadata/android/en-US/changelogs/121.txt
Normal file
@ -0,0 +1 @@
|
||||
- Ability to mark a story as read once scrolling past.
|
@ -27,12 +27,16 @@ PODS:
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
- OrderedSet (5.0.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- receive_sharing_intent (0.0.1):
|
||||
- Flutter
|
||||
@ -41,7 +45,7 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.2):
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- synced_shared_preferences (0.0.1):
|
||||
@ -67,10 +71,11 @@ DEPENDENCIES:
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- synced_shared_preferences (from `.symlinks/plugins/synced_shared_preferences/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
@ -81,6 +86,7 @@ DEPENDENCIES:
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- MTBBarcodeScanner
|
||||
- OrderedSet
|
||||
- ReachabilitySwift
|
||||
|
||||
@ -108,13 +114,15 @@ EXTERNAL SOURCES:
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
synced_shared_preferences:
|
||||
@ -129,8 +137,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
@ -139,19 +147,21 @@ SPEC CHECKSUMS:
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||
|
@ -10,6 +10,7 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
@ -22,7 +23,6 @@
|
||||
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
|
||||
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
|
||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -68,14 +68,14 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -83,8 +83,8 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
E51D52AD283B464E00FC8DD8 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E51D52AF283B464E00FC8DD8 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
E51D52B2283B464E00FC8DD8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
@ -107,7 +107,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
|
||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */,
|
||||
7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -183,8 +183,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
|
||||
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */,
|
||||
E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */,
|
||||
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -192,9 +192,9 @@
|
||||
D79CD63C88FF49EF451AFDDF /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */,
|
||||
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */,
|
||||
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */,
|
||||
0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */,
|
||||
D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */,
|
||||
B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@ -229,15 +229,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */,
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */,
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -291,7 +291,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1330;
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@ -365,6 +365,7 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
@ -373,7 +374,22 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */ = {
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@ -395,7 +411,7 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */ = {
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@ -412,21 +428,6 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -565,11 +566,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||
@ -583,7 +582,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@ -707,11 +705,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||
@ -725,7 +721,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -780,11 +775,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -802,7 +795,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -863,11 +855,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -884,7 +874,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -905,11 +894,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -927,7 +914,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -992,11 +978,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -1013,7 +997,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -76,5 +76,11 @@
|
||||
<false/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
<key>FLTEnableWideGamut</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -52,14 +52,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
state.copyWith(
|
||||
isLoggedIn: true,
|
||||
user: user,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoggedIn: false,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -81,7 +81,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
}
|
||||
|
||||
Future<void> onLogin(AuthLogin event, Emitter<AuthState> emit) async {
|
||||
emit(state.copyWith(status: AuthStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
final bool successful = await _authRepository.login(
|
||||
username: event.username,
|
||||
@ -94,11 +94,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
state.copyWith(
|
||||
user: user ?? User.emptyWithId(event.username),
|
||||
isLoggedIn: true,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(status: AuthStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,5 @@
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
enum AuthStatus {
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class AuthState extends Equatable {
|
||||
const AuthState({
|
||||
required this.user,
|
||||
@ -17,13 +11,13 @@ class AuthState extends Equatable {
|
||||
const AuthState.init()
|
||||
: user = const User.empty(),
|
||||
isLoggedIn = false,
|
||||
status = AuthStatus.loaded,
|
||||
status = Status.success,
|
||||
agreedToEULA = false;
|
||||
|
||||
final User user;
|
||||
final bool isLoggedIn;
|
||||
final bool agreedToEULA;
|
||||
final AuthStatus status;
|
||||
final Status status;
|
||||
|
||||
String get username => user.id;
|
||||
|
||||
@ -31,7 +25,7 @@ class AuthState extends Equatable {
|
||||
User? user,
|
||||
bool? isLoggedIn,
|
||||
bool? agreedToEULA,
|
||||
AuthStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return AuthState(
|
||||
user: user ?? this.user,
|
||||
|
@ -79,7 +79,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
const StoriesState.init().copyWith(
|
||||
isOfflineReading: hasCachedStories &&
|
||||
// Only go into offline mode in the next session.
|
||||
state.downloadStatus == StoriesDownloadStatus.initial,
|
||||
state.downloadStatus == StoriesDownloadStatus.idle,
|
||||
currentPageSize: pageSize,
|
||||
downloadStatus: state.downloadStatus,
|
||||
storiesDownloaded: state.storiesDownloaded,
|
||||
@ -133,10 +133,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesRefresh event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
if (state.statusByType[event.type] == Status.inProgress) return;
|
||||
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
to: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -144,7 +146,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
to: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@ -157,7 +159,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
to: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -216,7 +218,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
to: Status.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -243,7 +245,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded),
|
||||
state.copyWithStatusUpdated(type: event.type, to: Status.success),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,7 @@
|
||||
part of 'stories_bloc.dart';
|
||||
|
||||
enum StoriesStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum StoriesDownloadStatus {
|
||||
initial,
|
||||
idle,
|
||||
downloading,
|
||||
finished,
|
||||
failure,
|
||||
@ -43,12 +37,12 @@ class StoriesState extends Equatable {
|
||||
StoryType.ask: <int>[],
|
||||
StoryType.show: <int>[],
|
||||
},
|
||||
this.statusByType = const <StoryType, StoriesStatus>{
|
||||
StoryType.top: StoriesStatus.initial,
|
||||
StoryType.best: StoriesStatus.initial,
|
||||
StoryType.latest: StoriesStatus.initial,
|
||||
StoryType.ask: StoriesStatus.initial,
|
||||
StoryType.show: StoriesStatus.initial,
|
||||
this.statusByType = const <StoryType, Status>{
|
||||
StoryType.top: Status.idle,
|
||||
StoryType.best: Status.idle,
|
||||
StoryType.latest: Status.idle,
|
||||
StoryType.ask: Status.idle,
|
||||
StoryType.show: Status.idle,
|
||||
},
|
||||
this.currentPageByType = const <StoryType, int>{
|
||||
StoryType.top: 0,
|
||||
@ -58,7 +52,7 @@ class StoriesState extends Equatable {
|
||||
StoryType.show: 0,
|
||||
},
|
||||
}) : isOfflineReading = false,
|
||||
downloadStatus = StoriesDownloadStatus.initial,
|
||||
downloadStatus = StoriesDownloadStatus.idle,
|
||||
currentPageSize = 0,
|
||||
readStoriesIds = const <int>{},
|
||||
storiesDownloaded = 0,
|
||||
@ -66,7 +60,7 @@ class StoriesState extends Equatable {
|
||||
|
||||
final Map<StoryType, List<Story>> storiesByType;
|
||||
final Map<StoryType, List<int>> storyIdsByType;
|
||||
final Map<StoryType, StoriesStatus> statusByType;
|
||||
final Map<StoryType, Status> statusByType;
|
||||
final Map<StoryType, int> currentPageByType;
|
||||
final Set<int> readStoriesIds;
|
||||
final StoriesDownloadStatus downloadStatus;
|
||||
@ -78,7 +72,7 @@ class StoriesState extends Equatable {
|
||||
StoriesState copyWith({
|
||||
Map<StoryType, List<Story>>? storiesByType,
|
||||
Map<StoryType, List<int>>? storyIdsByType,
|
||||
Map<StoryType, StoriesStatus>? statusByType,
|
||||
Map<StoryType, Status>? statusByType,
|
||||
Map<StoryType, int>? currentPageByType,
|
||||
Set<int>? readStoriesIds,
|
||||
StoriesDownloadStatus? downloadStatus,
|
||||
@ -133,10 +127,10 @@ class StoriesState extends Equatable {
|
||||
|
||||
StoriesState copyWithStatusUpdated({
|
||||
required StoryType type,
|
||||
required StoriesStatus to,
|
||||
required Status to,
|
||||
}) {
|
||||
final Map<StoryType, StoriesStatus> newMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
final Map<StoryType, Status> newMap =
|
||||
Map<StoryType, Status>.from(statusByType);
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
statusByType: newMap,
|
||||
@ -162,9 +156,9 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, List<int>> newStoryIdsMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newStoryIdsMap[type] = <int>[];
|
||||
final Map<StoryType, StoriesStatus> newStatusMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newStatusMap[type] = StoriesStatus.loading;
|
||||
final Map<StoryType, Status> newStatusMap =
|
||||
Map<StoryType, Status>.from(statusByType);
|
||||
newStatusMap[type] = Status.inProgress;
|
||||
final Map<StoryType, int> newCurrentPageMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newCurrentPageMap[type] = 0;
|
||||
|
@ -39,8 +39,9 @@ abstract class Constants {
|
||||
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
|
||||
static const String featureLogIn = 'log_in';
|
||||
static const String featurePinToTop = 'pin_to_top';
|
||||
static const String featureJumpUpButton = 'jump_up_button';
|
||||
static const String featureJumpDownButton = 'jump_down_button';
|
||||
static const String featureJumpUpButton = 'jump_up_button_with_long_press';
|
||||
static const String featureJumpDownButton =
|
||||
'jump_down_button_with_long_press';
|
||||
|
||||
static final String happyFace = <String>[
|
||||
'(๑•̀ㅂ•́)و✧',
|
||||
@ -78,3 +79,15 @@ abstract class RegExpConstants {
|
||||
static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
|
||||
static const String number = '[0-9]+';
|
||||
}
|
||||
|
||||
abstract class Durations {
|
||||
static const Duration ms100 = Duration(milliseconds: 100);
|
||||
static const Duration ms200 = Duration(milliseconds: 200);
|
||||
static const Duration ms300 = Duration(milliseconds: 300);
|
||||
static const Duration ms400 = Duration(milliseconds: 400);
|
||||
static const Duration ms500 = Duration(milliseconds: 500);
|
||||
static const Duration ms600 = Duration(milliseconds: 600);
|
||||
static const Duration oneSecond = Duration(seconds: 1);
|
||||
static const Duration twoSeconds = Duration(seconds: 2);
|
||||
static const Duration tenSeconds = Duration(seconds: 10);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import 'package:logger/logger.dart';
|
||||
|
||||
class CustomLogFilter extends LogFilter {
|
||||
@override
|
||||
Level? get level => Level.verbose;
|
||||
Level? get level => Level.trace;
|
||||
|
||||
/// The minimal level allowed in production.
|
||||
static const Level _minimalLevel = Level.info;
|
||||
|
@ -1,49 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
|
||||
/// Custom router.
|
||||
///
|
||||
/// Handle named routing.
|
||||
class CustomRouter {
|
||||
/// Top level routing.
|
||||
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case HomeScreen.routeName:
|
||||
return HomeScreen.route();
|
||||
case ItemScreen.routeName:
|
||||
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
return _errorRoute();
|
||||
}
|
||||
}
|
||||
|
||||
/// Nested routing for bottom navigation bar.
|
||||
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case ItemScreen.routeName:
|
||||
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
return _errorRoute();
|
||||
}
|
||||
}
|
||||
|
||||
/// Error route.
|
||||
static Route<dynamic> _errorRoute() {
|
||||
return MaterialPageRoute<dynamic>(
|
||||
settings: const RouteSettings(name: '/error'),
|
||||
builder: (_) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Error'),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(Constants.errorMessage),
|
||||
final GoRouter router = GoRouter(
|
||||
observers: <NavigatorObserver>[
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
],
|
||||
initialLocation: HomeScreen.routeName,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: HomeScreen.routeName,
|
||||
builder: (_, __) => const HomeScreen(),
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: ItemScreen.routeName,
|
||||
builder: (_, GoRouterState state) {
|
||||
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
|
||||
if (args == null) {
|
||||
throw GoError("args can't be null");
|
||||
}
|
||||
return ItemScreen.phone(args);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${ItemScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
|
||||
if (args == null) {
|
||||
throw GoError("args can't be null");
|
||||
}
|
||||
return ItemScreen.phone(args);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${SubmitScreen.routeName}',
|
||||
builder: (_, __) => BlocProvider<SubmitCubit>(
|
||||
create: (_) => SubmitCubit(),
|
||||
child: const SubmitScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${QrCodeScannerScreen.routeName}',
|
||||
builder: (_, __) => const QrCodeScannerScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${QrCodeViewScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? data = state.extra as String?;
|
||||
if (data == null) {
|
||||
throw GoError("data can't be null");
|
||||
}
|
||||
return QrCodeViewScreen(
|
||||
data: data,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${WebViewScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? link = state.extra as String?;
|
||||
if (link == null) {
|
||||
throw GoError("link can't be null");
|
||||
}
|
||||
return WebViewScreen(
|
||||
url: link,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ class CustomFileOutput extends LogOutput {
|
||||
IOSink? _sink;
|
||||
|
||||
@override
|
||||
void init() {
|
||||
Future<void> init() async {
|
||||
_sink = file.openWrite(
|
||||
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
|
||||
encoding: encoding,
|
||||
|
@ -5,9 +5,10 @@ import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
@ -24,15 +25,15 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
CommentsCubit({
|
||||
required FilterCubit filterCubit,
|
||||
required CollapseCache collapseCache,
|
||||
required bool isOfflineReading,
|
||||
required Item item,
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
CommentCache? commentCache,
|
||||
OfflineRepository? offlineRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
Logger? logger,
|
||||
required bool isOfflineReading,
|
||||
required Item item,
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
}) : _filterCubit = filterCubit,
|
||||
_collapseCache = collapseCache,
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
@ -105,7 +106,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
status: CommentsStatus.inProgress,
|
||||
comments: <Comment>[],
|
||||
currentPage: 0,
|
||||
),
|
||||
@ -131,13 +132,11 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
);
|
||||
break;
|
||||
case FetchMode.eager:
|
||||
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +150,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
Future<void> refresh() async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
status: CommentsStatus.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -225,7 +224,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
void Function(Comment)? onCommentFetched,
|
||||
VoidCallback? onDone,
|
||||
}) {
|
||||
if (comment == null && state.status == CommentsStatus.loading) return;
|
||||
if (comment == null && state.status == CommentsStatus.inProgress) return;
|
||||
|
||||
switch (state.fetchMode) {
|
||||
case FetchMode.lazy:
|
||||
@ -268,30 +267,28 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
});
|
||||
|
||||
_streamSubscriptions[comment.id] = streamSubscription;
|
||||
break;
|
||||
case FetchMode.eager:
|
||||
if (_streamSubscription != null) {
|
||||
emit(state.copyWith(status: CommentsStatus.loading));
|
||||
emit(state.copyWith(status: CommentsStatus.inProgress));
|
||||
_streamSubscription
|
||||
?..resume()
|
||||
..onData(onCommentFetched);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadParentThread() async {
|
||||
HapticFeedbackUtil.light();
|
||||
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
|
||||
emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress));
|
||||
final Item? parent =
|
||||
await _storiesRepository.fetchItem(id: state.item.parent);
|
||||
|
||||
if (parent == null) {
|
||||
return;
|
||||
} else {
|
||||
await HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: ItemScreenArgs(item: parent),
|
||||
await router.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
extra: ItemScreenArgs(item: parent),
|
||||
);
|
||||
|
||||
emit(
|
||||
@ -304,7 +301,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
Future<void> loadRootThread() async {
|
||||
HapticFeedbackUtil.light();
|
||||
emit(state.copyWith(fetchRootStatus: CommentsStatus.loading));
|
||||
emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress));
|
||||
final Story? parent = await _storiesRepository
|
||||
.fetchParentStory(id: state.item.id)
|
||||
.then(_toBuildableStory);
|
||||
@ -312,9 +309,9 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
if (parent == null) {
|
||||
return;
|
||||
} else {
|
||||
await HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: ItemScreenArgs(item: parent),
|
||||
await router.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
extra: ItemScreenArgs(item: parent),
|
||||
);
|
||||
|
||||
emit(
|
||||
@ -352,7 +349,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
init(useCommentCache: true);
|
||||
}
|
||||
|
||||
void jump(
|
||||
/// Scroll to next root level comment.
|
||||
void scrollToNextRoot(
|
||||
ItemScrollController itemScrollController,
|
||||
ItemPositionsListener itemPositionsListener,
|
||||
) {
|
||||
@ -382,14 +380,15 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
itemScrollController.scrollTo(
|
||||
index: i + 1,
|
||||
alignment: 0.15,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
duration: Durations.ms400,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void jumpUp(
|
||||
/// Scroll to previous root level comment.
|
||||
void scrollToPreviousRoot(
|
||||
ItemScrollController itemScrollController,
|
||||
ItemPositionsListener itemPositionsListener,
|
||||
) {
|
||||
@ -420,7 +419,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
itemScrollController.scrollTo(
|
||||
index: i + 1,
|
||||
alignment: 0.15,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
duration: Durations.ms400,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
part of 'comments_cubit.dart';
|
||||
|
||||
enum CommentsStatus {
|
||||
init,
|
||||
loading,
|
||||
idle,
|
||||
inProgress,
|
||||
loaded,
|
||||
allLoaded,
|
||||
failure,
|
||||
error,
|
||||
}
|
||||
|
||||
class CommentsState extends Equatable {
|
||||
@ -28,9 +28,9 @@ class CommentsState extends Equatable {
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
}) : comments = <Comment>[],
|
||||
status = CommentsStatus.init,
|
||||
fetchParentStatus = CommentsStatus.init,
|
||||
fetchRootStatus = CommentsStatus.init,
|
||||
status = CommentsStatus.idle,
|
||||
fetchParentStatus = CommentsStatus.idle,
|
||||
fetchRootStatus = CommentsStatus.idle,
|
||||
onlyShowTargetComment = false,
|
||||
currentPage = 0;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -11,7 +12,7 @@ part 'edit_state.dart';
|
||||
class EditCubit extends HydratedCubit<EditState> {
|
||||
EditCubit({DraftCache? draftCache})
|
||||
: _draftCache = draftCache ?? locator.get<DraftCache>(),
|
||||
_debouncer = Debouncer(delay: const Duration(seconds: 1)),
|
||||
_debouncer = Debouncer(delay: Durations.oneSecond),
|
||||
super(const EditState.init());
|
||||
|
||||
final DraftCache _draftCache;
|
||||
|
@ -51,7 +51,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
.onDone(() {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FavStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -107,7 +107,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
}
|
||||
|
||||
void loadMore() {
|
||||
emit(state.copyWith(status: FavStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final int currentPage = state.currentPage;
|
||||
final int len = state.favIds.length;
|
||||
emit(state.copyWith(currentPage: currentPage + 1));
|
||||
@ -128,10 +128,10 @@ class FavCubit extends Cubit<FavState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
} else {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FavStatus.loading,
|
||||
status: Status.inProgress,
|
||||
currentPage: 0,
|
||||
favItems: <Item>[],
|
||||
favIds: <int>[],
|
||||
@ -155,7 +155,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'fav_cubit.dart';
|
||||
|
||||
enum FavStatus {
|
||||
init,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class FavState extends Equatable {
|
||||
const FavState({
|
||||
required this.favIds,
|
||||
@ -18,18 +11,18 @@ class FavState extends Equatable {
|
||||
FavState.init()
|
||||
: favIds = <int>[],
|
||||
favItems = <Item>[],
|
||||
status = FavStatus.init,
|
||||
status = Status.idle,
|
||||
currentPage = 0;
|
||||
|
||||
final List<int> favIds;
|
||||
final List<Item> favItems;
|
||||
final FavStatus status;
|
||||
final Status status;
|
||||
final int currentPage;
|
||||
|
||||
FavState copyWith({
|
||||
List<int>? favIds,
|
||||
List<Item>? favItems,
|
||||
FavStatus? status,
|
||||
Status? status,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return FavState(
|
||||
|
@ -54,7 +54,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
}
|
||||
|
||||
void loadMore() {
|
||||
emit(state.copyWith(status: HistoryStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final int currentPage = state.currentPage;
|
||||
final int len = state.submittedIds.length;
|
||||
emit(state.copyWith(currentPage: currentPage + 1));
|
||||
@ -75,10 +75,10 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: HistoryStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
} else {
|
||||
emit(state.copyWith(status: HistoryStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
final String username = _authBloc.state.username;
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: HistoryStatus.loading,
|
||||
status: Status.inProgress,
|
||||
currentPage: 0,
|
||||
submittedIds: <int>[],
|
||||
submittedItems: <Item>[],
|
||||
@ -107,7 +107,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: HistoryStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'history_cubit.dart';
|
||||
|
||||
enum HistoryStatus {
|
||||
init,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class HistoryState extends Equatable {
|
||||
const HistoryState({
|
||||
required this.submittedIds,
|
||||
@ -18,18 +11,18 @@ class HistoryState extends Equatable {
|
||||
HistoryState.init()
|
||||
: submittedIds = <int>[],
|
||||
submittedItems = <Item>[],
|
||||
status = HistoryStatus.init,
|
||||
status = Status.idle,
|
||||
currentPage = 0;
|
||||
|
||||
final List<int> submittedIds;
|
||||
final List<Item> submittedItems;
|
||||
final HistoryStatus status;
|
||||
final Status status;
|
||||
final int currentPage;
|
||||
|
||||
HistoryState copyWith({
|
||||
List<int>? submittedIds,
|
||||
List<Item>? submittedItems,
|
||||
HistoryStatus? status,
|
||||
Status? status,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return HistoryState(
|
||||
|
@ -4,6 +4,7 @@ import 'dart:math';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -31,7 +32,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
if (authState.isLoggedIn && authState.username != _username) {
|
||||
// Get the user setting.
|
||||
if (_preferenceCubit.state.notificationEnabled) {
|
||||
Future<void>.delayed(const Duration(seconds: 2), init);
|
||||
Future<void>.delayed(Durations.twoSeconds, init);
|
||||
}
|
||||
|
||||
// Listen for setting changes in the future.
|
||||
@ -99,7 +100,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
void markAsRead(int id) {
|
||||
Future.doWhile(() {
|
||||
if (state.status != NotificationStatus.loading) {
|
||||
if (state.status != Status.inProgress) {
|
||||
if (state.unreadCommentsIds.contains(id)) {
|
||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||
..remove(id);
|
||||
@ -115,7 +116,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
void markAllAsRead() {
|
||||
Future.doWhile(() {
|
||||
if (state.status != NotificationStatus.loading) {
|
||||
if (state.status != Status.inProgress) {
|
||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||
return false;
|
||||
@ -130,7 +131,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
_preferenceCubit.state.notificationEnabled) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: NotificationStatus.loading,
|
||||
status: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -140,14 +141,14 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: NotificationStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
emit(state.copyWith(status: NotificationStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
final int currentPage = state.currentPage + 1;
|
||||
final int lower = currentPage * _pageSize + state.offset;
|
||||
@ -168,7 +169,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: NotificationStatus.loaded,
|
||||
status: Status.success,
|
||||
currentPage: currentPage,
|
||||
),
|
||||
);
|
||||
@ -236,7 +237,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
}
|
||||
}).whenComplete(
|
||||
() => emit(
|
||||
state.copyWith(status: NotificationStatus.loaded),
|
||||
state.copyWith(status: Status.success),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'notification_cubit.dart';
|
||||
|
||||
enum NotificationStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class NotificationState extends Equatable {
|
||||
const NotificationState({
|
||||
required this.comments,
|
||||
@ -23,14 +16,14 @@ class NotificationState extends Equatable {
|
||||
allCommentsIds = <int>[],
|
||||
currentPage = 0,
|
||||
offset = 0,
|
||||
status = NotificationStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final List<Comment> comments;
|
||||
final List<int> allCommentsIds;
|
||||
final List<int> unreadCommentsIds;
|
||||
final int currentPage;
|
||||
final int offset;
|
||||
final NotificationStatus status;
|
||||
final Status status;
|
||||
|
||||
NotificationState copyWith({
|
||||
List<Comment>? comments,
|
||||
@ -38,7 +31,7 @@ class NotificationState extends Equatable {
|
||||
List<int>? unreadCommentsIds,
|
||||
int? currentPage,
|
||||
int? offset,
|
||||
NotificationStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return NotificationState(
|
||||
comments: comments ?? this.comments,
|
||||
|
@ -27,7 +27,7 @@ class PinCubit extends Cubit<PinState> {
|
||||
emit(state.copyWith(pinnedStoriesIds: ids));
|
||||
|
||||
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
|
||||
});
|
||||
}).whenComplete(() => emit(state.copyWith(status: Status.success)));
|
||||
}
|
||||
|
||||
void pinStory(Story story) {
|
||||
@ -52,7 +52,10 @@ class PinCubit extends Cubit<PinState> {
|
||||
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
||||
}
|
||||
|
||||
void refresh() => init();
|
||||
void refresh() {
|
||||
if (state.status.isLoading) return;
|
||||
init();
|
||||
}
|
||||
|
||||
void _onStoryFetched(Story story) {
|
||||
emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story]));
|
||||
|
@ -4,22 +4,27 @@ class PinState extends Equatable {
|
||||
const PinState({
|
||||
required this.pinnedStoriesIds,
|
||||
required this.pinnedStories,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
PinState.init()
|
||||
: pinnedStoriesIds = <int>[],
|
||||
pinnedStories = <Story>[];
|
||||
pinnedStories = <Story>[],
|
||||
status = Status.idle;
|
||||
|
||||
final List<int> pinnedStoriesIds;
|
||||
final List<Story> pinnedStories;
|
||||
final Status status;
|
||||
|
||||
PinState copyWith({
|
||||
List<int>? pinnedStoriesIds,
|
||||
List<Story>? pinnedStories,
|
||||
Status? status,
|
||||
}) {
|
||||
return PinState(
|
||||
pinnedStoriesIds: pinnedStoriesIds ?? this.pinnedStoriesIds,
|
||||
pinnedStories: pinnedStories ?? this.pinnedStories,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,5 +32,6 @@ class PinState extends Equatable {
|
||||
List<Object?> get props => <Object?>[
|
||||
pinnedStoriesIds,
|
||||
pinnedStories,
|
||||
status,
|
||||
];
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
emit(PollState.init());
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: PollStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
List<int> pollOptionsIds = _story.parts;
|
||||
|
||||
@ -42,7 +42,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
|
||||
// If pollOptionsIds is still empty, exit loading state.
|
||||
if (pollOptionsIds.isEmpty) {
|
||||
emit(state.copyWith(status: PollStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
);
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: PollStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'poll_cubit.dart';
|
||||
|
||||
enum PollStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class PollState extends Equatable {
|
||||
const PollState({
|
||||
required this.totalVotes,
|
||||
@ -19,18 +12,18 @@ class PollState extends Equatable {
|
||||
: totalVotes = 0,
|
||||
selections = <int>{},
|
||||
pollOptions = <PollOption>[],
|
||||
status = PollStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final int totalVotes;
|
||||
final Set<int> selections;
|
||||
final List<PollOption> pollOptions;
|
||||
final PollStatus status;
|
||||
final Status status;
|
||||
|
||||
PollState copyWith({
|
||||
int? totalVotes,
|
||||
Set<int>? selections,
|
||||
List<PollOption>? pollOptions,
|
||||
PollStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return PollState(
|
||||
totalVotes: totalVotes ?? this.totalVotes,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
|
||||
part 'post_state.dart';
|
||||
@ -14,34 +15,31 @@ class PostCubit extends Cubit<PostState> {
|
||||
final PostRepository _postRepository;
|
||||
|
||||
Future<void> post({required String text, required int to}) async {
|
||||
emit(state.copyWith(status: PostStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final bool successful = await _postRepository.comment(
|
||||
parentId: to,
|
||||
text: text,
|
||||
);
|
||||
|
||||
// final successful =
|
||||
// await Future<bool>.delayed(const Duration(seconds: 2), () => true);
|
||||
|
||||
if (successful) {
|
||||
emit(state.copyWith(status: PostStatus.successful));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
} else {
|
||||
emit(state.copyWith(status: PostStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> edit({required String text, required int id}) async {
|
||||
emit(state.copyWith(status: PostStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final bool successful = await _postRepository.edit(id: id, text: text);
|
||||
|
||||
if (successful) {
|
||||
emit(state.copyWith(status: PostStatus.successful));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
} else {
|
||||
emit(state.copyWith(status: PostStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
emit(state.copyWith(status: PostStatus.init));
|
||||
emit(state.copyWith(status: Status.idle));
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,13 @@
|
||||
part of 'post_cubit.dart';
|
||||
|
||||
enum PostStatus {
|
||||
init,
|
||||
loading,
|
||||
successful,
|
||||
failure,
|
||||
}
|
||||
|
||||
class PostState extends Equatable {
|
||||
const PostState({required this.status});
|
||||
|
||||
const PostState.init() : status = PostStatus.init;
|
||||
const PostState.init() : status = Status.idle;
|
||||
|
||||
final PostStatus status;
|
||||
final Status status;
|
||||
|
||||
PostState copyWith({PostStatus? status}) {
|
||||
PostState copyWith({Status? status}) {
|
||||
return PostState(
|
||||
status: status ?? this.status,
|
||||
);
|
||||
|
@ -67,10 +67,8 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
switch (T) {
|
||||
case int:
|
||||
_preferenceRepository.setInt(preference.key, value as int);
|
||||
break;
|
||||
case bool:
|
||||
_preferenceRepository.setBool(preference.key, value as bool);
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
@ -68,6 +68,8 @@ class PreferenceState extends Equatable {
|
||||
|
||||
bool get swipeGestureEnabled => _isOn<SwipeGesturePreference>();
|
||||
|
||||
bool get autoScrollEnabled => _isOn<AutoScrollModePreference>();
|
||||
|
||||
List<StoryType> get tabs {
|
||||
final String result =
|
||||
preferences.singleWhereType<TabOrderPreference>().val.toString();
|
||||
@ -85,6 +87,9 @@ class PreferenceState extends Equatable {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
StoryMarkingMode get storyMarkingMode => StoryMarkingMode.values
|
||||
.elementAt(preferences.singleWhereType<StoryMarkingModePreference>().val);
|
||||
|
||||
FetchMode get fetchMode => FetchMode.values
|
||||
.elementAt(preferences.singleWhereType<FetchModePreference>().val);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of 'search_cubit.dart';
|
||||
|
||||
enum SearchStatus {
|
||||
initial,
|
||||
idle,
|
||||
loading,
|
||||
loadingMore,
|
||||
loaded,
|
||||
@ -15,7 +15,7 @@ class SearchState extends Equatable {
|
||||
});
|
||||
|
||||
SearchState.init()
|
||||
: status = SearchStatus.initial,
|
||||
: status = SearchStatus.idle,
|
||||
results = <Item>[],
|
||||
params = SearchParams.init();
|
||||
|
||||
@ -23,6 +23,12 @@ class SearchState extends Equatable {
|
||||
final SearchStatus status;
|
||||
final SearchParams params;
|
||||
|
||||
bool get hasDateFilter =>
|
||||
params.filters.whereType<DateTimeRangeFilter>().isNotEmpty;
|
||||
|
||||
DateTimeRangeFilter? get dateFilter =>
|
||||
params.filters.whereType<DateTimeRangeFilter>().singleOrNull;
|
||||
|
||||
SearchState copyWith({
|
||||
List<Item>? results,
|
||||
SearchStatus? status,
|
||||
@ -42,3 +48,11 @@ class SearchState extends Equatable {
|
||||
params,
|
||||
];
|
||||
}
|
||||
|
||||
extension SearchStateExtension on SearchState {
|
||||
bool get showDateRangeShortcutChips {
|
||||
return hasDateFilter &&
|
||||
dateFilter?.startTime != null &&
|
||||
dateFilter?.endTime != null;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/post_repository.dart';
|
||||
|
||||
part 'submit_state.dart';
|
||||
@ -25,7 +26,7 @@ class SubmitCubit extends Cubit<SubmitState> {
|
||||
}
|
||||
|
||||
void onSubmitTapped() {
|
||||
emit(state.copyWith(status: SubmitStatus.submitting));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
if (state.title?.isNotEmpty ?? false) {
|
||||
_postRepository
|
||||
@ -35,9 +36,9 @@ class SubmitCubit extends Cubit<SubmitState> {
|
||||
text: state.text,
|
||||
)
|
||||
.then((bool successful) {
|
||||
emit(state.copyWith(status: SubmitStatus.submitted));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}).onError((Object? error, StackTrace stackTrace) {
|
||||
emit(state.copyWith(status: SubmitStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'submit_cubit.dart';
|
||||
|
||||
enum SubmitStatus {
|
||||
initial,
|
||||
submitting,
|
||||
submitted,
|
||||
failure,
|
||||
}
|
||||
|
||||
class SubmitState extends Equatable {
|
||||
const SubmitState({
|
||||
required this.title,
|
||||
@ -19,18 +12,18 @@ class SubmitState extends Equatable {
|
||||
: title = null,
|
||||
url = null,
|
||||
text = null,
|
||||
status = SubmitStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final String? title;
|
||||
final String? url;
|
||||
final String? text;
|
||||
final SubmitStatus status;
|
||||
final Status status;
|
||||
|
||||
SubmitState copyWith({
|
||||
String? title,
|
||||
String? url,
|
||||
String? text,
|
||||
SubmitStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return SubmitState(
|
||||
title: title ?? this.title,
|
||||
|
@ -15,16 +15,16 @@ class UserCubit extends Cubit<UserState> {
|
||||
final StoriesRepository _storiesRepository;
|
||||
|
||||
void init({required String userId}) {
|
||||
emit(state.copyWith(status: UserStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
_storiesRepository.fetchUser(id: userId).then((User? user) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
user: user ?? User.emptyWithId(userId),
|
||||
status: UserStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
}).onError((_, __) {
|
||||
emit(state.copyWith(status: UserStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'user_cubit.dart';
|
||||
|
||||
enum UserStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class UserState extends Equatable {
|
||||
const UserState({
|
||||
required this.user,
|
||||
@ -15,14 +8,14 @@ class UserState extends Equatable {
|
||||
|
||||
const UserState.init()
|
||||
: user = const User.empty(),
|
||||
status = UserStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final User user;
|
||||
final UserStatus status;
|
||||
final Status status;
|
||||
|
||||
UserState copyWith({
|
||||
User? user,
|
||||
UserStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return UserState(
|
||||
user: user ?? this.user,
|
||||
|
@ -6,7 +6,7 @@ enum Vote {
|
||||
}
|
||||
|
||||
enum VoteStatus {
|
||||
initial,
|
||||
idle,
|
||||
canceled,
|
||||
submitted,
|
||||
failureBeHumble,
|
||||
@ -24,7 +24,7 @@ class VoteState extends Equatable {
|
||||
|
||||
const VoteState.init({required this.item})
|
||||
: vote = null,
|
||||
status = VoteStatus.initial;
|
||||
status = VoteStatus.idle;
|
||||
|
||||
/// Null means user has not voted,
|
||||
/// True means user voted up,
|
||||
|
@ -14,6 +14,10 @@ extension ObjectExtension on Object {
|
||||
String identifier = '',
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
locator.get<Logger>().e(identifier, this, stackTrace ?? StackTrace.current);
|
||||
locator.get<Logger>().e(
|
||||
identifier,
|
||||
error: this,
|
||||
stackTrace: stackTrace ?? StackTrace.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/auth/auth_bloc.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/screens/item/models/models.dart';
|
||||
import 'package:hacki/screens/item/widgets/widgets.dart';
|
||||
@ -36,9 +36,9 @@ extension StateExtension on State {
|
||||
if (splitViewEnabled && !forceNewScreen) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
return HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: args,
|
||||
context.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
extra: args,
|
||||
);
|
||||
}
|
||||
|
||||
@ -75,16 +75,12 @@ extension StateExtension on State {
|
||||
break;
|
||||
case MenuAction.fav:
|
||||
onFavTapped(item);
|
||||
break;
|
||||
case MenuAction.share:
|
||||
onShareTapped(item, rect);
|
||||
break;
|
||||
case MenuAction.flag:
|
||||
onFlagTapped(item);
|
||||
break;
|
||||
case MenuAction.block:
|
||||
onBlockTapped(item, isBlocked: isBlocked);
|
||||
break;
|
||||
case MenuAction.cancel:
|
||||
break;
|
||||
}
|
||||
@ -116,12 +112,11 @@ extension StateExtension on State {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(context, item.url),
|
||||
onTap: () => context.pop(item.url),
|
||||
title: const Text('Link to article'),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
onTap: () => context.pop(
|
||||
'https://news.ycombinator.com/item?id=${item.id}',
|
||||
),
|
||||
title: const Text('Link to HN'),
|
||||
@ -159,13 +154,13 @@ extension StateExtension on State {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
),
|
||||
@ -197,13 +192,13 @@ extension StateExtension on State {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
),
|
||||
|
@ -17,7 +17,6 @@ import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/fetcher.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/theme_util.dart';
|
||||
@ -58,8 +57,8 @@ Future<void> main({bool testing = false}) async {
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
locator.get<Logger>().e(
|
||||
details.summary,
|
||||
details.exceptionAsString(),
|
||||
details.stack,
|
||||
error: details.exceptionAsString(),
|
||||
stackTrace: details.stack,
|
||||
);
|
||||
};
|
||||
|
||||
@ -155,19 +154,16 @@ Future<void> main({bool testing = false}) async {
|
||||
|
||||
class HackiApp extends StatelessWidget {
|
||||
const HackiApp({
|
||||
super.key,
|
||||
this.savedThemeMode,
|
||||
required this.trueDarkMode,
|
||||
required this.font,
|
||||
super.key,
|
||||
this.savedThemeMode,
|
||||
});
|
||||
|
||||
final AdaptiveThemeMode? savedThemeMode;
|
||||
final Font font;
|
||||
final bool trueDarkMode;
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@ -241,7 +237,7 @@ class HackiApp extends StatelessWidget {
|
||||
create: (BuildContext context) => TabCubit(
|
||||
preferenceCubit: context.read<PreferenceCubit>(),
|
||||
)..init(),
|
||||
)
|
||||
),
|
||||
],
|
||||
child: AdaptiveTheme(
|
||||
light: ThemeData(
|
||||
@ -269,8 +265,8 @@ class HackiApp extends StatelessWidget {
|
||||
AsyncSnapshot<AdaptiveThemeMode?> snapshot,
|
||||
) {
|
||||
final AdaptiveThemeMode? mode = snapshot.data;
|
||||
ThemeUtil.updateAndroidStatusBarSetting(
|
||||
Theme.of(context).brightness,
|
||||
ThemeUtil.updateStatusBarSetting(
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness,
|
||||
mode,
|
||||
);
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
@ -281,20 +277,18 @@ class HackiApp extends StatelessWidget {
|
||||
final bool useTrueDark = prefState.trueDarkEnabled &&
|
||||
(mode == AdaptiveThemeMode.dark ||
|
||||
(mode == AdaptiveThemeMode.system &&
|
||||
SchedulerBinding
|
||||
.instance.window.platformBrightness ==
|
||||
View.of(context)
|
||||
.platformDispatcher
|
||||
.platformBrightness ==
|
||||
Brightness.dark));
|
||||
return FeatureDiscovery(
|
||||
child: MaterialApp(
|
||||
child: MaterialApp.router(
|
||||
title: 'Hacki',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: useTrueDark ? trueDarkTheme : theme,
|
||||
navigatorKey: navigatorKey,
|
||||
navigatorObservers: <NavigatorObserver>[
|
||||
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||
],
|
||||
onGenerateRoute: CustomRouter.onGenerateRoute,
|
||||
initialRoute: HomeScreen.routeName,
|
||||
theme: (useTrueDark ? trueDarkTheme : theme).copyWith(
|
||||
useMaterial3: false,
|
||||
),
|
||||
routerConfig: router,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
14
lib/models/export_destination.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart' show IconData, Icons;
|
||||
|
||||
enum ExportDestination {
|
||||
qrCode('QR code', icon: Icons.qr_code),
|
||||
clipBoard('ClipBoard', icon: Icons.copy);
|
||||
|
||||
const ExportDestination(
|
||||
this.label, {
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
}
|
@ -4,7 +4,8 @@ enum FontSize {
|
||||
small('Small', TextDimens.pt15),
|
||||
regular('Regular', TextDimens.pt16),
|
||||
large('Large', TextDimens.pt17),
|
||||
xlarge('XLarge', TextDimens.pt18);
|
||||
xlarge('XLarge', TextDimens.pt18),
|
||||
xxlarge('XXLarge', TextDimens.pt19);
|
||||
|
||||
const FontSize(this.description, this.fontSize);
|
||||
|
||||
|
@ -35,6 +35,27 @@ class BuildableComment extends Comment with Buildable {
|
||||
hidden: comment.hidden,
|
||||
);
|
||||
|
||||
@override
|
||||
BuildableComment copyWith({
|
||||
int? level,
|
||||
bool? hidden,
|
||||
}) {
|
||||
return BuildableComment(
|
||||
id: id,
|
||||
time: time,
|
||||
parent: parent,
|
||||
score: score,
|
||||
by: by,
|
||||
text: text,
|
||||
kids: kids,
|
||||
dead: dead,
|
||||
deleted: deleted,
|
||||
hidden: hidden ?? this.hidden,
|
||||
level: level ?? this.level,
|
||||
elements: elements,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final List<LinkifyElement> elements;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export 'comments_order.dart';
|
||||
export 'export_destination.dart';
|
||||
export 'fetch_mode.dart';
|
||||
export 'font.dart';
|
||||
export 'font_size.dart';
|
||||
@ -6,5 +7,7 @@ export 'item/item.dart';
|
||||
export 'post_data.dart';
|
||||
export 'preference.dart';
|
||||
export 'search_params.dart';
|
||||
export 'status.dart';
|
||||
export 'story_marking_mode.dart';
|
||||
export 'story_type.dart';
|
||||
export 'user.dart';
|
||||
|
@ -23,16 +23,18 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
FontPreference(),
|
||||
FontSizePreference(),
|
||||
TabOrderPreference(),
|
||||
StoryMarkingModePreference(),
|
||||
// Order of items below matters and
|
||||
// reflects the order on settings screen.
|
||||
const DisplayModePreference(),
|
||||
const MetadataModePreference(),
|
||||
const StoryUrlModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
const NotificationModePreference(),
|
||||
const SwipeGesturePreference(),
|
||||
const AutoScrollModePreference(),
|
||||
const CollapseModePreference(),
|
||||
const ReaderModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
const EyeCandyModePreference(),
|
||||
const TrueDarkModePreference(),
|
||||
],
|
||||
@ -54,18 +56,21 @@ const bool _notificationModeDefaultValue = true;
|
||||
const bool _swipeGestureModeDefaultValue = false;
|
||||
const bool _displayModeDefaultValue = true;
|
||||
const bool _eyeCandyModeDefaultValue = false;
|
||||
const bool _trueDarkModeDefaultValue = false;
|
||||
const bool _trueDarkModeDefaultValue = true;
|
||||
const bool _readerModeDefaultValue = true;
|
||||
const bool _markReadStoriesModeDefaultValue = true;
|
||||
const bool _metadataModeDefaultValue = true;
|
||||
const bool _storyUrlModeDefaultValue = true;
|
||||
const bool _collapseModeDefaultValue = true;
|
||||
const bool _autoScrollModeDefaultValue = true;
|
||||
final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||
final int _fontSizeDefaultValue = FontSize.regular.index;
|
||||
final int _fontDefaultValue = Font.roboto.index;
|
||||
final int _tabOrderDefaultValue =
|
||||
StoryType.convertToSettingsValue(StoryType.values);
|
||||
final int _markStoriesAsReadWhenPreferenceDefaultValue =
|
||||
StoryMarkingMode.tap.index;
|
||||
|
||||
class SwipeGesturePreference extends BooleanPreference {
|
||||
const SwipeGesturePreference({bool? val})
|
||||
@ -127,6 +132,26 @@ class CollapseModePreference extends BooleanPreference {
|
||||
'''if disabled, tap on the top of comment tile to collapse.''';
|
||||
}
|
||||
|
||||
class AutoScrollModePreference extends BooleanPreference {
|
||||
const AutoScrollModePreference({bool? val})
|
||||
: super(val: val ?? _autoScrollModeDefaultValue);
|
||||
|
||||
@override
|
||||
AutoScrollModePreference copyWith({required bool? val}) {
|
||||
return AutoScrollModePreference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'autoScrollMode';
|
||||
|
||||
@override
|
||||
String get title => 'Auto-scroll on collapsing';
|
||||
|
||||
@override
|
||||
String get subtitle =>
|
||||
'''automatically scroll to next comment when you collapse a comment.''';
|
||||
}
|
||||
|
||||
/// The value deciding whether or not the story
|
||||
/// tile should display link preview. Defaults to true.
|
||||
class DisplayModePreference extends BooleanPreference {
|
||||
@ -342,3 +367,19 @@ class TabOrderPreference extends IntPreference {
|
||||
@override
|
||||
String get title => 'Tab order';
|
||||
}
|
||||
|
||||
class StoryMarkingModePreference extends IntPreference {
|
||||
StoryMarkingModePreference({int? val})
|
||||
: super(val: val ?? _markStoriesAsReadWhenPreferenceDefaultValue);
|
||||
|
||||
@override
|
||||
StoryMarkingModePreference copyWith({required int? val}) {
|
||||
return StoryMarkingModePreference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'storyMarkingMode';
|
||||
|
||||
@override
|
||||
String get title => 'Mark a Story as Read on';
|
||||
}
|
||||
|
@ -30,14 +30,27 @@ class DateTimeRangeFilter implements NumericFilter {
|
||||
|
||||
@override
|
||||
String get query {
|
||||
if (startTime == null || endTime == null) return '';
|
||||
|
||||
final int? startTimestamp = startTime == null
|
||||
? null
|
||||
: startTime!.toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
final int? endTimestamp = endTime == null
|
||||
int? endTimestamp = endTime == null
|
||||
? null
|
||||
: endTime!.toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
if (startTimestamp == endTimestamp) {
|
||||
endTimestamp = startTime!
|
||||
.add(const Duration(hours: 24))
|
||||
.toUtc()
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000;
|
||||
}
|
||||
|
||||
if (startTimestamp == null || endTimestamp == null) return '';
|
||||
|
||||
final String query =
|
||||
'''${startTimestamp == null ? '' : 'created_at_i>$startTimestamp'},${endTimestamp == null ? '' : 'created_at_i<$endTimestamp'}''';
|
||||
'''created_at_i>=$startTimestamp, created_at_i<=$endTimestamp''';
|
||||
|
||||
if (query.endsWith(',')) {
|
||||
return query.replaceFirst(',', '');
|
||||
|
14
lib/models/status.dart
Normal file
@ -0,0 +1,14 @@
|
||||
enum Status {
|
||||
idle,
|
||||
inProgress,
|
||||
success,
|
||||
failure,
|
||||
}
|
||||
|
||||
extension StatusExtension on Status {
|
||||
bool get isLoading => this == Status.inProgress;
|
||||
|
||||
bool get isSuccessful => this == Status.success;
|
||||
|
||||
bool get hasError => this == Status.failure;
|
||||
}
|
21
lib/models/story_marking_mode.dart
Normal file
@ -0,0 +1,21 @@
|
||||
/// Used for determining when to mark a story as read.
|
||||
enum StoryMarkingMode {
|
||||
// Mark a story as read after user scrolls past it.
|
||||
scrollPast('scrolling past'),
|
||||
// Mark a story as read after user taps on it.
|
||||
tap('tapping'),
|
||||
// Mark a story as read after user scrolls past or taps on it, whichever
|
||||
// happens the first.
|
||||
scrollPastOrTap('scrolling past or tapping');
|
||||
|
||||
const StoryMarkingMode(this.label);
|
||||
|
||||
final String label;
|
||||
|
||||
bool get shouldDetectScrollingPast =>
|
||||
this == StoryMarkingMode.scrollPast ||
|
||||
this == StoryMarkingMode.scrollPastOrTap;
|
||||
|
||||
bool get shouldDetectTapping =>
|
||||
this == StoryMarkingMode.tap || this == StoryMarkingMode.scrollPastOrTap;
|
||||
}
|
@ -64,7 +64,7 @@ class PostableRepository {
|
||||
validateStatus: validateStatus,
|
||||
),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
throw ServiceException(e.message);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
/// [StoriesRepository] is for fetching
|
||||
/// [Item] such as [Story], [PollOption], [Comment] or [User].
|
||||
@ -187,7 +186,7 @@ class StoriesRepository {
|
||||
|
||||
/// Fetch the parent [Story] of a [Comment] as well as
|
||||
/// the list of [Comment] traversed in order to reach the parent.
|
||||
Future<Tuple2<Story, List<Comment>>?> fetchParentStoryWithComments({
|
||||
Future<(Story, List<Comment>)?> fetchParentStoryWithComments({
|
||||
required int id,
|
||||
}) async {
|
||||
Item? item;
|
||||
@ -206,7 +205,7 @@ class StoriesRepository {
|
||||
parentComments[i].copyWith(level: parentComments.length - i - 1);
|
||||
}
|
||||
|
||||
return Tuple2<Story, List<Comment>>(
|
||||
return (
|
||||
item as Story,
|
||||
parentComments.reversed.toList(),
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
@ -30,13 +31,6 @@ class HomeScreen extends StatefulWidget {
|
||||
|
||||
static const String routeName = '/';
|
||||
|
||||
static Route<dynamic> route() {
|
||||
return MaterialPageRoute<HomeScreen>(
|
||||
settings: const RouteSettings(name: routeName),
|
||||
builder: (BuildContext context) => const HomeScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_HomeScreenState createState() => _HomeScreenState();
|
||||
}
|
||||
@ -57,7 +51,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
DeviceScreenType.mobile) {
|
||||
locator.get<Logger>().i('resetting comments in CommentCache');
|
||||
Future<void>.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
Durations.ms500,
|
||||
locator.get<CommentCache>().resetComments,
|
||||
);
|
||||
}
|
||||
@ -209,11 +203,13 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void onStoryTapped(Story story, {bool isPin = false}) {
|
||||
void onStoryTapped(Story story) {
|
||||
final bool useReader = context.read<PreferenceCubit>().state.readerEnabled;
|
||||
final bool offlineReading =
|
||||
context.read<StoriesBloc>().state.isOfflineReading;
|
||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||
final StoryMarkingMode storyMarkingMode =
|
||||
context.read<PreferenceCubit>().state.storyMarkingMode;
|
||||
|
||||
// If a story is a job story and it has a link to the job posting,
|
||||
// it would be better to just navigate to the web page.
|
||||
@ -222,23 +218,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (isJobWithLink) {
|
||||
context.read<ReminderCubit>().removeLastReadStoryId();
|
||||
} else {
|
||||
final ItemScreenArgs args = ItemScreenArgs(
|
||||
item: story,
|
||||
);
|
||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||
|
||||
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
|
||||
|
||||
if (splitViewEnabled) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
HackiApp.navigatorKey.currentState
|
||||
?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: args,
|
||||
)
|
||||
.whenComplete(() {
|
||||
context.read<ReminderCubit>().removeLastReadStoryId();
|
||||
});
|
||||
context.push('/${ItemScreen.routeName}', extra: args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,11 +237,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
context.read<StoriesBloc>().add(
|
||||
StoryRead(
|
||||
story: story,
|
||||
),
|
||||
);
|
||||
if (storyMarkingMode.shouldDetectTapping) {
|
||||
context.read<StoriesBloc>().add(StoryRead(story: story));
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
FlutterSiriSuggestions.instance.registerActivity(
|
||||
|
@ -6,8 +6,8 @@ import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class MobileHomeScreen extends StatelessWidget {
|
||||
const MobileHomeScreen({
|
||||
super.key,
|
||||
required this.homeScreen,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget homeScreen;
|
||||
|
@ -10,13 +10,13 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class PinnedStories extends StatelessWidget {
|
||||
const PinnedStories({
|
||||
super.key,
|
||||
required this.preferenceState,
|
||||
required this.onStoryTapped,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PreferenceState preferenceState;
|
||||
final void Function(Story story, {bool isPin}) onStoryTapped;
|
||||
final void Function(Story story) onStoryTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -49,7 +49,7 @@ class PinnedStories extends StatelessWidget {
|
||||
child: StoryTile(
|
||||
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
|
||||
story: story,
|
||||
onTap: () => onStoryTapped(story, isPin: true),
|
||||
onTap: () => onStoryTapped(story),
|
||||
showWebPreview: preferenceState.complexStoryTileEnabled,
|
||||
showMetadata: preferenceState.metadataEnabled,
|
||||
showUrl: preferenceState.urlEnabled,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
@ -8,8 +9,8 @@ import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
class TabletHomeScreen extends StatelessWidget {
|
||||
const TabletHomeScreen({
|
||||
super.key,
|
||||
required this.homeScreen,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget homeScreen;
|
||||
@ -36,7 +37,7 @@ class TabletHomeScreen extends StatelessWidget {
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
width: homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
duration: Durations.ms300,
|
||||
curve: Curves.elasticOut,
|
||||
child: homeScreen,
|
||||
),
|
||||
@ -52,7 +53,7 @@ class TabletHomeScreen extends StatelessWidget {
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
left: state.expanded ? Dimens.zero : homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
duration: Durations.ms300,
|
||||
curve: Curves.elasticOut,
|
||||
child: const _TabletStoryView(),
|
||||
),
|
||||
@ -75,7 +76,7 @@ class _TabletStoryView extends StatelessWidget {
|
||||
previous.itemScreenArgs != current.itemScreenArgs,
|
||||
builder: (BuildContext context, SplitViewState state) {
|
||||
if (state.itemScreenArgs != null) {
|
||||
return ItemScreen.build(context, state.itemScreenArgs!);
|
||||
return ItemScreen.tablet(context, state.itemScreenArgs!);
|
||||
}
|
||||
|
||||
return Material(
|
||||
|
@ -3,6 +3,7 @@ import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
@ -45,50 +46,45 @@ class ItemScreenArgs extends Equatable {
|
||||
|
||||
class ItemScreen extends StatefulWidget {
|
||||
const ItemScreen({
|
||||
super.key,
|
||||
this.splitViewEnabled = false,
|
||||
required this.item,
|
||||
required this.parentComments,
|
||||
super.key,
|
||||
this.splitViewEnabled = false,
|
||||
});
|
||||
|
||||
static const String routeName = '/item';
|
||||
static const String routeName = 'item';
|
||||
|
||||
static Route<dynamic> route(ItemScreenArgs args) {
|
||||
return MaterialPageRoute<ItemScreen>(
|
||||
settings: const RouteSettings(name: routeName),
|
||||
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
|
||||
create: (_) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
BlocProvider<CommentsCubit>(
|
||||
create: (BuildContext context) => CommentsCubit(
|
||||
filterCubit: context.read<FilterCubit>(),
|
||||
isOfflineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
item: args.item,
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode:
|
||||
context.read<PreferenceCubit>().state.fetchMode,
|
||||
defaultCommentsOrder:
|
||||
context.read<PreferenceCubit>().state.order,
|
||||
)..init(
|
||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||
targetAncestors: args.targetComments,
|
||||
useCommentCache: args.useCommentCache,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ItemScreen(
|
||||
item: args.item,
|
||||
parentComments: args.targetComments ?? <Comment>[],
|
||||
static Widget phone(ItemScreenArgs args) {
|
||||
return RepositoryProvider<CollapseCache>(
|
||||
create: (_) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
BlocProvider<CommentsCubit>(
|
||||
create: (BuildContext context) => CommentsCubit(
|
||||
filterCubit: context.read<FilterCubit>(),
|
||||
isOfflineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
item: args.item,
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode: context.read<PreferenceCubit>().state.fetchMode,
|
||||
defaultCommentsOrder: context.read<PreferenceCubit>().state.order,
|
||||
)..init(
|
||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||
targetAncestors: args.targetComments,
|
||||
useCommentCache: args.useCommentCache,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ItemScreen(
|
||||
item: args.item,
|
||||
parentComments: args.targetComments ?? <Comment>[],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget build(BuildContext context, ItemScreenArgs args) {
|
||||
static Widget tablet(BuildContext context, ItemScreenArgs args) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context.read<SplitViewCubit>().state.expanded) {
|
||||
@ -153,9 +149,9 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
);
|
||||
final GlobalKey fontSizeIconButtonKey = GlobalKey();
|
||||
|
||||
static const Duration _storyLinkTapThrottleDelay = Duration(seconds: 2);
|
||||
static const Duration _storyLinkTapThrottleDelay = Durations.twoSeconds;
|
||||
static const Duration _featureDiscoveryDismissThrottleDelay =
|
||||
Duration(seconds: 1);
|
||||
Durations.oneSecond;
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
@ -168,7 +164,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
SchedulerBinding.instance
|
||||
..addPostFrameCallback((_) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
@ -213,12 +208,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
listeners: <BlocListener<dynamic, dynamic>>[
|
||||
BlocListener<PostCubit, PostState>(
|
||||
listener: (BuildContext context, PostState postState) {
|
||||
if (postState.status == PostStatus.successful) {
|
||||
Navigator.popUntil(
|
||||
context,
|
||||
(Route<dynamic> route) =>
|
||||
route.settings.name == ItemScreen.routeName,
|
||||
);
|
||||
if (postState.status == Status.success) {
|
||||
context.pop();
|
||||
final String verb =
|
||||
context.read<EditCubit>().state.replyingTo == null
|
||||
? 'updated'
|
||||
@ -228,12 +219,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
showSnackBar(content: msg);
|
||||
context.read<EditCubit>().onReplySubmittedSuccessfully();
|
||||
context.read<PostCubit>().reset();
|
||||
} else if (postState.status == PostStatus.failure) {
|
||||
Navigator.popUntil(
|
||||
context,
|
||||
(Route<dynamic> route) =>
|
||||
route.settings.name == ItemScreen.routeName,
|
||||
);
|
||||
} else if (postState.status == Status.failure) {
|
||||
context.pop();
|
||||
showErrorSnackBar();
|
||||
context.read<PostCubit>().reset();
|
||||
}
|
||||
@ -355,6 +342,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -370,7 +358,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).viewInsets.bottom,
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -444,7 +432,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
leading: const Icon(Icons.av_timer),
|
||||
title: const Text('View ancestors'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
onTimeMachineActivated(comment);
|
||||
},
|
||||
enabled:
|
||||
@ -455,8 +443,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
title: const Text('View in separate thread'),
|
||||
onTap: () {
|
||||
locator.get<AppReviewService>().requestReview();
|
||||
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(
|
||||
item: comment,
|
||||
|
@ -7,11 +7,11 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class CustomAppBar extends AppBar {
|
||||
CustomAppBar({
|
||||
super.key,
|
||||
required Item item,
|
||||
required Color super.backgroundColor,
|
||||
required super.backgroundColor,
|
||||
required VoidCallback onFontSizeTap,
|
||||
required GlobalKey fontSizeIconButtonKey,
|
||||
super.key,
|
||||
bool splitViewEnabled = false,
|
||||
VoidCallback? onZoomTap,
|
||||
bool? expanded,
|
||||
@ -43,6 +43,7 @@ class CustomAppBar extends AppBar {
|
||||
fontFamily: FeatherIcons.type.fontFamily,
|
||||
package: FeatherIcons.type.fontPackage,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
onPressed: onFontSizeTap,
|
||||
),
|
||||
|
@ -10,9 +10,9 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class CustomFloatingActionButton extends StatelessWidget {
|
||||
const CustomFloatingActionButton({
|
||||
super.key,
|
||||
required this.itemScrollController,
|
||||
required this.itemPositionsListener,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ItemScrollController itemScrollController;
|
||||
@ -32,30 +32,32 @@ class CustomFloatingActionButton extends StatelessWidget {
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Palette.white,
|
||||
),
|
||||
title: const Text('Jump to previous root level comment.'),
|
||||
title: const Text('Shortcut'),
|
||||
description: const Text(
|
||||
'''Tapping on this button will take you to the previous off-screen root level comment.''',
|
||||
'''Tapping on this button will take you to the previous off-screen root level comment.\n\nLong press on it to jump to the very beginning of this thread.''',
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: InkWell(
|
||||
onLongPress: () => itemScrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: Durations.ms400,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
|
||||
/// Randomly generated string as heroTag to prevent
|
||||
/// default [FloatingActionButton] animation.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
if (state.status == CommentsStatus.loading) return;
|
||||
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().jumpUp(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: state.status == CommentsStatus.loading
|
||||
? Palette.grey
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
/// Randomly generated string as heroTag to prevent
|
||||
/// default [FloatingActionButton] animation.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().scrollToPreviousRoot(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -65,29 +67,31 @@ class CustomFloatingActionButton extends StatelessWidget {
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Palette.white,
|
||||
),
|
||||
title: const Text('Jump to next root level comment.'),
|
||||
title: const Text('Shortcut'),
|
||||
description: const Text(
|
||||
'''Tapping on this button will take you to the next off-screen root level comment.''',
|
||||
'''Tapping on this button will take you to the next off-screen root level comment.\n\nLong press on it to jump to the end of this thread.''',
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: InkWell(
|
||||
onLongPress: () => itemScrollController.scrollTo(
|
||||
index: state.comments.length,
|
||||
duration: Durations.ms400,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
|
||||
/// Same as above.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
if (state.status == CommentsStatus.loading) return;
|
||||
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().jump(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: state.status == CommentsStatus.loading
|
||||
? Palette.grey
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
/// Same as above.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().scrollToNextRoot(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -8,8 +8,8 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class FavIconButton extends StatelessWidget {
|
||||
const FavIconButton({
|
||||
super.key,
|
||||
required this.storyId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int storyId;
|
||||
|
@ -6,8 +6,8 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class LinkIconButton extends StatelessWidget {
|
||||
const LinkIconButton({
|
||||
super.key,
|
||||
required this.storyId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int storyId;
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/status.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -23,7 +25,7 @@ class _LoginDialogState extends State<LoginDialog> {
|
||||
return BlocConsumer<AuthBloc, AuthState>(
|
||||
listener: (BuildContext context, AuthState state) {
|
||||
if (state.isLoggedIn) {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
showSnackBar(
|
||||
content: 'Logged in successfully! ${Constants.happyFace}',
|
||||
);
|
||||
@ -32,7 +34,7 @@ class _LoginDialogState extends State<LoginDialog> {
|
||||
builder: (BuildContext context, AuthState state) {
|
||||
return SimpleDialog(
|
||||
children: <Widget>[
|
||||
if (state.status == AuthStatus.loading)
|
||||
if (state.status.isLoading)
|
||||
const SizedBox(
|
||||
height: Dimens.pt36,
|
||||
width: Dimens.pt36,
|
||||
@ -82,7 +84,7 @@ class _LoginDialogState extends State<LoginDialog> {
|
||||
const SizedBox(
|
||||
height: Dimens.pt16,
|
||||
),
|
||||
if (state.status == AuthStatus.failure)
|
||||
if (state.status == Status.failure)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt18,
|
||||
@ -141,7 +143,7 @@ class _LoginDialogState extends State<LoginDialog> {
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
@ -152,7 +154,7 @@ class _LoginDialogState extends State<LoginDialog> {
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
context.read<AuthBloc>().add(AuthInitialize());
|
||||
},
|
||||
child: const Text(
|
||||
|
@ -17,7 +17,6 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class MainView extends StatelessWidget {
|
||||
const MainView({
|
||||
super.key,
|
||||
required this.itemScrollController,
|
||||
required this.itemPositionsListener,
|
||||
required this.commentEditingController,
|
||||
@ -27,6 +26,7 @@ class MainView extends StatelessWidget {
|
||||
required this.onMoreTapped,
|
||||
required this.onRightMoreTapped,
|
||||
required this.onReplyTapped,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ItemScrollController itemScrollController;
|
||||
@ -139,6 +139,7 @@ class MainView extends StatelessWidget {
|
||||
},
|
||||
onMoreTapped: onMoreTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
itemScrollController: itemScrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -158,7 +159,7 @@ class MainView extends StatelessWidget {
|
||||
prev.status != current.status,
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
return AnimatedOpacity(
|
||||
opacity: state.status == CommentsStatus.loading
|
||||
opacity: state.status == CommentsStatus.inProgress
|
||||
? NumSwitch.on
|
||||
: NumSwitch.off,
|
||||
duration: const Duration(
|
||||
@ -210,161 +211,170 @@ class _ParentItemSection extends StatelessWidget {
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedbackUtil.light();
|
||||
DeviceGestureWrapper(
|
||||
child: Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedbackUtil.light();
|
||||
|
||||
if (state.item.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
if (state.item.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
|
||||
onReplyTapped();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
onReplyTapped();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
),
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Column(
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.readerEnabled,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.isOfflineReading,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.readerEnabled,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.isOfflineReading,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
semanticsLabel: state.item.title,
|
||||
text: state.item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color,
|
||||
),
|
||||
if (state.item.url.isNotEmpty)
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text:
|
||||
''' (${(state.item as Story).readableUrl})''',
|
||||
semanticsLabel: state.item.title,
|
||||
text: state.item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize:
|
||||
prefState.fontSize.fontSize - 4,
|
||||
color: Palette.orange,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (state.item.url.isNotEmpty)
|
||||
TextSpan(
|
||||
text:
|
||||
''' (${(state.item as Story).readableUrl})''',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize:
|
||||
prefState.fontSize.fontSize - 4,
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
FadeIn(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: ItemText(
|
||||
item: state.item,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: ItemText(
|
||||
item: state.item,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: const PollView(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: const PollView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
@ -397,6 +407,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
@ -406,27 +417,29 @@ class _ParentItemSection extends StatelessWidget {
|
||||
width: _viewParentButtonWidth,
|
||||
child: TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||
child: state.fetchParentStatus == CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
child:
|
||||
state.fetchParentStatus == CommentsStatus.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: _viewRootButtonWidth,
|
||||
child: TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadRootThread,
|
||||
child: state.fetchRootStatus == CommentsStatus.loading
|
||||
child: state.fetchRootStatus == CommentsStatus.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
@ -439,6 +452,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -457,6 +471,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -478,6 +493,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
@ -15,10 +16,10 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class MorePopupMenu extends StatelessWidget {
|
||||
const MorePopupMenu({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.isBlocked,
|
||||
required this.onLoginTapped,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Item item;
|
||||
@ -60,10 +61,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Navigator.pop(
|
||||
context,
|
||||
MenuAction.upvote,
|
||||
);
|
||||
context.pop(MenuAction.upvote);
|
||||
},
|
||||
builder: (BuildContext context, VoteState voteState) {
|
||||
final bool upvoted = voteState.vote == Vote.up;
|
||||
@ -81,7 +79,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
child: BlocBuilder<UserCubit, UserState>(
|
||||
builder: (BuildContext context, UserState state) {
|
||||
return Semantics(
|
||||
excludeSemantics: state.status == UserStatus.loading,
|
||||
excludeSemantics: state.status == Status.inProgress,
|
||||
child: ListTile(
|
||||
leading: const Icon(
|
||||
Icons.account_circle,
|
||||
@ -91,7 +89,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
state.user.description,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
@ -101,10 +99,10 @@ class MorePopupMenu extends StatelessWidget {
|
||||
'About ${state.user.id}',
|
||||
),
|
||||
content: state.user.about.isEmpty
|
||||
? Row(
|
||||
? const Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'empty',
|
||||
style: TextStyle(
|
||||
@ -130,7 +128,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
locator
|
||||
.get<AppReviewService>()
|
||||
.requestReview();
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
onSearchUserTapped(context);
|
||||
},
|
||||
child: const Text(
|
||||
@ -142,7 +140,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
locator
|
||||
.get<AppReviewService>()
|
||||
.requestReview();
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Okay',
|
||||
@ -196,10 +194,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
title: Text(
|
||||
isFav ? 'Unfavorite' : 'Favorite',
|
||||
),
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
MenuAction.fav,
|
||||
),
|
||||
onTap: () => context.pop(MenuAction.fav),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -208,20 +203,14 @@ class MorePopupMenu extends StatelessWidget {
|
||||
title: const Text(
|
||||
'Share',
|
||||
),
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
MenuAction.share,
|
||||
),
|
||||
onTap: () => context.pop(MenuAction.share),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.local_police),
|
||||
title: const Text(
|
||||
'Flag',
|
||||
),
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
MenuAction.flag,
|
||||
),
|
||||
onTap: () => context.pop(MenuAction.flag),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
@ -230,20 +219,14 @@ class MorePopupMenu extends StatelessWidget {
|
||||
title: Text(
|
||||
isBlocked ? 'Unblock' : 'Block',
|
||||
),
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
MenuAction.block,
|
||||
),
|
||||
onTap: () => context.pop(MenuAction.block),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.close),
|
||||
title: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
MenuAction.cancel,
|
||||
),
|
||||
onTap: () => context.pop(MenuAction.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -285,7 +268,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
child: SearchScreen(
|
||||
fromUserDialog: true,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -11,8 +11,8 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class PinIconButton extends StatelessWidget {
|
||||
const PinIconButton({
|
||||
super.key,
|
||||
required this.story,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Story story;
|
||||
|
@ -25,7 +25,7 @@ class _PollViewState extends State<PollView> {
|
||||
const SizedBox(
|
||||
height: Dimens.pt24,
|
||||
),
|
||||
if (state.status == PollStatus.loading) ...<Widget>[
|
||||
if (state.status == Status.inProgress) ...<Widget>[
|
||||
const LinearProgressIndicator(),
|
||||
const SizedBox(
|
||||
height: Dimens.pt24,
|
||||
@ -134,7 +134,7 @@ class _PollViewState extends State<PollView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -2,9 +2,11 @@ import 'package:clipboard/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/item/item.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
@ -12,12 +14,12 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class ReplyBox extends StatefulWidget {
|
||||
const ReplyBox({
|
||||
super.key,
|
||||
this.splitViewEnabled = false,
|
||||
required this.textEditingController,
|
||||
required this.onSendTapped,
|
||||
required this.onCloseTapped,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
this.splitViewEnabled = false,
|
||||
});
|
||||
|
||||
final bool splitViewEnabled;
|
||||
@ -34,11 +36,12 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
bool expanded = false;
|
||||
double? expandedHeight;
|
||||
|
||||
static const double _collapsedHeight = 100;
|
||||
static const double collapsedHeight = 140;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
expandedHeight ??= MediaQuery.of(context).size.height;
|
||||
expandedHeight ??= MediaQuery.of(context).size.height -
|
||||
MediaQuery.of(context).viewInsets.bottom;
|
||||
return BlocBuilder<EditCubit, EditState>(
|
||||
buildWhen: (EditState previous, EditState current) =>
|
||||
previous.showReplyBox != current.showReplyBox ||
|
||||
@ -48,7 +51,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
return BlocBuilder<PostCubit, PostState>(
|
||||
builder: (BuildContext context, PostState postState) {
|
||||
final Item? replyingTo = editState.replyingTo;
|
||||
final bool isLoading = postState.status == PostStatus.loading;
|
||||
final bool isLoading = postState.status.isLoading;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
@ -59,8 +62,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
: Dimens.zero,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
height: expanded ? expandedHeight : _collapsedHeight,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: expanded ? expandedHeight : collapsedHeight,
|
||||
duration: Durations.ms200,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: <BoxShadow>[
|
||||
if (!context.read<SplitViewCubit>().state.enabled)
|
||||
@ -72,6 +75,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
),
|
||||
child: Material(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (context.read<SplitViewCubit>().state.enabled)
|
||||
const Divider(
|
||||
@ -79,7 +83,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
),
|
||||
AnimatedContainer(
|
||||
height: expanded ? Dimens.pt36 : Dimens.zero,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: Durations.ms200,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@ -93,7 +97,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
child: Text(
|
||||
replyingTo == null
|
||||
? 'Editing'
|
||||
: 'Replying '
|
||||
: 'Replying to '
|
||||
'${replyingTo.by}',
|
||||
style: const TextStyle(color: Palette.grey),
|
||||
maxLines: 1,
|
||||
@ -107,7 +111,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
AnimatedOpacity(
|
||||
opacity:
|
||||
expanded ? NumSwitch.on : NumSwitch.off,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
duration: Durations.ms300,
|
||||
child: IconButton(
|
||||
key: const Key('quote'),
|
||||
icon: const Icon(
|
||||
@ -141,7 +145,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
color: Palette.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
|
||||
final EditState state =
|
||||
context.read<EditCubit>().state;
|
||||
@ -158,7 +162,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.deleteDraft();
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
},
|
||||
child: const Text(
|
||||
'No',
|
||||
@ -168,8 +172,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
@ -212,13 +215,21 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt16,
|
||||
padding: EdgeInsets.only(
|
||||
left: Dimens.pt16,
|
||||
right: Dimens.pt16,
|
||||
bottom: expanded
|
||||
// This padding here prevents keyboard
|
||||
// overlapping with TextField.
|
||||
? MediaQuery.of(context).viewInsets.bottom +
|
||||
Dimens.pt16
|
||||
: Dimens.zero,
|
||||
),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: widget.textEditingController,
|
||||
maxLines: 100,
|
||||
autofocus: true,
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
decoration: const InputDecoration(
|
||||
alignLabelWithHint: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@ -295,12 +306,6 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
setState(() {
|
||||
expanded = false;
|
||||
});
|
||||
Navigator.popUntil(
|
||||
context,
|
||||
(Route<dynamic> route) =>
|
||||
route.settings.name == ItemScreen.routeName ||
|
||||
route.isFirst,
|
||||
);
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(
|
||||
item: replyingTo,
|
||||
@ -327,7 +332,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
color: Palette.orange,
|
||||
size: TextDimens.pt18,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -344,6 +349,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
child: SingleChildScrollView(
|
||||
child: ItemText(
|
||||
item: replyingTo,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
@ -8,11 +9,11 @@ import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
class TimeMachineDialog extends StatelessWidget {
|
||||
const TimeMachineDialog({
|
||||
super.key,
|
||||
required this.comment,
|
||||
required this.size,
|
||||
required this.deviceType,
|
||||
required this.widthFactor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Comment comment;
|
||||
@ -56,7 +57,7 @@ class TimeMachineDialog extends StatelessWidget {
|
||||
Icons.close,
|
||||
size: Dimens.pt16,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
|
@ -1,11 +1,13 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
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/profile/models/models.dart';
|
||||
@ -15,7 +17,6 @@ import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
@ -30,9 +31,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
final RefreshController refreshControllerFav = RefreshController();
|
||||
final RefreshController refreshControllerNotification = RefreshController();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final Throttle throttle = Throttle(delay: const Duration(seconds: 2));
|
||||
final Throttle throttle = Throttle(delay: Durations.twoSeconds);
|
||||
|
||||
PageType pageType = PageType.notification;
|
||||
PageType? pageType;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@ -46,6 +47,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
pageType ??= context.read<AuthBloc>().state.isLoggedIn
|
||||
? PageType.notification
|
||||
: PageType.fav;
|
||||
super.build(context);
|
||||
return BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (BuildContext context, AuthState authState) {
|
||||
@ -54,7 +58,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
previous.status != current.status,
|
||||
listener:
|
||||
(BuildContext context, NotificationState notificationState) {
|
||||
if (notificationState.status == NotificationStatus.loaded) {
|
||||
if (notificationState.status == Status.success) {
|
||||
refreshControllerNotification
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
@ -72,7 +76,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
BuildContext context,
|
||||
HistoryState historyState,
|
||||
) {
|
||||
if (historyState.status == HistoryStatus.loaded) {
|
||||
if (historyState.status == Status.success) {
|
||||
refreshControllerHistory
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
@ -84,7 +88,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
) {
|
||||
if ((!authState.isLoggedIn ||
|
||||
historyState.submittedItems.isEmpty) &&
|
||||
historyState.status != HistoryStatus.loading) {
|
||||
historyState.status != Status.inProgress) {
|
||||
return const CenteredMessageView(
|
||||
content: 'Your past comments and stories will '
|
||||
'show up here.',
|
||||
@ -92,8 +96,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
}
|
||||
|
||||
return ItemsListView<Item>(
|
||||
showWebPreview: false,
|
||||
showMetadata: false,
|
||||
showWebPreviewOnStoryTile: false,
|
||||
showMetadataOnStoryTile: false,
|
||||
showUrl: false,
|
||||
useConsistentFontSize: true,
|
||||
refreshController: refreshControllerHistory,
|
||||
@ -127,7 +131,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
visible: pageType == PageType.fav,
|
||||
child: BlocConsumer<FavCubit, FavState>(
|
||||
listener: (BuildContext context, FavState favState) {
|
||||
if (favState.status == FavStatus.loaded) {
|
||||
if (favState.status == Status.success) {
|
||||
refreshControllerFav
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
@ -135,7 +139,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
builder: (BuildContext context, FavState favState) {
|
||||
if (favState.favItems.isEmpty &&
|
||||
favState.status != FavStatus.loading) {
|
||||
favState.status != Status.inProgress) {
|
||||
return const CenteredMessageView(
|
||||
content: 'Your favorite stories will show up here.'
|
||||
'\nThey will be synced to your Hacker '
|
||||
@ -158,8 +162,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return ItemsListView<Item>(
|
||||
showWebPreview: prefState.complexStoryTileEnabled,
|
||||
showMetadata: prefState.metadataEnabled,
|
||||
showWebPreviewOnStoryTile:
|
||||
prefState.complexStoryTileEnabled,
|
||||
showMetadataOnStoryTile:
|
||||
prefState.metadataEnabled,
|
||||
showUrl: prefState.urlEnabled,
|
||||
useCommentTile: true,
|
||||
refreshController: refreshControllerFav,
|
||||
@ -174,6 +180,28 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
onTap: (Item item) => goToItemScreen(
|
||||
args: ItemScreenArgs(item: item),
|
||||
),
|
||||
itemBuilder: (Widget child, Item item) {
|
||||
return Slidable(
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedbackUtil.light();
|
||||
context
|
||||
.read<FavCubit>()
|
||||
.removeFav(item.id);
|
||||
},
|
||||
backgroundColor: Palette.red,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.close,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -247,8 +275,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
selected: false,
|
||||
onSelected: (bool val) {
|
||||
if (authState.isLoggedIn) {
|
||||
HackiApp.navigatorKey.currentState
|
||||
?.pushNamed(SubmitScreen.routeName);
|
||||
context.push('/${SubmitScreen.routeName}');
|
||||
} else {
|
||||
showSnackBar(
|
||||
content: 'You need to log in first.',
|
||||
@ -353,16 +380,18 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
locator
|
||||
.get<StoriesRepository>()
|
||||
.fetchParentStoryWithComments(id: comment.parent)
|
||||
.then((Tuple2<Story, List<Comment>>? tuple) {
|
||||
if (tuple != null && mounted) {
|
||||
.then(((Story, List<Comment>)? res) {
|
||||
if (res != null && mounted) {
|
||||
final Story parent = res.$1;
|
||||
final List<Comment> children = res.$2;
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(
|
||||
item: tuple.item1,
|
||||
targetComments: tuple.item2.isEmpty
|
||||
item: parent,
|
||||
targetComments: children.isEmpty
|
||||
? <Comment>[comment]
|
||||
: <Comment>[
|
||||
...tuple.item2,
|
||||
comment.copyWith(level: tuple.item2.length)
|
||||
...children,
|
||||
comment.copyWith(level: children.length),
|
||||
],
|
||||
onlyShowTargetComment: true,
|
||||
),
|
||||
|
71
lib/screens/profile/qr_code_scanner_screen.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
class QrCodeScannerScreen extends StatefulWidget {
|
||||
const QrCodeScannerScreen({super.key});
|
||||
|
||||
static const String routeName = 'qr-code-scanner';
|
||||
|
||||
@override
|
||||
State<QrCodeScannerScreen> createState() => _QrCodeScannerScreenState();
|
||||
}
|
||||
|
||||
class _QrCodeScannerScreenState extends State<QrCodeScannerScreen> {
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
QRViewController? controller;
|
||||
bool isFlashOn = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Palette.transparent,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(isFlashOn ? Icons.flash_off : Icons.flash_on),
|
||||
onPressed: () {
|
||||
controller?.toggleFlash();
|
||||
setState(() {
|
||||
isFlashOn = !isFlashOn;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cameraswitch_outlined),
|
||||
onPressed: controller?.flipCamera,
|
||||
),
|
||||
],
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: onQRViewCreated,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onQRViewCreated(QRViewController controller) {
|
||||
setState(() {
|
||||
this.controller = controller;
|
||||
});
|
||||
controller.scannedDataStream.listen((Barcode scanData) {
|
||||
controller.stopCamera();
|
||||
context.pop(scanData.code);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
46
lib/screens/profile/qr_code_view_screen.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/styles/palette.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
class QrCodeViewScreen extends StatelessWidget {
|
||||
const QrCodeViewScreen({
|
||||
required this.data,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String data;
|
||||
|
||||
static const String routeName = 'qr-code-view';
|
||||
|
||||
static const int qrCodeVersion = 4;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Palette.transparent,
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Center(
|
||||
child: QrImageView(
|
||||
data: data,
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
version: qrCodeVersion,
|
||||
size: 300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,8 +3,8 @@ import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class CenteredMessageView extends StatelessWidget {
|
||||
const CenteredMessageView({
|
||||
super.key,
|
||||
required this.content,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String content;
|
||||
|
@ -8,7 +8,6 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
class InboxView extends StatelessWidget {
|
||||
const InboxView({
|
||||
super.key,
|
||||
required this.refreshController,
|
||||
required this.comments,
|
||||
required this.unreadCommentsIds,
|
||||
@ -16,6 +15,7 @@ class InboxView extends StatelessWidget {
|
||||
required this.onMarkAllAsReadTapped,
|
||||
required this.onLoadMore,
|
||||
required this.onRefresh,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final RefreshController refreshController;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
@ -64,15 +65,15 @@ class OfflineListTile extends StatelessWidget {
|
||||
title: const Text('Abort downloading?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text('No'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
@ -93,15 +94,15 @@ class OfflineListTile extends StatelessWidget {
|
||||
content: const Text('It will take longer time.'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text('No'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
|
@ -1,21 +1,27 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_email_sender/flutter_email_sender.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/profile/models/page_type.dart';
|
||||
import 'package:hacki/screens/profile/qr_code_scanner_screen.dart';
|
||||
import 'package:hacki/screens/profile/qr_code_view_screen.dart';
|
||||
import 'package:hacki/screens/profile/widgets/offline_list_tile.dart';
|
||||
import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
@ -27,15 +33,15 @@ import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class Settings extends StatefulWidget {
|
||||
const Settings({
|
||||
super.key,
|
||||
required this.authState,
|
||||
required this.magicWord,
|
||||
required this.pageType,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AuthState authState;
|
||||
final String magicWord;
|
||||
final PageType pageType;
|
||||
final PageType? pageType;
|
||||
|
||||
@override
|
||||
State<Settings> createState() => _SettingsState();
|
||||
@ -74,13 +80,13 @@ class _SettingsState extends State<Settings> {
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Flex(
|
||||
const Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: Dimens.pt16,
|
||||
),
|
||||
@ -91,7 +97,7 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
children: <Widget>[
|
||||
Text('Default comments order'),
|
||||
Spacer(),
|
||||
],
|
||||
@ -216,6 +222,51 @@ class _SettingsState extends State<Settings> {
|
||||
},
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
if (preference
|
||||
is MarkReadStoriesModePreference) ...<Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
StoryMarkingModePreference().title,
|
||||
style: TextStyle(
|
||||
color: !preferenceState.markReadStoriesEnabled
|
||||
? Palette.grey
|
||||
: null,
|
||||
),
|
||||
),
|
||||
trailing: DropdownButton<StoryMarkingMode>(
|
||||
value: preferenceState.storyMarkingMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: StoryMarkingMode.values
|
||||
.map(
|
||||
(StoryMarkingMode val) =>
|
||||
DropdownMenuItem<StoryMarkingMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.label,
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
color: !preferenceState
|
||||
.markReadStoriesEnabled
|
||||
? Palette.grey
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (StoryMarkingMode? storyMarkingMode) {
|
||||
if (storyMarkingMode != null) {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<PreferenceCubit>().update(
|
||||
StoryMarkingModePreference(),
|
||||
to: storyMarkingMode.index,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
if (preference is StoryUrlModePreference) const Divider(),
|
||||
],
|
||||
ListTile(
|
||||
@ -243,6 +294,13 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
onTap: onExportFavoritesTapped,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Import Favorites',
|
||||
),
|
||||
onTap: () =>
|
||||
onImportFavoritesTapped(context.read<FavCubit>()),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Clear Favorites',
|
||||
@ -288,14 +346,14 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
context.read<AuthBloc>().add(AuthLogout());
|
||||
context.read<HistoryCubit>().reset();
|
||||
},
|
||||
@ -338,8 +396,8 @@ class _SettingsState extends State<Settings> {
|
||||
style: TextStyle(fontFamily: font.name),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: const <Widget>[
|
||||
const Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'*Restart required',
|
||||
style: TextStyle(
|
||||
@ -349,7 +407,7 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
Spacer(),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -397,18 +455,16 @@ class _SettingsState extends State<Settings> {
|
||||
switch (val) {
|
||||
case AdaptiveThemeMode.light:
|
||||
AdaptiveTheme.of(context).setLight();
|
||||
break;
|
||||
case AdaptiveThemeMode.dark:
|
||||
AdaptiveTheme.of(context).setDark();
|
||||
break;
|
||||
case AdaptiveThemeMode.system:
|
||||
case null:
|
||||
AdaptiveTheme.of(context).setSystem();
|
||||
break;
|
||||
}
|
||||
|
||||
final Brightness brightness = Theme.of(context).brightness;
|
||||
ThemeUtil.updateAndroidStatusBarSetting(brightness, val);
|
||||
final Brightness brightness =
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
ThemeUtil.updateStatusBarSetting(brightness, val);
|
||||
}
|
||||
|
||||
void showClearCacheDialog() {
|
||||
@ -422,7 +478,7 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
@ -432,7 +488,7 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
locator
|
||||
.get<SembastRepository>()
|
||||
.deleteAllCachedComments()
|
||||
@ -485,8 +541,8 @@ class _SettingsState extends State<Settings> {
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.portfolioLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
FontAwesomeIcons.addressCard,
|
||||
),
|
||||
@ -501,8 +557,8 @@ class _SettingsState extends State<Settings> {
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.privacyPolicyLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.privacy_tip_outlined,
|
||||
),
|
||||
@ -515,8 +571,8 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: onReportIssueTapped,
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.bug_report_outlined,
|
||||
),
|
||||
@ -531,8 +587,8 @@ class _SettingsState extends State<Settings> {
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.githubLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
FontAwesomeIcons.github,
|
||||
),
|
||||
@ -549,8 +605,8 @@ class _SettingsState extends State<Settings> {
|
||||
? Constants.appStoreLink
|
||||
: Constants.googlePlayLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.thumb_up,
|
||||
),
|
||||
@ -565,8 +621,8 @@ class _SettingsState extends State<Settings> {
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.sponsorLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
FeatherIcons.coffee,
|
||||
),
|
||||
@ -590,8 +646,8 @@ class _SettingsState extends State<Settings> {
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: onSendEmailTapped,
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.email,
|
||||
),
|
||||
@ -604,8 +660,8 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => onGithubTapped(context.rect),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.bug_report_outlined,
|
||||
),
|
||||
@ -709,7 +765,7 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text(
|
||||
'Okay',
|
||||
),
|
||||
@ -732,7 +788,7 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
@ -742,7 +798,7 @@ class _SettingsState extends State<Settings> {
|
||||
final String keyword = controller.text.trim();
|
||||
if (keyword.isEmpty) return;
|
||||
context.read<FilterCubit>().addKeyword(keyword.toLowerCase());
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Confirm',
|
||||
@ -755,20 +811,68 @@ class _SettingsState extends State<Settings> {
|
||||
}
|
||||
|
||||
Future<void> onExportFavoritesTapped() async {
|
||||
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
|
||||
return showModalBottomSheet<ExportDestination>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
...ExportDestination.values.map(
|
||||
(ExportDestination e) => ListTile(
|
||||
leading: Icon(e.icon),
|
||||
title: Text(e.label),
|
||||
onTap: () => context.pop<ExportDestination>(e),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
).then(
|
||||
(ExportDestination? destination) => exportFavorites(to: destination),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onImportFavoritesTapped(FavCubit favCubit) async {
|
||||
final String? res =
|
||||
await router.push('/${QrCodeScannerScreen.routeName}') as String?;
|
||||
final List<int>? ids =
|
||||
res?.split('\n').map(int.tryParse).whereType<int>().toList();
|
||||
if (ids == null) return;
|
||||
for (final int id in ids) {
|
||||
await favCubit.addFav(id);
|
||||
}
|
||||
showSnackBar(content: 'Favorites imported successfully.');
|
||||
}
|
||||
|
||||
Future<void> exportFavorites({required ExportDestination? to}) async {
|
||||
final ExportDestination? destination = to;
|
||||
if (destination == null) return;
|
||||
|
||||
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
|
||||
if (allFavorites.isEmpty) {
|
||||
showSnackBar(content: "You don't have any favorite item.");
|
||||
return;
|
||||
}
|
||||
final String allFavoritesStr = allFavorites.join('\n');
|
||||
|
||||
try {
|
||||
await FlutterClipboard.copy(
|
||||
allFavorites.join('\n'),
|
||||
).whenComplete(HapticFeedbackUtil.selection);
|
||||
showSnackBar(content: 'Ids of favorites have been copied to clipboard.');
|
||||
} catch (error, stackTrace) {
|
||||
error.logError(stackTrace: stackTrace);
|
||||
switch (destination) {
|
||||
case ExportDestination.qrCode:
|
||||
await router.push(
|
||||
'/${QrCodeViewScreen.routeName}',
|
||||
extra: allFavoritesStr,
|
||||
);
|
||||
case ExportDestination.clipBoard:
|
||||
try {
|
||||
await FlutterClipboard.copy(allFavoritesStr)
|
||||
.whenComplete(HapticFeedbackUtil.selection);
|
||||
showSnackBar(
|
||||
content: 'Ids of favorites have been copied to clipboard.',
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
error.logError(stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -783,14 +887,14 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
try {
|
||||
context.read<FavCubit>().removeAll();
|
||||
showSnackBar(content: 'All favorites have been removed.');
|
||||
|
@ -20,8 +20,8 @@ class _TabBarSettingsState extends State<TabBarSettings> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: const <Widget>[
|
||||
const Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: Dimens.pt16,
|
||||
),
|
||||
|
@ -1,6 +1,8 @@
|
||||
export 'home/home_screen.dart';
|
||||
export 'item/item_screen.dart';
|
||||
export 'profile/profile_screen.dart';
|
||||
export 'profile/qr_code_scanner_screen.dart';
|
||||
export 'profile/qr_code_view_screen.dart';
|
||||
export 'search/search_screen.dart';
|
||||
export 'submit/submit_screen.dart';
|
||||
export 'web_view/web_view_screen.dart';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -28,7 +29,17 @@ class SearchScreen extends StatefulWidget {
|
||||
|
||||
class _SearchScreenState extends State<SearchScreen> {
|
||||
final RefreshController refreshController = RefreshController();
|
||||
final Debouncer debouncer = Debouncer(delay: const Duration(seconds: 1));
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
|
||||
|
||||
static const Duration chipsAnimationDuration = Durations.ms300;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
refreshController.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -72,6 +83,85 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
duration: chipsAnimationDuration,
|
||||
crossFadeState: state.showDateRangeShortcutChips
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
firstChild: SizedBox.fromSize(),
|
||||
secondChild: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.dayBefore(
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.dayAfter(
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.weekBefore(
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.weekAfter(
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.monthBefore(
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.monthAfter(
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
@ -80,7 +170,9 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeRangeFilterChip(
|
||||
filter: state.params.get<DateTimeRangeFilter>(),
|
||||
filter: state.dateFilter,
|
||||
initialStartDate: state.dateFilter?.startTime,
|
||||
initialEndDate: state.dateFilter?.endTime,
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
@ -200,11 +292,15 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
},
|
||||
),
|
||||
controller: refreshController,
|
||||
scrollController: scrollController,
|
||||
onRefresh: () {},
|
||||
onLoading: () {
|
||||
context.read<SearchCubit>().loadMore();
|
||||
},
|
||||
child: ListView(
|
||||
physics: state.results.isEmpty
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: <Widget>[
|
||||
...state.results
|
||||
.map(
|
||||
|
@ -17,9 +17,9 @@ enum CustomDateTimeRange {
|
||||
|
||||
class CustomRangeFilterChip extends StatelessWidget {
|
||||
const CustomRangeFilterChip({
|
||||
super.key,
|
||||
required this.range,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final CustomDateTimeRange range;
|
||||
|
@ -5,13 +5,17 @@ import 'package:intl/intl.dart';
|
||||
|
||||
class DateTimeRangeFilterChip extends StatelessWidget {
|
||||
const DateTimeRangeFilterChip({
|
||||
super.key,
|
||||
required this.filter,
|
||||
required this.initialStartDate,
|
||||
required this.initialEndDate,
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.onDateTimeRangeRemoved,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTimeRangeFilter? filter;
|
||||
final DateTime? initialStartDate;
|
||||
final DateTime? initialEndDate;
|
||||
final void Function(DateTime, DateTime) onDateTimeRangeUpdated;
|
||||
final VoidCallback onDateTimeRangeRemoved;
|
||||
|
||||
@ -25,6 +29,9 @@ class DateTimeRangeFilterChip extends StatelessWidget {
|
||||
context: context,
|
||||
firstDate: DateTime.now().subtract(const Duration(days: 20 * 365)),
|
||||
lastDate: DateTime.now(),
|
||||
initialDateRange: initialStartDate != null && initialEndDate != null
|
||||
? DateTimeRange(start: initialStartDate!, end: initialEndDate!)
|
||||
: null,
|
||||
).then((DateTimeRange? range) {
|
||||
if (range != null) {
|
||||
onDateTimeRangeUpdated(range.start, range.end);
|
||||
@ -34,11 +41,22 @@ class DateTimeRangeFilterChip extends StatelessWidget {
|
||||
});
|
||||
},
|
||||
selected: filter != null,
|
||||
label:
|
||||
'''from ${_formatDateTime(filter?.startTime) ?? 'X'} to ${_formatDateTime(filter?.endTime) ?? 'Y'}''',
|
||||
label: _label,
|
||||
);
|
||||
}
|
||||
|
||||
String get _label {
|
||||
final DateTime? start = filter?.startTime;
|
||||
final DateTime? end = filter?.endTime;
|
||||
if (start == null && end == null) {
|
||||
return '''from X to Y''';
|
||||
} else if (start == end) {
|
||||
return '''from ${_formatDateTime(start)}''';
|
||||
} else {
|
||||
return '''from ${_formatDateTime(start)} to ${_formatDateTime(end)}''';
|
||||
}
|
||||
}
|
||||
|
||||
static String? _formatDateTime(DateTime? dateTime) {
|
||||
if (dateTime == null) return null;
|
||||
|
||||
|
89
lib/screens/search/widgets/date_time_shortcut_chip.dart
Normal file
@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/screens/search/widgets/date_time_range_filter_chip.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart' show CustomChip;
|
||||
|
||||
typedef Calculator = DateTime Function(DateTime);
|
||||
|
||||
/// A set of chips that perform addition or subtraction on the date selected
|
||||
/// by [DateTimeRangeFilterChip]
|
||||
class DateTimeShortcutChip extends StatelessWidget {
|
||||
const DateTimeShortcutChip({
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.label,
|
||||
required Calculator calculator,
|
||||
super.key,
|
||||
}) : _calculator = calculator;
|
||||
|
||||
DateTimeShortcutChip.dayBefore({
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
super.key,
|
||||
}) : label = '- day',
|
||||
_calculator =
|
||||
((DateTime date) => date.subtract(const Duration(hours: 24)));
|
||||
|
||||
DateTimeShortcutChip.dayAfter({
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
super.key,
|
||||
}) : label = '+ day',
|
||||
_calculator = ((DateTime date) => date.add(const Duration(hours: 24)));
|
||||
|
||||
DateTimeShortcutChip.weekBefore({
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
super.key,
|
||||
}) : label = '- week',
|
||||
_calculator =
|
||||
((DateTime date) => date.subtract(const Duration(days: 7)));
|
||||
|
||||
DateTimeShortcutChip.weekAfter({
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
super.key,
|
||||
}) : label = '+ week',
|
||||
_calculator = ((DateTime date) => date.add(const Duration(days: 7)));
|
||||
|
||||
DateTimeShortcutChip.monthBefore({
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
super.key,
|
||||
}) : label = '- 30 days',
|
||||
_calculator =
|
||||
((DateTime date) => date.subtract(const Duration(days: 30)));
|
||||
|
||||
DateTimeShortcutChip.monthAfter({
|
||||
required this.onDateTimeRangeUpdated,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
super.key,
|
||||
}) : label = '+ 30 days',
|
||||
_calculator = ((DateTime date) => date.add(const Duration(days: 30)));
|
||||
|
||||
final void Function(DateTime, DateTime) onDateTimeRangeUpdated;
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final String label;
|
||||
final Calculator _calculator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomChip(
|
||||
onSelected: (bool value) {
|
||||
if (startDate == null || endDate == null) return;
|
||||
final DateTime updatedStartDate = _calculator(startDate!);
|
||||
final DateTime updatedEndDate = _calculator(endDate!);
|
||||
onDateTimeRangeUpdated(updatedStartDate, updatedEndDate);
|
||||
},
|
||||
selected: false,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/models/search_params.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class PostedByFilterChip extends StatelessWidget {
|
||||
const PostedByFilterChip({
|
||||
super.key,
|
||||
required this.filter,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PostedByFilter? filter;
|
||||
@ -67,13 +68,13 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
child: ButtonBar(
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, filter?.author),
|
||||
onPressed: () => context.pop(filter?.author),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
onPressed: () => context.pop(null),
|
||||
child: const Text(
|
||||
'Clear',
|
||||
),
|
||||
@ -81,7 +82,7 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final String text = usernameController.text.trim();
|
||||
Navigator.pop(context, text.isEmpty ? null : text);
|
||||
context.pop(text.isEmpty ? null : text);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'custom_range_filter_chip.dart';
|
||||
export 'date_time_range_filter_chip.dart';
|
||||
export 'date_time_shortcut_chip.dart';
|
||||
export 'posted_by_filter_chip.dart';
|
||||
|