Compare commits

..

20 Commits

Author SHA1 Message Date
b8ffa2ea2f bump version of Flutter. 2022-10-13 22:48:17 -07:00
caaa891700 bump version of Flutter. 2022-10-13 22:47:54 -07:00
e22dd18b91 updated action. 2022-09-24 02:54:22 -07:00
0633c80147 avoid running on tag creation. 2022-09-24 02:42:24 -07:00
ddf9b1457c fixed action. 2022-09-24 02:38:24 -07:00
e91293e625 revert actions. 2022-09-24 02:35:21 -07:00
f76a3109cc updated fastlane. 2022-09-24 02:25:38 -07:00
8373376b00 updated action. 2022-09-24 02:01:49 -07:00
651923a4dd updated actions 2022-09-24 01:54:42 -07:00
a6d8666c57 updated fastfile. 2022-09-24 01:40:54 -07:00
aa94d530d4 updated profile. 2022-09-24 01:30:33 -07:00
26f5b1ccca updated profile. 2022-09-24 00:49:48 -07:00
30dd0e137a updated profile 2022-09-24 00:37:21 -07:00
3da53c692e updated profile. 2022-09-24 00:20:13 -07:00
94ca2deda4 updated profile. 2022-09-24 00:12:50 -07:00
47eaf00cd2 updated provisioning profile. 2022-09-24 00:02:24 -07:00
2b1e757f46 Merge branch 'master' into v0.2.32 2022-09-23 22:25:20 -07:00
1e32fe5051 added github action. (#73) 2022-09-23 22:24:28 -07:00
17686a9e1b fixed comment tile overflow. 2022-09-23 21:30:09 -07:00
0248792e66 fixed background color of nav bar on Android. 2022-09-17 00:38:21 -07:00
256 changed files with 6590 additions and 11652 deletions

View File

@ -4,21 +4,19 @@ on:
push:
branches:
- "**"
- '!master'
jobs:
releases:
name: Check commit
runs-on: macos-latest
timeout-minutes: 30
runs-on: ubuntu-latest
env:
FLUTTER_VERSION: "3.3.4"
steps:
- name: checkout all the submodules
uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
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
- run: submodules/flutter/bin/flutter analyze lib test integration_test
- run: submodules/flutter/bin/flutter test
flutter-version: '3.3.4'
channel: 'stable'
- run: flutter pub get
- run: flutter format --set-exit-if-changed .
- run: flutter analyze

View File

@ -6,12 +6,13 @@ on:
# Run the workflow whenever a new tag named 'v*' is pushed
push:
branches:
- master
- "!*"
tags:
- "v*"
jobs:
build_and_publish:
runs-on: macos-latest
timeout-minutes: 30
env:
# Point the `ruby/setup-ruby` action at this Gemfile, so it
@ -20,22 +21,21 @@ jobs:
steps:
- name: Check out from git
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
- run: submodules/flutter/bin/flutter analyze lib test integration_test
- run: submodules/flutter/bin/flutter test
uses: actions/checkout@v2
# Configure ruby according to our .ruby-version
- name: Setup ruby & Bundler
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
# Set up flutter (feel free to adjust the version below)
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
cache: true
flutter-version: 3.3.4
- run: flutter pub get
- run: flutter format --set-exit-if-changed .
- run: flutter analyze
# Start an ssh-agent that will provide the SSH key from the
# SSH_PRIVATE_KEY secret to `fastlane match`
- name: Setup SSH key
@ -44,10 +44,11 @@ jobs:
run: |
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}"
- name: Download dependencies
run: flutter pub get
- name: Build & Publish to TestFlight with Fastlane
env:
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: cd ios && bundle exec fastlane beta
run: cd ios && bundle exec fastlane beta "build_name:${{ github.ref_name }}"

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "flutter"]
path = submodules/flutter
url = https://github.com/flutter/flutter

View File

@ -29,7 +29,6 @@ Features:
- Download stories and comments for offline reading.
- Pick up where you left off.
- Synced favorites and pins across devices. (iOS only)
- Export or import your favorites.
- Launch from system share sheet.
- And more...

View File

@ -1,4 +1,4 @@
include: package:very_good_analysis/analysis_options.5.0.0.yaml
include: package:very_good_analysis/analysis_options.2.4.0.yaml
linter:
rules:
parameter_assignments: false
@ -6,8 +6,4 @@ linter:
library_private_types_in_public_api: false
omit_local_variable_types: false
one_member_abstracts: false
always_specify_types: true
analyzer:
exclude:
- "submodules/**"
always_specify_types: true

View File

@ -64,15 +64,12 @@ android {
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
}
}
}
flutter {
@ -82,14 +79,3 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
}
}

View File

@ -37,6 +37,15 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

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

View File

@ -1,18 +0,0 @@
## End-user License Agreement
This policy applies to the usage of the Hacki app.
Please read this Mobile Application End User License Agreement (“EULA”) carefully before using the Hacki mobile application ("Mobile App"), which allows You to read and contribute to Hacker News from Your mobile device. This EULA forms a binding legal agreement between you (and any other entity on whose behalf you accept these terms) (collectively “You” or “Your”) and Hacki (each separately a “Party” and collectively the “Parties”) as of the date you download the Mobile App. Your use of the Mobile App is subject to this EULA.
### Changes to this EULA
Hacki reserves the right to modify this EULA at any time and for any reason. You are responsible for complying with the updated EULA. Your continued use of the Mobile App indicates Your consent to the updated terms.
### No Included Maintenance and Support
Hacki may deploy changes, updates, or enhancements to the Mobile App at any time. Hacki may provide maintenance and support for the Mobile App, but has no obligation whatsoever to furnish such services to You and may terminate such services at any time without notice.
### No Warranty
Hacki expressly disclaims all warranties of any kind, whether express or implied.
The Mobile App is only available for supported devices and might not work on every device. Determining whether Your device is a supported or compatible device for use of the Mobile App is solely Your responsibility, and downloading the Mobile App is done at Your own risk. Smartsheet does not represent or warrant that the Mobile App and Your device are compatible or that the Mobile App will work on Your device.
### Your Consent
By using the app, you consent to the end-user license agreement.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,48 +0,0 @@
## Privacy Policy
This policy applies to all information collected or submitted on Hacki.
### Information we collect
Hacki collects anonymous statistics such as crash reports and feature usage. These data are solely used to track app's health and are only stored locally on your device and only got sent to us when you choose to do so.
### Ads and analytics
Hacki does not serve ads.
Hacki collects aggregate, anonymous statistics to improve the app but these data are only stored locally on your device and only got sent to us when you choose to do so.
### Information usage
We use the information we collect to operate and improve our website, apps, and customer support.
We do not share personal information with outside parties except to the extent necessary to accomplish Hackis functionality.
We may disclose your information in response to subpoenas, court orders, or other legal requirements; to exercise our legal rights or defend against legal claims; to investigate, prevent, or take action regarding illegal activities, suspected fraud or abuse, violations of our policies; or to protect our rights and property.
### Security
Hacki uses the official Hacker News API for fetching data from Hacker News.
When logging in, usernames and passwords are securely sent to Hacker News' servers for authentication.
### Third-party links and content
Hacki displays links and content from third-party websites. These websites have their own independent privacy policies, and we have no responsibility or liability for their content or activities.
#### California Online Privacy Protection Act Compliance
Hacki complies with the California Online Privacy Protection Act. We therefore will not distribute your personal information to outside parties without your consent.
#### Childrens Online Privacy Protection Act Compliance
Hacki never collects or maintain information at our website from those we actually know are under 13, and no part of our website is structured to attract anyone under 13.
#### Information for European Union Customers
By using Hacki and providing your information, you authorize us to collect, use, and store your information outside of the European Union.
#### International Transfers of Information
Information may be processed, stored, and used outside of the country in which you are located. Data privacy laws vary across jurisdictions, and different laws may be applicable to your data depending on where it is processed, stored, or used.
### Your Consent
By using the app, you consent to the privacy policy.
### Contacting Us
If you have questions regarding this privacy policy, you may e-mail me us at jfeng@fastmail.com.
### Changes to this policy
If we decide to change this privacy policy, we will post those changes on this page.
February 27, 2023: First published.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,59 +0,0 @@
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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
include: package:very_good_analysis/analysis_options.3.1.0.yaml
include: package:very_good_analysis/analysis_options.2.4.0.yaml
linter:
rules:
parameter_assignments: false

View File

@ -1,82 +1,5 @@
import Flutter
import UIKit
import Foundation
typealias APNSHandler = ()->Void
let keyKey = "key"
let valKey = "val"
final class SharedPrefsCore {
fileprivate static let shared: SharedPrefsCore = SharedPrefsCore()
fileprivate func setBool(key: String?, val: Bool?) -> Bool {
guard let key = key,
let val = val else {
return false
}
let keyStore = NSUbiquitousKeyValueStore()
let allVals = keyStore.dictionaryRepresentation;
let allKeys = allVals.keys
// Limit is 1024, reserve rest slots for fav and pins.
if allKeys.count >= 1000 {
for key in allKeys.filter({ $0.contains("hasRead") }) {
keyStore.removeObject(forKey: key)
}
}
keyStore.set(val, forKey: key)
return true
}
fileprivate func getBool(key: String?) -> Bool {
guard let key = key else {
return false
}
let keyStore = NSUbiquitousKeyValueStore()
let val = keyStore.bool(forKey: key)
return val
}
fileprivate func setStringList(key: String?, val: [String]?) -> Bool {
guard let key = key,
let val = val else {
return false
}
let keyStore = NSUbiquitousKeyValueStore()
keyStore.set(val, forKey: key)
return true
}
fileprivate func getStringList(key: String?) -> [Any] {
guard let key = key else {
return [Any]()
}
let keyStore = NSUbiquitousKeyValueStore()
let list = keyStore.array(forKey: key) as [Any]? ?? [Any]()
return list
}
fileprivate func clearAll() -> Bool{
let keyStore = NSUbiquitousKeyValueStore()
let allVals = keyStore.dictionaryRepresentation;
let allKeys = allVals.keys
for key in allKeys.filter({ $0.contains("hasRead") }) {
keyStore.removeObject(forKey: key)
}
return true
}
}
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
@ -84,49 +7,46 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
let instance = SwiftSyncedSharedPreferencesPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "setBool":
if let params = call.arguments as? [String: Any] {
let val = params[valKey] as? Bool
let key = params[keyKey] as? String
let res = SharedPrefsCore.shared.setBool(key: key, val: val)
result(res)
let info: [String: Any] = ["result": result,
"params": params]
NotificationCenter.default.post(name: Notification.Name("setBool"), object: nil, userInfo: info)
}
return
case "getBool":
if let params = call.arguments as? [String: Any] {
let key = params[keyKey] as? String
let res = SharedPrefsCore.shared.getBool(key: key)
result(res)
let info: [String: Any] = ["result": result,
"params": params]
NotificationCenter.default.post(name: Notification.Name("getBool"), object: nil, userInfo: info)
}
return
case "setStringList":
if let params = call.arguments as? [String: Any] {
let val = params[valKey] as? [String]
let key = params[keyKey] as? String
let res = SharedPrefsCore.shared.setStringList(key: key, val: val)
result(res)
let info: [String: Any] = ["result": result,
"params": params]
NotificationCenter.default.post(name: Notification.Name("setStringList"), object: nil, userInfo: info)
}
return
case "getStringList":
if let params = call.arguments as? [String: Any] {
let key = params[keyKey] as? String
let res = SharedPrefsCore.shared.getStringList(key: key)
result(res)
let info: [String: Any] = ["result": result,
"params": params]
NotificationCenter.default.post(name: Notification.Name("getStringList"), object: nil, userInfo: info)
}
return
case "clearAll":
if let params = call.arguments as? [String: Any] {
let res = SharedPrefsCore.shared.clearAll()
result(res)
let info: [String: Any] = ["result": result,
"params": params]
NotificationCenter.default.post(name: Notification.Name("clearAll"), object: nil, userInfo: info)
}
return

View File

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

View File

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

View File

@ -1,2 +0,0 @@
- Fixed app icon.
- Added font size setting to comments screen.

View File

@ -1,2 +0,0 @@
- Fixed app icon.
- Added font size setting to comments screen.

View File

@ -1 +0,0 @@
- Fixed time machine.

View File

@ -1 +0,0 @@
- Fixed time machine.

View File

@ -1,3 +0,0 @@
- Customization of tab bar.
- Option to enable swipe gesture for switching between tabs.
- Access to action menu from home screen.

View File

@ -1,5 +0,0 @@
- Customization of tab bar.
- Option to enable swipe gesture for switching between tabs.
- Access to action menu from home screen.
- Access to Wikipedia and Wiktionary from text selection toolbar.
- Quotes and emphasis rendering.

View File

@ -1,5 +0,0 @@
- Customization of tab bar.
- Option to enable swipe gesture for switching between tabs.
- Access to action menu from home screen.
- Access to Wikipedia and Wiktionary from text selection toolbar.
- Quotes and emphasis rendering.

View File

@ -17,20 +17,20 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.680.0)
aws-sdk-core (3.168.4)
aws-partitions (1.636.0)
aws-sdk-core (3.154.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.61.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.117.2)
aws-sdk-core (~> 3, >= 3.165.0)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
aws-sigv4 (1.5.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
@ -86,7 +86,7 @@ GEM
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.95.0)
excon (0.92.5)
faraday (1.10.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@ -116,7 +116,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.211.0)
fastlane (2.210.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -159,9 +159,9 @@ GEM
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.32.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-core (0.9.2)
google-apis-androidpublisher_v3 (0.27.0)
google-apis-core (>= 0.7.2, < 2.a)
google-apis-core (0.9.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@ -170,27 +170,27 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.16.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-playcustomapp_v1 (0.12.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-apis-iamcredentials_v1 (0.14.0)
google-apis-core (>= 0.7.2, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.17.0)
google-apis-core (>= 0.7, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.0)
google-cloud-storage (1.44.0)
google-cloud-storage (1.42.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-apis-storage_v1 (~> 0.17.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.3.0)
googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@ -203,11 +203,11 @@ GEM
httpclient (2.8.3)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.6.3)
jmespath (1.6.1)
json (2.6.2)
jwt (2.5.0)
memoist (0.16.2)
mini_magick (4.12.0)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.16.3)
molinillo (0.8.0)

View File

@ -1,3 +1,6 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -2,11 +2,7 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_email_sender (0.0.1):
- Flutter
- flutter_inappwebview (0.0.1):
- Flutter
- flutter_inappwebview/Core (= 0.0.1)
@ -16,36 +12,26 @@ PODS:
- OrderedSet (~> 5.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- flutter_secure_storage (3.3.1):
- Flutter
- flutter_siri_suggestions (0.0.1):
- Flutter
- 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):
- path_provider_ios (0.0.1):
- 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
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- shared_preferences_ios (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3):
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
- synced_shared_preferences (0.0.1):
@ -61,21 +47,16 @@ PODS:
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_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/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/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/darwin`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- 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`)
@ -86,19 +67,14 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- FMDB
- MTBBarcodeScanner
- OrderedSet
- ReachabilitySwift
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_email_sender:
:path: ".symlinks/plugins/flutter_email_sender/ios"
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_local_notifications:
@ -107,22 +83,16 @@ 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/darwin"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/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/darwin"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
synced_shared_preferences:
@ -137,33 +107,27 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
integration_test: 13825b8a9334a850581300559b8839134b124670
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.3
COCOAPODS: 1.11.2

View File

@ -3,14 +3,13 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
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 +21,9 @@
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
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, ); }; };
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54B4752282B3B8900579261 /* HackiCore.swift */; };
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 */
@ -55,7 +56,7 @@
};
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 8;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
@ -63,19 +64,19 @@
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 1;
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
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>"; };
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>"; };
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 +84,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>"; };
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>"; };
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>"; };
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>"; };
@ -96,6 +97,7 @@
E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; };
E54B4752282B3B8900579261 /* HackiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackiCore.swift; sourceTree = "<group>"; };
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
@ -107,7 +109,7 @@
buildActionMask = 2147483647;
files = (
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */,
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -175,6 +177,7 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
E54B4752282B3B8900579261 /* HackiCore.swift */,
);
path = Runner;
sourceTree = "<group>";
@ -183,8 +186,8 @@
isa = PBXGroup;
children = (
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */,
E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */,
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -192,9 +195,9 @@
D79CD63C88FF49EF451AFDDF /* Pods */ = {
isa = PBXGroup;
children = (
0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */,
D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */,
B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */,
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */,
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */,
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -229,15 +232,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
41DC8215F9CFD708C36ECBA8 /* [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 +294,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@ -360,12 +363,10 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@ -374,22 +375,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
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 */ = {
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -411,7 +397,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;
};
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */ = {
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -428,6 +414,20 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
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 */
@ -437,6 +437,7 @@
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -566,22 +567,23 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.3.0;
MARKETING_VERSION = 0.2.32;
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";
@ -705,22 +707,23 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.3.0;
MARKETING_VERSION = 0.2.32;
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;
@ -740,19 +743,17 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.3.0;
MARKETING_VERSION = 0.2.32;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -775,9 +776,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Share Extension/Info.plist";
@ -795,6 +798,7 @@
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;
@ -817,7 +821,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11;
@ -855,9 +859,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Share Extension/Info.plist";
@ -874,6 +880,7 @@
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;
@ -894,9 +901,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Action Extension/Info.plist";
@ -914,6 +923,7 @@
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;
@ -938,7 +948,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11;
@ -978,9 +988,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Action Extension/Info.plist";
@ -997,6 +1009,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;

View File

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

View File

@ -1,9 +1,9 @@
import UIKit
import Flutter
import workmanager
import shared_preferences_foundation
import shared_preferences_ios
import flutter_secure_storage
import path_provider_foundation
import path_provider_ios
import flutter_local_notifications
@UIApplicationMain
@ -16,10 +16,10 @@ import flutter_local_notifications
let center = UNUserNotificationCenter.current()
center.delegate = self
HackiCore.start()
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
WorkmanagerPlugin.registerTask(withIdentifier: "workmanager.background.task")
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
@ -28,8 +28,8 @@ import flutter_local_notifications
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
PathProviderPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.pathprovider.PathProviderPlugin")!)
FLTSharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.pathprovider.PathProviderPlugin")!)
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin")!)
}

134
ios/Runner/HackiCore.swift Normal file
View File

@ -0,0 +1,134 @@
//
// HackiCore.swift
// Runner
//
// Created by Jiaqi Feng on 5/10/22.
//
import Foundation
import Flutter
extension Notification.Name {
static let setBool = Notification.Name("setBool")
static let getBool = Notification.Name("getBool")
static let setStringList = Notification.Name("setStringList")
static let getStringList = Notification.Name("getStringList")
static let clearAll = Notification.Name("clearAll")
}
typealias APNSHandler = ()->Void
final class HackiCore: NSObject {
private static let keyKey = "key"
private static let valKey = "val"
private static let shared: HackiCore = HackiCore()
private let notificationCenter = NotificationCenter.default
// Called at app launch
class func start() {
shared.registerNotifications()
}
private class func setupFlutterEvent(channelName: String, handler: NSObjectProtocol & FlutterStreamHandler) {
guard let rootVC = UIApplication.shared.delegate?.window.unsafelyUnwrapped?.rootViewController as? FlutterViewController else { return }
let eventChannel = FlutterEventChannel(name: channelName, binaryMessenger: rootVC.binaryMessenger)
eventChannel.setStreamHandler(handler)
}
private func registerNotifications() {
// SyncedSharedPreferences
notificationCenter.addObserver(self, selector: #selector(setBool(_:)), name: .setBool, object: nil)
notificationCenter.addObserver(self, selector: #selector(getBool(_:)), name: .getBool, object: nil)
notificationCenter.addObserver(self, selector: #selector(setStringList(_:)), name: .setStringList, object: nil)
notificationCenter.addObserver(self, selector: #selector(getStringList(_:)), name: .getStringList, object: nil)
notificationCenter.addObserver(self, selector: #selector(clearAll(_:)), name: .clearAll, object: nil)
}
@objc private func setBool(_ notification: Notification) {
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
guard
let params = notification.userInfo?["params"] as? [String: Any],
let key = params[HackiCore.keyKey] as? String,
let val = params[HackiCore.valKey] as? Bool else {
resultCompletionBlock(false)
return
}
let keyStore = NSUbiquitousKeyValueStore()
let allVals = keyStore.dictionaryRepresentation;
let allKeys = allVals.keys
// Limit is 1024, reserve rest slots for fav and pins.
if allKeys.count >= 1000 {
for key in allKeys.filter({ $0.contains("hasRead") }) {
keyStore.removeObject(forKey: key)
}
}
keyStore.set(val, forKey: key)
resultCompletionBlock(true)
}
@objc private func getBool(_ notification: Notification) {
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
guard
let params = notification.userInfo?["params"] as? [String: Any],
let key = params[HackiCore.keyKey] as? String else {
resultCompletionBlock(false)
return
}
let keyStore = NSUbiquitousKeyValueStore()
let val = keyStore.bool(forKey: key)
resultCompletionBlock(val)
}
@objc private func setStringList(_ notification: Notification) {
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
guard
let params = notification.userInfo?["params"] as? [String: Any],
let key = params[HackiCore.keyKey] as? String,
let val = params[HackiCore.valKey] as? [String] else {
resultCompletionBlock(false)
return
}
let keyStore = NSUbiquitousKeyValueStore()
keyStore.set(val, forKey: key)
resultCompletionBlock(true)
}
@objc private func getStringList(_ notification: Notification) {
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
guard
let params = notification.userInfo?["params"] as? [String: Any],
let key = params[HackiCore.keyKey] as? String else {
resultCompletionBlock(false)
return
}
let keyStore = NSUbiquitousKeyValueStore()
let list = keyStore.array(forKey: key) as [Any]? ?? [Any]()
resultCompletionBlock(list)
}
@objc private func clearAll(_ notification: Notification) {
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
let keyStore = NSUbiquitousKeyValueStore()
let allVals = keyStore.dictionaryRepresentation;
let allKeys = allVals.keys
for key in allKeys.filter({ $0.contains("hasRead") }) {
keyStore.removeObject(forKey: key)
}
resultCompletionBlock(true)
}
}

View File

@ -23,7 +23,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -38,7 +38,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
@ -72,15 +72,5 @@
<array>
<string>applinks:example.com</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<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>

View File

@ -31,6 +31,10 @@ platform :ios do
is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example'
if !is_example_repo && APP_IDENTIFIER == 'no.tjer.HelloWorld' then
UI.user_error! "You need to update your Fastfile to use your own `APP_IDENTIFIER`"
end
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
sync_code_signing(
type: "appstore",
@ -38,6 +42,15 @@ platform :ios do
readonly: true
)
if !is_example_repo then
if APPSTORECONNECT_ISSUER_ID == '69a6de83-feb7-47e3-e053-5b8c7c11a4d1' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_ISSUER_ID`"
end
if APPSTORECONNECT_KEY_ID == 'YRQDJRKMR9' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_KEY_ID`"
end
end
# We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH
app_store_connect_api_key(
key_id: APPSTORECONNECT_KEY_ID,
@ -46,18 +59,21 @@ platform :ios do
latest_testflight_build_number
# Figure out the build number (and optionally build name)
new_build_number = ( + 1)
extra_config_args = []
if options.key?(:build_name) then
extra_config_args = ["--build-name", options[:build_name].delete_prefix('v')]
end
# Prep the xcodeproject from Flutter without building (`--config-only`)
sh(
"/Users/runner/work/Hacki/Hacki/submodules/flutter/bin/flutter", "build", "ios", "--config-only",
"flutter", "build", "ios", "--config-only",
"--release", "--no-pub", "--no-codesign",
"--build-number", new_build_number.to_s
"--build-number", new_build_number.to_s,
*extra_config_args
)
version = get_version_number(xcodeproj: "Runner.xcodeproj", target: 'Runner')
increment_version_number(
version_number: version
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc')
)
increment_build_number({
@ -77,4 +93,4 @@ latest_testflight_build_number
skip_waiting_for_build_processing: true,
)
end
end
end

View File

@ -20,7 +20,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(),
super(const AuthState.init()) {
super(AuthState.init()) {
on<AuthInitialize>(onInitialize);
on<AuthLogin>(onLogin);
on<AuthLogout>(onLogout);
@ -41,25 +41,20 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
await _authRepository.loggedIn.then((bool loggedIn) async {
if (loggedIn) {
final String? username = await _authRepository.username;
User? user = await _storiesRepository.fetchUser(id: username!);
/// According to Hacker News' API documentation,
/// if user has no public activity (posting a comment or story),
/// then it will not be available from the API.
user ??= User.emptyWithId(username);
final User user =
await _storiesRepository.fetchUserBy(userId: username!);
emit(
state.copyWith(
isLoggedIn: true,
user: user,
status: Status.success,
),
);
} else {
emit(
state.copyWith(
status: AuthStatus.loaded,
isLoggedIn: false,
status: Status.success,
),
);
}
@ -81,7 +76,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
Future<void> onLogin(AuthLogin event, Emitter<AuthState> emit) async {
emit(state.copyWith(status: Status.inProgress));
emit(state.copyWith(status: AuthStatus.loading));
final bool successful = await _authRepository.login(
username: event.username,
@ -89,23 +84,24 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
);
if (successful) {
final User? user = await _storiesRepository.fetchUser(id: event.username);
final User user =
await _storiesRepository.fetchUserBy(userId: event.username);
emit(
state.copyWith(
user: user ?? User.emptyWithId(event.username),
user: user,
isLoggedIn: true,
status: Status.success,
status: AuthStatus.loaded,
),
);
} else {
emit(state.copyWith(status: Status.failure));
emit(state.copyWith(status: AuthStatus.failure));
}
}
Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async {
emit(
state.copyWith(
user: const User.empty(),
user: User.empty(),
isLoggedIn: false,
agreedToEULA: false,
),

View File

@ -1,5 +1,11 @@
part of 'auth_bloc.dart';
enum AuthStatus {
loading,
loaded,
failure,
}
class AuthState extends Equatable {
const AuthState({
required this.user,
@ -8,16 +14,16 @@ class AuthState extends Equatable {
required this.agreedToEULA,
});
const AuthState.init()
: user = const User.empty(),
AuthState.init()
: user = User.empty(),
isLoggedIn = false,
status = Status.success,
status = AuthStatus.loaded,
agreedToEULA = false;
final User user;
final bool isLoggedIn;
final bool agreedToEULA;
final Status status;
final AuthStatus status;
String get username => user.id;
@ -25,7 +31,7 @@ class AuthState extends Equatable {
User? user,
bool? isLoggedIn,
bool? agreedToEULA,
Status? status,
AuthStatus? status,
}) {
return AuthState(
user: user ?? this.user,

View File

@ -17,13 +17,11 @@ part 'stories_state.dart';
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesBloc({
required PreferenceCubit preferenceCubit,
required FilterCubit filterCubit,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository,
PreferenceRepository? preferenceRepository,
Logger? logger,
}) : _preferenceCubit = preferenceCubit,
_filterCubit = filterCubit,
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository =
@ -39,7 +37,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
on<StoryRead>(onStoryRead);
on<StoriesLoaded>(onStoriesLoaded);
on<StoriesDownload>(onDownload);
on<StoriesCancelDownload>(onCancelDownload);
on<StoryDownloaded>(onStoryDownloaded);
on<StoriesExitOffline>(onExitOffline);
on<StoriesPageSizeChanged>(onPageSizeChanged);
@ -47,7 +44,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
}
final PreferenceCubit _preferenceCubit;
final FilterCubit _filterCubit;
final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository;
final PreferenceRepository _preferenceRepository;
@ -59,72 +55,75 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
static const int _tabletSmallPageSize = 15;
static const int _tabletLargePageSize = 25;
/// Types of story to be shown in the tab bar.
static const Set<StoryType> types = <StoryType>{
StoryType.top,
StoryType.best,
StoryType.latest,
StoryType.ask,
StoryType.show,
};
Future<void> onInitialize(
StoriesInitialize event,
Emitter<StoriesState> emit,
) async {
_streamSubscription ??=
_preferenceCubit.stream.listen((PreferenceState event) {
final bool isComplexTile = event.complexStoryTileEnabled;
final int pageSize = getPageSize(isComplexTile: isComplexTile);
final bool isComplexTile = event.showComplexStoryTile;
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
if (pageSize != state.currentPageSize) {
add(StoriesPageSizeChanged(pageSize: pageSize));
}
});
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
final bool isComplexTile = _preferenceCubit.state.complexStoryTileEnabled;
final int pageSize = getPageSize(isComplexTile: isComplexTile);
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
emit(
const StoriesState.init().copyWith(
isOfflineReading: hasCachedStories &&
// Only go into offline mode in the next session.
state.downloadStatus == StoriesDownloadStatus.idle,
offlineReading: hasCachedStories,
currentPageSize: pageSize,
downloadStatus: state.downloadStatus,
storiesDownloaded: state.storiesDownloaded,
storiesToBeDownloaded: state.storiesToBeDownloaded,
),
);
for (final StoryType type in StoryType.values) {
await loadStories(type: type, emit: emit);
for (final StoryType type in types) {
await loadStories(of: type, emit: emit);
}
}
Future<void> loadStories({
required StoryType type,
required StoryType of,
required Emitter<StoriesState> emit,
}) async {
if (state.isOfflineReading) {
final List<int> ids =
await _offlineRepository.getCachedStoryIds(type: type);
if (state.offlineReading) {
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
emit(
state
.copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0),
.copyWithStoryIdsUpdated(of: of, to: ids)
.copyWithCurrentPageUpdated(of: of, to: 0),
);
_offlineRepository
.getCachedStoriesStream(
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
)
.listen((Story story) {
add(StoryLoaded(story: story, type: type));
add(StoryLoaded(story: story, type: of));
}).onDone(() {
add(StoriesLoaded(type: type));
add(StoriesLoaded(type: of));
});
} else {
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
final List<int> ids = await _storiesRepository.fetchStoryIds(of: of);
emit(
state
.copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0),
.copyWithStoryIdsUpdated(of: of, to: ids)
.copyWithCurrentPageUpdated(of: of, to: 0),
);
_storiesRepository
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
.listen((Story story) {
add(StoryLoaded(story: story, type: type));
add(StoryLoaded(story: story, type: of));
}).onDone(() {
add(StoriesLoaded(type: type));
add(StoriesLoaded(type: of));
});
}
}
@ -133,41 +132,37 @@ 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: Status.inProgress,
of: event.type,
to: StoriesStatus.loading,
),
);
if (state.isOfflineReading) {
if (state.offlineReading) {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: Status.success,
of: event.type,
to: StoriesStatus.loaded,
),
);
} else {
emit(state.copyWithRefreshed(type: event.type));
await loadStories(type: event.type, emit: emit);
emit(state.copyWithRefreshed(of: event.type));
await loadStories(of: event.type, emit: emit);
}
}
void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: Status.inProgress,
of: event.type,
to: StoriesStatus.loading,
),
);
final int currentPage = state.currentPageByType[event.type]!;
final int len = state.storyIdsByType[event.type]!.length;
emit(
state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1),
);
emit(state.copyWithCurrentPageUpdated(of: event.type, to: currentPage + 1));
final int currentPageSize = state.currentPageSize;
final int lower = currentPageSize * (currentPage + 1);
int upper = currentPageSize + lower;
@ -177,7 +172,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
upper = len;
}
if (state.isOfflineReading) {
if (state.offlineReading) {
_offlineRepository
.getCachedStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist(
@ -217,8 +212,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
} else {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: Status.success,
of: event.type,
to: StoriesStatus.loaded,
),
);
}
@ -229,24 +224,17 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
Emitter<StoriesState> emit,
) async {
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
final bool hidden = _filterCubit.state.keywords.any(
(String keyword) =>
event.story.title.toLowerCase().contains(keyword) ||
event.story.text.toLowerCase().contains(keyword),
);
emit(
state.copyWithStoryAdded(
type: event.type,
story: event.story.copyWith(hidden: hidden),
of: event.type,
story: event.story,
hasRead: hasRead,
),
);
}
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
emit(
state.copyWithStatusUpdated(type: event.type, to: Status.success),
);
emit(state.copyWithStatusUpdated(of: event.type, to: StoriesStatus.loaded));
}
Future<void> onDownload(
@ -264,15 +252,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _offlineRepository.deleteAllComments();
final Set<int> prioritizedIds = <int>{};
/// Prioritizing all types of stories except StoryType.latest since
/// new stories tend to have less or no comment at all.
final List<StoryType> prioritizedTypes = <StoryType>[...StoryType.values]
final List<StoryType> prioritizedTypes = <StoryType>[...types]
..remove(StoryType.latest);
for (final StoryType type in prioritizedTypes) {
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
await _offlineRepository.cacheStoryIds(type: type, ids: ids);
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
await _offlineRepository.cacheStoryIds(of: type, ids: ids);
prioritizedIds.addAll(ids);
}
@ -292,9 +277,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final Set<int> latestIds = <int>{};
final List<int> ids = await _storiesRepository.fetchStoryIds(
type: StoryType.latest,
of: StoryType.latest,
);
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
latestIds.addAll(ids);
await fetchAndCacheStories(
@ -311,41 +296,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
}
}
Future<void> onCancelDownload(
StoriesCancelDownload event,
Emitter<StoriesState> emit,
) async {
emit(
state.copyWith(
downloadStatus: StoriesDownloadStatus.canceled,
),
);
}
Future<void> fetchAndCacheStories(
Iterable<int> ids, {
required bool includingWebPage,
required bool isPrioritized,
}) async {
final List<StreamSubscription<Comment>> downloadStreams =
<StreamSubscription<Comment>>[];
for (final int id in ids) {
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
_logger.d('aborting downloading');
for (final StreamSubscription<Comment> stream in downloadStreams) {
await stream.cancel();
}
_logger.d('deleting downloaded contents');
await _offlineRepository.deleteAllStoryIds();
await _offlineRepository.deleteAllStories();
await _offlineRepository.deleteAllComments();
break;
}
_logger.d('fetching story $id');
final Story? story = await _storiesRepository.fetchStory(id: id);
final Story? story = await _storiesRepository.fetchStoryBy(id);
if (story == null) {
if (isPrioritized) {
@ -368,37 +325,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _offlineRepository.cacheUrl(url: story.url);
}
/// Not awaiting the completion of comments stream because otherwise
/// it's going to take forever to finish downloading all the stories
/// since we need to make a single http call for each comment.
///
/// In other words, we are prioritizing the story itself instead of
/// the comments in the story.
late final StreamSubscription<Comment>? downloadStream;
downloadStream = _storiesRepository
_storiesRepository
.fetchAllChildrenComments(ids: story.kids)
.whereType<Comment>()
.listen(
(Comment comment) {
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
_logger.d('aborting downloading from comments stream');
downloadStream?.cancel();
return;
}
_logger.d('fetched comment ${comment.id}');
unawaited(
_offlineRepository.cacheComment(comment: comment),
);
},
)..onDone(() {
_logger.d(
'''finished downloading story ${story.id} with ${story.descendants} comments''',
);
add(StoryDownloaded(skipped: false));
});
downloadStreams.add(downloadStream);
(Comment comment) => unawaited(
_offlineRepository.cacheComment(comment: comment),
),
)
.onDone(() => add(StoryDownloaded(skipped: false)));
}
}
@ -439,6 +374,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesPageSizeChanged event,
Emitter<StoriesState> emit,
) async {
emit(const StoriesState.init());
add(StoriesInitialize());
}
@ -450,7 +386,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _offlineRepository.deleteAllStories();
await _offlineRepository.deleteAllComments();
await _offlineRepository.deleteAllWebPages();
emit(state.copyWith(isOfflineReading: false));
emit(state.copyWith(offlineReading: false));
add(StoriesInitialize());
}
@ -482,7 +418,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
bool hasRead(Story story) => state.readStoriesIds.contains(story.id);
int getPageSize({required bool isComplexTile}) {
int _getPageSize({required bool isComplexTile}) {
int pageSize = isComplexTile ? _smallPageSize : _largePageSize;
if (deviceScreenType != DeviceScreenType.mobile) {

View File

@ -46,13 +46,6 @@ class StoriesDownload extends StoriesEvent {
List<Object?> get props => <Object?>[includingWebPage];
}
class StoriesCancelDownload extends StoriesEvent {
StoriesCancelDownload();
@override
List<Object?> get props => <Object?>[];
}
class StoryDownloaded extends StoriesEvent {
StoryDownloaded({required this.skipped});

View File

@ -1,11 +1,16 @@
part of 'stories_bloc.dart';
enum StoriesStatus {
initial,
loading,
loaded,
}
enum StoriesDownloadStatus {
idle,
initial,
downloading,
finished,
failure,
canceled,
}
class StoriesState extends Equatable {
@ -15,7 +20,7 @@ class StoriesState extends Equatable {
required this.statusByType,
required this.currentPageByType,
required this.readStoriesIds,
required this.isOfflineReading,
required this.offlineReading,
required this.downloadStatus,
required this.currentPageSize,
required this.storiesDownloaded,
@ -29,6 +34,7 @@ class StoriesState extends Equatable {
StoryType.latest: <Story>[],
StoryType.ask: <Story>[],
StoryType.show: <Story>[],
StoryType.jobs: <Story>[],
},
this.storyIdsByType = const <StoryType, List<int>>{
StoryType.top: <int>[],
@ -36,13 +42,15 @@ class StoriesState extends Equatable {
StoryType.latest: <int>[],
StoryType.ask: <int>[],
StoryType.show: <int>[],
StoryType.jobs: <int>[],
},
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.statusByType = const <StoryType, StoriesStatus>{
StoryType.top: StoriesStatus.initial,
StoryType.best: StoriesStatus.initial,
StoryType.latest: StoriesStatus.initial,
StoryType.ask: StoriesStatus.initial,
StoryType.show: StoriesStatus.initial,
StoryType.jobs: StoriesStatus.initial,
},
this.currentPageByType = const <StoryType, int>{
StoryType.top: 0,
@ -50,9 +58,10 @@ class StoriesState extends Equatable {
StoryType.latest: 0,
StoryType.ask: 0,
StoryType.show: 0,
StoryType.jobs: 0,
},
}) : isOfflineReading = false,
downloadStatus = StoriesDownloadStatus.idle,
}) : offlineReading = false,
downloadStatus = StoriesDownloadStatus.initial,
currentPageSize = 0,
readStoriesIds = const <int>{},
storiesDownloaded = 0,
@ -60,11 +69,11 @@ class StoriesState extends Equatable {
final Map<StoryType, List<Story>> storiesByType;
final Map<StoryType, List<int>> storyIdsByType;
final Map<StoryType, Status> statusByType;
final Map<StoryType, StoriesStatus> statusByType;
final Map<StoryType, int> currentPageByType;
final Set<int> readStoriesIds;
final StoriesDownloadStatus downloadStatus;
final bool isOfflineReading;
final bool offlineReading;
final int currentPageSize;
final int storiesDownloaded;
final int storiesToBeDownloaded;
@ -72,11 +81,11 @@ class StoriesState extends Equatable {
StoriesState copyWith({
Map<StoryType, List<Story>>? storiesByType,
Map<StoryType, List<int>>? storyIdsByType,
Map<StoryType, Status>? statusByType,
Map<StoryType, StoriesStatus>? statusByType,
Map<StoryType, int>? currentPageByType,
Set<int>? readStoriesIds,
StoriesDownloadStatus? downloadStatus,
bool? isOfflineReading,
bool? offlineReading,
int? currentPageSize,
int? storiesDownloaded,
int? storiesToBeDownloaded,
@ -87,7 +96,7 @@ class StoriesState extends Equatable {
statusByType: statusByType ?? this.statusByType,
currentPageByType: currentPageByType ?? this.currentPageByType,
readStoriesIds: readStoriesIds ?? this.readStoriesIds,
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
offlineReading: offlineReading ?? this.offlineReading,
downloadStatus: downloadStatus ?? this.downloadStatus,
currentPageSize: currentPageSize ?? this.currentPageSize,
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
@ -97,13 +106,13 @@ class StoriesState extends Equatable {
}
StoriesState copyWithStoryAdded({
required StoryType type,
required StoryType of,
required Story story,
required bool hasRead,
}) {
final Map<StoryType, List<Story>> newMap =
Map<StoryType, List<Story>>.from(storiesByType);
newMap[type] = List<Story>.from(newMap[type]!)..add(story);
newMap[of] = List<Story>.from(newMap[of]!)..add(story);
return copyWith(
storiesByType: newMap,
readStoriesIds: <int>{
@ -114,54 +123,54 @@ class StoriesState extends Equatable {
}
StoriesState copyWithStoryIdsUpdated({
required StoryType type,
required StoryType of,
required List<int> to,
}) {
final Map<StoryType, List<int>> newMap =
Map<StoryType, List<int>>.from(storyIdsByType);
newMap[type] = to;
newMap[of] = to;
return copyWith(
storyIdsByType: newMap,
);
}
StoriesState copyWithStatusUpdated({
required StoryType type,
required Status to,
required StoryType of,
required StoriesStatus to,
}) {
final Map<StoryType, Status> newMap =
Map<StoryType, Status>.from(statusByType);
newMap[type] = to;
final Map<StoryType, StoriesStatus> newMap =
Map<StoryType, StoriesStatus>.from(statusByType);
newMap[of] = to;
return copyWith(
statusByType: newMap,
);
}
StoriesState copyWithCurrentPageUpdated({
required StoryType type,
required StoryType of,
required int to,
}) {
final Map<StoryType, int> newMap =
Map<StoryType, int>.from(currentPageByType);
newMap[type] = to;
newMap[of] = to;
return copyWith(
currentPageByType: newMap,
);
}
StoriesState copyWithRefreshed({required StoryType type}) {
StoriesState copyWithRefreshed({required StoryType of}) {
final Map<StoryType, List<Story>> newStoriesMap =
Map<StoryType, List<Story>>.from(storiesByType);
newStoriesMap[type] = <Story>[];
newStoriesMap[of] = <Story>[];
final Map<StoryType, List<int>> newStoryIdsMap =
Map<StoryType, List<int>>.from(storyIdsByType);
newStoryIdsMap[type] = <int>[];
final Map<StoryType, Status> newStatusMap =
Map<StoryType, Status>.from(statusByType);
newStatusMap[type] = Status.inProgress;
newStoryIdsMap[of] = <int>[];
final Map<StoryType, StoriesStatus> newStatusMap =
Map<StoryType, StoriesStatus>.from(statusByType);
newStatusMap[of] = StoriesStatus.loading;
final Map<StoryType, int> newCurrentPageMap =
Map<StoryType, int>.from(currentPageByType);
newCurrentPageMap[type] = 0;
newCurrentPageMap[of] = 0;
return copyWith(
storiesByType: newStoriesMap,
storyIdsByType: newStoryIdsMap,
@ -177,7 +186,7 @@ class StoriesState extends Equatable {
statusByType,
currentPageByType,
readStoriesIds,
isOfflineReading,
offlineReading,
downloadStatus,
currentPageSize,
storiesDownloaded,

View File

@ -1,26 +1,15 @@
import 'package:hacki/extensions/extensions.dart';
abstract class Constants {
static const String endUserAgreementLink =
'https://github.com/Livinglist/Hacki/blob/master/assets/eula.md';
static const String privacyPolicyLink =
'https://github.com/Livinglist/Hacki/blob/master/assets/privacy_policy.md';
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
static const String hackerNewsLogoLink =
'https://pbs.twimg.com/profile_images/469397708986269696/iUrYEOpJ_400x400.png';
static const String portfolioLink = 'https://github.com/Livinglist';
static const String portfolioLink = 'https://livinglist.github.io';
static const String githubLink = 'https://github.com/Livinglist/Hacki';
static const String appStoreLink =
'https://apps.apple.com/us/app/hacki/id1602043763?action=write-review';
static const String googlePlayLink =
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
static const String sponsorLink = 'https://github.com/sponsors/Livinglist';
static const String guidelineLink =
'https://news.ycombinator.com/newsguidelines.html';
static const String githubIssueLink =
'$githubLink/issues/new?title=Found+a+bug+in+Hacki&body=Please+describe+the+problem.';
static const String wikipediaLink = 'https://en.wikipedia.org/wiki/';
static const String wiktionaryLink = 'https://en.wiktionary.org/wiki/';
static const String supportEmail = 'georgefung98@gmail.com';
static const String _imagePath = 'assets/images';
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
@ -31,19 +20,23 @@ abstract class Constants {
'$_imagePath/comment_tile_right_slide.png';
static const String commentTileTopTapPath =
'$_imagePath/comment_tile_top_tap.png';
static const String logFilename = 'hacki_log.txt';
static const String previousLogFileName = 'old_hacki_log.txt';
static final String happyFace = <String>[
/// Feature ids for feature discovery.
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
static const String featureLogIn = 'log_in';
static const String featurePinToTop = 'pin_to_top';
static const List<String> happyFaces = <String>[
'(๑•̀ㅂ•́)و✧',
'( ͡• ͜ʖ ͡•)',
'( ͡~ ͜ʖ ͡°)',
'٩(˘◡˘)۶',
'(─‿‿─)',
'(¬‿¬)',
].pickRandomly()!;
];
static final String sadFace = <String>[
static const List<String> sadFaces = <String>[
'ಥ_ಥ',
'(╯°□°)╯︵ ┻━┻',
r'¯\_(ツ)_/¯',
@ -53,32 +46,5 @@ abstract class Constants {
'(ㆆ_ㆆ)',
'ʕ•́ᴥ•̀ʔっ',
'(ㆆ_ㆆ)',
].pickRandomly()!;
static final String magicWord = <String>[
'to be over the rainbow!',
'to infinity and beyond!',
'to see the future.',
].pickRandomly()!;
static final String errorMessage = 'Something went wrong...$sadFace';
static final String loginErrorMessage =
'''Failed to log in $sadFace, this could happen if your account requires a CAPTCHA, please try logging in inside a browser to see if this is the case, if so, you may try logging in here again later after CAPTCHA is no longer needed.''';
}
abstract class RegExpConstants {
static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
static const String number = '[0-9]+';
}
abstract class Durations {
static const Duration ms100 = Duration(milliseconds: 100);
static const Duration ms200 = Duration(milliseconds: 200);
static const Duration ms300 = Duration(milliseconds: 300);
static const Duration ms400 = Duration(milliseconds: 400);
static const Duration ms500 = Duration(milliseconds: 500);
static const Duration ms600 = Duration(milliseconds: 600);
static const Duration oneSecond = Duration(seconds: 1);
static const Duration twoSeconds = Duration(seconds: 2);
static const Duration tenSeconds = Duration(seconds: 10);
];
}

View File

@ -2,7 +2,7 @@ import 'package:logger/logger.dart';
class CustomLogFilter extends LogFilter {
@override
Level? get level => Level.trace;
Level? get level => Level.verbose;
/// The minimal level allowed in production.
static const Level _minimalLevel = Level.info;

View File

@ -1,76 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/screens.dart';
final GoRouter router = GoRouter(
observers: <NavigatorObserver>[
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
],
initialLocation: HomeScreen.routeName,
routes: <RouteBase>[
GoRoute(
path: HomeScreen.routeName,
builder: (_, __) => const HomeScreen(),
routes: <RouteBase>[
GoRoute(
path: ItemScreen.routeName,
builder: (_, GoRouterState state) {
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
if (args == null) {
throw GoError("args can't be null");
}
return ItemScreen.phone(args);
},
/// 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: const Center(
child: Text('Something went wrong!'),
),
],
),
GoRoute(
path: '/${ItemScreen.routeName}',
builder: (_, GoRouterState state) {
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
if (args == null) {
throw GoError("args can't be null");
}
return ItemScreen.phone(args);
},
),
GoRoute(
path: '/${SubmitScreen.routeName}',
builder: (_, __) => BlocProvider<SubmitCubit>(
create: (_) => SubmitCubit(),
child: const SubmitScreen(),
),
),
GoRoute(
path: '/${QrCodeScannerScreen.routeName}',
builder: (_, __) => const QrCodeScannerScreen(),
),
GoRoute(
path: '/${QrCodeViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? data = state.extra as String?;
if (data == null) {
throw GoError("data can't be null");
}
return QrCodeViewScreen(
data: data,
);
},
),
GoRoute(
path: '/${WebViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? link = state.extra as String?;
if (link == null) {
throw GoError("link can't be null");
}
return WebViewScreen(
url: link,
);
},
),
],
);
);
}
}

View File

@ -1,41 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:logger/logger.dart';
/// Writes the log output to a file.
/// Temporary solution to not being able to access
// ignore: comment_references
/// the original [FileOutput] from [Logger]
class CustomFileOutput extends LogOutput {
CustomFileOutput({
required this.file,
this.overrideExisting = false,
this.encoding = utf8,
});
final File file;
final bool overrideExisting;
final Encoding encoding;
IOSink? _sink;
@override
Future<void> init() async {
_sink = file.openWrite(
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
encoding: encoding,
);
}
@override
void output(OutputEvent event) {
_sink?.writeAll(event.lines, '\n');
_sink?.writeln();
}
@override
Future<void> destroy() async {
await _sink?.flush();
await _sink?.close();
}
}

View File

@ -1,11 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:hacki/config/custom_log_filter.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/services/services.dart';
import 'package:hacki/utils/utils.dart';
import 'package:logger/logger.dart';
/// Global [GetIt.instance].
@ -13,16 +10,8 @@ final GetIt locator = GetIt.instance;
/// Set up [GetIt] locator.
Future<void> setUpLocator() async {
final File logOutputFile = await LogUtil.initLogFile();
locator
..registerSingleton<Logger>(
Logger(
filter: CustomLogFilter(),
printer: LogUtil.logPrinter,
output: LogUtil.logOutput(logOutputFile),
),
)
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()))
..registerSingleton<StoriesRepository>(StoriesRepository())
..registerSingleton<PreferenceRepository>(PreferenceRepository())
..registerSingleton<SearchRepository>(SearchRepository())
@ -32,8 +21,7 @@ Future<void> setUpLocator() async {
..registerSingleton<OfflineRepository>(OfflineRepository())
..registerSingleton<DraftCache>(DraftCache())
..registerSingleton<CommentCache>(CommentCache())
..registerSingleton<LocalNotificationService>(LocalNotificationService())
..registerSingleton(AppReviewService())
..registerSingleton<LocalNotification>(LocalNotification())
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
RouteObserver<ModalRoute<dynamic>>(),
);

View File

@ -43,12 +43,12 @@ class CollapseCubit extends Cubit<CollapseState> {
),
);
} else {
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
final int count = _collapseCache.collapse(_commentId);
emit(
state.copyWith(
collapsed: true,
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
collapsedCount: state.collapsed ? 0 : count,
),
);
}

View File

@ -1,41 +1,33 @@
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:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:flutter/services.dart';
import 'package:flutter_linkify/flutter_linkify.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/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';
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,
}) : _filterCubit = filterCubit,
_collapseCache = collapseCache,
required bool offlineReading,
required Item item,
required FetchMode defaultFetchMode,
required CommentsOrder defaultCommentsOrder,
}) : _collapseCache = collapseCache,
_commentCache = commentCache ?? locator.get<CommentCache>(),
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
@ -46,14 +38,13 @@ class CommentsCubit extends Cubit<CommentsState> {
_logger = logger ?? locator.get<Logger>(),
super(
CommentsState.init(
isOfflineReading: isOfflineReading,
offlineReading: offlineReading,
item: item,
fetchMode: defaultFetchMode,
order: defaultCommentsOrder,
),
);
final FilterCubit _filterCubit;
final CollapseCache _collapseCache;
final CommentCache _commentCache;
final OfflineRepository _offlineRepository;
@ -70,6 +61,8 @@ 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) {
@ -80,24 +73,22 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> init({
bool onlyShowTargetComment = false,
bool useCommentCache = false,
List<Comment>? targetAncestors,
List<Comment>? targetParents,
}) async {
if (onlyShowTargetComment && (targetAncestors?.isNotEmpty ?? false)) {
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
emit(
state.copyWith(
comments: targetAncestors,
comments: targetParents,
onlyShowTargetComment: true,
status: CommentsStatus.allLoaded,
status: CommentsStatus.loaded,
),
);
_streamSubscription = _storiesRepository
.fetchAllCommentsRecursivelyStream(
ids: targetAncestors!.last.kids,
level: targetAncestors.last.level + 1,
ids: targetParents!.last.kids,
level: targetParents.last.level + 1,
)
.asyncMap(_toBuildableComment)
.whereNotNull()
.listen(_onCommentFetched)
..onDone(_onDone);
@ -106,63 +97,65 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(
state.copyWith(
status: CommentsStatus.inProgress,
status: CommentsStatus.loading,
comments: <Comment>[],
currentPage: 0,
),
);
final Item item = state.item;
final Item updatedItem = state.isOfflineReading
final Item updatedItem = state.offlineReading
? item
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
item;
final List<int> kids = _sortKids(updatedItem.kids);
: await _storiesRepository.fetchItemBy(id: item.id) ?? item;
final List<int> kids = sortKids(updatedItem.kids);
emit(state.copyWith(item: updatedItem));
late final Stream<Comment> commentStream;
if (state.isOfflineReading) {
commentStream = _offlineRepository.getCachedCommentsStream(ids: kids);
if (state.offlineReading) {
_streamSubscription = _offlineRepository
.getCachedCommentsStream(ids: kids)
.listen(_onCommentFetched)
..onDone(_onDone);
} else {
switch (state.fetchMode) {
case FetchMode.lazy:
commentStream = _storiesRepository.fetchCommentsStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
_streamSubscription = _storiesRepository
.fetchCommentsStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
)
.listen(_onCommentFetched)
..onDone(_onDone);
break;
case FetchMode.eager:
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
_streamSubscription = _storiesRepository
.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
)
.listen(_onCommentFetched)
..onDone(_onDone);
break;
}
}
_streamSubscription = commentStream
.asyncMap(_toBuildableComment)
.whereNotNull()
.listen(_onCommentFetched)
..onDone(_onDone);
}
Future<void> refresh() async {
emit(
state.copyWith(
status: CommentsStatus.inProgress,
),
);
if (state.isOfflineReading) {
if (state.offlineReading) {
emit(
state.copyWith(
status: CommentsStatus.allLoaded,
status: CommentsStatus.loaded,
),
);
return;
}
emit(
state.copyWith(
status: CommentsStatus.loading,
),
);
_collapseCache.resetCollapsedComments();
await _streamSubscription?.cancel();
@ -180,35 +173,35 @@ class CommentsCubit extends Cubit<CommentsState> {
final Item item = state.item;
final Item updatedItem =
await _storiesRepository.fetchItem(id: item.id) ?? item;
final List<int> kids = _sortKids(updatedItem.kids);
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
final List<int> kids = sortKids(updatedItem.kids);
late final Stream<Comment> commentStream;
if (state.fetchMode == FetchMode.lazy) {
commentStream = _storiesRepository.fetchCommentsStream(
ids: kids,
);
_streamSubscription = _storiesRepository
.fetchCommentsStream(
ids: kids,
)
.listen(_onCommentFetched)
..onDone(_onDone);
} else {
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
);
_streamSubscription = _storiesRepository
.fetchAllCommentsRecursivelyStream(
ids: kids,
)
.listen(_onCommentFetched)
..onDone(_onDone);
}
_streamSubscription = commentStream
.asyncMap(_toBuildableComment)
.whereNotNull()
.listen(_onCommentFetched)
..onDone(_onDone);
emit(
state.copyWith(
item: updatedItem,
status: CommentsStatus.loaded,
),
);
}
void loadAll(Story story) {
HapticFeedbackUtil.light();
HapticFeedback.lightImpact();
emit(
state.copyWith(
onlyShowTargetComment: false,
@ -219,13 +212,7 @@ class CommentsCubit extends Cubit<CommentsState> {
}
/// [comment] is only used for lazy fetching.
void loadMore({
Comment? comment,
void Function(Comment)? onCommentFetched,
VoidCallback? onDone,
}) {
if (comment == null && state.status == CommentsStatus.inProgress) return;
void loadMore({Comment? comment}) {
switch (state.fetchMode) {
case FetchMode.lazy:
if (comment == null) return;
@ -239,18 +226,23 @@ class CommentsCubit extends Cubit<CommentsState> {
final StreamSubscription<Comment> streamSubscription =
_storiesRepository
.fetchCommentsStream(ids: comment.kids)
.asyncMap(_toBuildableComment)
.whereNotNull()
.listen((Comment cmt) {
_collapseCache.addKid(cmt.id, to: cmt.parent);
_commentCache.cacheComment(cmt);
_sembastRepository.cacheComment(cmt);
final List<LinkifyElement> elements = _linkify(
cmt.text,
);
final BuildableComment buildableComment =
BuildableComment.fromComment(cmt, elements: elements);
emit(
state.copyWith(
comments: <Comment>[...state.comments]..insert(
state.comments.indexOf(comment) + offset + 1,
cmt.copyWith(level: level),
buildableComment.copyWith(level: level),
),
),
);
@ -267,28 +259,28 @@ class CommentsCubit extends Cubit<CommentsState> {
});
_streamSubscriptions[comment.id] = streamSubscription;
break;
case FetchMode.eager:
if (_streamSubscription != null) {
emit(state.copyWith(status: CommentsStatus.inProgress));
_streamSubscription
?..resume()
..onData(onCommentFetched);
emit(state.copyWith(status: CommentsStatus.loading));
_streamSubscription?.resume();
}
break;
}
}
Future<void> loadParentThread() async {
HapticFeedbackUtil.light();
emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress));
final Item? parent =
await _storiesRepository.fetchItem(id: state.item.parent);
unawaited(HapticFeedback.lightImpact());
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
final Story? parent =
await _storiesRepository.fetchParentStory(id: state.item.id);
if (parent == null) {
return;
} else {
await router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: parent),
await HackiApp.navigatorKey.currentState?.pushNamed(
ItemScreen.routeName,
arguments: ItemScreenArgs(item: parent),
);
emit(
@ -299,33 +291,10 @@ 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;
HapticFeedbackUtil.selection();
HapticFeedback.selectionClick();
_streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
@ -339,7 +308,7 @@ class CommentsCubit extends Cubit<CommentsState> {
if (fetchMode == null) return;
if (state.fetchMode == fetchMode) return;
_collapseCache.resetCollapsedComments();
HapticFeedbackUtil.selection();
HapticFeedback.selectionClick();
_streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
@ -349,84 +318,7 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true);
}
/// 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) {
List<int> sortKids(List<int> kids) {
switch (state.order) {
case CommentsOrder.natural:
return kids;
@ -447,69 +339,71 @@ class CommentsCubit extends Cubit<CommentsState> {
);
}
void _onCommentFetched(BuildableComment? comment) {
void _onCommentFetched(Comment? comment) {
if (comment != null) {
_collapseCache.addKid(comment.id, to: comment.parent);
_commentCache.cacheComment(comment);
_sembastRepository.cacheComment(comment);
final bool hidden = _filterCubit.state.keywords.any(
(String keyword) => comment.text.toLowerCase().contains(keyword),
final List<LinkifyElement> elements = _linkify(
comment.text,
);
final BuildableComment buildableComment =
BuildableComment.fromComment(comment, elements: elements);
final List<Comment> updatedComments = <Comment>[
...state.comments,
comment.copyWith(hidden: hidden),
buildableComment
];
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(
currentPage: state.currentPage + 1,
status: CommentsStatus.loaded,
),
);
}
}
}
}
static Future<Item?> _toBuildable(Item? item) async {
if (item == null) return null;
static List<LinkifyElement> _linkify(
String text, {
LinkifyOptions options = const LinkifyOptions(),
List<Linkifier> linkifiers = const <Linkifier>[
UrlLinkifier(),
EmailLinkifier(),
],
}) {
List<LinkifyElement> list = <LinkifyElement>[TextElement(text)];
switch (item.runtimeType) {
case Comment:
return _toBuildableComment(item as Comment);
case Story:
return _toBuildableStory(item as Story);
if (text.isEmpty) {
return <LinkifyElement>[];
}
return null;
}
static Future<BuildableComment?> _toBuildableComment(Comment? comment) async {
if (comment == null) return null;
final List<LinkifyElement> elements =
await compute<String, List<LinkifyElement>>(
LinkifierUtil.linkify,
comment.text,
);
final BuildableComment buildableComment =
BuildableComment.fromComment(comment, elements: elements);
return buildableComment;
}
static Future<BuildableStory?> _toBuildableStory(Story? story) async {
if (story == null) {
return null;
} else if (story.text.isEmpty) {
return BuildableStory.fromTitleOnlyStory(story);
if (linkifiers.isEmpty) {
return list;
}
final List<LinkifyElement> elements =
await compute<String, List<LinkifyElement>>(
LinkifierUtil.linkify,
story.text,
);
for (final Linkifier linkifier in linkifiers) {
list = linkifier.parse(list, options);
}
final BuildableStory buildableStory =
BuildableStory.fromStory(story, elements: elements);
return buildableStory;
return list;
}
@override

View File

@ -1,11 +1,22 @@
part of 'comments_cubit.dart';
enum CommentsStatus {
idle,
inProgress,
init,
loading,
loaded,
allLoaded,
error,
failure,
}
enum CommentsOrder {
natural,
newestFirst,
oldestFirst,
}
enum FetchMode {
lazy,
eager,
}
class CommentsState extends Equatable {
@ -14,23 +25,21 @@ 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,
required this.isOfflineReading,
required this.offlineReading,
required this.currentPage,
});
CommentsState.init({
required this.isOfflineReading,
required this.offlineReading,
required this.item,
required this.fetchMode,
required this.order,
}) : comments = <Comment>[],
status = CommentsStatus.idle,
fetchParentStatus = CommentsStatus.idle,
fetchRootStatus = CommentsStatus.idle,
status = CommentsStatus.init,
fetchParentStatus = CommentsStatus.init,
onlyShowTargetComment = false,
currentPage = 0;
@ -38,11 +47,10 @@ 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;
final bool isOfflineReading;
final bool offlineReading;
final int currentPage;
CommentsState copyWith({
@ -50,24 +58,22 @@ class CommentsState extends Equatable {
List<Comment>? comments,
CommentsStatus? status,
CommentsStatus? fetchParentStatus,
CommentsStatus? fetchRootStatus,
CommentsOrder? order,
FetchMode? fetchMode,
bool? onlyShowTargetComment,
bool? isOfflineReading,
bool? offlineReading,
int? currentPage,
}) {
return CommentsState(
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,
onlyShowTargetComment:
onlyShowTargetComment ?? this.onlyShowTargetComment,
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
offlineReading: offlineReading ?? this.offlineReading,
currentPage: currentPage ?? this.currentPage,
);
}
@ -77,14 +83,13 @@ class CommentsState extends Equatable {
@override
List<Object?> get props => <Object?>[
item,
comments,
status,
fetchParentStatus,
fetchRootStatus,
order,
fetchMode,
onlyShowTargetComment,
isOfflineReading,
offlineReading,
currentPage,
comments,
];
}

View File

@ -3,7 +3,6 @@ export 'collapse/collapse_cubit.dart';
export 'comments/comments_cubit.dart';
export 'edit/edit_cubit.dart';
export 'fav/fav_cubit.dart';
export 'filter/filter_cubit.dart';
export 'history/history_cubit.dart';
export 'notification/notification_cubit.dart';
export 'pin/pin_cubit.dart';
@ -14,7 +13,6 @@ export 'reminder/reminder_cubit.dart';
export 'search/search_cubit.dart';
export 'split_view/split_view_cubit.dart';
export 'submit/submit_cubit.dart';
export 'tab/tab_cubit.dart';
export 'time_machine/time_machine_cubit.dart';
export 'user/user_cubit.dart';
export 'vote/vote_cubit.dart';

View File

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

View File

@ -51,7 +51,7 @@ class FavCubit extends Cubit<FavState> {
.onDone(() {
emit(
state.copyWith(
status: Status.success,
status: FavStatus.loaded,
),
);
});
@ -73,7 +73,7 @@ class FavCubit extends Cubit<FavState> {
),
);
final Item? item = await _storiesRepository.fetchItem(id: id);
final Item? item = await _storiesRepository.fetchItemBy(id: id);
if (item == null) return;
@ -107,7 +107,7 @@ class FavCubit extends Cubit<FavState> {
}
void loadMore() {
emit(state.copyWith(status: Status.inProgress));
emit(state.copyWith(status: FavStatus.loading));
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: Status.success));
emit(state.copyWith(status: FavStatus.loaded));
});
} else {
emit(state.copyWith(status: Status.success));
emit(state.copyWith(status: FavStatus.loaded));
}
}
@ -140,7 +140,7 @@ class FavCubit extends Cubit<FavState> {
emit(
state.copyWith(
status: Status.inProgress,
status: FavStatus.loading,
currentPage: 0,
favItems: <Item>[],
favIds: <int>[],
@ -155,18 +155,11 @@ class FavCubit extends Cubit<FavState> {
)
.listen(_onItemLoaded)
.onDone(() {
emit(state.copyWith(status: Status.success));
emit(state.copyWith(status: FavStatus.loaded));
});
});
}
void removeAll() {
_preferenceRepository
..clearAllFavs(username: '')
..clearAllFavs(username: _authBloc.state.username);
emit(FavState.init());
}
void _onItemLoaded(Item item) {
emit(
state.copyWith(

View File

@ -1,5 +1,12 @@
part of 'fav_cubit.dart';
enum FavStatus {
init,
loading,
loaded,
failure,
}
class FavState extends Equatable {
const FavState({
required this.favIds,
@ -11,18 +18,18 @@ class FavState extends Equatable {
FavState.init()
: favIds = <int>[],
favItems = <Item>[],
status = Status.idle,
status = FavStatus.init,
currentPage = 0;
final List<int> favIds;
final List<Item> favItems;
final Status status;
final FavStatus status;
final int currentPage;
FavState copyWith({
List<int>? favIds,
List<Item>? favItems,
Status? status,
FavStatus? status,
int? currentPage,
}) {
return FavState(
@ -35,9 +42,9 @@ class FavState extends Equatable {
@override
List<Object?> get props => <Object?>[
status,
currentPage,
favIds,
favItems,
status,
currentPage,
];
}

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