Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
268f4054a3 | |||
988c5d9881 | |||
e748e2f818 | |||
1b0a0dbda9 | |||
64d68389ba | |||
381c99b353 | |||
39ee3137f8 | |||
0d76be8634 | |||
9986f72e11 | |||
ef557e7b84 | |||
ec065c0122 | |||
2960c6e59e | |||
92dac6b932 | |||
20365393a3 | |||
8d238744c7 | |||
e33ff417fb | |||
d8922c2641 | |||
c6e0461857 | |||
30ca356dc8 | |||
7d11398e6d | |||
a4f52284ef | |||
c7d1a42d5a | |||
f83fd66bcc | |||
c2ec3647e2 | |||
ba63852b7d | |||
438041183c | |||
114540edd7 | |||
588b3e9508 | |||
2f0376f8f8 |
3
.github/workflows/commit_check.yml
vendored
@ -9,13 +9,14 @@ on:
|
||||
jobs:
|
||||
releases:
|
||||
name: Check commit
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: checkout all the submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
|
1
.github/workflows/publish_ios.yml
vendored
@ -23,6 +23,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
|
@ -29,6 +29,7 @@ Features:
|
||||
- Download stories and comments for offline reading.
|
||||
- Pick up where you left off.
|
||||
- Synced favorites and pins across devices. (iOS only)
|
||||
- Export or import your favorites.
|
||||
- Launch from system share sheet.
|
||||
- And more...
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
include: package:very_good_analysis/analysis_options.3.1.0.yaml
|
||||
include: package:very_good_analysis/analysis_options.5.0.0.yaml
|
||||
linter:
|
||||
rules:
|
||||
parameter_assignments: false
|
||||
|
@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 940 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.4 KiB |
@ -24,6 +24,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
BIN
assets/fonts/noto_serif/NotoSerif-Bold.ttf
Normal file
BIN
assets/fonts/noto_serif/NotoSerif-Regular.ttf
Normal file
BIN
assets/hacki-github.xcf
Normal file
BIN
assets/hacki.xcf
Normal file
BIN
assets/screenshots/hacki-1.png
Normal file
After Width: | Height: | Size: 890 KiB |
BIN
assets/screenshots/hacki-2.png
Normal file
After Width: | Height: | Size: 873 KiB |
BIN
assets/screenshots/hacki-3.png
Normal file
After Width: | Height: | Size: 770 KiB |
BIN
assets/screenshots/hacki-4.png
Normal file
After Width: | Height: | Size: 517 KiB |
30
components/in_app_review/.gitignore
vendored
Normal 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/
|
39
components/in_app_review/.metadata
Normal 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'
|
102
components/in_app_review/CHANGELOG.md
Normal 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
|
21
components/in_app_review/LICENSE
Normal 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.
|
0
components/in_app_review/analysis_options.yaml
Normal file
9
components/in_app_review/android/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
49
components/in_app_review/android/build.gradle
Normal 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 {
|
||||
|
||||
}
|
||||
}
|
1
components/in_app_review/android/settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
rootProject.name = 'in_app_review'
|
@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="dev.britannio.in_app_review">
|
||||
</manifest>
|
@ -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
@ -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
|
0
components/in_app_review/ios/Assets/.gitkeep
Normal file
4
components/in_app_review/ios/Classes/InAppReviewPlugin.h
Normal file
@ -0,0 +1,4 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface InAppReviewPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
107
components/in_app_review/ios/Classes/InAppReviewPlugin.m
Normal 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
|
23
components/in_app_review/ios/in_app_review.podspec
Normal 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
|
50
components/in_app_review/lib/in_app_review.dart
Normal 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,
|
||||
);
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
22
components/in_app_review/macos/in_app_review.podspec
Normal 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
|
46
components/in_app_review/pubspec.yaml
Normal 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
|
12
components/in_app_review_platform_interface/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
.dart_tool/
|
||||
|
||||
.packages
|
||||
.pub/
|
||||
|
||||
build/
|
||||
|
||||
|
||||
pubspec.lock
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
46
components/in_app_review_platform_interface/CHANGELOG.md
Normal 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.
|
21
components/in_app_review_platform_interface/LICENSE
Normal 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.
|
26
components/in_app_review_platform_interface/README.md
Normal 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
|
@ -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.');
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
24
components/in_app_review_platform_interface/pubspec.yaml
Normal 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
|
||||
|
1
fastlane/metadata/android/en-US/changelogs/108.txt
Normal file
@ -0,0 +1 @@
|
||||
- Navigation shortcuts.
|
1
fastlane/metadata/android/en-US/changelogs/121.txt
Normal file
@ -0,0 +1 @@
|
||||
- Ability to mark a story as read once scrolling past.
|
@ -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,8 +137,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
@ -133,19 +146,22 @@ SPEC CHECKSUMS:
|
||||
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: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||
|
@ -10,6 +10,7 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
@ -22,7 +23,6 @@
|
||||
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
|
||||
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
|
||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -68,14 +68,14 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -83,8 +83,8 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
E51D52AD283B464E00FC8DD8 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E51D52AF283B464E00FC8DD8 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
E51D52B2283B464E00FC8DD8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
@ -107,7 +107,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
|
||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */,
|
||||
7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -183,8 +183,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
|
||||
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */,
|
||||
E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */,
|
||||
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -192,9 +192,9 @@
|
||||
D79CD63C88FF49EF451AFDDF /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */,
|
||||
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */,
|
||||
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */,
|
||||
0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */,
|
||||
D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */,
|
||||
B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@ -229,15 +229,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */,
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */,
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -291,7 +291,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1330;
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@ -365,6 +365,7 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
@ -373,7 +374,22 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */ = {
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@ -395,7 +411,7 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */ = {
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@ -412,21 +428,6 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -565,11 +566,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||
@ -583,7 +582,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@ -707,11 +705,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||
@ -725,7 +721,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -780,11 +775,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -802,7 +795,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -863,11 +855,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -884,7 +874,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -905,11 +894,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -927,7 +914,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -992,11 +978,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -1013,7 +997,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -76,5 +76,11 @@
|
||||
<false/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
<key>FLTEnableWideGamut</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -52,14 +52,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
state.copyWith(
|
||||
isLoggedIn: true,
|
||||
user: user,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoggedIn: false,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -81,7 +81,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
}
|
||||
|
||||
Future<void> onLogin(AuthLogin event, Emitter<AuthState> emit) async {
|
||||
emit(state.copyWith(status: AuthStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
final bool successful = await _authRepository.login(
|
||||
username: event.username,
|
||||
@ -94,11 +94,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
state.copyWith(
|
||||
user: user ?? User.emptyWithId(event.username),
|
||||
isLoggedIn: true,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(status: AuthStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,5 @@
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
enum AuthStatus {
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class AuthState extends Equatable {
|
||||
const AuthState({
|
||||
required this.user,
|
||||
@ -17,13 +11,13 @@ class AuthState extends Equatable {
|
||||
const AuthState.init()
|
||||
: user = const User.empty(),
|
||||
isLoggedIn = false,
|
||||
status = AuthStatus.loaded,
|
||||
status = Status.success,
|
||||
agreedToEULA = false;
|
||||
|
||||
final User user;
|
||||
final bool isLoggedIn;
|
||||
final bool agreedToEULA;
|
||||
final AuthStatus status;
|
||||
final Status status;
|
||||
|
||||
String get username => user.id;
|
||||
|
||||
@ -31,7 +25,7 @@ class AuthState extends Equatable {
|
||||
User? user,
|
||||
bool? isLoggedIn,
|
||||
bool? agreedToEULA,
|
||||
AuthStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return AuthState(
|
||||
user: user ?? this.user,
|
||||
|
@ -79,7 +79,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
const StoriesState.init().copyWith(
|
||||
isOfflineReading: hasCachedStories &&
|
||||
// Only go into offline mode in the next session.
|
||||
state.downloadStatus == StoriesDownloadStatus.initial,
|
||||
state.downloadStatus == StoriesDownloadStatus.idle,
|
||||
currentPageSize: pageSize,
|
||||
downloadStatus: state.downloadStatus,
|
||||
storiesDownloaded: state.storiesDownloaded,
|
||||
@ -133,10 +133,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesRefresh event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
if (state.statusByType[event.type] == Status.inProgress) return;
|
||||
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
to: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -144,7 +146,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
to: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@ -157,7 +159,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
to: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -216,7 +218,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
to: Status.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -243,7 +245,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded),
|
||||
state.copyWithStatusUpdated(type: event.type, to: Status.success),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,7 @@
|
||||
part of 'stories_bloc.dart';
|
||||
|
||||
enum StoriesStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum StoriesDownloadStatus {
|
||||
initial,
|
||||
idle,
|
||||
downloading,
|
||||
finished,
|
||||
failure,
|
||||
@ -43,12 +37,12 @@ class StoriesState extends Equatable {
|
||||
StoryType.ask: <int>[],
|
||||
StoryType.show: <int>[],
|
||||
},
|
||||
this.statusByType = const <StoryType, StoriesStatus>{
|
||||
StoryType.top: StoriesStatus.initial,
|
||||
StoryType.best: StoriesStatus.initial,
|
||||
StoryType.latest: StoriesStatus.initial,
|
||||
StoryType.ask: StoriesStatus.initial,
|
||||
StoryType.show: StoriesStatus.initial,
|
||||
this.statusByType = const <StoryType, Status>{
|
||||
StoryType.top: Status.idle,
|
||||
StoryType.best: Status.idle,
|
||||
StoryType.latest: Status.idle,
|
||||
StoryType.ask: Status.idle,
|
||||
StoryType.show: Status.idle,
|
||||
},
|
||||
this.currentPageByType = const <StoryType, int>{
|
||||
StoryType.top: 0,
|
||||
@ -58,7 +52,7 @@ class StoriesState extends Equatable {
|
||||
StoryType.show: 0,
|
||||
},
|
||||
}) : isOfflineReading = false,
|
||||
downloadStatus = StoriesDownloadStatus.initial,
|
||||
downloadStatus = StoriesDownloadStatus.idle,
|
||||
currentPageSize = 0,
|
||||
readStoriesIds = const <int>{},
|
||||
storiesDownloaded = 0,
|
||||
@ -66,7 +60,7 @@ class StoriesState extends Equatable {
|
||||
|
||||
final Map<StoryType, List<Story>> storiesByType;
|
||||
final Map<StoryType, List<int>> storyIdsByType;
|
||||
final Map<StoryType, StoriesStatus> statusByType;
|
||||
final Map<StoryType, Status> statusByType;
|
||||
final Map<StoryType, int> currentPageByType;
|
||||
final Set<int> readStoriesIds;
|
||||
final StoriesDownloadStatus downloadStatus;
|
||||
@ -78,7 +72,7 @@ class StoriesState extends Equatable {
|
||||
StoriesState copyWith({
|
||||
Map<StoryType, List<Story>>? storiesByType,
|
||||
Map<StoryType, List<int>>? storyIdsByType,
|
||||
Map<StoryType, StoriesStatus>? statusByType,
|
||||
Map<StoryType, Status>? statusByType,
|
||||
Map<StoryType, int>? currentPageByType,
|
||||
Set<int>? readStoriesIds,
|
||||
StoriesDownloadStatus? downloadStatus,
|
||||
@ -133,10 +127,10 @@ class StoriesState extends Equatable {
|
||||
|
||||
StoriesState copyWithStatusUpdated({
|
||||
required StoryType type,
|
||||
required StoriesStatus to,
|
||||
required Status to,
|
||||
}) {
|
||||
final Map<StoryType, StoriesStatus> newMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
final Map<StoryType, Status> newMap =
|
||||
Map<StoryType, Status>.from(statusByType);
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
statusByType: newMap,
|
||||
@ -162,9 +156,9 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, List<int>> newStoryIdsMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newStoryIdsMap[type] = <int>[];
|
||||
final Map<StoryType, StoriesStatus> newStatusMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newStatusMap[type] = StoriesStatus.loading;
|
||||
final Map<StoryType, Status> newStatusMap =
|
||||
Map<StoryType, Status>.from(statusByType);
|
||||
newStatusMap[type] = Status.inProgress;
|
||||
final Map<StoryType, int> newCurrentPageMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newCurrentPageMap[type] = 0;
|
||||
|
@ -39,6 +39,9 @@ abstract class Constants {
|
||||
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
|
||||
static const String featureLogIn = 'log_in';
|
||||
static const String featurePinToTop = 'pin_to_top';
|
||||
static const String featureJumpUpButton = 'jump_up_button_with_long_press';
|
||||
static const String featureJumpDownButton =
|
||||
'jump_down_button_with_long_press';
|
||||
|
||||
static final String happyFace = <String>[
|
||||
'(๑•̀ㅂ•́)و✧',
|
||||
@ -76,3 +79,15 @@ abstract class RegExpConstants {
|
||||
static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
|
||||
static const String number = '[0-9]+';
|
||||
}
|
||||
|
||||
abstract class Durations {
|
||||
static const Duration ms100 = Duration(milliseconds: 100);
|
||||
static const Duration ms200 = Duration(milliseconds: 200);
|
||||
static const Duration ms300 = Duration(milliseconds: 300);
|
||||
static const Duration ms400 = Duration(milliseconds: 400);
|
||||
static const Duration ms500 = Duration(milliseconds: 500);
|
||||
static const Duration ms600 = Duration(milliseconds: 600);
|
||||
static const Duration oneSecond = Duration(seconds: 1);
|
||||
static const Duration twoSeconds = Duration(seconds: 2);
|
||||
static const Duration tenSeconds = Duration(seconds: 10);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import 'package:logger/logger.dart';
|
||||
|
||||
class CustomLogFilter extends LogFilter {
|
||||
@override
|
||||
Level? get level => Level.verbose;
|
||||
Level? get level => Level.trace;
|
||||
|
||||
/// The minimal level allowed in production.
|
||||
static const Level _minimalLevel = Level.info;
|
||||
|
@ -1,49 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
|
||||
/// Custom router.
|
||||
///
|
||||
/// Handle named routing.
|
||||
class CustomRouter {
|
||||
/// Top level routing.
|
||||
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case HomeScreen.routeName:
|
||||
return HomeScreen.route();
|
||||
case ItemScreen.routeName:
|
||||
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
return _errorRoute();
|
||||
}
|
||||
}
|
||||
|
||||
/// Nested routing for bottom navigation bar.
|
||||
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case ItemScreen.routeName:
|
||||
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
return _errorRoute();
|
||||
}
|
||||
}
|
||||
|
||||
/// Error route.
|
||||
static Route<dynamic> _errorRoute() {
|
||||
return MaterialPageRoute<dynamic>(
|
||||
settings: const RouteSettings(name: '/error'),
|
||||
builder: (_) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Error'),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(Constants.errorMessage),
|
||||
final GoRouter router = GoRouter(
|
||||
observers: <NavigatorObserver>[
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
],
|
||||
initialLocation: HomeScreen.routeName,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: HomeScreen.routeName,
|
||||
builder: (_, __) => const HomeScreen(),
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: ItemScreen.routeName,
|
||||
builder: (_, GoRouterState state) {
|
||||
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
|
||||
if (args == null) {
|
||||
throw GoError("args can't be null");
|
||||
}
|
||||
return ItemScreen.phone(args);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${ItemScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
|
||||
if (args == null) {
|
||||
throw GoError("args can't be null");
|
||||
}
|
||||
return ItemScreen.phone(args);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${SubmitScreen.routeName}',
|
||||
builder: (_, __) => BlocProvider<SubmitCubit>(
|
||||
create: (_) => SubmitCubit(),
|
||||
child: const SubmitScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${QrCodeScannerScreen.routeName}',
|
||||
builder: (_, __) => const QrCodeScannerScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${QrCodeViewScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? data = state.extra as String?;
|
||||
if (data == null) {
|
||||
throw GoError("data can't be null");
|
||||
}
|
||||
return QrCodeViewScreen(
|
||||
data: data,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/${WebViewScreen.routeName}',
|
||||
builder: (_, GoRouterState state) {
|
||||
final String? link = state.extra as String?;
|
||||
if (link == null) {
|
||||
throw GoError("link can't be null");
|
||||
}
|
||||
return WebViewScreen(
|
||||
url: link,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ class CustomFileOutput extends LogOutput {
|
||||
IOSink? _sink;
|
||||
|
||||
@override
|
||||
void init() {
|
||||
Future<void> init() async {
|
||||
_sink = file.openWrite(
|
||||
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
|
||||
encoding: encoding,
|
||||
|
@ -32,7 +32,8 @@ Future<void> setUpLocator() async {
|
||||
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||
..registerSingleton<DraftCache>(DraftCache())
|
||||
..registerSingleton<CommentCache>(CommentCache())
|
||||
..registerSingleton<LocalNotification>(LocalNotification())
|
||||
..registerSingleton<LocalNotificationService>(LocalNotificationService())
|
||||
..registerSingleton(AppReviewService())
|
||||
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
);
|
||||
|
@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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 +10,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() {
|
||||
@ -47,16 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (_commentsCubit == null) 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(
|
||||
|
@ -1,21 +1,23 @@
|
||||
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/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,15 +25,15 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
CommentsCubit({
|
||||
required FilterCubit filterCubit,
|
||||
required CollapseCache collapseCache,
|
||||
required bool isOfflineReading,
|
||||
required Item item,
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
CommentCache? commentCache,
|
||||
OfflineRepository? offlineRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
Logger? logger,
|
||||
required bool isOfflineReading,
|
||||
required Item item,
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
}) : _filterCubit = filterCubit,
|
||||
_collapseCache = collapseCache,
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
@ -68,8 +70,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) {
|
||||
@ -106,7 +106,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
status: CommentsStatus.inProgress,
|
||||
comments: <Comment>[],
|
||||
currentPage: 0,
|
||||
),
|
||||
@ -117,7 +117,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
? item
|
||||
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
|
||||
item;
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
final List<int> kids = _sortKids(updatedItem.kids);
|
||||
|
||||
emit(state.copyWith(item: updatedItem));
|
||||
|
||||
@ -132,13 +132,11 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
);
|
||||
break;
|
||||
case FetchMode.eager:
|
||||
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,7 +150,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
Future<void> refresh() async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
status: CommentsStatus.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -183,7 +181,7 @@ 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);
|
||||
final List<int> kids = _sortKids(updatedItem.kids);
|
||||
|
||||
late final Stream<Comment> commentStream;
|
||||
if (state.fetchMode == FetchMode.lazy) {
|
||||
@ -210,7 +208,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
void loadAll(Story story) {
|
||||
HapticFeedback.lightImpact();
|
||||
HapticFeedbackUtil.light();
|
||||
emit(
|
||||
state.copyWith(
|
||||
onlyShowTargetComment: false,
|
||||
@ -221,8 +219,12 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
/// [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:
|
||||
@ -265,29 +267,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 _storiesRepository.fetchItem(id: state.item.parent);
|
||||
|
||||
if (parent == null) {
|
||||
return;
|
||||
} else {
|
||||
await HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: ItemScreenArgs(item: parent),
|
||||
await router.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
extra: ItemScreenArgs(item: parent),
|
||||
);
|
||||
|
||||
emit(
|
||||
@ -298,10 +299,33 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadRootThread() async {
|
||||
HapticFeedbackUtil.light();
|
||||
emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress));
|
||||
final Story? parent = await _storiesRepository
|
||||
.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 onOrderChanged(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();
|
||||
@ -315,7 +339,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
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 +349,84 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
init(useCommentCache: true);
|
||||
}
|
||||
|
||||
List<int> sortKids(List<int> kids) {
|
||||
/// Scroll to next root level comment.
|
||||
void scrollToNextRoot(
|
||||
ItemScrollController itemScrollController,
|
||||
ItemPositionsListener itemPositionsListener,
|
||||
) {
|
||||
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.7)
|
||||
.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 last comment visible on screen.
|
||||
final int lastVisibleIndex = state.comments.indexOf(onScreenComments.last);
|
||||
final int startIndex = min(lastVisibleIndex + 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll to previous root level comment.
|
||||
void scrollToPreviousRoot(
|
||||
ItemScrollController itemScrollController,
|
||||
ItemPositionsListener itemPositionsListener,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<int> _sortKids(List<int> kids) {
|
||||
switch (state.order) {
|
||||
case CommentsOrder.natural:
|
||||
return kids;
|
||||
@ -361,31 +462,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
];
|
||||
|
||||
emit(state.copyWith(comments: updatedComments));
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
part of 'comments_cubit.dart';
|
||||
|
||||
enum CommentsStatus {
|
||||
init,
|
||||
loading,
|
||||
idle,
|
||||
inProgress,
|
||||
loaded,
|
||||
allLoaded,
|
||||
failure,
|
||||
error,
|
||||
}
|
||||
|
||||
class CommentsState extends Equatable {
|
||||
@ -14,6 +14,7 @@ class CommentsState extends Equatable {
|
||||
required this.comments,
|
||||
required this.status,
|
||||
required this.fetchParentStatus,
|
||||
required this.fetchRootStatus,
|
||||
required this.order,
|
||||
required this.fetchMode,
|
||||
required this.onlyShowTargetComment,
|
||||
@ -27,8 +28,9 @@ class CommentsState extends Equatable {
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
}) : comments = <Comment>[],
|
||||
status = CommentsStatus.init,
|
||||
fetchParentStatus = CommentsStatus.init,
|
||||
status = CommentsStatus.idle,
|
||||
fetchParentStatus = CommentsStatus.idle,
|
||||
fetchRootStatus = CommentsStatus.idle,
|
||||
onlyShowTargetComment = false,
|
||||
currentPage = 0;
|
||||
|
||||
@ -36,6 +38,7 @@ class CommentsState extends Equatable {
|
||||
final List<Comment> comments;
|
||||
final CommentsStatus status;
|
||||
final CommentsStatus fetchParentStatus;
|
||||
final CommentsStatus fetchRootStatus;
|
||||
final CommentsOrder order;
|
||||
final FetchMode fetchMode;
|
||||
final bool onlyShowTargetComment;
|
||||
@ -47,6 +50,7 @@ class CommentsState extends Equatable {
|
||||
List<Comment>? comments,
|
||||
CommentsStatus? status,
|
||||
CommentsStatus? fetchParentStatus,
|
||||
CommentsStatus? fetchRootStatus,
|
||||
CommentsOrder? order,
|
||||
FetchMode? fetchMode,
|
||||
bool? onlyShowTargetComment,
|
||||
@ -57,6 +61,7 @@ class CommentsState extends Equatable {
|
||||
item: item ?? this.item,
|
||||
comments: comments ?? this.comments,
|
||||
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
||||
fetchRootStatus: fetchRootStatus ?? this.fetchRootStatus,
|
||||
status: status ?? this.status,
|
||||
order: order ?? this.order,
|
||||
fetchMode: fetchMode ?? this.fetchMode,
|
||||
@ -74,6 +79,7 @@ class CommentsState extends Equatable {
|
||||
item,
|
||||
status,
|
||||
fetchParentStatus,
|
||||
fetchRootStatus,
|
||||
order,
|
||||
fetchMode,
|
||||
onlyShowTargetComment,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -11,7 +12,7 @@ part 'edit_state.dart';
|
||||
class EditCubit extends HydratedCubit<EditState> {
|
||||
EditCubit({DraftCache? draftCache})
|
||||
: _draftCache = draftCache ?? locator.get<DraftCache>(),
|
||||
_debouncer = Debouncer(delay: const Duration(seconds: 1)),
|
||||
_debouncer = Debouncer(delay: Durations.oneSecond),
|
||||
super(const EditState.init());
|
||||
|
||||
final DraftCache _draftCache;
|
||||
|
@ -51,7 +51,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
.onDone(() {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FavStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -107,7 +107,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
}
|
||||
|
||||
void loadMore() {
|
||||
emit(state.copyWith(status: FavStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final int currentPage = state.currentPage;
|
||||
final int len = state.favIds.length;
|
||||
emit(state.copyWith(currentPage: currentPage + 1));
|
||||
@ -128,10 +128,10 @@ class FavCubit extends Cubit<FavState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
} else {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FavStatus.loading,
|
||||
status: Status.inProgress,
|
||||
currentPage: 0,
|
||||
favItems: <Item>[],
|
||||
favIds: <int>[],
|
||||
@ -155,7 +155,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'fav_cubit.dart';
|
||||
|
||||
enum FavStatus {
|
||||
init,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class FavState extends Equatable {
|
||||
const FavState({
|
||||
required this.favIds,
|
||||
@ -18,18 +11,18 @@ class FavState extends Equatable {
|
||||
FavState.init()
|
||||
: favIds = <int>[],
|
||||
favItems = <Item>[],
|
||||
status = FavStatus.init,
|
||||
status = Status.idle,
|
||||
currentPage = 0;
|
||||
|
||||
final List<int> favIds;
|
||||
final List<Item> favItems;
|
||||
final FavStatus status;
|
||||
final Status status;
|
||||
final int currentPage;
|
||||
|
||||
FavState copyWith({
|
||||
List<int>? favIds,
|
||||
List<Item>? favItems,
|
||||
FavStatus? status,
|
||||
Status? status,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return FavState(
|
||||
|
@ -54,7 +54,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
}
|
||||
|
||||
void loadMore() {
|
||||
emit(state.copyWith(status: HistoryStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final int currentPage = state.currentPage;
|
||||
final int len = state.submittedIds.length;
|
||||
emit(state.copyWith(currentPage: currentPage + 1));
|
||||
@ -75,10 +75,10 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: HistoryStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
} else {
|
||||
emit(state.copyWith(status: HistoryStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
final String username = _authBloc.state.username;
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: HistoryStatus.loading,
|
||||
status: Status.inProgress,
|
||||
currentPage: 0,
|
||||
submittedIds: <int>[],
|
||||
submittedItems: <Item>[],
|
||||
@ -107,7 +107,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: HistoryStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'history_cubit.dart';
|
||||
|
||||
enum HistoryStatus {
|
||||
init,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class HistoryState extends Equatable {
|
||||
const HistoryState({
|
||||
required this.submittedIds,
|
||||
@ -18,18 +11,18 @@ class HistoryState extends Equatable {
|
||||
HistoryState.init()
|
||||
: submittedIds = <int>[],
|
||||
submittedItems = <Item>[],
|
||||
status = HistoryStatus.init,
|
||||
status = Status.idle,
|
||||
currentPage = 0;
|
||||
|
||||
final List<int> submittedIds;
|
||||
final List<Item> submittedItems;
|
||||
final HistoryStatus status;
|
||||
final Status status;
|
||||
final int currentPage;
|
||||
|
||||
HistoryState copyWith({
|
||||
List<int>? submittedIds,
|
||||
List<Item>? submittedItems,
|
||||
HistoryStatus? status,
|
||||
Status? status,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return HistoryState(
|
||||
|
@ -4,6 +4,7 @@ import 'dart:math';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -31,7 +32,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
if (authState.isLoggedIn && authState.username != _username) {
|
||||
// Get the user setting.
|
||||
if (_preferenceCubit.state.notificationEnabled) {
|
||||
Future<void>.delayed(const Duration(seconds: 2), init);
|
||||
Future<void>.delayed(Durations.twoSeconds, init);
|
||||
}
|
||||
|
||||
// Listen for setting changes in the future.
|
||||
@ -99,7 +100,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
void markAsRead(int id) {
|
||||
Future.doWhile(() {
|
||||
if (state.status != NotificationStatus.loading) {
|
||||
if (state.status != Status.inProgress) {
|
||||
if (state.unreadCommentsIds.contains(id)) {
|
||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||
..remove(id);
|
||||
@ -115,7 +116,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
void markAllAsRead() {
|
||||
Future.doWhile(() {
|
||||
if (state.status != NotificationStatus.loading) {
|
||||
if (state.status != Status.inProgress) {
|
||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||
return false;
|
||||
@ -130,7 +131,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
_preferenceCubit.state.notificationEnabled) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: NotificationStatus.loading,
|
||||
status: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -140,14 +141,14 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: NotificationStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
emit(state.copyWith(status: NotificationStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
final int currentPage = state.currentPage + 1;
|
||||
final int lower = currentPage * _pageSize + state.offset;
|
||||
@ -168,7 +169,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: NotificationStatus.loaded,
|
||||
status: Status.success,
|
||||
currentPage: currentPage,
|
||||
),
|
||||
);
|
||||
@ -236,7 +237,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
}
|
||||
}).whenComplete(
|
||||
() => emit(
|
||||
state.copyWith(status: NotificationStatus.loaded),
|
||||
state.copyWith(status: Status.success),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'notification_cubit.dart';
|
||||
|
||||
enum NotificationStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class NotificationState extends Equatable {
|
||||
const NotificationState({
|
||||
required this.comments,
|
||||
@ -23,14 +16,14 @@ class NotificationState extends Equatable {
|
||||
allCommentsIds = <int>[],
|
||||
currentPage = 0,
|
||||
offset = 0,
|
||||
status = NotificationStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final List<Comment> comments;
|
||||
final List<int> allCommentsIds;
|
||||
final List<int> unreadCommentsIds;
|
||||
final int currentPage;
|
||||
final int offset;
|
||||
final NotificationStatus status;
|
||||
final Status status;
|
||||
|
||||
NotificationState copyWith({
|
||||
List<Comment>? comments,
|
||||
@ -38,7 +31,7 @@ class NotificationState extends Equatable {
|
||||
List<int>? unreadCommentsIds,
|
||||
int? currentPage,
|
||||
int? offset,
|
||||
NotificationStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return NotificationState(
|
||||
comments: comments ?? this.comments,
|
||||
|
@ -27,7 +27,7 @@ class PinCubit extends Cubit<PinState> {
|
||||
emit(state.copyWith(pinnedStoriesIds: ids));
|
||||
|
||||
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
|
||||
});
|
||||
}).whenComplete(() => emit(state.copyWith(status: Status.success)));
|
||||
}
|
||||
|
||||
void pinStory(Story story) {
|
||||
@ -52,7 +52,10 @@ class PinCubit extends Cubit<PinState> {
|
||||
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
||||
}
|
||||
|
||||
void refresh() => init();
|
||||
void refresh() {
|
||||
if (state.status.isLoading) return;
|
||||
init();
|
||||
}
|
||||
|
||||
void _onStoryFetched(Story story) {
|
||||
emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story]));
|
||||
|
@ -4,22 +4,27 @@ class PinState extends Equatable {
|
||||
const PinState({
|
||||
required this.pinnedStoriesIds,
|
||||
required this.pinnedStories,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
PinState.init()
|
||||
: pinnedStoriesIds = <int>[],
|
||||
pinnedStories = <Story>[];
|
||||
pinnedStories = <Story>[],
|
||||
status = Status.idle;
|
||||
|
||||
final List<int> pinnedStoriesIds;
|
||||
final List<Story> pinnedStories;
|
||||
final Status status;
|
||||
|
||||
PinState copyWith({
|
||||
List<int>? pinnedStoriesIds,
|
||||
List<Story>? pinnedStories,
|
||||
Status? status,
|
||||
}) {
|
||||
return PinState(
|
||||
pinnedStoriesIds: pinnedStoriesIds ?? this.pinnedStoriesIds,
|
||||
pinnedStories: pinnedStories ?? this.pinnedStories,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,5 +32,6 @@ class PinState extends Equatable {
|
||||
List<Object?> get props => <Object?>[
|
||||
pinnedStoriesIds,
|
||||
pinnedStories,
|
||||
status,
|
||||
];
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
emit(PollState.init());
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: PollStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
List<int> pollOptionsIds = _story.parts;
|
||||
|
||||
@ -42,7 +42,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
|
||||
// If pollOptionsIds is still empty, exit loading state.
|
||||
if (pollOptionsIds.isEmpty) {
|
||||
emit(state.copyWith(status: PollStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
);
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: PollStatus.loaded));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'poll_cubit.dart';
|
||||
|
||||
enum PollStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class PollState extends Equatable {
|
||||
const PollState({
|
||||
required this.totalVotes,
|
||||
@ -19,18 +12,18 @@ class PollState extends Equatable {
|
||||
: totalVotes = 0,
|
||||
selections = <int>{},
|
||||
pollOptions = <PollOption>[],
|
||||
status = PollStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final int totalVotes;
|
||||
final Set<int> selections;
|
||||
final List<PollOption> pollOptions;
|
||||
final PollStatus status;
|
||||
final Status status;
|
||||
|
||||
PollState copyWith({
|
||||
int? totalVotes,
|
||||
Set<int>? selections,
|
||||
List<PollOption>? pollOptions,
|
||||
PollStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return PollState(
|
||||
totalVotes: totalVotes ?? this.totalVotes,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
|
||||
part 'post_state.dart';
|
||||
@ -14,34 +15,31 @@ class PostCubit extends Cubit<PostState> {
|
||||
final PostRepository _postRepository;
|
||||
|
||||
Future<void> post({required String text, required int to}) async {
|
||||
emit(state.copyWith(status: PostStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final bool successful = await _postRepository.comment(
|
||||
parentId: to,
|
||||
text: text,
|
||||
);
|
||||
|
||||
// final successful =
|
||||
// await Future<bool>.delayed(const Duration(seconds: 2), () => true);
|
||||
|
||||
if (successful) {
|
||||
emit(state.copyWith(status: PostStatus.successful));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
} else {
|
||||
emit(state.copyWith(status: PostStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> edit({required String text, required int id}) async {
|
||||
emit(state.copyWith(status: PostStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
final bool successful = await _postRepository.edit(id: id, text: text);
|
||||
|
||||
if (successful) {
|
||||
emit(state.copyWith(status: PostStatus.successful));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
} else {
|
||||
emit(state.copyWith(status: PostStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
emit(state.copyWith(status: PostStatus.init));
|
||||
emit(state.copyWith(status: Status.idle));
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,13 @@
|
||||
part of 'post_cubit.dart';
|
||||
|
||||
enum PostStatus {
|
||||
init,
|
||||
loading,
|
||||
successful,
|
||||
failure,
|
||||
}
|
||||
|
||||
class PostState extends Equatable {
|
||||
const PostState({required this.status});
|
||||
|
||||
const PostState.init() : status = PostStatus.init;
|
||||
const PostState.init() : status = Status.idle;
|
||||
|
||||
final PostStatus status;
|
||||
final Status status;
|
||||
|
||||
PostState copyWith({PostStatus? status}) {
|
||||
PostState copyWith({Status? status}) {
|
||||
return PostState(
|
||||
status: status ?? this.status,
|
||||
);
|
||||
|
@ -67,10 +67,8 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
switch (T) {
|
||||
case int:
|
||||
_preferenceRepository.setInt(preference.key, value as int);
|
||||
break;
|
||||
case bool:
|
||||
_preferenceRepository.setBool(preference.key, value as bool);
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
@ -68,6 +68,8 @@ class PreferenceState extends Equatable {
|
||||
|
||||
bool get swipeGestureEnabled => _isOn<SwipeGesturePreference>();
|
||||
|
||||
bool get autoScrollEnabled => _isOn<AutoScrollModePreference>();
|
||||
|
||||
List<StoryType> get tabs {
|
||||
final String result =
|
||||
preferences.singleWhereType<TabOrderPreference>().val.toString();
|
||||
@ -85,6 +87,9 @@ class PreferenceState extends Equatable {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
StoryMarkingMode get storyMarkingMode => StoryMarkingMode.values
|
||||
.elementAt(preferences.singleWhereType<StoryMarkingModePreference>().val);
|
||||
|
||||
FetchMode get fetchMode => FetchMode.values
|
||||
.elementAt(preferences.singleWhereType<FetchModePreference>().val);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of 'search_cubit.dart';
|
||||
|
||||
enum SearchStatus {
|
||||
initial,
|
||||
idle,
|
||||
loading,
|
||||
loadingMore,
|
||||
loaded,
|
||||
@ -15,7 +15,7 @@ class SearchState extends Equatable {
|
||||
});
|
||||
|
||||
SearchState.init()
|
||||
: status = SearchStatus.initial,
|
||||
: status = SearchStatus.idle,
|
||||
results = <Item>[],
|
||||
params = SearchParams.init();
|
||||
|
||||
@ -23,6 +23,12 @@ class SearchState extends Equatable {
|
||||
final SearchStatus status;
|
||||
final SearchParams params;
|
||||
|
||||
bool get hasDateFilter =>
|
||||
params.filters.whereType<DateTimeRangeFilter>().isNotEmpty;
|
||||
|
||||
DateTimeRangeFilter? get dateFilter =>
|
||||
params.filters.whereType<DateTimeRangeFilter>().singleOrNull;
|
||||
|
||||
SearchState copyWith({
|
||||
List<Item>? results,
|
||||
SearchStatus? status,
|
||||
@ -42,3 +48,11 @@ class SearchState extends Equatable {
|
||||
params,
|
||||
];
|
||||
}
|
||||
|
||||
extension SearchStateExtension on SearchState {
|
||||
bool get showDateRangeShortcutChips {
|
||||
return hasDateFilter &&
|
||||
dateFilter?.startTime != null &&
|
||||
dateFilter?.endTime != null;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/post_repository.dart';
|
||||
|
||||
part 'submit_state.dart';
|
||||
@ -25,7 +26,7 @@ class SubmitCubit extends Cubit<SubmitState> {
|
||||
}
|
||||
|
||||
void onSubmitTapped() {
|
||||
emit(state.copyWith(status: SubmitStatus.submitting));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
if (state.title?.isNotEmpty ?? false) {
|
||||
_postRepository
|
||||
@ -35,9 +36,9 @@ class SubmitCubit extends Cubit<SubmitState> {
|
||||
text: state.text,
|
||||
)
|
||||
.then((bool successful) {
|
||||
emit(state.copyWith(status: SubmitStatus.submitted));
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}).onError((Object? error, StackTrace stackTrace) {
|
||||
emit(state.copyWith(status: SubmitStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'submit_cubit.dart';
|
||||
|
||||
enum SubmitStatus {
|
||||
initial,
|
||||
submitting,
|
||||
submitted,
|
||||
failure,
|
||||
}
|
||||
|
||||
class SubmitState extends Equatable {
|
||||
const SubmitState({
|
||||
required this.title,
|
||||
@ -19,18 +12,18 @@ class SubmitState extends Equatable {
|
||||
: title = null,
|
||||
url = null,
|
||||
text = null,
|
||||
status = SubmitStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final String? title;
|
||||
final String? url;
|
||||
final String? text;
|
||||
final SubmitStatus status;
|
||||
final Status status;
|
||||
|
||||
SubmitState copyWith({
|
||||
String? title,
|
||||
String? url,
|
||||
String? text,
|
||||
SubmitStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return SubmitState(
|
||||
title: title ?? this.title,
|
||||
|
@ -15,16 +15,16 @@ class UserCubit extends Cubit<UserState> {
|
||||
final StoriesRepository _storiesRepository;
|
||||
|
||||
void init({required String userId}) {
|
||||
emit(state.copyWith(status: UserStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
_storiesRepository.fetchUser(id: userId).then((User? user) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
user: user ?? User.emptyWithId(userId),
|
||||
status: UserStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
}).onError((_, __) {
|
||||
emit(state.copyWith(status: UserStatus.failure));
|
||||
emit(state.copyWith(status: Status.failure));
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
part of 'user_cubit.dart';
|
||||
|
||||
enum UserStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class UserState extends Equatable {
|
||||
const UserState({
|
||||
required this.user,
|
||||
@ -15,14 +8,14 @@ class UserState extends Equatable {
|
||||
|
||||
const UserState.init()
|
||||
: user = const User.empty(),
|
||||
status = UserStatus.initial;
|
||||
status = Status.idle;
|
||||
|
||||
final User user;
|
||||
final UserStatus status;
|
||||
final Status status;
|
||||
|
||||
UserState copyWith({
|
||||
User? user,
|
||||
UserStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return UserState(
|
||||
user: user ?? this.user,
|
||||
|
@ -6,7 +6,7 @@ enum Vote {
|
||||
}
|
||||
|
||||
enum VoteStatus {
|
||||
initial,
|
||||
idle,
|
||||
canceled,
|
||||
submitted,
|
||||
failureBeHumble,
|
||||
@ -24,7 +24,7 @@ class VoteState extends Equatable {
|
||||
|
||||
const VoteState.init({required this.item})
|
||||
: vote = null,
|
||||
status = VoteStatus.initial;
|
||||
status = VoteStatus.idle;
|
||||
|
||||
/// Null means user has not voted,
|
||||
/// True means user voted up,
|
||||
|
@ -30,7 +30,6 @@ extension ContextExtension on BuildContext {
|
||||
textColor: Theme.of(this).textTheme.bodyLarge?.color,
|
||||
)
|
||||
: null,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ extension ObjectExtension on Object {
|
||||
String identifier = '',
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
locator.get<Logger>().e(identifier, this, stackTrace ?? StackTrace.current);
|
||||
locator.get<Logger>().e(
|
||||
identifier,
|
||||
error: this,
|
||||
stackTrace: stackTrace ?? StackTrace.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/item/models/models.dart';
|
||||
import 'package:hacki/screens/item/widgets/widgets.dart';
|
||||
import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs;
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
extension StateExtension on State {
|
||||
@ -36,9 +36,9 @@ extension StateExtension on State {
|
||||
if (splitViewEnabled && !forceNewScreen) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
return HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: args,
|
||||
context.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
extra: args,
|
||||
);
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ extension StateExtension on State {
|
||||
}
|
||||
|
||||
void onMoreTapped(Item item, Rect? rect) {
|
||||
HapticFeedback.lightImpact();
|
||||
HapticFeedbackUtil.light();
|
||||
|
||||
if (item.dead || item.deleted) {
|
||||
return;
|
||||
@ -75,16 +75,12 @@ extension StateExtension on State {
|
||||
break;
|
||||
case MenuAction.fav:
|
||||
onFavTapped(item);
|
||||
break;
|
||||
case MenuAction.share:
|
||||
onShareTapped(item, rect);
|
||||
break;
|
||||
case MenuAction.flag:
|
||||
onFlagTapped(item);
|
||||
break;
|
||||
case MenuAction.block:
|
||||
onBlockTapped(item, isBlocked: isBlocked);
|
||||
break;
|
||||
case MenuAction.cancel:
|
||||
break;
|
||||
}
|
||||
@ -116,12 +112,11 @@ extension StateExtension on State {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(context, item.url),
|
||||
onTap: () => context.pop(item.url),
|
||||
title: const Text('Link to article'),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
onTap: () => context.pop(
|
||||
'https://news.ycombinator.com/item?id=${item.id}',
|
||||
),
|
||||
title: const Text('Link to HN'),
|
||||
@ -159,13 +154,13 @@ extension StateExtension on State {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
),
|
||||
@ -197,13 +192,13 @@ extension StateExtension on State {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
),
|
||||
|
@ -17,8 +17,6 @@ import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/custom_bloc_observer.dart';
|
||||
import 'package:hacki/services/fetcher.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/theme_util.dart';
|
||||
@ -59,8 +57,8 @@ Future<void> main({bool testing = false}) async {
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
locator.get<Logger>().e(
|
||||
details.summary,
|
||||
details.exceptionAsString(),
|
||||
details.stack,
|
||||
error: details.exceptionAsString(),
|
||||
stackTrace: details.stack,
|
||||
);
|
||||
};
|
||||
|
||||
@ -140,11 +138,8 @@ Future<void> main({bool testing = false}) async {
|
||||
prefs.getInt(FontPreference().key) ?? Font.roboto.index,
|
||||
);
|
||||
|
||||
// ignore: prefer_asserts_with_message
|
||||
assert(() {
|
||||
Bloc.observer = CustomBlocObserver();
|
||||
return true;
|
||||
}());
|
||||
//Uncomment this line to log events from bloc/cubit.
|
||||
//Bloc.observer = CustomBlocObserver();
|
||||
|
||||
HydratedBloc.storage = storage;
|
||||
|
||||
@ -159,19 +154,16 @@ Future<void> main({bool testing = false}) async {
|
||||
|
||||
class HackiApp extends StatelessWidget {
|
||||
const HackiApp({
|
||||
super.key,
|
||||
this.savedThemeMode,
|
||||
required this.trueDarkMode,
|
||||
required this.font,
|
||||
super.key,
|
||||
this.savedThemeMode,
|
||||
});
|
||||
|
||||
final AdaptiveThemeMode? savedThemeMode;
|
||||
final Font font;
|
||||
final bool trueDarkMode;
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@ -245,7 +237,7 @@ class HackiApp extends StatelessWidget {
|
||||
create: (BuildContext context) => TabCubit(
|
||||
preferenceCubit: context.read<PreferenceCubit>(),
|
||||
)..init(),
|
||||
)
|
||||
),
|
||||
],
|
||||
child: AdaptiveTheme(
|
||||
light: ThemeData(
|
||||
@ -273,8 +265,8 @@ class HackiApp extends StatelessWidget {
|
||||
AsyncSnapshot<AdaptiveThemeMode?> snapshot,
|
||||
) {
|
||||
final AdaptiveThemeMode? mode = snapshot.data;
|
||||
ThemeUtil.updateAndroidStatusBarSetting(
|
||||
Theme.of(context).brightness,
|
||||
ThemeUtil.updateStatusBarSetting(
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness,
|
||||
mode,
|
||||
);
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
@ -285,20 +277,18 @@ class HackiApp extends StatelessWidget {
|
||||
final bool useTrueDark = prefState.trueDarkEnabled &&
|
||||
(mode == AdaptiveThemeMode.dark ||
|
||||
(mode == AdaptiveThemeMode.system &&
|
||||
SchedulerBinding
|
||||
.instance.window.platformBrightness ==
|
||||
View.of(context)
|
||||
.platformDispatcher
|
||||
.platformBrightness ==
|
||||
Brightness.dark));
|
||||
return FeatureDiscovery(
|
||||
child: MaterialApp(
|
||||
child: MaterialApp.router(
|
||||
title: 'Hacki',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: useTrueDark ? trueDarkTheme : theme,
|
||||
navigatorKey: navigatorKey,
|
||||
navigatorObservers: <NavigatorObserver>[
|
||||
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||
],
|
||||
onGenerateRoute: CustomRouter.onGenerateRoute,
|
||||
initialRoute: HomeScreen.routeName,
|
||||
theme: (useTrueDark ? trueDarkTheme : theme).copyWith(
|
||||
useMaterial3: false,
|
||||
),
|
||||
routerConfig: router,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
14
lib/models/export_destination.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart' show IconData, Icons;
|
||||
|
||||
enum ExportDestination {
|
||||
qrCode('QR code', icon: Icons.qr_code),
|
||||
clipBoard('ClipBoard', icon: Icons.copy);
|
||||
|
||||
const ExportDestination(
|
||||
this.label, {
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
}
|
@ -1,10 +1,28 @@
|
||||
enum Font {
|
||||
roboto('Roboto'),
|
||||
robotoSlab('Roboto Slab'),
|
||||
robotoSlab('Roboto Slab', isSerif: true),
|
||||
ubuntu('Ubuntu'),
|
||||
ubuntuMono('Ubuntu Mono');
|
||||
ubuntuMono('Ubuntu Mono'),
|
||||
notoSerif('Noto Serif', isSerif: true);
|
||||
|
||||
const Font(this.label);
|
||||
const Font(this.uiLabel, {this.isSerif = false});
|
||||
|
||||
final String label;
|
||||
final String uiLabel;
|
||||
final bool isSerif;
|
||||
|
||||
static Font fromString(String? val) {
|
||||
switch (val) {
|
||||
case 'robotoSlab':
|
||||
return Font.robotoSlab;
|
||||
case 'ubuntu':
|
||||
return Font.ubuntu;
|
||||
case 'ubuntuMono':
|
||||
return Font.ubuntuMono;
|
||||
case 'notoSerif':
|
||||
return Font.notoSerif;
|
||||
case 'roboto':
|
||||
default:
|
||||
return Font.roboto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ enum FontSize {
|
||||
small('Small', TextDimens.pt15),
|
||||
regular('Regular', TextDimens.pt16),
|
||||
large('Large', TextDimens.pt17),
|
||||
xlarge('XLarge', TextDimens.pt18);
|
||||
xlarge('XLarge', TextDimens.pt18),
|
||||
xxlarge('XXLarge', TextDimens.pt19);
|
||||
|
||||
const FontSize(this.description, this.fontSize);
|
||||
|
||||
|
@ -35,6 +35,27 @@ class BuildableComment extends Comment with Buildable {
|
||||
hidden: comment.hidden,
|
||||
);
|
||||
|
||||
@override
|
||||
BuildableComment copyWith({
|
||||
int? level,
|
||||
bool? hidden,
|
||||
}) {
|
||||
return BuildableComment(
|
||||
id: id,
|
||||
time: time,
|
||||
parent: parent,
|
||||
score: score,
|
||||
by: by,
|
||||
text: text,
|
||||
kids: kids,
|
||||
dead: dead,
|
||||
deleted: deleted,
|
||||
hidden: hidden ?? this.hidden,
|
||||
level: level ?? this.level,
|
||||
elements: elements,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final List<LinkifyElement> elements;
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ class Comment extends Item {
|
||||
|
||||
String get metadata => '''by $by $timeAgo''';
|
||||
|
||||
bool get isRoot => level == 0;
|
||||
|
||||
Comment copyWith({
|
||||
int? level,
|
||||
bool? hidden,
|
||||
|
@ -1,4 +1,5 @@
|
||||
export 'comments_order.dart';
|
||||
export 'export_destination.dart';
|
||||
export 'fetch_mode.dart';
|
||||
export 'font.dart';
|
||||
export 'font_size.dart';
|
||||
@ -6,5 +7,7 @@ export 'item/item.dart';
|
||||
export 'post_data.dart';
|
||||
export 'preference.dart';
|
||||
export 'search_params.dart';
|
||||
export 'status.dart';
|
||||
export 'story_marking_mode.dart';
|
||||
export 'story_type.dart';
|
||||
export 'user.dart';
|
||||
|
@ -23,16 +23,18 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
FontPreference(),
|
||||
FontSizePreference(),
|
||||
TabOrderPreference(),
|
||||
StoryMarkingModePreference(),
|
||||
// Order of items below matters and
|
||||
// reflects the order on settings screen.
|
||||
const DisplayModePreference(),
|
||||
const MetadataModePreference(),
|
||||
const StoryUrlModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
const NotificationModePreference(),
|
||||
const SwipeGesturePreference(),
|
||||
const AutoScrollModePreference(),
|
||||
const CollapseModePreference(),
|
||||
const ReaderModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
const EyeCandyModePreference(),
|
||||
const TrueDarkModePreference(),
|
||||
],
|
||||
@ -54,18 +56,21 @@ const bool _notificationModeDefaultValue = true;
|
||||
const bool _swipeGestureModeDefaultValue = false;
|
||||
const bool _displayModeDefaultValue = true;
|
||||
const bool _eyeCandyModeDefaultValue = false;
|
||||
const bool _trueDarkModeDefaultValue = false;
|
||||
const bool _trueDarkModeDefaultValue = true;
|
||||
const bool _readerModeDefaultValue = true;
|
||||
const bool _markReadStoriesModeDefaultValue = true;
|
||||
const bool _metadataModeDefaultValue = true;
|
||||
const bool _storyUrlModeDefaultValue = true;
|
||||
const bool _collapseModeDefaultValue = true;
|
||||
const bool _autoScrollModeDefaultValue = true;
|
||||
final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||
final int _fontSizeDefaultValue = FontSize.regular.index;
|
||||
final int _fontDefaultValue = Font.roboto.index;
|
||||
final int _tabOrderDefaultValue =
|
||||
StoryType.convertToSettingsValue(StoryType.values);
|
||||
final int _markStoriesAsReadWhenPreferenceDefaultValue =
|
||||
StoryMarkingMode.tap.index;
|
||||
|
||||
class SwipeGesturePreference extends BooleanPreference {
|
||||
const SwipeGesturePreference({bool? val})
|
||||
@ -127,6 +132,26 @@ class CollapseModePreference extends BooleanPreference {
|
||||
'''if disabled, tap on the top of comment tile to collapse.''';
|
||||
}
|
||||
|
||||
class AutoScrollModePreference extends BooleanPreference {
|
||||
const AutoScrollModePreference({bool? val})
|
||||
: super(val: val ?? _autoScrollModeDefaultValue);
|
||||
|
||||
@override
|
||||
AutoScrollModePreference copyWith({required bool? val}) {
|
||||
return AutoScrollModePreference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'autoScrollMode';
|
||||
|
||||
@override
|
||||
String get title => 'Auto-scroll on collapsing';
|
||||
|
||||
@override
|
||||
String get subtitle =>
|
||||
'''automatically scroll to next comment when you collapse a comment.''';
|
||||
}
|
||||
|
||||
/// The value deciding whether or not the story
|
||||
/// tile should display link preview. Defaults to true.
|
||||
class DisplayModePreference extends BooleanPreference {
|
||||
@ -342,3 +367,19 @@ class TabOrderPreference extends IntPreference {
|
||||
@override
|
||||
String get title => 'Tab order';
|
||||
}
|
||||
|
||||
class StoryMarkingModePreference extends IntPreference {
|
||||
StoryMarkingModePreference({int? val})
|
||||
: super(val: val ?? _markStoriesAsReadWhenPreferenceDefaultValue);
|
||||
|
||||
@override
|
||||
StoryMarkingModePreference copyWith({required int? val}) {
|
||||
return StoryMarkingModePreference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'storyMarkingMode';
|
||||
|
||||
@override
|
||||
String get title => 'Mark a Story as Read on';
|
||||
}
|
||||
|
@ -30,14 +30,27 @@ class DateTimeRangeFilter implements NumericFilter {
|
||||
|
||||
@override
|
||||
String get query {
|
||||
if (startTime == null || endTime == null) return '';
|
||||
|
||||
final int? startTimestamp = startTime == null
|
||||
? null
|
||||
: startTime!.toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
final int? endTimestamp = endTime == null
|
||||
int? endTimestamp = endTime == null
|
||||
? null
|
||||
: endTime!.toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
if (startTimestamp == endTimestamp) {
|
||||
endTimestamp = startTime!
|
||||
.add(const Duration(hours: 24))
|
||||
.toUtc()
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000;
|
||||
}
|
||||
|
||||
if (startTimestamp == null || endTimestamp == null) return '';
|
||||
|
||||
final String query =
|
||||
'''${startTimestamp == null ? '' : 'created_at_i>$startTimestamp'},${endTimestamp == null ? '' : 'created_at_i<$endTimestamp'}''';
|
||||
'''created_at_i>=$startTimestamp, created_at_i<=$endTimestamp''';
|
||||
|
||||
if (query.endsWith(',')) {
|
||||
return query.replaceFirst(',', '');
|
||||
|
14
lib/models/status.dart
Normal file
@ -0,0 +1,14 @@
|
||||
enum Status {
|
||||
idle,
|
||||
inProgress,
|
||||
success,
|
||||
failure,
|
||||
}
|
||||
|
||||
extension StatusExtension on Status {
|
||||
bool get isLoading => this == Status.inProgress;
|
||||
|
||||
bool get isSuccessful => this == Status.success;
|
||||
|
||||
bool get hasError => this == Status.failure;
|
||||
}
|
21
lib/models/story_marking_mode.dart
Normal file
@ -0,0 +1,21 @@
|
||||
/// Used for determining when to mark a story as read.
|
||||
enum StoryMarkingMode {
|
||||
// Mark a story as read after user scrolls past it.
|
||||
scrollPast('scrolling past'),
|
||||
// Mark a story as read after user taps on it.
|
||||
tap('tapping'),
|
||||
// Mark a story as read after user scrolls past or taps on it, whichever
|
||||
// happens the first.
|
||||
scrollPastOrTap('scrolling past or tapping');
|
||||
|
||||
const StoryMarkingMode(this.label);
|
||||
|
||||
final String label;
|
||||
|
||||
bool get shouldDetectScrollingPast =>
|
||||
this == StoryMarkingMode.scrollPast ||
|
||||
this == StoryMarkingMode.scrollPastOrTap;
|
||||
|
||||
bool get shouldDetectTapping =>
|
||||
this == StoryMarkingMode.tap || this == StoryMarkingMode.scrollPastOrTap;
|
||||
}
|
@ -64,7 +64,7 @@ class PostableRepository {
|
||||
validateStatus: validateStatus,
|
||||
),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
throw ServiceException(e.message);
|
||||
}
|
||||
}
|
||||
|