Compare commits
117 Commits
Author | SHA1 | Date | |
---|---|---|---|
e33ff417fb | |||
d8922c2641 | |||
c6e0461857 | |||
30ca356dc8 | |||
7d11398e6d | |||
a4f52284ef | |||
c7d1a42d5a | |||
f83fd66bcc | |||
c2ec3647e2 | |||
ba63852b7d | |||
438041183c | |||
114540edd7 | |||
588b3e9508 | |||
2f0376f8f8 | |||
ab4051c018 | |||
c230c21218 | |||
c24e12237e | |||
e15dcba93b | |||
1362b93a74 | |||
ac18793f98 | |||
e52f65c773 | |||
06212a0d72 | |||
e77c0e3e73 | |||
cb6f41ec49 | |||
ab1e90ccad | |||
0ca3e96d91 | |||
d1c8eed3de | |||
aa6a2c684c | |||
d4778d9530 | |||
c702e08481 | |||
2af10391bc | |||
c420dd3ca4 | |||
da7d0757cd | |||
32ae2087bc | |||
0b5329d050 | |||
c375def289 | |||
3469543c7b | |||
ab755581fd | |||
6b75eb8549 | |||
36ded8a8e3 | |||
582ac7b0be | |||
e5e3391785 | |||
9159fe0fe1 | |||
7c51bad35e | |||
6836138d11 | |||
2f71964277 | |||
c24c5c1b7a | |||
755b112382 | |||
d44b64d249 | |||
35ed917e66 | |||
15b75ef37c | |||
f39408fbcc | |||
ca2f063297 | |||
1ad231adbb | |||
60b09fd81e | |||
fe162208ca | |||
58139ba7a3 | |||
33a31acbe2 | |||
0fcfcbb7e3 | |||
a98f52c90b | |||
8e8e48c44a | |||
603b7cc939 | |||
649fa33df3 | |||
81d4a0f2df | |||
24112a471e | |||
c7824eaef3 | |||
c2b66d29c3 | |||
e0a53e44b2 | |||
4cf8379db0 | |||
c1c26bf0e0 | |||
29e2f4163d | |||
c3de80015d | |||
436cd9ce8b | |||
efb326be68 | |||
047903fe24 | |||
41068ddf89 | |||
196516ce85 | |||
7f647b127d | |||
a50a0874e7 | |||
b176be96fb | |||
1e5af07691 | |||
ecf8c902dc | |||
d3ede8546b | |||
53562ad260 | |||
6c8e7a7cb9 | |||
56c0245335 | |||
0cbd38a530 | |||
7c6da2c36a | |||
185140feb4 | |||
03c01a0b78 | |||
f823fdf241 | |||
fe87ddd8ff | |||
613ba12b05 | |||
8d7f66ecbc | |||
461aae253b | |||
a1b491cf0d | |||
edf0c82040 | |||
946a3c5a9a | |||
d8bc60c071 | |||
48477cd5c8 | |||
38df6293fe | |||
a5fe9e45fc | |||
9de5baa77a | |||
2daccd64e8 | |||
d0c68f9419 | |||
5f1dbfc510 | |||
90eee37c17 | |||
5630e61a74 | |||
eaad4b01dd | |||
3ab172f3d3 | |||
5450eba64b | |||
e2d6bb44d0 | |||
ffbd3a2449 | |||
2405a6d30c | |||
01085e5fd3 | |||
b5e11a72bf | |||
f55bbb6f84 |
19
.github/workflows/commit_check.yml
vendored
@ -4,19 +4,20 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- '!master'
|
||||
|
||||
jobs:
|
||||
releases:
|
||||
name: Check commit
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FLUTTER_VERSION: "3.3.4"
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v2
|
||||
- name: checkout all the submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
flutter-version: '3.3.4'
|
||||
channel: 'stable'
|
||||
- run: flutter pub get
|
||||
- run: flutter format --set-exit-if-changed .
|
||||
- run: flutter analyze
|
||||
submodules: recursive
|
||||
- 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
|
30
.github/workflows/publish_ios.yml
vendored
@ -6,13 +6,12 @@ on:
|
||||
# Run the workflow whenever a new tag named 'v*' is pushed
|
||||
push:
|
||||
branches:
|
||||
- "!*"
|
||||
tags:
|
||||
- "v*"
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build_and_publish:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
# Point the `ruby/setup-ruby` action at this Gemfile, so it
|
||||
@ -21,21 +20,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out from git
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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
|
||||
|
||||
# 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,11 +43,10 @@ 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 "build_name:${{ github.ref_name }}"
|
||||
run: cd ios && bundle exec fastlane beta
|
||||
|
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "flutter"]
|
||||
path = submodules/flutter
|
||||
url = https://github.com/flutter/flutter
|
@ -1,4 +1,4 @@
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||
include: package:very_good_analysis/analysis_options.5.0.0.yaml
|
||||
linter:
|
||||
rules:
|
||||
parameter_assignments: false
|
||||
@ -6,4 +6,8 @@ linter:
|
||||
library_private_types_in_public_api: false
|
||||
omit_local_variable_types: false
|
||||
one_member_abstracts: false
|
||||
always_specify_types: true
|
||||
always_specify_types: true
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- "submodules/**"
|
||||
|
@ -64,12 +64,15 @@ android {
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
flutter {
|
||||
@ -79,3 +82,14 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,15 +37,6 @@
|
||||
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"/>
|
||||
|
@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 4.7 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 940 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 5.4 KiB |
@ -24,6 +24,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
18
assets/eula.md
Normal file
@ -0,0 +1,18 @@
|
||||
## 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.
|
BIN
assets/fonts/noto_serif/NotoSerif-Bold.ttf
Normal file
BIN
assets/fonts/noto_serif/NotoSerif-Regular.ttf
Normal file
BIN
assets/fonts/roboto_slab/RobotoSlab-Bold.ttf
Normal file
BIN
assets/fonts/roboto_slab/RobotoSlab-Regular.ttf
Normal file
BIN
assets/fonts/ubuntu/Ubuntu-Bold.ttf
Normal file
BIN
assets/fonts/ubuntu/Ubuntu-Regular.ttf
Normal file
BIN
assets/fonts/ubuntu_mono/UbuntuMono-Bold.ttf
Normal file
BIN
assets/fonts/ubuntu_mono/UbuntuMono-Regular.ttf
Normal file
48
assets/privacy_policy.md
Normal file
@ -0,0 +1,48 @@
|
||||
## 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 Hacki’s 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.
|
||||
|
||||
#### Children’s 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.
|
30
components/in_app_review/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
39
components/in_app_review/.metadata
Normal file
@ -0,0 +1,39 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
channel: stable
|
||||
|
||||
project_type: plugin
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
- platform: android
|
||||
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
- platform: ios
|
||||
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
- platform: macos
|
||||
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
- platform: windows
|
||||
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
102
components/in_app_review/CHANGELOG.md
Normal file
@ -0,0 +1,102 @@
|
||||
# [2.0.6]
|
||||
- Update Android Play Core dependency to Play Review 2.0.1.
|
||||
|
||||
# [2.0.5]
|
||||
|
||||
- Migrate Android Play Core dependency to Play Review 2.0.0.
|
||||
- Recreate the example app.
|
||||
- Update in_app_review_platform_interface to 2.0.4
|
||||
|
||||
# [2.0.4]
|
||||
|
||||
- Migrate maven repository from jcenter to mavenCentral
|
||||
- `isAvailable()` now returns `false` on web.
|
||||
|
||||
# [2.0.3]
|
||||
|
||||
- Fix iOS no-scene exception. ([#41](https://github.com/britannio/in_app_review/issues/41))
|
||||
# [2.0.2]
|
||||
|
||||
- Replace iOS Swift code with Objective-C to add compatibility with Objective-C Flutter apps.
|
||||
|
||||
# [2.0.1]
|
||||
|
||||
- Fix rare null pointer exception on Android
|
||||
- Fix MissingPluginException on MacOS
|
||||
- Bump the minimum Dart SDK version from `2.12.0-0` to `2.12.0`.
|
||||
- Bump the minimum Flutter version to `2.0.0`.
|
||||
- Update in_app_review_platform_interface to 2.0.2
|
||||
|
||||
# [2.0.0]
|
||||
|
||||
- Migrate to null safety.
|
||||
|
||||
# [1.0.4]
|
||||
|
||||
- Update in_app_review_platform_interface to 1.0.5
|
||||
- Remove dependency on `package_info`.
|
||||
- Handle `openStoreListing()` with native code for Android, iOS and MacOS.
|
||||
|
||||
# [1.0.3]
|
||||
|
||||
- Update in_app_review_platform_interface to 1.0.4
|
||||
- Update android compileSdkVersion to 29.
|
||||
- Lower dependency version constraints.
|
||||
|
||||
# [1.0.2]
|
||||
|
||||
- Update in_app_review_platform_interface to 1.0.3
|
||||
- Open the App Store directly instead of via the Safari View Controller.
|
||||
- Add automated tests.
|
||||
- Improve docs.
|
||||
|
||||
# [1.0.1+1]
|
||||
|
||||
- Update in_app_review_platform_interface to 1.0.2
|
||||
|
||||
# [1.0.0]
|
||||
|
||||
- Migrate to use `in_app_review_platform_interface`.
|
||||
- Add Windows support for `openStoreListing`.
|
||||
|
||||
# [0.2.1+1]
|
||||
|
||||
- Improve iOS testing docs.
|
||||
|
||||
# [0.2.1]
|
||||
|
||||
- Update dependencies.
|
||||
- Android Play Core Library V1.8.2 release notes:
|
||||
- Fixed UI flickering in the In-App Review API
|
||||
|
||||
# [0.2.0+4]
|
||||
|
||||
- Remove deprecated API warning.
|
||||
- Update dependencies.
|
||||
|
||||
# [0.2.0+3]
|
||||
|
||||
- Instructions in the README have been improved along with the example.
|
||||
|
||||
# [0.2.0+2]
|
||||
|
||||
- Update changelog format
|
||||
|
||||
# [0.2.0+1]
|
||||
|
||||
- Update MacOS testing instructions
|
||||
|
||||
# [0.2.0] Breaking Change
|
||||
|
||||
- Add MacOS support
|
||||
- Rename `openStoreListing(iOSAppStoreId: '')` to `openStoreListing(appStoreId: '')`
|
||||
|
||||
# [0.1.0]
|
||||
|
||||
- Improve docs
|
||||
- Set Android minSdkVersion to 16
|
||||
- Refactor Android Plugin
|
||||
|
||||
# [0.0.1]
|
||||
|
||||
Initial release
|
21
components/in_app_review/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Britannio Jarrett
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
0
components/in_app_review/analysis_options.yaml
Normal file
9
components/in_app_review/android/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
49
components/in_app_review/android/build.gradle
Normal file
@ -0,0 +1,49 @@
|
||||
group 'dev.britannio.in_app_review'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
dependencies {
|
||||
|
||||
}
|
||||
}
|
1
components/in_app_review/android/settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
rootProject.name = 'in_app_review'
|
@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="dev.britannio.in_app_review">
|
||||
</manifest>
|
@ -0,0 +1,59 @@
|
||||
package dev.britannio.in_app_review;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
|
||||
/**
|
||||
* InAppReviewPlugin
|
||||
*/
|
||||
public class InAppReviewPlugin implements FlutterPlugin, MethodCallHandler {
|
||||
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
///
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
/// when the Flutter Engine is detached from the Activity
|
||||
private MethodChannel channel;
|
||||
|
||||
|
||||
private final String TAG = "InAppReviewPlugin";
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
|
||||
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "dev.britannio.in_app_review");
|
||||
channel.setMethodCallHandler(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
|
||||
Log.i(TAG, "onMethodCall: " + call.method);
|
||||
switch (call.method) {
|
||||
case "isAvailable":
|
||||
case "requestReview":
|
||||
case "openStoreListing":
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
channel.setMethodCallHandler(null);
|
||||
}
|
||||
}
|
38
components/in_app_review/ios/.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
.idea/
|
||||
.vagrant/
|
||||
.sconsign.dblite
|
||||
.svn/
|
||||
|
||||
.DS_Store
|
||||
*.swp
|
||||
profile
|
||||
|
||||
DerivedData/
|
||||
build/
|
||||
GeneratedPluginRegistrant.h
|
||||
GeneratedPluginRegistrant.m
|
||||
|
||||
.generated/
|
||||
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
|
||||
!default.pbxuser
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.perspectivev3
|
||||
|
||||
xcuserdata
|
||||
|
||||
*.moved-aside
|
||||
|
||||
*.pyc
|
||||
*sync/
|
||||
Icon?
|
||||
.tags*
|
||||
|
||||
/Flutter/Generated.xcconfig
|
||||
/Flutter/ephemeral/
|
||||
/Flutter/flutter_export_environment.sh
|
0
components/in_app_review/ios/Assets/.gitkeep
Normal file
4
components/in_app_review/ios/Classes/InAppReviewPlugin.h
Normal file
@ -0,0 +1,4 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface InAppReviewPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
107
components/in_app_review/ios/Classes/InAppReviewPlugin.m
Normal file
@ -0,0 +1,107 @@
|
||||
#import "InAppReviewPlugin.h"
|
||||
|
||||
@import StoreKit;
|
||||
@implementation InAppReviewPlugin
|
||||
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
|
||||
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"dev.britannio.in_app_review" binaryMessenger:[registrar messenger]];
|
||||
|
||||
InAppReviewPlugin* instance = [[InAppReviewPlugin alloc] init];
|
||||
[registrar addMethodCallDelegate:instance channel:channel];
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
|
||||
|
||||
[self logMessage:@"handle" details:call.method];
|
||||
|
||||
if ([call.method isEqual:@"requestReview"]) {
|
||||
[self requestReview:result];
|
||||
} else if ([call.method isEqual:@"isAvailable"]) {
|
||||
[self isAvailable:result];
|
||||
} else if ([call.method isEqual:@"openStoreListing"]) {
|
||||
[self openStoreListingWithStoreId:call.arguments result:result];
|
||||
} else {
|
||||
[self logMessage:@"method not implemented"];
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) requestReview:(FlutterResult)result {
|
||||
if (@available(iOS 14, *)) {
|
||||
[self logMessage:@"iOS 14+"];
|
||||
UIWindowScene *scene = [self findActiveScene];
|
||||
[SKStoreReviewController requestReviewInScene:scene];
|
||||
result(nil);
|
||||
} else if (@available(iOS 10.3, *)) {
|
||||
[self logMessage:@"iOS 10.3+"];
|
||||
[SKStoreReviewController requestReview];
|
||||
result(nil);
|
||||
} else {
|
||||
result([FlutterError errorWithCode:@"unavailable"
|
||||
message:@"In-App Review unavailable"
|
||||
details:nil]);
|
||||
}
|
||||
}
|
||||
|
||||
- (UIWindowScene *) findActiveScene API_AVAILABLE(ios(13.0)){
|
||||
for (UIWindowScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
||||
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive) {
|
||||
return scene;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void) isAvailable:(FlutterResult)result {
|
||||
if (@available(iOS 10.3, *)) {
|
||||
[self logMessage:@"available"];
|
||||
result(@YES);
|
||||
} else {
|
||||
[self logMessage:@"unavailable"];
|
||||
result(@NO);
|
||||
}
|
||||
}
|
||||
|
||||
- (void) openStoreListingWithStoreId:(NSString *)storeId result:(FlutterResult)result {
|
||||
|
||||
if (!storeId) {
|
||||
result([FlutterError errorWithCode:@"no-store-id"
|
||||
message:@"Your store id must be passed as the method channel's argument"
|
||||
details:nil]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://apps.apple.com/app/id%@?action=write-review", storeId]];
|
||||
|
||||
if (!url) {
|
||||
result([FlutterError errorWithCode:@"url-construct-fail"
|
||||
message:@"Failed to construct url"
|
||||
details:nil]);
|
||||
return;
|
||||
}
|
||||
|
||||
UIApplication *app = [UIApplication sharedApplication];
|
||||
if (@available(iOS 10.0, *)) {
|
||||
[app openURL:url options:@{} completionHandler:nil];
|
||||
} else {
|
||||
[app openURL:url];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Logging Helpers
|
||||
|
||||
- (void) logMessage:(NSString *) message {
|
||||
NSLog(@"InAppReviewPlugin: %@", message);
|
||||
}
|
||||
|
||||
- (void) logMessage:(NSString *) message
|
||||
details:(NSString *) details {
|
||||
NSLog(@"InAppReviewPlugin: %@ %@", message, details);
|
||||
}
|
||||
|
||||
@end
|
23
components/in_app_review/ios/in_app_review.podspec
Normal file
@ -0,0 +1,23 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||
# Run `pod lib lint in_app_review.podspec` to validate before publishing.
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'in_app_review'
|
||||
s.version = '0.2.0'
|
||||
s.summary = 'Flutter plugin for showing the In-App Review/System Rating pop up.'
|
||||
s.description = <<-DESC
|
||||
Flutter plugin for showing the In-App Review/System Rating pop up..
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Britannio Jarrett' => 'britanniojarrett@gmail.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
s.platform = :ios, '9.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
50
components/in_app_review/lib/in_app_review.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:in_app_review_platform_interface/in_app_review_platform_interface.dart';
|
||||
|
||||
class InAppReview {
|
||||
InAppReview._();
|
||||
|
||||
static final InAppReview instance = InAppReview._();
|
||||
|
||||
/// Checks if the device is able to show a review dialog.
|
||||
///
|
||||
/// On Android the Google Play Store must be installed and the device must be
|
||||
/// running **Android 5 Lollipop(API 21)** or higher.
|
||||
///
|
||||
/// iOS devices must be running **iOS version 10.3** or higher.
|
||||
///
|
||||
/// MacOS devices must be running **MacOS version 10.14** or higher
|
||||
Future<bool> isAvailable() => InAppReviewPlatform.instance.isAvailable();
|
||||
|
||||
/// Attempts to show the review dialog. It's recommended to first check if
|
||||
/// the device supports this feature via [isAvailable].
|
||||
///
|
||||
/// To improve the users experience, iOS and Android enforce limitations
|
||||
/// that might prevent this from working after a few tries. iOS & MacOS users
|
||||
/// can also disable this feature entirely in the App Store settings.
|
||||
///
|
||||
/// More info and guidance:
|
||||
/// https://developer.android.com/guide/playcore/in-app-review#when-to-request
|
||||
/// https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/
|
||||
/// https://developer.apple.com/design/human-interface-guidelines/macos/system-capabilities/ratings-and-reviews/
|
||||
Future<void> requestReview() => InAppReviewPlatform.instance.requestReview();
|
||||
|
||||
/// Opens the Play Store on Android, the App Store with a review
|
||||
/// screen on iOS & MacOS and the Microsoft Store on Windows.
|
||||
///
|
||||
/// [appStoreId] is required for iOS & MacOS.
|
||||
///
|
||||
/// [microsoftStoreId] is required for Windows.
|
||||
Future<void> openStoreListing({
|
||||
/// Required for iOS & MacOS.
|
||||
String? appStoreId,
|
||||
|
||||
/// Required for Windows.
|
||||
String? microsoftStoreId,
|
||||
}) =>
|
||||
InAppReviewPlatform.instance.openStoreListing(
|
||||
appStoreId: appStoreId,
|
||||
microsoftStoreId: microsoftStoreId,
|
||||
);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
import StoreKit
|
||||
|
||||
public class InAppReviewPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "dev.britannio.in_app_review", binaryMessenger: registrar.messenger)
|
||||
let instance = InAppReviewPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "requestReview":
|
||||
//App Store Review
|
||||
if #available(OSX 10.14, *) {
|
||||
SKStoreReviewController.requestReview()
|
||||
result(nil)
|
||||
} else {
|
||||
result(FlutterError(code: "unavailable", message: "In-App Review unavailable", details: nil))
|
||||
}
|
||||
case "isAvailable":
|
||||
if #available(OSX 10.14, *) {
|
||||
result(true)
|
||||
} else {
|
||||
result(false)
|
||||
}
|
||||
case "openStoreListing":
|
||||
let storeId : String = call.arguments as! String;
|
||||
|
||||
guard let writeReviewURL = URL(string: "macappstore://apps.apple.com/app/id" + storeId + "?action=write-review")
|
||||
else {
|
||||
result(FlutterError(code: "url_construct_fail", message: "Failed to construct url", details: nil))
|
||||
return
|
||||
}
|
||||
NSWorkspace.shared.open(writeReviewURL)
|
||||
result(nil);
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
22
components/in_app_review/macos/in_app_review.podspec
Normal file
@ -0,0 +1,22 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||
# Run `pod lib lint in_app_review.podspec' to validate before publishing.
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'in_app_review'
|
||||
s.version = '0.2.0'
|
||||
s.summary = 'Flutter plugin for showing the In-App Review/System Rating pop up.'
|
||||
s.description = <<-DESC
|
||||
Flutter plugin for showing the In-App Review/System Rating pop up.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/britannio/in_app_review'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Britannio Jarrett' => 'britanniojarrett@gmail.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'FlutterMacOS'
|
||||
|
||||
s.platform = :osx, '10.11'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
46
components/in_app_review/pubspec.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
name: in_app_review
|
||||
description: Flutter plugin for showing the In-App Review/System Rating pop up on Android, iOS and MacOS. It makes it easy for users to rate your app.
|
||||
version: 2.0.6
|
||||
homepage: https://github.com/britannio/in_app_review/tree/master/in_app_review
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
flutter: ">=2.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
in_app_review_platform_interface: ^2.0.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mockito: ^5.0.0
|
||||
plugin_platform_interface: ^2.0.0
|
||||
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
# This section identifies this Flutter project as a plugin project.
|
||||
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
|
||||
# which should be registered in the plugin registry. This is required for
|
||||
# using method channels.
|
||||
# The Android 'package' specifies package in which the registered class is.
|
||||
# This is required for using method channels on Android.
|
||||
# The 'ffiPlugin' specifies that native code should be built and bundled.
|
||||
# This is required for using `dart:ffi`.
|
||||
# All these are used by the tooling to maintain consistency when
|
||||
# adding or updating assets for this project.
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: dev.britannio.in_app_review
|
||||
pluginClass: InAppReviewPlugin
|
||||
ios:
|
||||
pluginClass: InAppReviewPlugin
|
||||
macos:
|
||||
pluginClass: InAppReviewPlugin
|
12
components/in_app_review_platform_interface/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
.dart_tool/
|
||||
|
||||
.packages
|
||||
.pub/
|
||||
|
||||
build/
|
||||
|
||||
|
||||
pubspec.lock
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
46
components/in_app_review_platform_interface/CHANGELOG.md
Normal file
@ -0,0 +1,46 @@
|
||||
# [2.0.4]
|
||||
|
||||
- Update usage of `pkg:url_launcher` to address deprecations.
|
||||
|
||||
# [2.0.3]
|
||||
|
||||
- `isAvailable()` now returns `false` on web.
|
||||
|
||||
# [2.0.2]
|
||||
|
||||
- Bump the minimum Flutter version to `2.0.0`.
|
||||
|
||||
# [2.0.1]
|
||||
|
||||
- Bump the minimum Dart SDK version from `2.12.0-0` to `2.12.0`.
|
||||
|
||||
# [2.0.0]
|
||||
|
||||
- Migrate to null safety.
|
||||
|
||||
# [1.0.5]
|
||||
|
||||
- Remove dependency on `package_info`.
|
||||
- Handle `openStoreListing()` with native code for Android, iOS and MacOS.
|
||||
|
||||
# [1.0.4]
|
||||
|
||||
- Lower dependency version constraints
|
||||
|
||||
# [1.0.3]
|
||||
|
||||
- Open the App Store directly instead of via the Safari View Controller.
|
||||
- Add automated tests.
|
||||
|
||||
# [1.0.2]
|
||||
|
||||
- Rename `openStoreListing(windowsStoreId: '')` to `openStoreListing(microsoftStoreId: '')`.
|
||||
- Update dependencies.
|
||||
|
||||
# [1.0.1]
|
||||
|
||||
- Remove unnecessary files.
|
||||
|
||||
# [1.0.0]
|
||||
|
||||
- Initial release.
|
21
components/in_app_review_platform_interface/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Britannio Jarrett
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
26
components/in_app_review_platform_interface/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# in_app_review_platform_interface
|
||||
|
||||
A common platform interface for the [`in_app_review`][1] plugin.
|
||||
|
||||
This interface allows platform-specific implementations of the `in_app_review`
|
||||
plugin, as well as the plugin itself, to ensure they are supporting the
|
||||
same interface.
|
||||
|
||||
# Usage
|
||||
|
||||
To implement a new platform-specific implementation of `in_app_review`, extend
|
||||
[`InAppReviewPlatform`][2] with an implementation that performs the
|
||||
platform-specific behavior, and when you register your plugin, set the default
|
||||
`InAppReviewPlatform` by calling
|
||||
`InAppReviewPlatform.instance = MyInAppReview()`.
|
||||
|
||||
# Note on breaking changes
|
||||
|
||||
Strongly prefer non-breaking changes (such as adding a method to the interface)
|
||||
over breaking changes for this package.
|
||||
|
||||
See https://flutter.dev/go/platform-interface-breaking-changes for a discussion
|
||||
on why a less-clean interface is preferable to a breaking change.
|
||||
|
||||
[1]: ../in_app_review
|
||||
[2]: lib/in_app_review_platform_interface.dart
|
@ -0,0 +1,71 @@
|
||||
import 'package:in_app_review_platform_interface/method_channel_in_app_review.dart';
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
|
||||
/// The interface that implementations of in_app_review must implement.
|
||||
///
|
||||
/// Platform implementations should extend this class rather than implement it
|
||||
/// as `in_app_review` does not consider newly added methods to be breaking
|
||||
/// changes. Extending this class (using `extends`) ensures that the subclass
|
||||
/// will get the default implementation, while platform implementations that
|
||||
/// `implements` this interface will be broken by newly added
|
||||
/// [InAppReviewPlatform] methods.
|
||||
abstract class InAppReviewPlatform extends PlatformInterface {
|
||||
InAppReviewPlatform() : super(token: _token);
|
||||
|
||||
static InAppReviewPlatform _instance = MethodChannelInAppReview();
|
||||
|
||||
static final Object _token = Object();
|
||||
|
||||
static InAppReviewPlatform get instance => _instance;
|
||||
|
||||
/// Platform-specific plugins should set this with their own platform-specific
|
||||
/// class that extends [InAppReviewPlatform] when they register themselves.
|
||||
static set instance(InAppReviewPlatform instance) {
|
||||
PlatformInterface.verifyToken(instance, _token);
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
/// Checks if the device is able to show a review dialog.
|
||||
///
|
||||
/// On Android the Google Play Store must be installed and the device must be
|
||||
/// running **Android 5 Lollipop(API 21)** or higher.
|
||||
///
|
||||
/// iOS devices must be running **iOS version 10.3** or higher.
|
||||
///
|
||||
/// MacOS devices must be running **MacOS version 10.14** or higher
|
||||
Future<bool> isAvailable() {
|
||||
throw UnimplementedError('isAvailable() has not been implemented.');
|
||||
}
|
||||
|
||||
/// Attempts to show the review dialog. It's recommended to first check if
|
||||
/// this cannot be done via [isAvailable]. If it is not available then
|
||||
/// you can open the store listing via [openStoreListing].
|
||||
///
|
||||
/// To improve the users experience, iOS and Android enforce limitations
|
||||
/// that might prevent this from working after a few tries. iOS & MacOS users
|
||||
/// can also disable this feature entirely in the App Store settings.
|
||||
///
|
||||
/// More info and guidance:
|
||||
/// https://developer.android.com/guide/playcore/in-app-review#when-to-request
|
||||
/// https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/
|
||||
/// https://developer.apple.com/design/human-interface-guidelines/macos/system-capabilities/ratings-and-reviews/
|
||||
Future<void> requestReview() {
|
||||
throw UnimplementedError('requestReview() has not been implemented.');
|
||||
}
|
||||
|
||||
/// Opens the Play Store on Android, the App Store with a review
|
||||
/// screen on iOS & MacOS and the Microsoft Store on Windows.
|
||||
///
|
||||
/// [appStoreId] is required for iOS & MacOS.
|
||||
///
|
||||
/// [microsoftStoreId] is required for Windows.
|
||||
Future<void> openStoreListing({
|
||||
/// Required for iOS & MacOS.
|
||||
String? appStoreId,
|
||||
|
||||
/// Required for Windows.
|
||||
String? microsoftStoreId,
|
||||
}) {
|
||||
throw UnimplementedError('openStoreListing() has not been implemented.');
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'in_app_review_platform_interface.dart';
|
||||
|
||||
/// An implementation of [InAppReviewPlatform] that uses method channels.
|
||||
class MethodChannelInAppReview extends InAppReviewPlatform {
|
||||
MethodChannel _channel = MethodChannel('dev.britannio.in_app_review');
|
||||
Platform _platform = const LocalPlatform();
|
||||
|
||||
@visibleForTesting
|
||||
set channel(MethodChannel channel) => _channel = channel;
|
||||
|
||||
@visibleForTesting
|
||||
set platform(Platform platform) => _platform = platform;
|
||||
|
||||
@override
|
||||
Future<bool> isAvailable() async {
|
||||
if (kIsWeb) return false;
|
||||
return _channel
|
||||
.invokeMethod<bool>('isAvailable')
|
||||
.then((bool? available) => available ?? false, onError: (_) => false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestReview() => _channel.invokeMethod('requestReview');
|
||||
|
||||
@override
|
||||
Future<void> openStoreListing({
|
||||
String? appStoreId,
|
||||
String? microsoftStoreId,
|
||||
}) async {
|
||||
final bool isiOS = _platform.isIOS;
|
||||
final bool isMacOS = _platform.isMacOS;
|
||||
final bool isAndroid = _platform.isAndroid;
|
||||
final bool isWindows = _platform.isWindows;
|
||||
|
||||
if (isiOS || isMacOS) {
|
||||
await _channel.invokeMethod(
|
||||
'openStoreListing',
|
||||
ArgumentError.checkNotNull(appStoreId, 'appStoreId'),
|
||||
);
|
||||
} else if (isAndroid) {
|
||||
await _channel.invokeMethod('openStoreListing');
|
||||
} else if (isWindows) {
|
||||
ArgumentError.checkNotNull(microsoftStoreId, 'microsoftStoreId');
|
||||
await _launchUrl(
|
||||
'ms-windows-store://review/?ProductId=$microsoftStoreId',
|
||||
);
|
||||
} else {
|
||||
throw UnsupportedError(
|
||||
'Platform(${_platform.operatingSystem}) not supported',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _launchUrl(String url) async {
|
||||
if (!await canLaunchUrlString(url)) return;
|
||||
await launchUrlString(url, mode: LaunchMode.externalNonBrowserApplication);
|
||||
}
|
||||
}
|
24
components/in_app_review_platform_interface/pubspec.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
name: in_app_review_platform_interface
|
||||
description: A common platform interface for the in_app_review plugin.
|
||||
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
|
||||
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
|
||||
version: 2.0.4
|
||||
homepage: https://github.com/britannio/in_app_review/tree/master/in_app_review_platform_interface
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
flutter: ">=2.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
url_launcher: ^6.1.0
|
||||
plugin_platform_interface: ^2.0.0
|
||||
platform: ^3.0.0
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
@ -0,0 +1,121 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:in_app_review_platform_interface/method_channel_in_app_review.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
late MethodChannelInAppReview methodChannelInAppReview;
|
||||
late List<MethodCall> log = <MethodCall>[];
|
||||
const MethodChannel channel = MethodChannel('dev.britannio.in_app_review');
|
||||
|
||||
setUp(() {
|
||||
methodChannelInAppReview = MethodChannelInAppReview();
|
||||
methodChannelInAppReview.channel = channel;
|
||||
log = <MethodCall>[];
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
log.clear();
|
||||
});
|
||||
|
||||
group('isAvailable', () {
|
||||
test(
|
||||
'should invoke the isAvailable method channel',
|
||||
() async {
|
||||
// ACT
|
||||
final bool result = await methodChannelInAppReview.isAvailable();
|
||||
|
||||
// ASSERT
|
||||
expect(log, <Matcher>[isMethodCall('isAvailable', arguments: null)]);
|
||||
expect(result, isTrue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('requestReview', () {
|
||||
test(
|
||||
'should invoke the requestReview method channel',
|
||||
() async {
|
||||
// ACT
|
||||
await methodChannelInAppReview.requestReview();
|
||||
|
||||
// ASSERT
|
||||
expect(log, <Matcher>[isMethodCall('requestReview', arguments: null)]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('openStoreListing', () {
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on Android',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'android');
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing();
|
||||
|
||||
// ASSERT
|
||||
expect(
|
||||
log,
|
||||
<Matcher>[isMethodCall('openStoreListing', arguments: null)],
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on iOS',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'ios');
|
||||
final String appStoreId = "store_id";
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
|
||||
|
||||
// ASSERT
|
||||
expect(log,
|
||||
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on MacOS',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'macos');
|
||||
final String appStoreId = "store_id";
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
|
||||
|
||||
// ASSERT
|
||||
expect(log,
|
||||
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'should invoke the openStoreListing method channel on Windows',
|
||||
() async {
|
||||
// ARRANGE
|
||||
methodChannelInAppReview.platform =
|
||||
FakePlatform(operatingSystem: 'windows');
|
||||
final String microsoftStoreId = 'store_id';
|
||||
|
||||
// ACT
|
||||
await methodChannelInAppReview.openStoreListing(
|
||||
microsoftStoreId: microsoftStoreId,
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(log, <Matcher>[
|
||||
isMethodCall('openStoreListing', arguments: microsoftStoreId)
|
||||
]);
|
||||
},
|
||||
skip:
|
||||
'The windows uwp implementation still uses the url_launcher package',
|
||||
);
|
||||
});
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||
include: package:very_good_analysis/analysis_options.3.1.0.yaml
|
||||
linter:
|
||||
rules:
|
||||
parameter_assignments: false
|
||||
|
@ -1,5 +1,82 @@
|
||||
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) {
|
||||
@ -7,46 +84,49 @@ 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 info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("setBool"), object: nil, userInfo: info)
|
||||
let val = params[valKey] as? Bool
|
||||
let key = params[keyKey] as? String
|
||||
|
||||
let res = SharedPrefsCore.shared.setBool(key: key, val: val)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "getBool":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("getBool"), object: nil, userInfo: info)
|
||||
let key = params[keyKey] as? String
|
||||
let res = SharedPrefsCore.shared.getBool(key: key)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "setStringList":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("setStringList"), object: nil, userInfo: info)
|
||||
let val = params[valKey] as? [String]
|
||||
let key = params[keyKey] as? String
|
||||
|
||||
let res = SharedPrefsCore.shared.setStringList(key: key, val: val)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "getStringList":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("getStringList"), object: nil, userInfo: info)
|
||||
let key = params[keyKey] as? String
|
||||
let res = SharedPrefsCore.shared.getStringList(key: key)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "clearAll":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("clearAll"), object: nil, userInfo: info)
|
||||
let res = SharedPrefsCore.shared.clearAll()
|
||||
result(res)
|
||||
}
|
||||
|
||||
return
|
||||
|
1
fastlane/metadata/android/en-US/changelogs/108.txt
Normal file
@ -0,0 +1 @@
|
||||
- Navigation shortcuts.
|
2
fastlane/metadata/android/en-US/changelogs/76.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Fixed app icon.
|
||||
- Added font size setting to comments screen.
|
2
fastlane/metadata/android/en-US/changelogs/77.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Fixed app icon.
|
||||
- Added font size setting to comments screen.
|
1
fastlane/metadata/android/en-US/changelogs/78.txt
Normal file
@ -0,0 +1 @@
|
||||
- Fixed time machine.
|
1
fastlane/metadata/android/en-US/changelogs/79.txt
Normal file
@ -0,0 +1 @@
|
||||
- Fixed time machine.
|
0
fastlane/metadata/android/en-US/changelogs/81.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/84.txt
Normal file
@ -0,0 +1,3 @@
|
||||
- Customization of tab bar.
|
||||
- Option to enable swipe gesture for switching between tabs.
|
||||
- Access to action menu from home screen.
|
5
fastlane/metadata/android/en-US/changelogs/91.txt
Normal file
@ -0,0 +1,5 @@
|
||||
- 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.
|
5
fastlane/metadata/android/en-US/changelogs/92.txt
Normal file
@ -0,0 +1,5 @@
|
||||
- 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.
|
@ -17,20 +17,20 @@ GEM
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.636.0)
|
||||
aws-sdk-core (3.154.0)
|
||||
aws-partitions (1.680.0)
|
||||
aws-sdk-core (3.168.4)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.58.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (1.61.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-s3 (1.117.2)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.1)
|
||||
aws-sigv4 (1.5.2)
|
||||
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.92.5)
|
||||
excon (0.95.0)
|
||||
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.210.1)
|
||||
fastlane (2.211.0)
|
||||
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.27.0)
|
||||
google-apis-core (>= 0.7.2, < 2.a)
|
||||
google-apis-core (0.9.0)
|
||||
google-apis-androidpublisher_v3 (0.32.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-core (0.9.2)
|
||||
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.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-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-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.42.0)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.17.0)
|
||||
google-apis-storage_v1 (~> 0.19.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.2.0)
|
||||
googleauth (1.3.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.1)
|
||||
json (2.6.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.5.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.16.3)
|
||||
molinillo (0.8.0)
|
||||
|
@ -1,6 +1,3 @@
|
||||
# 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'
|
||||
|
||||
|
@ -2,7 +2,11 @@ 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)
|
||||
@ -12,26 +16,32 @@ PODS:
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (3.3.1):
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- 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
|
||||
- OrderedSet (5.0.0)
|
||||
- path_provider_ios (0.0.1):
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- receive_sharing_intent (0.0.1):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- synced_shared_preferences (0.0.1):
|
||||
@ -47,16 +57,20 @@ 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`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- synced_shared_preferences (from `.symlinks/plugins/synced_shared_preferences/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
@ -73,8 +87,12 @@ SPEC REPOS:
|
||||
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:
|
||||
@ -83,16 +101,20 @@ 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"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_ios:
|
||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
synced_shared_preferences:
|
||||
@ -107,27 +129,31 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
@ -3,13 +3,14 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objectVersion = 54;
|
||||
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 */; };
|
||||
@ -21,9 +22,7 @@
|
||||
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 */
|
||||
@ -56,7 +55,7 @@
|
||||
};
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 8;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
@ -64,19 +63,19 @@
|
||||
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
runOnlyForDeploymentPostprocessing = 1;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -84,8 +83,8 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
E51D52AD283B464E00FC8DD8 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E51D52AF283B464E00FC8DD8 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
E51D52B2283B464E00FC8DD8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
@ -97,7 +96,6 @@
|
||||
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>"; };
|
||||
@ -109,7 +107,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
|
||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */,
|
||||
7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -177,7 +175,6 @@
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
E54B4752282B3B8900579261 /* HackiCore.swift */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
@ -186,8 +183,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
|
||||
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */,
|
||||
E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */,
|
||||
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -195,9 +192,9 @@
|
||||
D79CD63C88FF49EF451AFDDF /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */,
|
||||
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */,
|
||||
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */,
|
||||
0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */,
|
||||
D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */,
|
||||
B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@ -232,15 +229,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */,
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */,
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -363,10 +360,12 @@
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
@ -375,7 +374,22 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */ = {
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@ -397,7 +411,7 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */ = {
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@ -414,20 +428,6 @@
|
||||
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,7 +437,6 @@
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -567,23 +566,22 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 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.2.32;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@ -707,23 +705,22 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 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.2.32;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
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;
|
||||
@ -743,17 +740,19 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
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.2.32;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -776,11 +775,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -798,7 +795,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -821,7 +817,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -859,11 +855,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -880,7 +874,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -901,11 +894,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -923,7 +914,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -948,7 +938,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -988,11 +978,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -1009,7 +997,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
import workmanager
|
||||
import shared_preferences_ios
|
||||
import shared_preferences_foundation
|
||||
import flutter_secure_storage
|
||||
import path_provider_ios
|
||||
import path_provider_foundation
|
||||
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)
|
||||
FLTSharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
|
||||
FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.pathprovider.PathProviderPlugin")!)
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "io.flutter.plugins.pathprovider.PathProviderPlugin")!)
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin")!)
|
||||
}
|
||||
|
||||
|
@ -1,134 +0,0 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@ -38,7 +38,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
@ -72,5 +72,9 @@
|
||||
<array>
|
||||
<string>applinks:example.com</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -31,10 +31,6 @@ 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",
|
||||
@ -42,15 +38,6 @@ 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,
|
||||
@ -59,21 +46,18 @@ 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(
|
||||
"flutter", "build", "ios", "--config-only",
|
||||
"/Users/runner/work/Hacki/Hacki/submodules/flutter/bin/flutter", "build", "ios", "--config-only",
|
||||
"--release", "--no-pub", "--no-codesign",
|
||||
"--build-number", new_build_number.to_s,
|
||||
*extra_config_args
|
||||
"--build-number", new_build_number.to_s
|
||||
)
|
||||
|
||||
version = get_version_number(xcodeproj: "Runner.xcodeproj", target: 'Runner')
|
||||
|
||||
increment_version_number(
|
||||
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc')
|
||||
version_number: version
|
||||
)
|
||||
|
||||
increment_build_number({
|
||||
@ -93,4 +77,4 @@ latest_testflight_build_number
|
||||
skip_waiting_for_build_processing: true,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -20,7 +20,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(AuthState.init()) {
|
||||
super(const AuthState.init()) {
|
||||
on<AuthInitialize>(onInitialize);
|
||||
on<AuthLogin>(onLogin);
|
||||
on<AuthLogout>(onLogout);
|
||||
@ -41,20 +41,25 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
await _authRepository.loggedIn.then((bool loggedIn) async {
|
||||
if (loggedIn) {
|
||||
final String? username = await _authRepository.username;
|
||||
final User user =
|
||||
await _storiesRepository.fetchUserBy(userId: 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);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoggedIn: true,
|
||||
user: user,
|
||||
status: AuthStatus.loaded,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: AuthStatus.loaded,
|
||||
isLoggedIn: false,
|
||||
status: AuthStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -84,11 +89,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
);
|
||||
|
||||
if (successful) {
|
||||
final User user =
|
||||
await _storiesRepository.fetchUserBy(userId: event.username);
|
||||
final User? user = await _storiesRepository.fetchUser(id: event.username);
|
||||
emit(
|
||||
state.copyWith(
|
||||
user: user,
|
||||
user: user ?? User.emptyWithId(event.username),
|
||||
isLoggedIn: true,
|
||||
status: AuthStatus.loaded,
|
||||
),
|
||||
@ -101,7 +105,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
user: User.empty(),
|
||||
user: const User.empty(),
|
||||
isLoggedIn: false,
|
||||
agreedToEULA: false,
|
||||
),
|
||||
|
@ -14,8 +14,8 @@ class AuthState extends Equatable {
|
||||
required this.agreedToEULA,
|
||||
});
|
||||
|
||||
AuthState.init()
|
||||
: user = User.empty(),
|
||||
const AuthState.init()
|
||||
: user = const User.empty(),
|
||||
isLoggedIn = false,
|
||||
status = AuthStatus.loaded,
|
||||
agreedToEULA = false;
|
||||
|
@ -17,11 +17,13 @@ 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 =
|
||||
@ -37,6 +39,7 @@ 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);
|
||||
@ -44,6 +47,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
final PreferenceCubit _preferenceCubit;
|
||||
final FilterCubit _filterCubit;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
@ -55,75 +59,72 @@ 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.showComplexStoryTile;
|
||||
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
||||
final bool isComplexTile = event.complexStoryTileEnabled;
|
||||
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.showComplexStoryTile;
|
||||
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
||||
final bool isComplexTile = _preferenceCubit.state.complexStoryTileEnabled;
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
emit(
|
||||
const StoriesState.init().copyWith(
|
||||
offlineReading: hasCachedStories,
|
||||
isOfflineReading: hasCachedStories &&
|
||||
// Only go into offline mode in the next session.
|
||||
state.downloadStatus == StoriesDownloadStatus.initial,
|
||||
currentPageSize: pageSize,
|
||||
downloadStatus: state.downloadStatus,
|
||||
storiesDownloaded: state.storiesDownloaded,
|
||||
storiesToBeDownloaded: state.storiesToBeDownloaded,
|
||||
),
|
||||
);
|
||||
for (final StoryType type in types) {
|
||||
await loadStories(of: type, emit: emit);
|
||||
for (final StoryType type in StoryType.values) {
|
||||
await loadStories(type: type, emit: emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadStories({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required Emitter<StoriesState> emit,
|
||||
}) async {
|
||||
if (state.offlineReading) {
|
||||
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
|
||||
if (state.isOfflineReading) {
|
||||
final List<int> ids =
|
||||
await _offlineRepository.getCachedStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(of: of, to: ids)
|
||||
.copyWithCurrentPageUpdated(of: of, to: 0),
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||
);
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: of));
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: of));
|
||||
add(StoriesLoaded(type: type));
|
||||
});
|
||||
} else {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(of: of);
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(of: of, to: ids)
|
||||
.copyWithCurrentPageUpdated(of: of, to: 0),
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||
);
|
||||
_storiesRepository
|
||||
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: of));
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: of));
|
||||
add(StoriesLoaded(type: type));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -134,35 +135,37 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
) async {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
),
|
||||
);
|
||||
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWithRefreshed(of: event.type));
|
||||
await loadStories(of: event.type, emit: emit);
|
||||
emit(state.copyWithRefreshed(type: event.type));
|
||||
await loadStories(type: event.type, emit: emit);
|
||||
}
|
||||
}
|
||||
|
||||
void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
),
|
||||
);
|
||||
|
||||
final int currentPage = state.currentPageByType[event.type]!;
|
||||
final int len = state.storyIdsByType[event.type]!.length;
|
||||
emit(state.copyWithCurrentPageUpdated(of: event.type, to: currentPage + 1));
|
||||
emit(
|
||||
state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1),
|
||||
);
|
||||
final int currentPageSize = state.currentPageSize;
|
||||
final int lower = currentPageSize * (currentPage + 1);
|
||||
int upper = currentPageSize + lower;
|
||||
@ -172,7 +175,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
upper = len;
|
||||
}
|
||||
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
@ -212,7 +215,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
} else {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
),
|
||||
);
|
||||
@ -224,17 +227,24 @@ 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(
|
||||
of: event.type,
|
||||
story: event.story,
|
||||
type: event.type,
|
||||
story: event.story.copyWith(hidden: hidden),
|
||||
hasRead: hasRead,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
|
||||
emit(state.copyWithStatusUpdated(of: event.type, to: StoriesStatus.loaded));
|
||||
emit(
|
||||
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onDownload(
|
||||
@ -252,12 +262,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.deleteAllComments();
|
||||
|
||||
final Set<int> prioritizedIds = <int>{};
|
||||
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
||||
|
||||
/// 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]
|
||||
..remove(StoryType.latest);
|
||||
|
||||
for (final StoryType type in prioritizedTypes) {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
|
||||
await _offlineRepository.cacheStoryIds(of: type, ids: ids);
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
|
||||
await _offlineRepository.cacheStoryIds(type: type, ids: ids);
|
||||
prioritizedIds.addAll(ids);
|
||||
}
|
||||
|
||||
@ -277,9 +290,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
final Set<int> latestIds = <int>{};
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||
of: StoryType.latest,
|
||||
type: StoryType.latest,
|
||||
);
|
||||
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
|
||||
latestIds.addAll(ids);
|
||||
|
||||
await fetchAndCacheStories(
|
||||
@ -296,13 +309,41 @@ 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) {
|
||||
final Story? story = await _storiesRepository.fetchStoryBy(id);
|
||||
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);
|
||||
|
||||
if (story == null) {
|
||||
if (isPrioritized) {
|
||||
@ -325,15 +366,37 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.cacheUrl(url: story.url);
|
||||
}
|
||||
|
||||
_storiesRepository
|
||||
/// 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
|
||||
.fetchAllChildrenComments(ids: story.kids)
|
||||
.whereType<Comment>()
|
||||
.listen(
|
||||
(Comment comment) => unawaited(
|
||||
_offlineRepository.cacheComment(comment: comment),
|
||||
),
|
||||
)
|
||||
.onDone(() => add(StoryDownloaded(skipped: false)));
|
||||
(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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -374,7 +437,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesPageSizeChanged event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
emit(const StoriesState.init());
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
||||
@ -386,7 +448,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllWebPages();
|
||||
emit(state.copyWith(offlineReading: false));
|
||||
emit(state.copyWith(isOfflineReading: false));
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
||||
@ -418,7 +480,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) {
|
||||
|
@ -46,6 +46,13 @@ 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});
|
||||
|
||||
|
@ -11,6 +11,7 @@ enum StoriesDownloadStatus {
|
||||
downloading,
|
||||
finished,
|
||||
failure,
|
||||
canceled,
|
||||
}
|
||||
|
||||
class StoriesState extends Equatable {
|
||||
@ -20,7 +21,7 @@ class StoriesState extends Equatable {
|
||||
required this.statusByType,
|
||||
required this.currentPageByType,
|
||||
required this.readStoriesIds,
|
||||
required this.offlineReading,
|
||||
required this.isOfflineReading,
|
||||
required this.downloadStatus,
|
||||
required this.currentPageSize,
|
||||
required this.storiesDownloaded,
|
||||
@ -34,7 +35,6 @@ 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>[],
|
||||
@ -42,7 +42,6 @@ class StoriesState extends Equatable {
|
||||
StoryType.latest: <int>[],
|
||||
StoryType.ask: <int>[],
|
||||
StoryType.show: <int>[],
|
||||
StoryType.jobs: <int>[],
|
||||
},
|
||||
this.statusByType = const <StoryType, StoriesStatus>{
|
||||
StoryType.top: StoriesStatus.initial,
|
||||
@ -50,7 +49,6 @@ class StoriesState extends Equatable {
|
||||
StoryType.latest: StoriesStatus.initial,
|
||||
StoryType.ask: StoriesStatus.initial,
|
||||
StoryType.show: StoriesStatus.initial,
|
||||
StoryType.jobs: StoriesStatus.initial,
|
||||
},
|
||||
this.currentPageByType = const <StoryType, int>{
|
||||
StoryType.top: 0,
|
||||
@ -58,9 +56,8 @@ class StoriesState extends Equatable {
|
||||
StoryType.latest: 0,
|
||||
StoryType.ask: 0,
|
||||
StoryType.show: 0,
|
||||
StoryType.jobs: 0,
|
||||
},
|
||||
}) : offlineReading = false,
|
||||
}) : isOfflineReading = false,
|
||||
downloadStatus = StoriesDownloadStatus.initial,
|
||||
currentPageSize = 0,
|
||||
readStoriesIds = const <int>{},
|
||||
@ -73,7 +70,7 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, int> currentPageByType;
|
||||
final Set<int> readStoriesIds;
|
||||
final StoriesDownloadStatus downloadStatus;
|
||||
final bool offlineReading;
|
||||
final bool isOfflineReading;
|
||||
final int currentPageSize;
|
||||
final int storiesDownloaded;
|
||||
final int storiesToBeDownloaded;
|
||||
@ -85,7 +82,7 @@ class StoriesState extends Equatable {
|
||||
Map<StoryType, int>? currentPageByType,
|
||||
Set<int>? readStoriesIds,
|
||||
StoriesDownloadStatus? downloadStatus,
|
||||
bool? offlineReading,
|
||||
bool? isOfflineReading,
|
||||
int? currentPageSize,
|
||||
int? storiesDownloaded,
|
||||
int? storiesToBeDownloaded,
|
||||
@ -96,7 +93,7 @@ class StoriesState extends Equatable {
|
||||
statusByType: statusByType ?? this.statusByType,
|
||||
currentPageByType: currentPageByType ?? this.currentPageByType,
|
||||
readStoriesIds: readStoriesIds ?? this.readStoriesIds,
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||
downloadStatus: downloadStatus ?? this.downloadStatus,
|
||||
currentPageSize: currentPageSize ?? this.currentPageSize,
|
||||
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
|
||||
@ -106,13 +103,13 @@ class StoriesState extends Equatable {
|
||||
}
|
||||
|
||||
StoriesState copyWithStoryAdded({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required Story story,
|
||||
required bool hasRead,
|
||||
}) {
|
||||
final Map<StoryType, List<Story>> newMap =
|
||||
Map<StoryType, List<Story>>.from(storiesByType);
|
||||
newMap[of] = List<Story>.from(newMap[of]!)..add(story);
|
||||
newMap[type] = List<Story>.from(newMap[type]!)..add(story);
|
||||
return copyWith(
|
||||
storiesByType: newMap,
|
||||
readStoriesIds: <int>{
|
||||
@ -123,54 +120,54 @@ class StoriesState extends Equatable {
|
||||
}
|
||||
|
||||
StoriesState copyWithStoryIdsUpdated({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required List<int> to,
|
||||
}) {
|
||||
final Map<StoryType, List<int>> newMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newMap[of] = to;
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
storyIdsByType: newMap,
|
||||
);
|
||||
}
|
||||
|
||||
StoriesState copyWithStatusUpdated({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required StoriesStatus to,
|
||||
}) {
|
||||
final Map<StoryType, StoriesStatus> newMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newMap[of] = to;
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
statusByType: newMap,
|
||||
);
|
||||
}
|
||||
|
||||
StoriesState copyWithCurrentPageUpdated({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required int to,
|
||||
}) {
|
||||
final Map<StoryType, int> newMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newMap[of] = to;
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
currentPageByType: newMap,
|
||||
);
|
||||
}
|
||||
|
||||
StoriesState copyWithRefreshed({required StoryType of}) {
|
||||
StoriesState copyWithRefreshed({required StoryType type}) {
|
||||
final Map<StoryType, List<Story>> newStoriesMap =
|
||||
Map<StoryType, List<Story>>.from(storiesByType);
|
||||
newStoriesMap[of] = <Story>[];
|
||||
newStoriesMap[type] = <Story>[];
|
||||
final Map<StoryType, List<int>> newStoryIdsMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newStoryIdsMap[of] = <int>[];
|
||||
newStoryIdsMap[type] = <int>[];
|
||||
final Map<StoryType, StoriesStatus> newStatusMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newStatusMap[of] = StoriesStatus.loading;
|
||||
newStatusMap[type] = StoriesStatus.loading;
|
||||
final Map<StoryType, int> newCurrentPageMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newCurrentPageMap[of] = 0;
|
||||
newCurrentPageMap[type] = 0;
|
||||
return copyWith(
|
||||
storiesByType: newStoriesMap,
|
||||
storyIdsByType: newStoryIdsMap,
|
||||
@ -186,7 +183,7 @@ class StoriesState extends Equatable {
|
||||
statusByType,
|
||||
currentPageByType,
|
||||
readStoriesIds,
|
||||
offlineReading,
|
||||
isOfflineReading,
|
||||
downloadStatus,
|
||||
currentPageSize,
|
||||
storiesDownloaded,
|
||||
|
@ -1,15 +1,26 @@
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
|
||||
abstract class Constants {
|
||||
static const String endUserAgreementLink =
|
||||
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
|
||||
'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';
|
||||
static const String hackerNewsLogoLink =
|
||||
'https://pbs.twimg.com/profile_images/469397708986269696/iUrYEOpJ_400x400.png';
|
||||
static const String portfolioLink = 'https://livinglist.github.io';
|
||||
static const String portfolioLink = 'https://github.com/Livinglist';
|
||||
static const String githubLink = 'https://github.com/Livinglist/Hacki';
|
||||
static const String appStoreLink =
|
||||
'https://apps.apple.com/us/app/hacki/id1602043763?action=write-review';
|
||||
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';
|
||||
@ -20,23 +31,27 @@ 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';
|
||||
|
||||
/// 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 String featureJumpUpButton = 'jump_up_button';
|
||||
static const String featureJumpDownButton = 'jump_down_button';
|
||||
|
||||
static const List<String> happyFaces = <String>[
|
||||
static final String happyFace = <String>[
|
||||
'(๑•̀ㅂ•́)و✧',
|
||||
'( ͡• ͜ʖ ͡•)',
|
||||
'( ͡~ ͜ʖ ͡°)',
|
||||
'٩(˘◡˘)۶',
|
||||
'(─‿‿─)',
|
||||
'(¬‿¬)',
|
||||
];
|
||||
].pickRandomly()!;
|
||||
|
||||
static const List<String> sadFaces = <String>[
|
||||
static final String sadFace = <String>[
|
||||
'ಥ_ಥ',
|
||||
'(╯°□°)╯︵ ┻━┻',
|
||||
r'¯\_(ツ)_/¯',
|
||||
@ -46,5 +61,32 @@ 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);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
|
||||
/// Custom router.
|
||||
@ -39,8 +40,8 @@ class CustomRouter {
|
||||
appBar: AppBar(
|
||||
title: const Text('Error'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Something went wrong!'),
|
||||
body: Center(
|
||||
child: Text(Constants.errorMessage),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
41
lib/config/file_output.dart
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
||||
void init() {
|
||||
_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();
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
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].
|
||||
@ -10,8 +13,16 @@ 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()))
|
||||
..registerSingleton<Logger>(
|
||||
Logger(
|
||||
filter: CustomLogFilter(),
|
||||
printer: LogUtil.logPrinter,
|
||||
output: LogUtil.logOutput(logOutputFile),
|
||||
),
|
||||
)
|
||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||
..registerSingleton<SearchRepository>(SearchRepository())
|
||||
@ -21,7 +32,8 @@ Future<void> setUpLocator() async {
|
||||
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||
..registerSingleton<DraftCache>(DraftCache())
|
||||
..registerSingleton<CommentCache>(CommentCache())
|
||||
..registerSingleton<LocalNotification>(LocalNotification())
|
||||
..registerSingleton<LocalNotificationService>(LocalNotificationService())
|
||||
..registerSingleton(AppReviewService())
|
||||
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
);
|
||||
|
@ -43,12 +43,12 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final int count = _collapseCache.collapse(_commentId);
|
||||
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
collapsed: true,
|
||||
collapsedCount: state.collapsed ? 0 : count,
|
||||
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,33 +1,41 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/config/constants.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,
|
||||
required bool offlineReading,
|
||||
required Item item,
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
}) : _collapseCache = collapseCache,
|
||||
}) : _filterCubit = filterCubit,
|
||||
_collapseCache = collapseCache,
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
@ -38,13 +46,14 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(
|
||||
CommentsState.init(
|
||||
offlineReading: offlineReading,
|
||||
isOfflineReading: isOfflineReading,
|
||||
item: item,
|
||||
fetchMode: defaultFetchMode,
|
||||
order: defaultCommentsOrder,
|
||||
),
|
||||
);
|
||||
|
||||
final FilterCubit _filterCubit;
|
||||
final CollapseCache _collapseCache;
|
||||
final CommentCache _commentCache;
|
||||
final OfflineRepository _offlineRepository;
|
||||
@ -61,8 +70,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
|
||||
<int, StreamSubscription<Comment>>{};
|
||||
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
void emit(CommentsState state) {
|
||||
if (!isClosed) {
|
||||
@ -73,22 +80,24 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
Future<void> init({
|
||||
bool onlyShowTargetComment = false,
|
||||
bool useCommentCache = false,
|
||||
List<Comment>? targetParents,
|
||||
List<Comment>? targetAncestors,
|
||||
}) async {
|
||||
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
|
||||
if (onlyShowTargetComment && (targetAncestors?.isNotEmpty ?? false)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
comments: targetParents,
|
||||
comments: targetAncestors,
|
||||
onlyShowTargetComment: true,
|
||||
status: CommentsStatus.loaded,
|
||||
status: CommentsStatus.allLoaded,
|
||||
),
|
||||
);
|
||||
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchAllCommentsRecursivelyStream(
|
||||
ids: targetParents!.last.kids,
|
||||
level: targetParents.last.level + 1,
|
||||
ids: targetAncestors!.last.kids,
|
||||
level: targetAncestors.last.level + 1,
|
||||
)
|
||||
.asyncMap(_toBuildableComment)
|
||||
.whereNotNull()
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
|
||||
@ -104,58 +113,56 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
);
|
||||
|
||||
final Item item = state.item;
|
||||
final Item updatedItem = state.offlineReading
|
||||
final Item updatedItem = state.isOfflineReading
|
||||
? item
|
||||
: await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
|
||||
item;
|
||||
final List<int> kids = _sortKids(updatedItem.kids);
|
||||
|
||||
emit(state.copyWith(item: updatedItem));
|
||||
|
||||
if (state.offlineReading) {
|
||||
_streamSubscription = _offlineRepository
|
||||
.getCachedCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
late final Stream<Comment> commentStream;
|
||||
|
||||
if (state.isOfflineReading) {
|
||||
commentStream = _offlineRepository.getCachedCommentsStream(ids: kids);
|
||||
} else {
|
||||
switch (state.fetchMode) {
|
||||
case FetchMode.lazy:
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
break;
|
||||
commentStream = _storiesRepository.fetchCommentsStream(
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
);
|
||||
case FetchMode.eager:
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
break;
|
||||
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_streamSubscription = commentStream
|
||||
.asyncMap(_toBuildableComment)
|
||||
.whereNotNull()
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (state.offlineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
),
|
||||
);
|
||||
|
||||
if (state.isOfflineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.allLoaded,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_collapseCache.resetCollapsedComments();
|
||||
|
||||
await _streamSubscription?.cancel();
|
||||
@ -173,35 +180,35 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
final Item item = state.item;
|
||||
final Item updatedItem =
|
||||
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
await _storiesRepository.fetchItem(id: item.id) ?? item;
|
||||
final List<int> kids = _sortKids(updatedItem.kids);
|
||||
|
||||
late final Stream<Comment> commentStream;
|
||||
if (state.fetchMode == FetchMode.lazy) {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(
|
||||
ids: kids,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
commentStream = _storiesRepository.fetchCommentsStream(
|
||||
ids: kids,
|
||||
);
|
||||
} else {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
);
|
||||
}
|
||||
|
||||
_streamSubscription = commentStream
|
||||
.asyncMap(_toBuildableComment)
|
||||
.whereNotNull()
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
item: updatedItem,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void loadAll(Story story) {
|
||||
HapticFeedback.lightImpact();
|
||||
HapticFeedbackUtil.light();
|
||||
emit(
|
||||
state.copyWith(
|
||||
onlyShowTargetComment: false,
|
||||
@ -212,7 +219,13 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
/// [comment] is only used for lazy fetching.
|
||||
void loadMore({Comment? comment}) {
|
||||
void loadMore({
|
||||
Comment? comment,
|
||||
void Function(Comment)? onCommentFetched,
|
||||
VoidCallback? onDone,
|
||||
}) {
|
||||
if (comment == null && state.status == CommentsStatus.loading) return;
|
||||
|
||||
switch (state.fetchMode) {
|
||||
case FetchMode.lazy:
|
||||
if (comment == null) return;
|
||||
@ -226,23 +239,18 @@ 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,
|
||||
buildableComment.copyWith(level: level),
|
||||
cmt.copyWith(level: level),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -259,21 +267,21 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
});
|
||||
|
||||
_streamSubscriptions[comment.id] = streamSubscription;
|
||||
break;
|
||||
case FetchMode.eager:
|
||||
if (_streamSubscription != null) {
|
||||
emit(state.copyWith(status: CommentsStatus.loading));
|
||||
_streamSubscription?.resume();
|
||||
_streamSubscription
|
||||
?..resume()
|
||||
..onData(onCommentFetched);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadParentThread() async {
|
||||
unawaited(HapticFeedback.lightImpact());
|
||||
HapticFeedbackUtil.light();
|
||||
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
|
||||
final Story? parent =
|
||||
await _storiesRepository.fetchParentStory(id: state.item.id);
|
||||
final Item? parent =
|
||||
await _storiesRepository.fetchItem(id: state.item.parent);
|
||||
|
||||
if (parent == null) {
|
||||
return;
|
||||
@ -291,10 +299,33 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadRootThread() async {
|
||||
HapticFeedbackUtil.light();
|
||||
emit(state.copyWith(fetchRootStatus: CommentsStatus.loading));
|
||||
final Story? parent = await _storiesRepository
|
||||
.fetchParentStory(id: state.item.id)
|
||||
.then(_toBuildableStory);
|
||||
|
||||
if (parent == null) {
|
||||
return;
|
||||
} else {
|
||||
await HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: ItemScreenArgs(item: parent),
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
fetchRootStatus: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onOrderChanged(CommentsOrder? order) {
|
||||
if (order == null) return;
|
||||
if (state.order == order) return;
|
||||
HapticFeedback.selectionClick();
|
||||
HapticFeedbackUtil.selection();
|
||||
_streamSubscription?.cancel();
|
||||
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||
s.cancel();
|
||||
@ -308,7 +339,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
if (fetchMode == null) return;
|
||||
if (state.fetchMode == fetchMode) return;
|
||||
_collapseCache.resetCollapsedComments();
|
||||
HapticFeedback.selectionClick();
|
||||
HapticFeedbackUtil.selection();
|
||||
_streamSubscription?.cancel();
|
||||
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||
s.cancel();
|
||||
@ -318,7 +349,84 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
init(useCommentCache: true);
|
||||
}
|
||||
|
||||
List<int> sortKids(List<int> kids) {
|
||||
/// Jump to next root level comment.
|
||||
void jump(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Jump to previous root level comment.
|
||||
void jumpUp(
|
||||
ItemScrollController itemScrollController,
|
||||
ItemPositionsListener itemPositionsListener,
|
||||
) {
|
||||
final List<Comment> onScreenComments = itemPositionsListener
|
||||
.itemPositions.value
|
||||
// The header is also a part of the list view,
|
||||
// thus ignoring it here.
|
||||
.where((ItemPosition e) => e.index >= 1 && e.itemLeadingEdge > 0)
|
||||
.sorted((ItemPosition a, ItemPosition b) => a.index.compareTo(b.index))
|
||||
.map(
|
||||
(ItemPosition e) => e.index <= state.comments.length
|
||||
? state.comments.elementAt(e.index - 1)
|
||||
: null,
|
||||
)
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
|
||||
/// The index of first comment visible on screen.
|
||||
final int firstVisibleIndex = state.comments.indexOf(
|
||||
onScreenComments.firstOrNull ?? state.comments.last,
|
||||
);
|
||||
final int startIndex = max(0, firstVisibleIndex - 1);
|
||||
|
||||
for (int i = startIndex; i >= 0; i--) {
|
||||
final Comment cmt = state.comments.elementAt(i);
|
||||
|
||||
if (cmt.isRoot && (cmt.deleted || cmt.dead) == false) {
|
||||
itemScrollController.scrollTo(
|
||||
index: i + 1,
|
||||
alignment: 0.15,
|
||||
duration: Durations.ms400,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<int> _sortKids(List<int> kids) {
|
||||
switch (state.order) {
|
||||
case CommentsOrder.natural:
|
||||
return kids;
|
||||
@ -339,71 +447,69 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onCommentFetched(Comment? comment) {
|
||||
void _onCommentFetched(BuildableComment? comment) {
|
||||
if (comment != null) {
|
||||
_collapseCache.addKid(comment.id, to: comment.parent);
|
||||
_commentCache.cacheComment(comment);
|
||||
_sembastRepository.cacheComment(comment);
|
||||
|
||||
final List<LinkifyElement> elements = _linkify(
|
||||
comment.text,
|
||||
final bool hidden = _filterCubit.state.keywords.any(
|
||||
(String keyword) => comment.text.toLowerCase().contains(keyword),
|
||||
);
|
||||
|
||||
final BuildableComment buildableComment =
|
||||
BuildableComment.fromComment(comment, elements: elements);
|
||||
|
||||
final List<Comment> updatedComments = <Comment>[
|
||||
...state.comments,
|
||||
buildableComment
|
||||
comment.copyWith(hidden: hidden),
|
||||
];
|
||||
|
||||
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 List<LinkifyElement> _linkify(
|
||||
String text, {
|
||||
LinkifyOptions options = const LinkifyOptions(),
|
||||
List<Linkifier> linkifiers = const <Linkifier>[
|
||||
UrlLinkifier(),
|
||||
EmailLinkifier(),
|
||||
],
|
||||
}) {
|
||||
List<LinkifyElement> list = <LinkifyElement>[TextElement(text)];
|
||||
static Future<Item?> _toBuildable(Item? item) async {
|
||||
if (item == null) return null;
|
||||
|
||||
if (text.isEmpty) {
|
||||
return <LinkifyElement>[];
|
||||
switch (item.runtimeType) {
|
||||
case Comment:
|
||||
return _toBuildableComment(item as Comment);
|
||||
case Story:
|
||||
return _toBuildableStory(item as Story);
|
||||
}
|
||||
|
||||
if (linkifiers.isEmpty) {
|
||||
return list;
|
||||
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);
|
||||
}
|
||||
|
||||
for (final Linkifier linkifier in linkifiers) {
|
||||
list = linkifier.parse(list, options);
|
||||
}
|
||||
final List<LinkifyElement> elements =
|
||||
await compute<String, List<LinkifyElement>>(
|
||||
LinkifierUtil.linkify,
|
||||
story.text,
|
||||
);
|
||||
|
||||
return list;
|
||||
final BuildableStory buildableStory =
|
||||
BuildableStory.fromStory(story, elements: elements);
|
||||
|
||||
return buildableStory;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -8,38 +8,29 @@ enum CommentsStatus {
|
||||
failure,
|
||||
}
|
||||
|
||||
enum CommentsOrder {
|
||||
natural,
|
||||
newestFirst,
|
||||
oldestFirst,
|
||||
}
|
||||
|
||||
enum FetchMode {
|
||||
lazy,
|
||||
eager,
|
||||
}
|
||||
|
||||
class CommentsState extends Equatable {
|
||||
const CommentsState({
|
||||
required this.item,
|
||||
required this.comments,
|
||||
required this.status,
|
||||
required this.fetchParentStatus,
|
||||
required this.fetchRootStatus,
|
||||
required this.order,
|
||||
required this.fetchMode,
|
||||
required this.onlyShowTargetComment,
|
||||
required this.offlineReading,
|
||||
required this.isOfflineReading,
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
CommentsState.init({
|
||||
required this.offlineReading,
|
||||
required this.isOfflineReading,
|
||||
required this.item,
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
}) : comments = <Comment>[],
|
||||
status = CommentsStatus.init,
|
||||
fetchParentStatus = CommentsStatus.init,
|
||||
fetchRootStatus = CommentsStatus.init,
|
||||
onlyShowTargetComment = false,
|
||||
currentPage = 0;
|
||||
|
||||
@ -47,10 +38,11 @@ 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 offlineReading;
|
||||
final bool isOfflineReading;
|
||||
final int currentPage;
|
||||
|
||||
CommentsState copyWith({
|
||||
@ -58,22 +50,24 @@ class CommentsState extends Equatable {
|
||||
List<Comment>? comments,
|
||||
CommentsStatus? status,
|
||||
CommentsStatus? fetchParentStatus,
|
||||
CommentsStatus? fetchRootStatus,
|
||||
CommentsOrder? order,
|
||||
FetchMode? fetchMode,
|
||||
bool? onlyShowTargetComment,
|
||||
bool? offlineReading,
|
||||
bool? isOfflineReading,
|
||||
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,
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
);
|
||||
}
|
||||
@ -83,13 +77,14 @@ class CommentsState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
item,
|
||||
comments,
|
||||
status,
|
||||
fetchParentStatus,
|
||||
fetchRootStatus,
|
||||
order,
|
||||
fetchMode,
|
||||
onlyShowTargetComment,
|
||||
offlineReading,
|
||||
isOfflineReading,
|
||||
currentPage,
|
||||
comments,
|
||||
];
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
@ -13,6 +14,7 @@ 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';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -11,7 +12,7 @@ part 'edit_state.dart';
|
||||
class EditCubit extends HydratedCubit<EditState> {
|
||||
EditCubit({DraftCache? draftCache})
|
||||
: _draftCache = draftCache ?? locator.get<DraftCache>(),
|
||||
_debouncer = Debouncer(delay: const Duration(seconds: 1)),
|
||||
_debouncer = Debouncer(delay: Durations.oneSecond),
|
||||
super(const EditState.init());
|
||||
|
||||
final DraftCache _draftCache;
|
||||
|
@ -73,7 +73,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
),
|
||||
);
|
||||
|
||||
final Item? item = await _storiesRepository.fetchItemBy(id: id);
|
||||
final Item? item = await _storiesRepository.fetchItem(id: id);
|
||||
|
||||
if (item == null) return;
|
||||
|
||||
@ -160,6 +160,13 @@ class FavCubit extends Cubit<FavState> {
|
||||
});
|
||||
}
|
||||
|
||||
void removeAll() {
|
||||
_preferenceRepository
|
||||
..clearAllFavs(username: '')
|
||||
..clearAllFavs(username: _authBloc.state.username);
|
||||
emit(FavState.init());
|
||||
}
|
||||
|
||||
void _onItemLoaded(Item item) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -42,9 +42,9 @@ class FavState extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
favIds,
|
||||
favItems,
|
||||
status,
|
||||
currentPage,
|
||||
favIds,
|
||||
favItems,
|
||||
];
|
||||
}
|
||||
|
40
lib/cubits/filter/filter_cubit.dart
Normal file
@ -0,0 +1,40 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
|
||||
part 'filter_state.dart';
|
||||
|
||||
class FilterCubit extends Cubit<FilterState> {
|
||||
FilterCubit({PreferenceRepository? preferenceRepository})
|
||||
: _preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
super(FilterState.init()) {
|
||||
init();
|
||||
}
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
|
||||
void init() {
|
||||
_preferenceRepository.filterKeywords.then(
|
||||
(List<String> keywords) => emit(
|
||||
state.copyWith(
|
||||
keywords: keywords.toSet(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void addKeyword(String keyword) {
|
||||
final Set<String> updated = Set<String>.from(state.keywords)..add(keyword);
|
||||
emit(state.copyWith(keywords: updated));
|
||||
_preferenceRepository.updateFilterKeywords(updated.toList(growable: false));
|
||||
}
|
||||
|
||||
void removeKeyword(String keyword) {
|
||||
final Set<String> updated = Set<String>.from(state.keywords)
|
||||
..remove(keyword);
|
||||
emit(state.copyWith(keywords: updated));
|
||||
_preferenceRepository.updateFilterKeywords(updated.toList(growable: false));
|
||||
}
|
||||
}
|
20
lib/cubits/filter/filter_state.dart
Normal file
@ -0,0 +1,20 @@
|
||||
part of 'filter_cubit.dart';
|
||||
|
||||
class FilterState extends Equatable {
|
||||
const FilterState({
|
||||
required this.keywords,
|
||||
});
|
||||
|
||||
FilterState.init() : keywords = <String>{};
|
||||
|
||||
final Set<String> keywords;
|
||||
|
||||
FilterState copyWith({Set<String>? keywords}) {
|
||||
return FilterState(
|
||||
keywords: keywords ?? this.keywords,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[keywords];
|
||||
}
|
@ -28,7 +28,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
final String username = authState.username;
|
||||
|
||||
_storiesRepository
|
||||
.fetchSubmitted(of: username)
|
||||
.fetchSubmitted(userId: username)
|
||||
.then((List<int>? submittedIds) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -94,7 +94,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
);
|
||||
|
||||
_storiesRepository
|
||||
.fetchSubmitted(of: username)
|
||||
.fetchSubmitted(userId: username)
|
||||
.then((List<int>? submittedIds) {
|
||||
emit(state.copyWith(submittedIds: submittedIds));
|
||||
if (submittedIds != null) {
|
||||
|
@ -42,9 +42,9 @@ class HistoryState extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
submittedIds,
|
||||
submittedItems,
|
||||
status,
|
||||
currentPage,
|
||||
submittedIds,
|
||||
submittedItems,
|
||||
];
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'dart:math';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -30,20 +31,16 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
_authBloc.stream.listen((AuthState authState) {
|
||||
if (authState.isLoggedIn && authState.username != _username) {
|
||||
// Get the user setting.
|
||||
_preferenceRepository.shouldShowNotification
|
||||
.then((bool showNotification) {
|
||||
if (showNotification) {
|
||||
// Delaying the initialization to prevent janks in home screen.
|
||||
Future<void>.delayed(const Duration(seconds: 2), init);
|
||||
}
|
||||
});
|
||||
if (_preferenceCubit.state.notificationEnabled) {
|
||||
Future<void>.delayed(Durations.twoSeconds, init);
|
||||
}
|
||||
|
||||
// Listen for setting changes in the future.
|
||||
_preferenceCubit.stream.listen((PreferenceState prefState) {
|
||||
final bool isActive = _timer?.isActive ?? false;
|
||||
if (prefState.showNotification && !isActive) {
|
||||
if (prefState.notificationEnabled && !isActive) {
|
||||
init();
|
||||
} else if (!prefState.showNotification) {
|
||||
} else if (!prefState.notificationEnabled) {
|
||||
_timer?.cancel();
|
||||
}
|
||||
});
|
||||
@ -85,7 +82,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
for (final int id in commentsToBeLoaded) {
|
||||
Comment? comment = await _sembastRepository.getComment(id: id);
|
||||
comment ??= await _storiesRepository.fetchCommentBy(id: id);
|
||||
comment ??= await _storiesRepository.fetchComment(id: id);
|
||||
if (comment != null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -130,7 +127,8 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (_authBloc.state.isLoggedIn && _preferenceCubit.state.showNotification) {
|
||||
if (_authBloc.state.isLoggedIn &&
|
||||
_preferenceCubit.state.notificationEnabled) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: NotificationStatus.loading,
|
||||
@ -162,7 +160,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
for (final int id in commentsToBeLoaded) {
|
||||
Comment? comment = await _sembastRepository.getComment(id: id);
|
||||
comment ??= await _storiesRepository.fetchCommentBy(id: id);
|
||||
comment ??= await _storiesRepository.fetchComment(id: id);
|
||||
if (comment != null) {
|
||||
emit(state.copyWith(comments: <Comment>[...state.comments, comment]));
|
||||
}
|
||||
@ -187,7 +185,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
Future<void> _fetchReplies() {
|
||||
return _storiesRepository
|
||||
.fetchSubmitted(of: _authBloc.state.username)
|
||||
.fetchSubmitted(userId: _authBloc.state.username)
|
||||
.then((List<int>? submittedItems) async {
|
||||
if (submittedItems != null) {
|
||||
final List<int> subscribedItems = submittedItems.sublist(
|
||||
@ -196,7 +194,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
);
|
||||
|
||||
for (final int id in subscribedItems) {
|
||||
await _storiesRepository.fetchItemBy(id: id).then((Item? item) async {
|
||||
await _storiesRepository.fetchItem(id: id).then((Item? item) async {
|
||||
final List<int> kids = item?.kids ?? <int>[];
|
||||
final List<int> previousKids =
|
||||
(await _sembastRepository.kids(of: id)) ?? <int>[];
|
||||
@ -219,7 +217,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
|
||||
);
|
||||
await _storiesRepository
|
||||
.fetchCommentBy(id: newCommentId)
|
||||
.fetchComment(id: newCommentId)
|
||||
.then((Comment? comment) {
|
||||
if (comment != null && !comment.dead && !comment.deleted) {
|
||||
_sembastRepository
|
||||
|
@ -77,11 +77,11 @@ class NotificationState extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
comments,
|
||||
unreadCommentsIds,
|
||||
allCommentsIds,
|
||||
currentPage,
|
||||
offset,
|
||||
status,
|
||||
comments,
|
||||
unreadCommentsIds,
|
||||
allCommentsIds,
|
||||
];
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
|
||||
if (pollOptionsIds.isEmpty || refresh) {
|
||||
final Story? updatedStory =
|
||||
await _storiesRepository.fetchStoryBy(_story.id);
|
||||
await _storiesRepository.fetchStory(id: _story.id);
|
||||
|
||||
if (updatedStory != null) {
|
||||
pollOptionsIds = updatedStory.parts;
|
||||
|
@ -20,9 +20,6 @@ class PostCubit extends Cubit<PostState> {
|
||||
text: text,
|
||||
);
|
||||
|
||||
// final successful =
|
||||
// await Future<bool>.delayed(const Duration(seconds: 2), () => true);
|
||||
|
||||
if (successful) {
|
||||
emit(state.copyWith(status: PostStatus.successful));
|
||||
} else {
|
||||
|
@ -1,97 +1,76 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'preference_state.dart';
|
||||
|
||||
class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
PreferenceCubit({PreferenceRepository? storageRepository})
|
||||
: _preferenceRepository =
|
||||
storageRepository ?? locator.get<PreferenceRepository>(),
|
||||
super(const PreferenceState.init()) {
|
||||
PreferenceCubit({
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(PreferenceState.init()) {
|
||||
init();
|
||||
}
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
|
||||
void init() {
|
||||
_preferenceRepository.shouldShowNotification
|
||||
.then((bool value) => emit(state.copyWith(showNotification: value)));
|
||||
_preferenceRepository.shouldShowComplexStoryTile.then(
|
||||
(bool value) => emit(state.copyWith(showComplexStoryTile: value)),
|
||||
);
|
||||
_preferenceRepository.shouldShowWebFirst
|
||||
.then((bool value) => emit(state.copyWith(showWebFirst: value)));
|
||||
_preferenceRepository.shouldShowEyeCandy
|
||||
.then((bool value) => emit(state.copyWith(showEyeCandy: value)));
|
||||
_preferenceRepository.trueDarkMode
|
||||
.then((bool value) => emit(state.copyWith(useTrueDark: value)));
|
||||
_preferenceRepository.readerMode
|
||||
.then((bool value) => emit(state.copyWith(useReader: value)));
|
||||
_preferenceRepository.markReadStories
|
||||
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
||||
_preferenceRepository.shouldShowMetadata
|
||||
.then((bool value) => emit(state.copyWith(showMetadata: value)));
|
||||
_preferenceRepository.fetchMode
|
||||
.then((FetchMode value) => emit(state.copyWith(fetchMode: value)));
|
||||
_preferenceRepository.commentsOrder
|
||||
.then((CommentsOrder value) => emit(state.copyWith(order: value)));
|
||||
for (final BooleanPreference p
|
||||
in Preference.allPreferences.whereType<BooleanPreference>()) {
|
||||
initPreference<bool>(p).then<bool?>((bool? value) {
|
||||
final Preference<dynamic> updatedPreference = p.copyWith(val: value);
|
||||
emit(state.copyWithPreference(updatedPreference));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
for (final IntPreference p
|
||||
in Preference.allPreferences.whereType<IntPreference>()) {
|
||||
initPreference<int>(p).then<int?>((int? value) {
|
||||
final Preference<dynamic> updatedPreference = p.copyWith(val: value);
|
||||
emit(state.copyWithPreference(updatedPreference));
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void toggleNotificationMode() {
|
||||
emit(state.copyWith(showNotification: !state.showNotification));
|
||||
_preferenceRepository.toggleNotificationMode();
|
||||
Future<T?> initPreference<T>(Preference<T> preference) async {
|
||||
switch (T) {
|
||||
case int:
|
||||
final int? value = await _preferenceRepository.getInt(preference.key);
|
||||
return value as T?;
|
||||
case bool:
|
||||
final bool? value = await _preferenceRepository.getBool(preference.key);
|
||||
return value as T?;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
void toggleDisplayMode() {
|
||||
emit(state.copyWith(showComplexStoryTile: !state.showComplexStoryTile));
|
||||
_preferenceRepository.toggleDisplayMode();
|
||||
}
|
||||
void update<T>(Preference<T> preference, {required T to}) {
|
||||
final T value = to;
|
||||
final Preference<T> updatedPreference = preference.copyWith(val: value);
|
||||
|
||||
void toggleNavigationMode() {
|
||||
emit(state.copyWith(showWebFirst: !state.showWebFirst));
|
||||
_preferenceRepository.toggleNavigationMode();
|
||||
}
|
||||
_logger.i('updating $preference to $value');
|
||||
|
||||
void toggleEyeCandyMode() {
|
||||
emit(state.copyWith(showEyeCandy: !state.showEyeCandy));
|
||||
_preferenceRepository.toggleEyeCandyMode();
|
||||
}
|
||||
emit(state.copyWithPreference(updatedPreference));
|
||||
|
||||
void toggleTrueDarkMode() {
|
||||
emit(state.copyWith(useTrueDark: !state.useTrueDark));
|
||||
_preferenceRepository.toggleTrueDarkMode();
|
||||
}
|
||||
|
||||
void toggleReaderMode() {
|
||||
emit(state.copyWith(useReader: !state.useReader));
|
||||
_preferenceRepository.toggleReaderMode();
|
||||
}
|
||||
|
||||
void toggleMarkReadStoriesMode() {
|
||||
emit(state.copyWith(markReadStories: !state.markReadStories));
|
||||
_preferenceRepository.toggleMarkReadStoriesMode();
|
||||
}
|
||||
|
||||
void toggleMetadataMode() {
|
||||
emit(state.copyWith(showMetadata: !state.showMetadata));
|
||||
_preferenceRepository.toggleMetadataMode();
|
||||
}
|
||||
|
||||
void selectFetchMode(FetchMode? fetchMode) {
|
||||
if (fetchMode == null || state.fetchMode == fetchMode) return;
|
||||
HapticFeedback.lightImpact();
|
||||
emit(state.copyWith(fetchMode: fetchMode));
|
||||
_preferenceRepository.selectFetchMode(fetchMode);
|
||||
}
|
||||
|
||||
void selectCommentsOrder(CommentsOrder? order) {
|
||||
if (order == null || state.order == order) return;
|
||||
HapticFeedback.lightImpact();
|
||||
emit(state.copyWith(order: order));
|
||||
_preferenceRepository.selectCommentsOrder(order);
|
||||
switch (T) {
|
||||
case int:
|
||||
_preferenceRepository.setInt(preference.key, value as int);
|
||||
case bool:
|
||||
_preferenceRepository.setBool(preference.key, value as bool);
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|