Compare commits

...

94 Commits

Author SHA1 Message Date
4f21d3e6bd update pubspec.yaml (#325) 2023-11-15 10:50:00 -08:00
96d0fe9e5e fix new comment indicator. (#324) 2023-11-15 01:15:10 -08:00
69eee3e278 fix url rendering. (#323) 2023-11-14 23:52:05 -08:00
36bcd996c0 bump Flutter version to 3.13.9 (#322) 2023-11-14 23:22:09 -08:00
5fc39d8b8b fix code block formatting. (#321) 2023-11-14 20:25:42 -08:00
5dce7787e1 improve text rendering performance. (#320) 2023-11-14 17:14:06 -08:00
8888dde792 allow marking stories as read from homepage. (#319) 2023-11-14 14:35:27 -08:00
6c8fc4cf87 fix response indicator when lazy fetching is enabled. (#317) 2023-11-13 21:10:47 -08:00
ae9cc109db revert "improve caching strategy. (#312)" (#316) 2023-11-13 19:42:20 -08:00
c8976ed17b improve caching strategy. (#312) 2023-11-11 00:31:09 -08:00
ff7e115418 fix manual pagination button. (#310) 2023-11-06 22:46:44 -08:00
0310507c96 revert html util change. (#309) 2023-11-06 19:40:53 -08:00
58c646e232 update html_util.dart (#308) 2023-11-06 17:10:10 -08:00
08328e2ca1 update url_linkifier.dart (#307) 2023-11-06 14:19:25 -08:00
86b7228ffd improve response indicator. (#306) 2023-11-06 12:45:46 -08:00
e103c88ca6 fix favorites export. (#305) 2023-11-05 22:47:45 -08:00
94323a04e0 fix response indicator. (#304) 2023-11-05 21:22:02 -08:00
4776c375a1 UX improvements on HN and in-thread search. (#303) 2023-11-05 19:48:01 -08:00
1f4e6cf41c fix pagination button. (#298) 2023-11-02 21:50:09 -07:00
be6ed35888 update version. (#297) 2023-11-02 21:09:55 -07:00
b2ea50cea6 add pagination. (#296) 2023-11-02 20:22:51 -07:00
109b9287cf fix offline webview. (#295) 2023-11-02 17:17:46 -07:00
939d55ef0d fix in-thread search. (#294) 2023-11-02 14:51:46 -07:00
3ee60e1a44 improve in-thread search UX. (#293) 2023-11-02 14:34:24 -07:00
6fe567fa02 update design of about dialog. (#292) 2023-11-02 13:42:33 -07:00
bc2d4f32c9 show index on comment tile. (#291) 2023-11-02 13:11:10 -07:00
91290e9743 update README.md (#290) 2023-11-02 12:28:09 -07:00
934f184b6f fix material 3 colors. (#289) 2023-11-02 12:04:43 -07:00
dbd48eae99 fix reply box. (#288) 2023-11-01 23:00:00 -07:00
279007191b update feature description. (#287) 2023-11-01 22:17:57 -07:00
b3fdc20fc5 add ability to use material 3. (#286) 2023-11-01 19:48:09 -07:00
3fbf5d4eea improve shortcut button. (#284) 2023-10-22 20:34:09 -07:00
332ffbb773 bump version. (#282) 2023-10-22 00:14:12 -07:00
346a6c709e fix inconsistent font size. (#281) 2023-10-21 23:50:06 -07:00
d4fe042245 fix border color of comment tile. (#280) 2023-10-21 21:25:29 -07:00
b82c4a1777 update changelogs. (#279) 2023-10-21 20:45:24 -07:00
7e0d1f0f1d add ability to use custom tabs. (#278) 2023-10-21 20:25:45 -07:00
f405a10c2e fix color of quote element. (#277) 2023-10-21 19:43:06 -07:00
edbad79cd3 add ability to customize text scale factor and improve keyword filter. (#276) 2023-10-21 18:50:51 -07:00
c9d8b2950a add ability to change app's primary color. (#275) 2023-10-21 01:02:44 -07:00
f2bc48f980 update project.pbxproj file. (#271) 2023-09-29 19:30:56 -07:00
d56697c57c add ability to render code block inside comment text. (#266) 2023-09-29 18:50:40 -07:00
320ec41aae update url linkifier. (#270) 2023-09-29 16:21:41 -07:00
d85b3535d5 update url linkifier. 2023-09-29 16:15:06 -07:00
f8cd1cbba0 update url_linkifier.dart (#269) 2023-09-29 14:56:11 -07:00
817ec208d6 fix url parser. (#268) 2023-09-29 12:34:20 -07:00
554a165789 fix selectable text. (#267) 2023-09-28 23:46:24 -07:00
0c680370ef add ability to long press on story title to copy link. (#265) 2023-09-28 14:10:08 -07:00
59541d2fcc update Fastfile (#264) 2023-09-28 01:48:20 -07:00
32083c3564 update fastlane. (#263) 2023-09-28 00:05:16 -07:00
258dbc4b8b fix url parsing. (#262) 2023-09-27 23:17:31 -07:00
6c8047ebac feature discovery cleanup. (#259) 2023-09-19 00:16:27 -07:00
00a0135867 fix draft saving. (#258) 2023-09-18 22:49:11 -07:00
1db7be7a2c fix draft saving. (#257) 2023-09-18 22:16:47 -07:00
ff400f9c40 fix reply view. (#256) 2023-09-18 20:31:44 -07:00
f03b45a98a update pubspec.lock (#255) 2023-09-17 17:59:29 -07:00
cbe5bba986 bump flutter version. (#254) 2023-09-17 17:38:44 -07:00
268f4054a3 improve story marking. (#253) 2023-09-11 20:42:33 -07:00
988c5d9881 add haptic feedback. (#252) 2023-09-11 18:08:21 -07:00
e748e2f818 allow swipe gesture in fav screen. (#251) 2023-09-11 17:01:42 -07:00
1b0a0dbda9 add changelog. (#250) 2023-09-11 15:22:32 -07:00
64d68389ba migrate from Navigator to GoRouter (#249) 2023-09-10 22:26:46 -07:00
381c99b353 fix crashing. (#248) 2023-09-08 09:07:48 -07:00
39ee3137f8 fix reply box in full screen. (#247) 2023-09-05 15:17:23 -07:00
0d76be8634 bump flutter version. (#243) 2023-08-22 06:54:59 -07:00
9986f72e11 improve shortcut buttons. (#242) 2023-07-19 21:09:24 -07:00
ef557e7b84 fix text scaling and url parsing. (#237) 2023-07-10 10:18:12 -07:00
ec065c0122 fix QR code view. (#231) 2023-06-22 19:00:19 -07:00
2960c6e59e add ability to import favorites using QR code. (#230) 2023-06-22 18:14:09 -07:00
92dac6b932 update device_gesture_wrapper.dart (#227) 2023-06-06 18:42:29 -07:00
20365393a3 fix capitalization. (#226) 2023-06-05 21:46:01 -07:00
8d238744c7 add option to disable auto-scroll. (#225) 2023-06-05 18:23:29 -07:00
e33ff417fb update project.pbxproj (#224) 2023-06-04 21:31:14 -07:00
d8922c2641 prevent over scrolling after collapsing a comment. (#223) 2023-06-04 19:16:01 -07:00
c6e0461857 improve date time range picker in search screen and add monochrome icons. (#219) 2023-05-26 21:33:43 -08:00
30ca356dc8 add date filter shortcuts. (#218) 2023-05-26 13:44:50 -08:00
7d11398e6d fix comment tile. (#215) 2023-05-19 12:37:21 -07:00
a4f52284ef bump flutter version. (#214) 2023-05-18 17:00:47 -07:00
c7d1a42d5a add View root button. (#212) 2023-04-16 22:55:23 -07:00
f83fd66bcc fix onboarding flow. (#211) 2023-04-16 20:21:48 -07:00
c2ec3647e2 bump Flutter version and add Noto Serif font. (#210) 2023-04-16 19:42:18 -07:00
ba63852b7d update tablet view. (#207) 2023-04-11 21:12:48 -07:00
438041183c fix in app review for android. (#206) 2023-04-11 19:09:35 -07:00
114540edd7 cleanup. (#205) 2023-04-11 18:30:03 -07:00
588b3e9508 fix reply box. (#204) 2023-04-11 13:23:11 -07:00
2f0376f8f8 add shortcuts to jump to previous or next root level comment. (#203) 2023-04-11 00:05:56 -07:00
ab4051c018 remove scroll bar. (#202) 2023-04-09 19:47:30 -07:00
c230c21218 update scrollbar. (#201) 2023-04-09 18:35:40 -07:00
c24e12237e fix status bar for android. (#200) 2023-04-09 17:55:29 -07:00
e15dcba93b update dev link. (#197) 2023-04-06 12:24:41 -07:00
1362b93a74 update pubspec file. (#196) 2023-04-06 11:07:02 -07:00
ac18793f98 bump flutter version. (#195) 2023-04-06 09:54:21 -07:00
e52f65c773 update tap target of story tile. (#194) 2023-04-04 15:31:50 -07:00
06212a0d72 fix auth for user with no activity. (#191) 2023-04-01 10:06:32 -07:00
202 changed files with 6373 additions and 2931 deletions

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
# <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News
A [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough.
A [Hacker News](https://news.ycombinator.com/) client built with Flutter.
[![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
[![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
@ -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...

View File

@ -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

View File

@ -50,7 +50,7 @@ android {
defaultConfig {
applicationId "com.jiaqifeng.hacki"
minSdkVersion 26
minSdkVersion 25
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@ -13,6 +13,9 @@
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
<application

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -24,6 +24,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

Binary file not shown.

Binary file not shown.

BIN
assets/hacki-github.xcf Normal file

Binary file not shown.

BIN
assets/hacki.xcf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

30
components/in_app_review/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

View File

@ -0,0 +1,39 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
channel: stable
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: android
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: ios
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: macos
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: windows
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@ -0,0 +1,102 @@
# [2.0.6]
- Update Android Play Core dependency to Play Review 2.0.1.
# [2.0.5]
- Migrate Android Play Core dependency to Play Review 2.0.0.
- Recreate the example app.
- Update in_app_review_platform_interface to 2.0.4
# [2.0.4]
- Migrate maven repository from jcenter to mavenCentral
- `isAvailable()` now returns `false` on web.
# [2.0.3]
- Fix iOS no-scene exception. ([#41](https://github.com/britannio/in_app_review/issues/41))
# [2.0.2]
- Replace iOS Swift code with Objective-C to add compatibility with Objective-C Flutter apps.
# [2.0.1]
- Fix rare null pointer exception on Android
- Fix MissingPluginException on MacOS
- Bump the minimum Dart SDK version from `2.12.0-0` to `2.12.0`.
- Bump the minimum Flutter version to `2.0.0`.
- Update in_app_review_platform_interface to 2.0.2
# [2.0.0]
- Migrate to null safety.
# [1.0.4]
- Update in_app_review_platform_interface to 1.0.5
- Remove dependency on `package_info`.
- Handle `openStoreListing()` with native code for Android, iOS and MacOS.
# [1.0.3]
- Update in_app_review_platform_interface to 1.0.4
- Update android compileSdkVersion to 29.
- Lower dependency version constraints.
# [1.0.2]
- Update in_app_review_platform_interface to 1.0.3
- Open the App Store directly instead of via the Safari View Controller.
- Add automated tests.
- Improve docs.
# [1.0.1+1]
- Update in_app_review_platform_interface to 1.0.2
# [1.0.0]
- Migrate to use `in_app_review_platform_interface`.
- Add Windows support for `openStoreListing`.
# [0.2.1+1]
- Improve iOS testing docs.
# [0.2.1]
- Update dependencies.
- Android Play Core Library V1.8.2 release notes:
- Fixed UI flickering in the In-App Review API
# [0.2.0+4]
- Remove deprecated API warning.
- Update dependencies.
# [0.2.0+3]
- Instructions in the README have been improved along with the example.
# [0.2.0+2]
- Update changelog format
# [0.2.0+1]
- Update MacOS testing instructions
# [0.2.0] Breaking Change
- Add MacOS support
- Rename `openStoreListing(iOSAppStoreId: '')` to `openStoreListing(appStoreId: '')`
# [0.1.0]
- Improve docs
- Set Android minSdkVersion to 16
- Refactor Android Plugin
# [0.0.1]
Initial release

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Britannio Jarrett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

View File

@ -0,0 +1,49 @@
group 'dev.britannio.in_app_review'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 16
}
dependencies {
}
}

View File

@ -0,0 +1 @@
rootProject.name = 'in_app_review'

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.britannio.in_app_review">
</manifest>

View File

@ -0,0 +1,59 @@
package dev.britannio.in_app_review;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
/**
* InAppReviewPlugin
*/
public class InAppReviewPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
private final String TAG = "InAppReviewPlugin";
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "dev.britannio.in_app_review");
channel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
Log.i(TAG, "onMethodCall: " + call.method);
switch (call.method) {
case "isAvailable":
case "requestReview":
case "openStoreListing":
default:
result.notImplemented();
break;
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}

38
components/in_app_review/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

View File

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface InAppReviewPlugin : NSObject<FlutterPlugin>
@end

View File

@ -0,0 +1,107 @@
#import "InAppReviewPlugin.h"
@import StoreKit;
@implementation InAppReviewPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"dev.britannio.in_app_review" binaryMessenger:[registrar messenger]];
InAppReviewPlugin* instance = [[InAppReviewPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
[self logMessage:@"handle" details:call.method];
if ([call.method isEqual:@"requestReview"]) {
[self requestReview:result];
} else if ([call.method isEqual:@"isAvailable"]) {
[self isAvailable:result];
} else if ([call.method isEqual:@"openStoreListing"]) {
[self openStoreListingWithStoreId:call.arguments result:result];
} else {
[self logMessage:@"method not implemented"];
result(FlutterMethodNotImplemented);
}
}
- (void) requestReview:(FlutterResult)result {
if (@available(iOS 14, *)) {
[self logMessage:@"iOS 14+"];
UIWindowScene *scene = [self findActiveScene];
[SKStoreReviewController requestReviewInScene:scene];
result(nil);
} else if (@available(iOS 10.3, *)) {
[self logMessage:@"iOS 10.3+"];
[SKStoreReviewController requestReview];
result(nil);
} else {
result([FlutterError errorWithCode:@"unavailable"
message:@"In-App Review unavailable"
details:nil]);
}
}
- (UIWindowScene *) findActiveScene API_AVAILABLE(ios(13.0)){
for (UIWindowScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive) {
return scene;
}
}
return nil;
}
- (void) isAvailable:(FlutterResult)result {
if (@available(iOS 10.3, *)) {
[self logMessage:@"available"];
result(@YES);
} else {
[self logMessage:@"unavailable"];
result(@NO);
}
}
- (void) openStoreListingWithStoreId:(NSString *)storeId result:(FlutterResult)result {
if (!storeId) {
result([FlutterError errorWithCode:@"no-store-id"
message:@"Your store id must be passed as the method channel's argument"
details:nil]);
return;
}
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://apps.apple.com/app/id%@?action=write-review", storeId]];
if (!url) {
result([FlutterError errorWithCode:@"url-construct-fail"
message:@"Failed to construct url"
details:nil]);
return;
}
UIApplication *app = [UIApplication sharedApplication];
if (@available(iOS 10.0, *)) {
[app openURL:url options:@{} completionHandler:nil];
} else {
[app openURL:url];
}
}
#pragma mark - Logging Helpers
- (void) logMessage:(NSString *) message {
NSLog(@"InAppReviewPlugin: %@", message);
}
- (void) logMessage:(NSString *) message
details:(NSString *) details {
NSLog(@"InAppReviewPlugin: %@ %@", message, details);
}
@end

View File

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint in_app_review.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'in_app_review'
s.version = '0.2.0'
s.summary = 'Flutter plugin for showing the In-App Review/System Rating pop up.'
s.description = <<-DESC
Flutter plugin for showing the In-App Review/System Rating pop up..
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Britannio Jarrett' => 'britanniojarrett@gmail.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '9.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

View File

@ -0,0 +1,50 @@
import 'dart:async';
import 'package:in_app_review_platform_interface/in_app_review_platform_interface.dart';
class InAppReview {
InAppReview._();
static final InAppReview instance = InAppReview._();
/// Checks if the device is able to show a review dialog.
///
/// On Android the Google Play Store must be installed and the device must be
/// running **Android 5 Lollipop(API 21)** or higher.
///
/// iOS devices must be running **iOS version 10.3** or higher.
///
/// MacOS devices must be running **MacOS version 10.14** or higher
Future<bool> isAvailable() => InAppReviewPlatform.instance.isAvailable();
/// Attempts to show the review dialog. It's recommended to first check if
/// the device supports this feature via [isAvailable].
///
/// To improve the users experience, iOS and Android enforce limitations
/// that might prevent this from working after a few tries. iOS & MacOS users
/// can also disable this feature entirely in the App Store settings.
///
/// More info and guidance:
/// https://developer.android.com/guide/playcore/in-app-review#when-to-request
/// https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/
/// https://developer.apple.com/design/human-interface-guidelines/macos/system-capabilities/ratings-and-reviews/
Future<void> requestReview() => InAppReviewPlatform.instance.requestReview();
/// Opens the Play Store on Android, the App Store with a review
/// screen on iOS & MacOS and the Microsoft Store on Windows.
///
/// [appStoreId] is required for iOS & MacOS.
///
/// [microsoftStoreId] is required for Windows.
Future<void> openStoreListing({
/// Required for iOS & MacOS.
String? appStoreId,
/// Required for Windows.
String? microsoftStoreId,
}) =>
InAppReviewPlatform.instance.openStoreListing(
appStoreId: appStoreId,
microsoftStoreId: microsoftStoreId,
);
}

View File

@ -0,0 +1,42 @@
import Cocoa
import FlutterMacOS
import StoreKit
public class InAppReviewPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "dev.britannio.in_app_review", binaryMessenger: registrar.messenger)
let instance = InAppReviewPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "requestReview":
//App Store Review
if #available(OSX 10.14, *) {
SKStoreReviewController.requestReview()
result(nil)
} else {
result(FlutterError(code: "unavailable", message: "In-App Review unavailable", details: nil))
}
case "isAvailable":
if #available(OSX 10.14, *) {
result(true)
} else {
result(false)
}
case "openStoreListing":
let storeId : String = call.arguments as! String;
guard let writeReviewURL = URL(string: "macappstore://apps.apple.com/app/id" + storeId + "?action=write-review")
else {
result(FlutterError(code: "url_construct_fail", message: "Failed to construct url", details: nil))
return
}
NSWorkspace.shared.open(writeReviewURL)
result(nil);
default:
result(FlutterMethodNotImplemented)
}
}
}

View File

@ -0,0 +1,22 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint in_app_review.podspec' to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'in_app_review'
s.version = '0.2.0'
s.summary = 'Flutter plugin for showing the In-App Review/System Rating pop up.'
s.description = <<-DESC
Flutter plugin for showing the In-App Review/System Rating pop up.
DESC
s.homepage = 'https://github.com/britannio/in_app_review'
s.license = { :file => '../LICENSE' }
s.author = { 'Britannio Jarrett' => 'britanniojarrett@gmail.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
s.platform = :osx, '10.11'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'
end

View File

@ -0,0 +1,46 @@
name: in_app_review
description: Flutter plugin for showing the In-App Review/System Rating pop up on Android, iOS and MacOS. It makes it easy for users to rate your app.
version: 2.0.6
homepage: https://github.com/britannio/in_app_review/tree/master/in_app_review
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
in_app_review_platform_interface: ^2.0.4
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.0.0
plugin_platform_interface: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: dev.britannio.in_app_review
pluginClass: InAppReviewPlugin
ios:
pluginClass: InAppReviewPlugin
macos:
pluginClass: InAppReviewPlugin

View File

@ -0,0 +1,12 @@
.DS_Store
.dart_tool/
.packages
.pub/
build/
pubspec.lock
.flutter-plugins
.flutter-plugins-dependencies

View File

@ -0,0 +1,46 @@
# [2.0.4]
- Update usage of `pkg:url_launcher` to address deprecations.
# [2.0.3]
- `isAvailable()` now returns `false` on web.
# [2.0.2]
- Bump the minimum Flutter version to `2.0.0`.
# [2.0.1]
- Bump the minimum Dart SDK version from `2.12.0-0` to `2.12.0`.
# [2.0.0]
- Migrate to null safety.
# [1.0.5]
- Remove dependency on `package_info`.
- Handle `openStoreListing()` with native code for Android, iOS and MacOS.
# [1.0.4]
- Lower dependency version constraints
# [1.0.3]
- Open the App Store directly instead of via the Safari View Controller.
- Add automated tests.
# [1.0.2]
- Rename `openStoreListing(windowsStoreId: '')` to `openStoreListing(microsoftStoreId: '')`.
- Update dependencies.
# [1.0.1]
- Remove unnecessary files.
# [1.0.0]
- Initial release.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Britannio Jarrett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,26 @@
# in_app_review_platform_interface
A common platform interface for the [`in_app_review`][1] plugin.
This interface allows platform-specific implementations of the `in_app_review`
plugin, as well as the plugin itself, to ensure they are supporting the
same interface.
# Usage
To implement a new platform-specific implementation of `in_app_review`, extend
[`InAppReviewPlatform`][2] with an implementation that performs the
platform-specific behavior, and when you register your plugin, set the default
`InAppReviewPlatform` by calling
`InAppReviewPlatform.instance = MyInAppReview()`.
# Note on breaking changes
Strongly prefer non-breaking changes (such as adding a method to the interface)
over breaking changes for this package.
See https://flutter.dev/go/platform-interface-breaking-changes for a discussion
on why a less-clean interface is preferable to a breaking change.
[1]: ../in_app_review
[2]: lib/in_app_review_platform_interface.dart

View File

@ -0,0 +1,71 @@
import 'package:in_app_review_platform_interface/method_channel_in_app_review.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
/// The interface that implementations of in_app_review must implement.
///
/// Platform implementations should extend this class rather than implement it
/// as `in_app_review` does not consider newly added methods to be breaking
/// changes. Extending this class (using `extends`) ensures that the subclass
/// will get the default implementation, while platform implementations that
/// `implements` this interface will be broken by newly added
/// [InAppReviewPlatform] methods.
abstract class InAppReviewPlatform extends PlatformInterface {
InAppReviewPlatform() : super(token: _token);
static InAppReviewPlatform _instance = MethodChannelInAppReview();
static final Object _token = Object();
static InAppReviewPlatform get instance => _instance;
/// Platform-specific plugins should set this with their own platform-specific
/// class that extends [InAppReviewPlatform] when they register themselves.
static set instance(InAppReviewPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
/// Checks if the device is able to show a review dialog.
///
/// On Android the Google Play Store must be installed and the device must be
/// running **Android 5 Lollipop(API 21)** or higher.
///
/// iOS devices must be running **iOS version 10.3** or higher.
///
/// MacOS devices must be running **MacOS version 10.14** or higher
Future<bool> isAvailable() {
throw UnimplementedError('isAvailable() has not been implemented.');
}
/// Attempts to show the review dialog. It's recommended to first check if
/// this cannot be done via [isAvailable]. If it is not available then
/// you can open the store listing via [openStoreListing].
///
/// To improve the users experience, iOS and Android enforce limitations
/// that might prevent this from working after a few tries. iOS & MacOS users
/// can also disable this feature entirely in the App Store settings.
///
/// More info and guidance:
/// https://developer.android.com/guide/playcore/in-app-review#when-to-request
/// https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/
/// https://developer.apple.com/design/human-interface-guidelines/macos/system-capabilities/ratings-and-reviews/
Future<void> requestReview() {
throw UnimplementedError('requestReview() has not been implemented.');
}
/// Opens the Play Store on Android, the App Store with a review
/// screen on iOS & MacOS and the Microsoft Store on Windows.
///
/// [appStoreId] is required for iOS & MacOS.
///
/// [microsoftStoreId] is required for Windows.
Future<void> openStoreListing({
/// Required for iOS & MacOS.
String? appStoreId,
/// Required for Windows.
String? microsoftStoreId,
}) {
throw UnimplementedError('openStoreListing() has not been implemented.');
}
}

View File

@ -0,0 +1,65 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:platform/platform.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'in_app_review_platform_interface.dart';
/// An implementation of [InAppReviewPlatform] that uses method channels.
class MethodChannelInAppReview extends InAppReviewPlatform {
MethodChannel _channel = MethodChannel('dev.britannio.in_app_review');
Platform _platform = const LocalPlatform();
@visibleForTesting
set channel(MethodChannel channel) => _channel = channel;
@visibleForTesting
set platform(Platform platform) => _platform = platform;
@override
Future<bool> isAvailable() async {
if (kIsWeb) return false;
return _channel
.invokeMethod<bool>('isAvailable')
.then((bool? available) => available ?? false, onError: (_) => false);
}
@override
Future<void> requestReview() => _channel.invokeMethod('requestReview');
@override
Future<void> openStoreListing({
String? appStoreId,
String? microsoftStoreId,
}) async {
final bool isiOS = _platform.isIOS;
final bool isMacOS = _platform.isMacOS;
final bool isAndroid = _platform.isAndroid;
final bool isWindows = _platform.isWindows;
if (isiOS || isMacOS) {
await _channel.invokeMethod(
'openStoreListing',
ArgumentError.checkNotNull(appStoreId, 'appStoreId'),
);
} else if (isAndroid) {
await _channel.invokeMethod('openStoreListing');
} else if (isWindows) {
ArgumentError.checkNotNull(microsoftStoreId, 'microsoftStoreId');
await _launchUrl(
'ms-windows-store://review/?ProductId=$microsoftStoreId',
);
} else {
throw UnsupportedError(
'Platform(${_platform.operatingSystem}) not supported',
);
}
}
Future<void> _launchUrl(String url) async {
if (!await canLaunchUrlString(url)) return;
await launchUrlString(url, mode: LaunchMode.externalNonBrowserApplication);
}
}

View File

@ -0,0 +1,24 @@
name: in_app_review_platform_interface
description: A common platform interface for the in_app_review plugin.
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
version: 2.0.4
homepage: https://github.com/britannio/in_app_review/tree/master/in_app_review_platform_interface
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
url_launcher: ^6.1.0
plugin_platform_interface: ^2.0.0
platform: ^3.0.0
dev_dependencies:
flutter_test:
sdk: flutter

View File

@ -76,6 +76,15 @@ final class SharedPrefsCore {
return true
}
fileprivate func remove(key: String?) -> Bool{
if let key = key {
let keyStore = NSUbiquitousKeyValueStore()
keyStore.removeObject(forKey: key)
}
return true
}
}
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
@ -87,6 +96,14 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "remove":
if let params = call.arguments as? [String: Any] {
let key = params[keyKey] as? String
let res = SharedPrefsCore.shared.remove(key: key)
result(res)
}
case "setBool":
if let params = call.arguments as? [String: Any] {
let val = params[valKey] as? Bool

View File

@ -15,6 +15,14 @@ class SyncedSharedPreferences {
const MethodChannel(channel),
);
Future<bool?> remove({
required String key,
}) async {
return _channel.invokeMethod('remove', <String, dynamic>{
'key': key,
});
}
Future<bool?> setBool({
required String key,
required bool val,

View File

@ -0,0 +1 @@
- Navigation shortcuts.

View File

@ -0,0 +1 @@
- Ability to mark a story as read once scrolling past.

View File

@ -0,0 +1,2 @@
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,4 @@
- Ability to use Material 3.
- Ability to search in thread.
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,5 @@
- Ability to use pagination on home screen.
- Ability to use Material 3 (experimental).
- Ability to search in thread.
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,5 @@
- Ability to use manual pagination on home screen.
- Ability to use Material 3 (experimental).
- Ability to search in thread.
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,4 @@
- New comment indicator.
- Ability to mark stories as read from home page.
- Text rendering improvements.
- Performance improvements.

View File

@ -0,0 +1,4 @@
- New comment indicator.
- Ability to mark stories as read from home page.
- Text rendering improvements.
- Performance improvements.

View File

@ -23,14 +23,20 @@ PODS:
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- in_app_review (0.2.0):
- 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
@ -39,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):
@ -62,12 +68,14 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
- 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`)
@ -78,6 +86,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- FMDB
- MTBBarcodeScanner
- OrderedSet
- ReachabilitySwift
@ -98,18 +107,22 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_siri_suggestions:
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
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:
@ -124,30 +137,33 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
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: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
COCOAPODS: 1.11.3
COCOAPODS: 1.13.0

View File

@ -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;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -76,5 +76,9 @@
<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/>
</dict>
</plist>

View File

@ -11,13 +11,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc({
AuthRepository? authRepository,
PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository,
HackerNewsRepository? hackerNewsRepository,
SembastRepository? sembastRepository,
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(),
super(const AuthState.init()) {
@ -31,7 +31,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _authRepository;
final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
final SembastRepository _sembastRepository;
Future<void> onInitialize(
@ -41,20 +41,25 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
await _authRepository.loggedIn.then((bool loggedIn) async {
if (loggedIn) {
final String? username = await _authRepository.username;
final User user = await _storiesRepository.fetchUser(id: username!);
User? user = await _hackerNewsRepository.fetchUser(id: username!);
/// According to Hacker News' API documentation,
/// if user has no public activity (posting a comment or story),
/// then it will not be available from the API.
user ??= User.emptyWithId(username);
emit(
state.copyWith(
isLoggedIn: true,
user: user,
status: AuthStatus.loaded,
status: Status.success,
),
);
} else {
emit(
state.copyWith(
isLoggedIn: false,
status: AuthStatus.loaded,
status: Status.success,
),
);
}
@ -76,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,
@ -84,16 +89,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
);
if (successful) {
final User user = await _storiesRepository.fetchUser(id: event.username);
final User? user =
await _hackerNewsRepository.fetchUser(id: event.username);
emit(
state.copyWith(
user: user,
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));
}
}

View File

@ -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,

View File

@ -19,15 +19,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
required PreferenceCubit preferenceCubit,
required FilterCubit filterCubit,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository,
HackerNewsRepository? hackerNewsRepository,
PreferenceRepository? preferenceRepository,
Logger? logger,
}) : _preferenceCubit = preferenceCubit,
_filterCubit = filterCubit,
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_logger = logger ?? locator.get<Logger>(),
@ -37,6 +37,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
on<StoriesLoadMore>(onLoadMore);
on<StoryLoaded>(onStoryLoaded);
on<StoryRead>(onStoryRead);
on<StoryUnread>(onStoryUnread);
on<StoriesLoaded>(onStoriesLoaded);
on<StoriesDownload>(onDownload);
on<StoriesCancelDownload>(onCancelDownload);
@ -49,7 +50,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final PreferenceCubit _preferenceCubit;
final FilterCubit _filterCubit;
final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
final PreferenceRepository _preferenceRepository;
final Logger _logger;
DeviceScreenType? deviceScreenType;
@ -79,7 +80,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,
@ -113,13 +114,14 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
add(StoriesLoaded(type: type));
});
} else {
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
final List<int> ids =
await _hackerNewsRepository.fetchStoryIds(type: type);
emit(
state
.copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0),
);
_storiesRepository
_hackerNewsRepository
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
.listen((Story story) {
add(StoryLoaded(story: story, type: type));
@ -133,10 +135,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 +148,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: StoriesStatus.loaded,
to: Status.success,
),
);
} else {
@ -157,7 +161,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: StoriesStatus.loading,
to: Status.inProgress,
),
);
@ -194,7 +198,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
add(StoriesLoaded(type: event.type));
});
} else {
_storiesRepository
_hackerNewsRepository
.fetchStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist(
lower,
@ -216,7 +220,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: StoriesStatus.loaded,
to: Status.success,
),
);
}
@ -226,16 +230,18 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoryLoaded event,
Emitter<StoriesState> emit,
) async {
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
final bool hidden = _filterCubit.state.keywords.any(
(String keyword) =>
event.story.title.toLowerCase().contains(keyword) ||
event.story.text.toLowerCase().contains(keyword),
);
final Story story = event.story;
final bool hasRead = await _preferenceRepository.hasRead(story.id);
final bool hidden = _filterCubit.state.keywords.any((String keyword) {
// Match word only.
final RegExp regExp = RegExp('\\b($keyword)\\b');
return regExp.hasMatch(story.title.toLowerCase()) ||
regExp.hasMatch(story.text.toLowerCase());
});
emit(
state.copyWithStoryAdded(
type: event.type,
story: event.story.copyWith(hidden: hidden),
story: story.copyWith(hidden: hidden),
hasRead: hasRead,
),
);
@ -243,7 +249,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),
);
}
@ -269,7 +275,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
..remove(StoryType.latest);
for (final StoryType type in prioritizedTypes) {
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
final List<int> ids =
await _hackerNewsRepository.fetchStoryIds(type: type);
await _offlineRepository.cacheStoryIds(type: type, ids: ids);
prioritizedIds.addAll(ids);
}
@ -289,7 +296,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
);
final Set<int> latestIds = <int>{};
final List<int> ids = await _storiesRepository.fetchStoryIds(
final List<int> ids = await _hackerNewsRepository.fetchStoryIds(
type: StoryType.latest,
);
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
@ -343,7 +350,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
}
_logger.d('fetching story $id');
final Story? story = await _storiesRepository.fetchStory(id: id);
final Story? story = await _hackerNewsRepository.fetchStory(id: id);
if (story == null) {
if (isPrioritized) {
@ -373,7 +380,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
/// In other words, we are prioritizing the story itself instead of
/// the comments in the story.
late final StreamSubscription<Comment>? downloadStream;
downloadStream = _storiesRepository
downloadStream = _hackerNewsRepository
.fetchAllChildrenComments(ids: story.kids)
.whereType<Comment>()
.listen(
@ -456,7 +463,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoryRead event,
Emitter<StoriesState> emit,
) async {
unawaited(_preferenceRepository.updateHasRead(event.story.id));
unawaited(_preferenceRepository.addHasRead(event.story.id));
emit(
state.copyWith(
@ -465,6 +472,19 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
);
}
Future<void> onStoryUnread(
StoryUnread event,
Emitter<StoriesState> emit,
) async {
unawaited(_preferenceRepository.removeHasRead(event.story.id));
emit(
state.copyWith(
readStoriesIds: <int>{...state.readStoriesIds}..remove(event.story.id),
),
);
}
Future<void> onClearAllReadStories(
ClearAllReadStories event,
Emitter<StoriesState> emit,

View File

@ -95,6 +95,15 @@ class StoryRead extends StoriesEvent {
List<Object?> get props => <Object?>[story];
}
class StoryUnread extends StoriesEvent {
StoryUnread({required this.story});
final Story story;
@override
List<Object?> get props => <Object?>[story];
}
class ClearAllReadStories extends StoriesEvent {
@override
List<Object?> get props => <Object?>[];

View File

@ -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;

View File

@ -7,7 +7,7 @@ abstract class Constants {
'https://github.com/Livinglist/Hacki/blob/master/assets/privacy_policy.md';
static const String hackerNewsLogoLink =
'https://pbs.twimg.com/profile_images/469397708986269696/iUrYEOpJ_400x400.png';
static const String portfolioLink = 'https://livinglist.github.io';
static const String portfolioLink = 'https://github.com/Livinglist';
static const String githubLink = 'https://github.com/Livinglist/Hacki';
static const String appStoreLink =
'https://apps.apple.com/us/app/hacki/id1602043763?action=write-review';
@ -20,6 +20,8 @@ abstract class Constants {
'$githubLink/issues/new?title=Found+a+bug+in+Hacki&body=Please+describe+the+problem.';
static const String wikipediaLink = 'https://en.wikipedia.org/wiki/';
static const String wiktionaryLink = 'https://en.wiktionary.org/wiki/';
static const String hackerNewsItemLinkPrefix =
'https://news.ycombinator.com/item?id=';
static const String supportEmail = 'georgefung98@gmail.com';
static const String _imagePath = 'assets/images';
@ -34,12 +36,6 @@ abstract class Constants {
static const String logFilename = 'hacki_log.txt';
static const String previousLogFileName = 'old_hacki_log.txt';
/// Feature ids for feature discovery.
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
static const String featureLogIn = 'log_in';
static const String featurePinToTop = 'pin_to_top';
static final String happyFace = <String>[
'(๑•̀ㅂ•́)و✧',
'( ͡• ͜ʖ ͡•)',
@ -76,3 +72,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);
}

View File

@ -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;

View File

@ -1,49 +1,76 @@
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/config/locator.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>[
locator.get<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,
);
},
),
],
);

View File

@ -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,

View File

@ -23,16 +23,17 @@ Future<void> setUpLocator() async {
output: LogUtil.logOutput(logOutputFile),
),
)
..registerSingleton<StoriesRepository>(StoriesRepository())
..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<HackerNewsRepository>(HackerNewsRepository())
..registerSingleton<PreferenceRepository>(PreferenceRepository())
..registerSingleton<SearchRepository>(SearchRepository())
..registerSingleton<AuthRepository>(AuthRepository())
..registerSingleton<PostRepository>(PostRepository())
..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<OfflineRepository>(OfflineRepository())
..registerSingleton<DraftCache>(DraftCache())
..registerSingleton<CommentCache>(CommentCache())
..registerSingleton<LocalNotification>(LocalNotification())
..registerSingleton<LocalNotificationService>(LocalNotificationService())
..registerSingleton(AppReviewService())
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
RouteObserver<ModalRoute<dynamic>>(),
);

View File

@ -2,8 +2,8 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/services/services.dart';
part 'collapse_state.dart';
@ -11,16 +11,13 @@ part 'collapse_state.dart';
class CollapseCubit extends Cubit<CollapseState> {
CollapseCubit({
required int commentId,
required CommentsCubit? commentsCubit,
CollapseCache? collapseCache,
}) : _commentId = commentId,
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
_commentsCubit = commentsCubit,
super(const CollapseState.init());
final int _commentId;
final CollapseCache _collapseCache;
final CommentsCubit? _commentsCubit;
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
void init() {
@ -32,11 +29,12 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: _collapseCache.totalHidden(_commentId),
collapsed: _collapseCache.isCollapsed(_commentId),
hidden: _collapseCache.isHidden(_commentId),
locked: _collapseCache.lockedId == _commentId,
),
);
}
void collapse() {
void collapse({required VoidCallback onStateChanged}) {
if (state.collapsed) {
_collapseCache.uncollapse(_commentId);
@ -46,17 +44,15 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: 0,
),
);
onStateChanged();
} else {
if (_commentsCubit == null) return;
if (state.locked) {
emit(state.copyWith(locked: false));
return;
}
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
final int lastCommentId = _commentsCubit!.state.comments.last.id;
final bool shouldLoadMore = _commentId == lastCommentId ||
collapsedCommentIds.contains(lastCommentId);
if (shouldLoadMore) {
_commentsCubit!.loadMore();
}
emit(
state.copyWith(
@ -64,6 +60,8 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
),
);
onStateChanged();
}
}
@ -98,6 +96,13 @@ class CollapseCubit extends Cubit<CollapseState> {
}
}
/// Prevent the item to be able to collapse, used when the comment
/// text is selected.
void lock() {
_collapseCache.lockedId = _commentId;
emit(state.copyWith(locked: true));
}
@override
Future<void> close() async {
await _streamSubscription.cancel();

View File

@ -4,26 +4,39 @@ class CollapseState extends Equatable {
const CollapseState({
required this.collapsed,
required this.hidden,
required this.locked,
required this.collapsedCount,
});
const CollapseState.init()
: collapsed = false,
hidden = false,
locked = false,
collapsedCount = 0;
final bool collapsed;
/// The value determining whether or not the comment should show up in the
/// screen, this is true when the comment's parent is collapsed.
final bool hidden;
/// The value determining whether or not the comment is collapsable.
/// If [locked] is true then the comment is not collapsable and vice versa.
final bool locked;
/// The number of children under this collapsed comment.
final int collapsedCount;
CollapseState copyWith({
bool? collapsed,
bool? hidden,
bool? locked,
int? collapsedCount,
}) {
return CollapseState(
collapsed: collapsed ?? this.collapsed,
hidden: hidden ?? this.hidden,
locked: locked ?? this.locked,
collapsedCount: collapsedCount ?? this.collapsedCount,
);
}
@ -32,6 +45,7 @@ class CollapseState extends Equatable {
List<Object?> get props => <Object?>[
collapsed,
hidden,
locked,
collapsedCount,
];
}

View File

@ -1,21 +1,24 @@
import 'dart:async';
import 'dart:math';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.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/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/services.dart';
import 'package:hacki/utils/linkifier_util.dart';
import 'package:hacki/utils/utils.dart';
import 'package:linkify/linkify.dart';
import 'package:logger/logger.dart';
import 'package:rxdart/rxdart.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
part 'comments_state.dart';
@ -23,24 +26,21 @@ class CommentsCubit extends Cubit<CommentsState> {
CommentsCubit({
required FilterCubit filterCubit,
required CollapseCache collapseCache,
CommentCache? commentCache,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository,
SembastRepository? sembastRepository,
Logger? logger,
required bool isOfflineReading,
required Item item,
required FetchMode defaultFetchMode,
required CommentsOrder defaultCommentsOrder,
CommentCache? commentCache,
OfflineRepository? offlineRepository,
HackerNewsRepository? hackerNewsRepository,
Logger? logger,
}) : _filterCubit = filterCubit,
_collapseCache = collapseCache,
_commentCache = commentCache ?? locator.get<CommentCache>(),
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(
CommentsState.init(
@ -55,10 +55,13 @@ class CommentsCubit extends Cubit<CommentsState> {
final CollapseCache _collapseCache;
final CommentCache _commentCache;
final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository;
final SembastRepository _sembastRepository;
final HackerNewsRepository _hackerNewsRepository;
final Logger _logger;
final ItemScrollController itemScrollController = ItemScrollController();
final ItemPositionsListener itemPositionsListener =
ItemPositionsListener.create();
/// The [StreamSubscription] for stream (both lazy or eager)
/// fetching comments posted directly to the story.
StreamSubscription<Comment>? _streamSubscription;
@ -68,8 +71,6 @@ class CommentsCubit extends Cubit<CommentsState> {
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
<int, StreamSubscription<Comment>>{};
static const int _pageSize = 20;
@override
void emit(CommentsState state) {
if (!isClosed) {
@ -91,7 +92,7 @@ class CommentsCubit extends Cubit<CommentsState> {
),
);
_streamSubscription = _storiesRepository
_streamSubscription = _hackerNewsRepository
.fetchAllCommentsRecursivelyStream(
ids: targetAncestors!.last.kids,
level: targetAncestors.last.level + 1,
@ -106,8 +107,10 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(
state.copyWith(
status: CommentsStatus.loading,
status: CommentsStatus.inProgress,
comments: <Comment>[],
matchedComments: <int>[],
inThreadSearchQuery: '',
currentPage: 0,
),
);
@ -115,9 +118,12 @@ class CommentsCubit extends Cubit<CommentsState> {
final Item item = state.item;
final Item updatedItem = state.isOfflineReading
? item
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
: await _hackerNewsRepository
.fetchItem(id: item.id)
.then(_toBuildable)
.onError((_, __) => item) ??
item;
final List<int> kids = sortKids(updatedItem.kids);
final List<int> kids = _sortKids(updatedItem.kids);
emit(state.copyWith(item: updatedItem));
@ -128,17 +134,16 @@ class CommentsCubit extends Cubit<CommentsState> {
} else {
switch (state.fetchMode) {
case FetchMode.lazy:
commentStream = _storiesRepository.fetchCommentsStream(
commentStream = _hackerNewsRepository.fetchCommentsStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
break;
case FetchMode.eager:
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
break;
}
}
@ -152,7 +157,7 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> refresh() async {
emit(
state.copyWith(
status: CommentsStatus.loading,
status: CommentsStatus.inProgress,
),
);
@ -182,16 +187,16 @@ class CommentsCubit extends Cubit<CommentsState> {
final Item item = state.item;
final Item updatedItem =
await _storiesRepository.fetchItem(id: item.id) ?? item;
final List<int> kids = sortKids(updatedItem.kids);
await _hackerNewsRepository.fetchItem(id: item.id) ?? item;
final List<int> kids = _sortKids(updatedItem.kids);
late final Stream<Comment> commentStream;
if (state.fetchMode == FetchMode.lazy) {
commentStream = _storiesRepository.fetchCommentsStream(
commentStream = _hackerNewsRepository.fetchCommentsStream(
ids: kids,
);
} else {
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
commentStream = _hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
);
}
@ -210,19 +215,24 @@ class CommentsCubit extends Cubit<CommentsState> {
}
void loadAll(Story story) {
HapticFeedback.lightImpact();
HapticFeedbackUtil.light();
emit(
state.copyWith(
onlyShowTargetComment: false,
item: story,
matchedComments: <int>[],
),
);
init();
}
/// [comment] is only used for lazy fetching.
void loadMore({Comment? comment}) {
if (comment == null && state.status == CommentsStatus.loading) return;
void loadMore({
Comment? comment,
void Function(Comment)? onCommentFetched,
VoidCallback? onDone,
}) {
if (comment == null && state.status == CommentsStatus.inProgress) return;
switch (state.fetchMode) {
case FetchMode.lazy:
@ -235,14 +245,17 @@ class CommentsCubit extends Cubit<CommentsState> {
/// Ignoring because the subscription will be cancelled in close()
// ignore: cancel_subscriptions
final StreamSubscription<Comment> streamSubscription =
_storiesRepository
_hackerNewsRepository
.fetchCommentsStream(ids: comment.kids)
.asyncMap(_toBuildableComment)
.whereNotNull()
.listen((Comment cmt) {
_collapseCache.addKid(cmt.id, to: cmt.parent);
_commentCache.cacheComment(cmt);
_sembastRepository.cacheComment(cmt);
final Map<int, Comment> updatedIdToCommentMap =
Map<int, Comment>.from(state.idToCommentMap);
updatedIdToCommentMap[comment.id] = comment;
emit(
state.copyWith(
@ -250,6 +263,7 @@ class CommentsCubit extends Cubit<CommentsState> {
state.comments.indexOf(comment) + offset + 1,
cmt.copyWith(level: level),
),
idToCommentMap: updatedIdToCommentMap,
),
);
offset++;
@ -265,29 +279,28 @@ class CommentsCubit extends Cubit<CommentsState> {
});
_streamSubscriptions[comment.id] = streamSubscription;
break;
case FetchMode.eager:
if (_streamSubscription != null) {
emit(state.copyWith(status: CommentsStatus.loading));
_streamSubscription?.resume();
emit(state.copyWith(status: CommentsStatus.inProgress));
_streamSubscription
?..resume()
..onData(onCommentFetched);
}
break;
}
}
Future<void> loadParentThread() async {
unawaited(HapticFeedback.lightImpact());
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
final Story? parent = await _storiesRepository
.fetchParentStory(id: state.item.id)
.then(_toBuildableStory);
HapticFeedbackUtil.light();
emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress));
final Item? parent =
await _hackerNewsRepository.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(
@ -298,10 +311,33 @@ class CommentsCubit extends Cubit<CommentsState> {
}
}
void onOrderChanged(CommentsOrder? order) {
Future<void> loadRootThread() async {
HapticFeedbackUtil.light();
emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress));
final Story? parent = await _hackerNewsRepository
.fetchParentStory(id: state.item.id)
.then(_toBuildableStory);
if (parent == null) {
return;
} else {
await router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: parent),
);
emit(
state.copyWith(
fetchRootStatus: CommentsStatus.loaded,
),
);
}
}
void updateOrder(CommentsOrder? order) {
if (order == null) return;
if (state.order == order) return;
HapticFeedback.selectionClick();
HapticFeedbackUtil.selection();
_streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
@ -311,11 +347,11 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true);
}
void onFetchModeChanged(FetchMode? fetchMode) {
void updateFetchMode(FetchMode? fetchMode) {
if (fetchMode == null) return;
if (state.fetchMode == fetchMode) return;
_collapseCache.resetCollapsedComments();
HapticFeedback.selectionClick();
HapticFeedbackUtil.selection();
_streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
@ -325,7 +361,158 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true);
}
List<int> sortKids(List<int> kids) {
void scrollTo({
required int index,
double alignment = 0.0,
}) {
debugPrint('Scrolling to: $index, alignment: $alignment');
itemScrollController.scrollTo(
index: index,
alignment: alignment,
duration: Durations.ms400,
);
}
/// Scroll to next root level comment.
void scrollToNextRoot({VoidCallback? onError}) {
final int totalComments = state.comments.length;
final List<Comment> onScreenComments = itemPositionsListener
.itemPositions.value
// The header is also a part of the list view,
// thus ignoring it here.
.where((ItemPosition e) => e.index >= 1 && e.itemLeadingEdge > 0.1)
.sorted((ItemPosition a, ItemPosition b) => a.index.compareTo(b.index))
.map(
(ItemPosition e) => e.index <= state.comments.length
? state.comments.elementAt(e.index - 1)
: null,
)
.whereNotNull()
.toList();
if (onScreenComments.isEmpty && state.comments.isNotEmpty) {
itemScrollController.scrollTo(
index: 1,
alignment: 0.15,
duration: Durations.ms400,
);
return;
}
final Comment? firstVisibleRootComment =
onScreenComments.firstWhereOrNull((Comment e) => e.isRoot);
late int startIndex;
if (firstVisibleRootComment != null) {
/// The index of first root level comment visible on screen.
final int firstVisibleRootCommentIndex =
state.comments.indexOf(firstVisibleRootComment);
startIndex = min(firstVisibleRootCommentIndex + 1, totalComments);
} else {
final int lastVisibleCommentIndex =
state.comments.indexOf(onScreenComments.last);
startIndex = min(lastVisibleCommentIndex + 1, totalComments);
}
for (int i = startIndex; i < totalComments; i++) {
final Comment cmt = state.comments.elementAt(i);
if (cmt.isRoot && (cmt.deleted || cmt.dead) == false) {
itemScrollController.scrollTo(
index: i + 1,
alignment: 0.15,
duration: Durations.ms400,
);
return;
}
}
if (state.status == CommentsStatus.allLoaded) {
onError?.call();
}
}
/// Scroll to previous root level comment.
void scrollToPreviousRoot() {
final List<Comment> onScreenComments = itemPositionsListener
.itemPositions.value
// The header is also a part of the list view,
// thus ignoring it here.
.where((ItemPosition e) => e.index >= 1 && e.itemLeadingEdge > 0)
.sorted((ItemPosition a, ItemPosition b) => a.index.compareTo(b.index))
.map(
(ItemPosition e) => e.index <= state.comments.length
? state.comments.elementAt(e.index - 1)
: null,
)
.whereNotNull()
.toList();
/// The index of first comment visible on screen.
final int firstVisibleIndex = state.comments.indexOf(
onScreenComments.firstOrNull ?? state.comments.last,
);
final int startIndex = max(0, firstVisibleIndex - 1);
for (int i = startIndex; i >= 0; i--) {
final Comment cmt = state.comments.elementAt(i);
if (cmt.isRoot && (cmt.deleted || cmt.dead) == false) {
itemScrollController.scrollTo(
index: i + 1,
alignment: 0.15,
duration: Durations.ms400,
);
return;
}
}
}
void search(String query, {String author = ''}) {
resetSearch();
late final bool Function(Comment cmt) conditionSatisfied;
final String lowercaseQuery = query.toLowerCase();
if (query.isEmpty && author.isEmpty) {
return;
} else if (author.isEmpty) {
conditionSatisfied =
(Comment cmt) => cmt.text.toLowerCase().contains(lowercaseQuery);
} else if (query.isEmpty) {
conditionSatisfied = (Comment cmt) => cmt.by == author;
} else {
conditionSatisfied = (Comment cmt) =>
cmt.text.toLowerCase().contains(lowercaseQuery) && cmt.by == author;
}
emit(
state.copyWith(
inThreadSearchQuery: query,
inThreadSearchAuthor: author,
),
);
for (final int i in 0.to(state.comments.length, inclusive: false)) {
final Comment cmt = state.comments.elementAt(i);
if (conditionSatisfied(cmt)) {
emit(
state.copyWith(
matchedComments: <int>[...state.matchedComments, i],
),
);
}
}
}
void resetSearch() => emit(
state.copyWith(
matchedComments: <int>[],
inThreadSearchQuery: '',
inThreadSearchAuthor: '',
),
);
List<int> _sortKids(List<int> kids) {
switch (state.order) {
case CommentsOrder.natural:
return kids;
@ -350,8 +537,8 @@ class CommentsCubit extends Cubit<CommentsState> {
if (comment != null) {
_collapseCache.addKid(comment.id, to: comment.parent);
_commentCache.cacheComment(comment);
_sembastRepository.cacheComment(comment);
// Hide comment that matches any of the filter keywords.
final bool hidden = _filterCubit.state.keywords.any(
(String keyword) => comment.text.toLowerCase().contains(keyword),
);
@ -360,32 +547,16 @@ class CommentsCubit extends Cubit<CommentsState> {
comment.copyWith(hidden: hidden),
];
emit(state.copyWith(comments: updatedComments));
final Map<int, Comment> updatedIdToCommentMap =
Map<int, Comment>.from(state.idToCommentMap);
updatedIdToCommentMap[comment.id] = comment;
if (state.fetchMode == FetchMode.eager) {
if (updatedComments.length >=
_pageSize + _pageSize * state.currentPage &&
updatedComments.length <=
_pageSize * 2 + _pageSize * state.currentPage) {
final bool isHidden = _collapseCache.isHidden(comment.id);
if (!isHidden) {
_streamSubscription?.pause();
emit(
state.copyWith(
status: CommentsStatus.loaded,
),
);
}
emit(
state.copyWith(
currentPage: state.currentPage + 1,
),
);
}
}
emit(
state.copyWith(
comments: updatedComments,
idToCommentMap: updatedIdToCommentMap,
),
);
}
}

View File

@ -1,24 +1,29 @@
part of 'comments_cubit.dart';
enum CommentsStatus {
init,
loading,
idle,
inProgress,
loaded,
allLoaded,
failure,
error,
}
class CommentsState extends Equatable {
const CommentsState({
required this.item,
required this.comments,
required this.matchedComments,
required this.idToCommentMap,
required this.status,
required this.fetchParentStatus,
required this.fetchRootStatus,
required this.order,
required this.fetchMode,
required this.onlyShowTargetComment,
required this.isOfflineReading,
required this.currentPage,
required this.inThreadSearchQuery,
required this.inThreadSearchAuthor,
});
CommentsState.init({
@ -27,36 +32,55 @@ class CommentsState extends Equatable {
required this.fetchMode,
required this.order,
}) : comments = <Comment>[],
status = CommentsStatus.init,
fetchParentStatus = CommentsStatus.init,
matchedComments = <int>[],
idToCommentMap = <int, Comment>{},
status = CommentsStatus.idle,
fetchParentStatus = CommentsStatus.idle,
fetchRootStatus = CommentsStatus.idle,
onlyShowTargetComment = false,
currentPage = 0;
currentPage = 0,
inThreadSearchQuery = '',
inThreadSearchAuthor = '';
final Item item;
final List<Comment> comments;
final Map<int, Comment> idToCommentMap;
final CommentsStatus status;
final CommentsStatus fetchParentStatus;
final CommentsStatus fetchRootStatus;
final CommentsOrder order;
final FetchMode fetchMode;
final bool onlyShowTargetComment;
final bool isOfflineReading;
final int currentPage;
final String inThreadSearchQuery;
final String inThreadSearchAuthor;
/// Indexes of comments that matches the query for in-thread search.
final List<int> matchedComments;
CommentsState copyWith({
Item? item,
List<Comment>? comments,
List<int>? matchedComments,
Map<int, Comment>? idToCommentMap,
CommentsStatus? status,
CommentsStatus? fetchParentStatus,
CommentsStatus? fetchRootStatus,
CommentsOrder? order,
FetchMode? fetchMode,
bool? onlyShowTargetComment,
bool? isOfflineReading,
int? currentPage,
String? inThreadSearchQuery,
String? inThreadSearchAuthor,
}) {
return CommentsState(
item: item ?? this.item,
comments: comments ?? this.comments,
matchedComments: matchedComments ?? this.matchedComments,
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
fetchRootStatus: fetchRootStatus ?? this.fetchRootStatus,
status: status ?? this.status,
order: order ?? this.order,
fetchMode: fetchMode ?? this.fetchMode,
@ -64,21 +88,56 @@ class CommentsState extends Equatable {
onlyShowTargetComment ?? this.onlyShowTargetComment,
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
currentPage: currentPage ?? this.currentPage,
inThreadSearchQuery: inThreadSearchQuery ?? this.inThreadSearchQuery,
inThreadSearchAuthor: inThreadSearchAuthor ?? this.inThreadSearchAuthor,
idToCommentMap: idToCommentMap ?? this.idToCommentMap,
);
}
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
static final Map<int, bool> _isResponseCache = <int, bool>{};
bool isResponse(Comment comment) {
if (_isResponseCache.containsKey(comment.id)) {
return _isResponseCache[comment.id]!;
}
if (comment.isRoot) {
_isResponseCache[comment.id] = false;
return false;
}
final Comment? precedingComment = idToCommentMap[comment.parent];
if (precedingComment == null) {
_isResponseCache[comment.id] = false;
return false;
} else if (item.id == precedingComment.parent && item.by == comment.by) {
_isResponseCache[comment.id] = true;
return true;
} else if (idToCommentMap[precedingComment.parent]?.by == comment.by) {
_isResponseCache[comment.id] = true;
return true;
} else {
_isResponseCache[comment.id] = false;
return false;
}
}
@override
List<Object?> get props => <Object?>[
item,
status,
fetchParentStatus,
fetchRootStatus,
order,
fetchMode,
onlyShowTargetComment,
isOfflineReading,
currentPage,
comments,
matchedComments,
inThreadSearchQuery,
inThreadSearchAuthor,
idToCommentMap,
];
}

View File

@ -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,12 +12,14 @@ 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;
final Debouncer _debouncer;
void reset() => emit(const EditState.init());
void onReplyTapped(Item item) {
emit(
EditState(
@ -35,14 +38,6 @@ class EditCubit extends HydratedCubit<EditState> {
);
}
void onReplyBoxClosed() {
emit(const EditState.init());
}
void onScrolled() {
emit(const EditState.init());
}
void onReplySubmittedSuccessfully() {
if (state.replyingTo != null) {
_draftCache.removeDraft(replyingTo: state.replyingTo!.id);
@ -64,9 +59,14 @@ class EditCubit extends HydratedCubit<EditState> {
}
}
void deleteDraft() => clear();
bool called = false;
void deleteDraft() {
// Remove draft in storage.
clear();
// Reset cached state.
_cachedState = const EditState.init();
// Reset to init state;
reset();
}
@override
EditState? fromJson(Map<String, dynamic> json) {
@ -95,6 +95,7 @@ class EditCubit extends HydratedCubit<EditState> {
Map<String, dynamic>? toJson(EditState state) {
EditState selected = state;
// Override previous draft only when current draft is not empty.
if (state.replyingTo == null ||
(state.replyingTo?.id != _cachedState.replyingTo?.id &&
state.text.isNullOrEmpty)) {

View File

@ -12,13 +12,13 @@ class FavCubit extends Cubit<FavState> {
required AuthBloc authBloc,
AuthRepository? authRepository,
PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository,
HackerNewsRepository? hackerNewsRepository,
}) : _authBloc = authBloc,
_authRepository = authRepository ?? locator.get<AuthRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
super(FavState.init()) {
init();
}
@ -26,7 +26,7 @@ class FavCubit extends Cubit<FavState> {
final AuthBloc _authBloc;
final AuthRepository _authRepository;
final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
static const int _pageSize = 20;
String? _username;
@ -43,7 +43,7 @@ class FavCubit extends Cubit<FavState> {
currentPage: 0,
),
);
_storiesRepository
_hackerNewsRepository
.fetchItemsStream(
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
)
@ -51,7 +51,7 @@ class FavCubit extends Cubit<FavState> {
.onDone(() {
emit(
state.copyWith(
status: FavStatus.loaded,
status: Status.success,
),
);
});
@ -73,7 +73,7 @@ class FavCubit extends Cubit<FavState> {
),
);
final Item? item = await _storiesRepository.fetchItem(id: id);
final Item? item = await _hackerNewsRepository.fetchItem(id: id);
if (item == null) return;
@ -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));
@ -119,7 +119,7 @@ class FavCubit extends Cubit<FavState> {
upper = len;
}
_storiesRepository
_hackerNewsRepository
.fetchItemsStream(
ids: state.favIds.sublist(
lower,
@ -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>[],
@ -149,13 +149,13 @@ class FavCubit extends Cubit<FavState> {
_preferenceRepository.favList(of: username).then((List<int> favIds) {
emit(state.copyWith(favIds: favIds));
_storiesRepository
_hackerNewsRepository
.fetchItemsStream(
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
)
.listen(_onItemLoaded)
.onDone(() {
emit(state.copyWith(status: FavStatus.loaded));
emit(state.copyWith(status: Status.success));
});
});
}

View File

@ -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(

View File

@ -10,16 +10,16 @@ part 'history_state.dart';
class HistoryCubit extends Cubit<HistoryState> {
HistoryCubit({
required AuthBloc authBloc,
StoriesRepository? storiesRepository,
HackerNewsRepository? hackerNewsRepository,
}) : _authBloc = authBloc,
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
super(HistoryState.init()) {
init();
}
final AuthBloc _authBloc;
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
static const int _pageSize = 20;
void init() {
@ -27,7 +27,7 @@ class HistoryCubit extends Cubit<HistoryState> {
if (authState.isLoggedIn) {
final String username = authState.username;
_storiesRepository
_hackerNewsRepository
.fetchSubmitted(userId: username)
.then((List<int>? submittedIds) {
emit(
@ -38,7 +38,7 @@ class HistoryCubit extends Cubit<HistoryState> {
),
);
if (submittedIds != null) {
_storiesRepository
_hackerNewsRepository
.fetchItemsStream(
ids: submittedIds.sublist(
0,
@ -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));
@ -66,7 +66,7 @@ class HistoryCubit extends Cubit<HistoryState> {
upper = len;
}
_storiesRepository
_hackerNewsRepository
.fetchItemsStream(
ids: state.submittedIds.sublist(
lower,
@ -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,19 +86,19 @@ 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>[],
),
);
_storiesRepository
_hackerNewsRepository
.fetchSubmitted(userId: username)
.then((List<int>? submittedIds) {
emit(state.copyWith(submittedIds: submittedIds));
if (submittedIds != null) {
_storiesRepository
_hackerNewsRepository
.fetchItemsStream(
ids: submittedIds.sublist(
0,
@ -107,7 +107,7 @@ class HistoryCubit extends Cubit<HistoryState> {
)
.listen(_onItemLoaded)
.onDone(() {
emit(state.copyWith(status: HistoryStatus.loaded));
emit(state.copyWith(status: Status.success));
});
}
});

View File

@ -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(

View File

@ -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';
@ -15,13 +16,13 @@ class NotificationCubit extends Cubit<NotificationState> {
NotificationCubit({
required AuthBloc authBloc,
required PreferenceCubit preferenceCubit,
StoriesRepository? storiesRepository,
HackerNewsRepository? hackerNewsRepository,
PreferenceRepository? preferenceRepository,
SembastRepository? sembastRepository,
}) : _authBloc = authBloc,
_preferenceCubit = preferenceCubit,
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_sembastRepository =
@ -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.
@ -53,7 +54,7 @@ class NotificationCubit extends Cubit<NotificationState> {
final AuthBloc _authBloc;
final PreferenceCubit _preferenceCubit;
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
final PreferenceRepository _preferenceRepository;
final SembastRepository _sembastRepository;
String? _username;
@ -81,7 +82,7 @@ class NotificationCubit extends Cubit<NotificationState> {
for (final int id in commentsToBeLoaded) {
Comment? comment = await _sembastRepository.getComment(id: id);
comment ??= await _storiesRepository.fetchComment(id: id);
comment ??= await _hackerNewsRepository.fetchComment(id: id);
if (comment != null) {
emit(
state.copyWith(
@ -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;
@ -159,7 +160,7 @@ class NotificationCubit extends Cubit<NotificationState> {
for (final int id in commentsToBeLoaded) {
Comment? comment = await _sembastRepository.getComment(id: id);
comment ??= await _storiesRepository.fetchComment(id: id);
comment ??= await _hackerNewsRepository.fetchComment(id: id);
if (comment != null) {
emit(state.copyWith(comments: <Comment>[...state.comments, comment]));
}
@ -168,7 +169,7 @@ class NotificationCubit extends Cubit<NotificationState> {
emit(
state.copyWith(
status: NotificationStatus.loaded,
status: Status.success,
currentPage: currentPage,
),
);
@ -183,7 +184,7 @@ class NotificationCubit extends Cubit<NotificationState> {
}
Future<void> _fetchReplies() {
return _storiesRepository
return _hackerNewsRepository
.fetchSubmitted(userId: _authBloc.state.username)
.then((List<int>? submittedItems) async {
if (submittedItems != null) {
@ -193,7 +194,9 @@ class NotificationCubit extends Cubit<NotificationState> {
);
for (final int id in subscribedItems) {
await _storiesRepository.fetchItem(id: id).then((Item? item) async {
await _hackerNewsRepository
.fetchItem(id: id)
.then((Item? item) async {
final List<int> kids = item?.kids ?? <int>[];
final List<int> previousKids =
(await _sembastRepository.kids(of: id)) ?? <int>[];
@ -215,7 +218,7 @@ class NotificationCubit extends Cubit<NotificationState> {
...state.unreadCommentsIds,
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
);
await _storiesRepository
await _hackerNewsRepository
.fetchComment(id: newCommentId)
.then((Comment? comment) {
if (comment != null && !comment.dead && !comment.deleted) {
@ -236,7 +239,7 @@ class NotificationCubit extends Cubit<NotificationState> {
}
}).whenComplete(
() => emit(
state.copyWith(status: NotificationStatus.loaded),
state.copyWith(status: Status.success),
),
);
}

View File

@ -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,

View File

@ -1,5 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
@ -9,28 +10,33 @@ part 'pin_state.dart';
class PinCubit extends Cubit<PinState> {
PinCubit({
PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository,
HackerNewsRepository? hackerNewsRepository,
}) : _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
super(PinState.init()) {
init();
}
final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
void init() {
emit(PinState.init());
_preferenceRepository.pinnedStoriesIds.then((List<int> ids) {
emit(state.copyWith(pinnedStoriesIds: ids));
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
});
_hackerNewsRepository
.fetchStoriesStream(ids: ids)
.listen(_onStoryFetched);
}).whenComplete(() => emit(state.copyWith(status: Status.success)));
}
void pinStory(Story story) {
void pinStory(
Story story, {
VoidCallback? onDone,
}) {
if (!state.pinnedStoriesIds.contains(story.id)) {
emit(
state.copyWith(
@ -39,10 +45,14 @@ class PinCubit extends Cubit<PinState> {
),
);
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
onDone?.call();
}
}
void unpinStory(Story story) {
void unpinStory(
Story story, {
VoidCallback? onDone,
}) {
emit(
state.copyWith(
pinnedStoriesIds: <int>[...state.pinnedStoriesIds]..remove(story.id),
@ -50,9 +60,13 @@ class PinCubit extends Cubit<PinState> {
),
);
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
onDone?.call();
}
void refresh() => init();
void refresh() {
if (state.status.isLoading) return;
init();
}
void _onStoryFetched(Story story) {
emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story]));

View File

@ -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,
];
}

View File

@ -11,13 +11,13 @@ part 'poll_state.dart';
class PollCubit extends Cubit<PollState> {
PollCubit({
required Story story,
StoriesRepository? storiesRepository,
HackerNewsRepository? hackerNewsRepository,
}) : _story = story,
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
super(PollState.init());
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
final Story _story;
Future<void> init({
@ -27,13 +27,13 @@ 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;
if (pollOptionsIds.isEmpty || refresh) {
final Story? updatedStory =
await _storiesRepository.fetchStory(id: _story.id);
await _hackerNewsRepository.fetchStory(id: _story.id);
if (updatedStory != null) {
pollOptionsIds = updatedStory.parts;
@ -42,12 +42,12 @@ 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;
}
if (pollOptionsIds.isNotEmpty) {
final List<PollOption> pollOptions = (await _storiesRepository
final List<PollOption> pollOptions = (await _hackerNewsRepository
.fetchPollOptionsStream(ids: pollOptionsIds)
.toSet())
.toList();
@ -72,7 +72,7 @@ class PollCubit extends Cubit<PollState> {
);
}
emit(state.copyWith(status: PollStatus.loaded));
emit(state.copyWith(status: Status.success));
}
}

View File

@ -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,

View File

@ -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,41 @@ 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));
}
@Deprecated('For debugging only')
Future<bool> getFakeResult() async {
final bool result = await Future<bool>.delayed(
const Duration(seconds: 2),
() => true,
);
return result;
}
}

View File

@ -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,
);

View File

@ -1,5 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
@ -41,6 +43,16 @@ class PreferenceCubit extends Cubit<PreferenceState> {
return null;
});
}
for (final DoublePreference p
in Preference.allPreferences.whereType<DoublePreference>()) {
initPreference<double>(p).then<double?>((double? value) {
final Preference<dynamic> updatedPreference = p.copyWith(val: value);
emit(state.copyWithPreference(updatedPreference));
return null;
});
}
}
Future<T?> initPreference<T>(Preference<T> preference) async {
@ -48,6 +60,10 @@ class PreferenceCubit extends Cubit<PreferenceState> {
case int:
final int? value = await _preferenceRepository.getInt(preference.key);
return value as T?;
case double:
final double? value =
await _preferenceRepository.getDouble(preference.key);
return value as T?;
case bool:
final bool? value = await _preferenceRepository.getBool(preference.key);
return value as T?;
@ -56,21 +72,27 @@ class PreferenceCubit extends Cubit<PreferenceState> {
}
}
void update<T>(Preference<T> preference, {required T to}) {
final T value = to;
final Preference<T> updatedPreference = preference.copyWith(val: value);
void update<T>(Preference<T> preference) {
_logger.i('updating $preference to ${preference.val}');
_logger.i('updating $preference to $value');
emit(state.copyWithPreference(updatedPreference));
emit(state.copyWithPreference(preference));
switch (T) {
case int:
_preferenceRepository.setInt(preference.key, value as int);
break;
_preferenceRepository.setInt(
preference.key,
preference.val as int,
);
case double:
_preferenceRepository.setDouble(
preference.key,
preference.val as double,
);
case bool:
_preferenceRepository.setBool(preference.key, value as bool);
break;
_preferenceRepository.setBool(
preference.key,
preference.val as bool,
);
default:
throw UnimplementedError();
}

View File

@ -52,12 +52,8 @@ class PreferenceState extends Equatable {
bool get complexStoryTileEnabled => _isOn<DisplayModePreference>();
bool get webFirstEnabled => _isOn<NavigationModePreference>();
bool get eyeCandyEnabled => _isOn<EyeCandyModePreference>();
bool get trueDarkEnabled => _isOn<TrueDarkModePreference>();
bool get readerEnabled => _isOn<ReaderModePreference>();
bool get markReadStoriesEnabled => _isOn<MarkReadStoriesModePreference>();
@ -70,6 +66,27 @@ class PreferenceState extends Equatable {
bool get swipeGestureEnabled => _isOn<SwipeGesturePreference>();
bool get autoScrollEnabled => _isOn<AutoScrollModePreference>();
bool get customTabEnabled => _isOn<CustomTabPreference>();
bool get material3Enabled => _isOn<Material3Preference>();
bool get manualPaginationEnabled => _isOn<ManualPaginationPreference>();
bool get trueDarkModeEnabled => _isOn<TrueDarkModePreference>();
bool get hapticFeedbackEnabled => _isOn<HapticFeedbackPreference>();
double get textScaleFactor =>
preferences.singleWhereType<TextScaleFactorPreference>().val;
MaterialColor get appColor {
return materialColors.elementAt(
preferences.singleWhereType<AppColorPreference>().val,
) as MaterialColor;
}
List<StoryType> get tabs {
final String result =
preferences.singleWhereType<TabOrderPreference>().val.toString();
@ -87,6 +104,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);

View File

@ -102,6 +102,18 @@ class SearchCubit extends Cubit<SearchState> {
search(state.params.query);
}
void onExactMatchToggled() {
emit(
state.copyWith(
params: state.params.copyWith(
exactMatch: !state.params.exactMatch,
),
),
);
search(state.params.query);
}
void onDateTimeRangeUpdated(DateTime start, DateTime end) {
final DateTime updatedStart = start.copyWith(
second: 0,

View File

@ -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;
}
}

View File

@ -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));
});
}
}

View File

@ -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,

View File

@ -38,8 +38,7 @@ class TabCubit extends Cubit<TabState> {
// Check to make sure there's no duplicate.
if (updatedTabs.toSet().length == StoryType.values.length) {
_preferenceCubit.update<int>(
TabOrderPreference(),
to: StoryType.convertToSettingsValue(updatedTabs),
TabOrderPreference(val: StoryType.convertToSettingsValue(updatedTabs)),
);
}
}

View File

@ -7,19 +7,24 @@ import 'package:hacki/repositories/repositories.dart';
part 'user_state.dart';
class UserCubit extends Cubit<UserState> {
UserCubit({StoriesRepository? storiesRepository})
: _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
UserCubit({HackerNewsRepository? hackerNewsRepository})
: _hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
super(const UserState.init());
final StoriesRepository _storiesRepository;
final HackerNewsRepository _hackerNewsRepository;
void init({required String userId}) {
emit(state.copyWith(status: UserStatus.loading));
_storiesRepository.fetchUser(id: userId).then((User user) {
emit(state.copyWith(user: user, status: UserStatus.loaded));
emit(state.copyWith(status: Status.inProgress));
_hackerNewsRepository.fetchUser(id: userId).then((User? user) {
emit(
state.copyWith(
user: user ?? User.emptyWithId(userId),
status: Status.success,
),
);
}).onError((_, __) {
emit(state.copyWith(status: UserStatus.failure));
emit(state.copyWith(status: Status.failure));
return;
});
}

View File

@ -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,

View File

@ -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,

View File

@ -3,7 +3,6 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/styles/styles.dart';
extension ContextExtension on BuildContext {
T? tryRead<T>() {
@ -21,16 +20,20 @@ extension ContextExtension on BuildContext {
}) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
backgroundColor: Palette.deepOrange,
content: Text(content),
backgroundColor: Theme.of(this).primaryColor,
content: Text(
content,
style: TextStyle(
color: Theme.of(this).colorScheme.onPrimary,
),
),
action: action != null && label != null
? SnackBarAction(
label: label,
onPressed: action,
textColor: Theme.of(this).textTheme.bodyLarge?.color,
textColor: Theme.of(this).colorScheme.onPrimary,
)
: null,
behavior: SnackBarBehavior.floating,
),
);
}
@ -82,4 +85,8 @@ extension ContextExtension on BuildContext {
int get storyTileMaxLines {
return _storyTileMaxLines;
}
double get topPadding {
return MediaQuery.of(this).padding.top + kToolbarHeight;
}
}

Some files were not shown because too many files have changed in this diff Show More