Compare commits

..

18 Commits

Author SHA1 Message Date
01085e5fd3 rename search filters. (#79) 2022-12-17 18:14:43 -08:00
b5e11a72bf add split abi. (#78) 2022-12-17 13:10:37 -08:00
f55bbb6f84 v0.2.33 (#76)
* bump version.

* tap anywhere to collapse.

* bump version.

* add feedback.

* refactor preference.

* renaming.

* bump version.

* nit.

* cleanup.

* bump version.

* add feedback.

* nit.

* nit.

* fix android icon.

* update description.
2022-12-16 22:07:55 -08:00
b3e994269c v0.2.32 (#74)
* fixed background color of nav bar on Android.

* fixed comment tile overflow.

* added github action. (#73)

* updated provisioning profile.

* updated profile.

* updated profile.

* updated profile

* updated profile.

* updated profile.

* updated fastfile.

* updated actions

* updated action.

* updated fastlane.

* revert actions.

* fixed action.

* avoid running on tag creation.

* updated action.

* bump version of Flutter.

* bump version of Flutter.
2022-10-14 19:39:26 -07:00
a2c66a0075 fixed background color of nav bar on Android. (#72) 2022-09-17 00:44:27 -07:00
5f43fd6968 v0.2.31 (#71)
* bumped version.

* bumped version.

* bumped version

* bumped version.

* downgrade flutter version.

* Use transparent color for navigation bar (#70)

* Use transparent color for navigation bar

* Fixed whitespace

Co-authored-by: Niklas Weinhart <git@weinhart.net>

* bumped ios version.

* bumped versions.

* updated min screen width.

* removed print.

* fixed stack overflow in EditCubit.

* added integration test.

* bumped version.

Co-authored-by: wnhrt <github@weinhart.net>
Co-authored-by: Niklas Weinhart <git@weinhart.net>
2022-09-16 22:37:16 -07:00
d83381a7fd v0.2.30 (#68)
* bumped version.

* cache  in case app gets killed in the background.

* bumped flutter version.

* updated

* improved 'ReplyBox'.

* fixed lint.

* fixed EditCubit.

* updated EditCubit.

* fixed navigation in tablet mode.

* clear cache after submission.
2022-08-05 22:13:30 -07:00
764ff09345 v0.2.29-hotfix (#67)
* fixed datetime extension.

* updated README.md
2022-07-06 20:37:18 -07:00
ab449adce2 v0.2.29 (#66)
* fixed stuff.

* fixed UI.

* bumped version.
2022-07-06 17:04:14 -07:00
2ec41b26f2 updated README.md 2022-07-06 16:23:05 -07:00
19f2107d95 v0.2.28 (#65)
* bumped version.

* fixed comments cubit and story tile.

* cancel subscription on error.
2022-07-02 01:14:40 -07:00
c9b2d82dfc v0.2.27 (#64)
* bumped version.

* fixed comment cubit.

* fixed share button.

* fixed share dialog.

* added lazy loading.

* bumped version.

* fixed lazy loading.

* bumped version.

* updated screenshots.

* added customization of fetch mode and comments order.

* updated screenshots.

* added haptic feedback.
2022-06-30 18:32:11 -07:00
56e442b09f v0.2.26 (#63)
* added integration test.

* replace listview with listview.builder

* added cache.

* bumped version.

* bumped version.

* updated github action.

* bumped version.

* fixed time machine cubit.

* fixed time machine cubit.

* reverted changes.

* removed keepAliveMixin
2022-06-28 17:32:50 -07:00
9069efcced improved logging. 2022-06-28 12:08:02 -07:00
bf6a5667dc fixed naming. 2022-06-28 12:01:36 -07:00
cff73a010b v0.2.25 (#62)
* bumped version.

* improved cache.

* improved comment cache.

* updated default val for navigationMode.
2022-06-28 00:08:07 -07:00
f0d6cac3fd v0.2.24 (#61)
* bumped version.

* improved collapse.

* improved download speed.

* improved err handling.

* improved error handling.

* improved error handling.

* improved logging.

* improved logging.

* bumped version.
2022-06-27 00:47:22 -07:00
a90d52f348 v0.2.23 (#60)
* fixed #58

* bumped version.
2022-06-23 19:38:14 -07:00
148 changed files with 3774 additions and 1969 deletions

22
.github/workflows/commit_check.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Commit Guard
on:
push:
branches:
- "**"
jobs:
releases:
name: Check commit
runs-on: ubuntu-latest
env:
FLUTTER_VERSION: "3.3.10"
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.3.10'
channel: 'stable'
- run: flutter pub get
- run: flutter format --set-exit-if-changed .
- run: flutter analyze

View File

@ -1,32 +0,0 @@
name: Releases
on:
push:
# tags:
# - '*'
jobs:
releases:
name: release apk
runs-on: ubuntu-latest
env:
JAVA_VERSION: "11.0"
FLUTTER_VERSION: "3.0.0"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.0.0'
channel: 'stable'
- run: flutter pub get
- run: flutter analyze
# - run: flutter test
# - run: flutter build apk --release
# - uses: ncipollo/release-action@v1
# with:
# artifacts: "build/app/outputs/flutter-apk/*.apk"
# token: ${{ secrets.GITHUB_TOKEN }}

54
.github/workflows/publish_ios.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Publish (iOS)
on:
# Allow manual builds of this workflow
workflow_dispatch: {}
# Run the workflow whenever a new tag named 'v*' is pushed
push:
branches:
- "!*"
tags:
- "v*"
jobs:
build_and_publish:
runs-on: macos-latest
env:
# Point the `ruby/setup-ruby` action at this Gemfile, so it
# caches dependencies for us.
BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile
steps:
- name: Check out from git
uses: actions/checkout@v2
# Configure ruby according to our .ruby-version
- name: Setup ruby & Bundler
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
# Set up flutter (feel free to adjust the version below)
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
cache: true
flutter-version: 3.3.10
- 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
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
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 }}"

1
.ruby-version Normal file
View File

@ -0,0 +1 @@
2.7.5

View File

@ -1,14 +1,13 @@
# <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News # <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News
A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough. A [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough.
[![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
[![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/) [![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
[![GH version](https://img.shields.io/github/release/livinglist/hacki.svg?logo=github)](https://github.com/Livinglist/Hacki/releases/latest) [![GH version](https://img.shields.io/github/release/livinglist/hacki.svg?logo=github)](https://github.com/Livinglist/Hacki/releases/latest)
[![Visits Badge](https://badges.pufler.dev/visits/livinglist/Hacki)](https://badges.pufler.dev)
[![GitHub](https://img.shields.io/github/stars/livinglist/Hacki?style=social)](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://pub.dev/packages/effective_dart) [![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://pub.dev/packages/effective_dart)
[![GitHub](https://img.shields.io/github/stars/livinglist/Hacki?style=social)](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
[<img src="assets/images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [<img src="assets/images/google_play_badge.png" height="50">](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US) [<img src="assets/images/f_droid_badge.png" height="50">](https://f-droid.org/en/packages/com.jiaqifeng.hacki/) [<img src="assets/images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [<img src="assets/images/google_play_badge.png" height="50">](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US) [<img src="assets/images/f_droid_badge.png" height="50">](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)

View File

@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
compileSdkVersion 32 compileSdkVersion 33
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -51,7 +51,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.jiaqifeng.hacki" applicationId "com.jiaqifeng.hacki"
minSdkVersion 26 minSdkVersion 26
targetSdkVersion 32 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@ -79,3 +79,14 @@ flutter {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 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
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 406 KiB

View File

@ -1 +0,0 @@
- Bugfixes.

View File

@ -1 +0,0 @@
- Updates to UI.

View File

@ -1 +0,0 @@
- Updates to UI.

View File

@ -1 +0,0 @@
- Updates to UI.

View File

@ -1 +0,0 @@
- Tapping on comments in notification and history screen will lead you directly to the comment.

View File

@ -1,3 +0,0 @@
- Tapping on comment in notification or history screen will now lead you directly to the comment.
- Fixed the bug where reply box cannot be expanded in editing mode.
- Fixed inconsistent font size in history screen.

View File

@ -1 +0,0 @@
- Added offline mode.

View File

@ -1,2 +0,0 @@
- Added offline mode.
- Bugfixes.

View File

@ -1,2 +0,0 @@
- Added offline mode.
- Bugfixes.

View File

@ -1,2 +0,0 @@
- Added offline mode.
- Bugfixes.

View File

@ -1,3 +0,0 @@
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,3 +0,0 @@
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,3 +0,0 @@
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,5 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,7 +0,0 @@
- You can share links.
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -0,0 +1,2 @@
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,2 @@
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,2 @@
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,2 @@
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,2 @@
- Bumped Flutter version.
- Updated navigation bar background color.

View File

@ -0,0 +1,2 @@
- Bumped Flutter version.
- Updated navigation bar background color.

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 406 KiB

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hacki/main.dart' as app;
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:integration_test/integration_test.dart';
void main() {
final IntegrationTestWidgetsFlutterBinding binding =
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Scrolling test', (WidgetTester tester) async {
await app.main(testing: true);
await tester.pumpAndSettle();
final Finder bestTabFinder = find.widgetWithText(Tab, 'BEST');
expect(bestTabFinder, findsOneWidget);
Future<void> scrollDown(WidgetTester tester) async {
await tester.timedDragFrom(
const Offset(100, 200),
const Offset(100, -700),
const Duration(seconds: 2),
);
await tester.pump();
}
Future<void> scrollUp(WidgetTester tester) async {
await tester.timedDragFrom(
const Offset(100, 200),
const Offset(100, 700),
const Duration(seconds: 1),
);
await tester.pump();
}
await binding.traceAction(
() async {
await tester.tap(bestTabFinder);
await tester.pump();
const int count = 10;
for (int i = 0; i < count; i++) {
await scrollDown(tester);
}
for (int i = 0; i < count - 3; i++) {
await scrollUp(tester);
}
await tester.pumpAndSettle(const Duration(seconds: 2));
final Finder storyFinder = find.byType(StoryTile);
expect(storyFinder, findsWidgets);
final Finder firstStoryFinder = storyFinder.first;
expect(firstStoryFinder, findsOneWidget);
await tester.tap(firstStoryFinder);
await tester.pump(const Duration(seconds: 4));
},
reportKey: 'scrolling_timeline',
);
});
}

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>9.0</string> <string>11.0</string>
</dict> </dict>
</plist> </plist>

4
ios/Gemfile Normal file
View File

@ -0,0 +1,4 @@
source "https://rubygems.org"
gem "fastlane"
gem "cocoapods"

285
ios/Gemfile.lock Normal file
View File

@ -0,0 +1,285 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
activesupport (6.1.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
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-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.10)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.92.5)
faraday (1.10.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.210.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
ffi (1.15.5)
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)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
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-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)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.17.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.5.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.16.3)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
zeitwerk (2.6.0)
PLATFORMS
universal-darwin-21
x86_64-darwin-19
DEPENDENCIES
cocoapods
fastlane
BUNDLED WITH
2.3.22

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
platform :ios, '13.0' # platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -37,8 +37,5 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end end
end end

View File

@ -19,6 +19,8 @@ PODS:
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- integration_test (0.0.1):
- Flutter
- OrderedSet (5.0.0) - OrderedSet (5.0.0)
- path_provider_ios (0.0.1): - path_provider_ios (0.0.1):
- Flutter - Flutter
@ -50,6 +52,7 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`) - flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
@ -80,6 +83,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_siri_suggestions: flutter_siri_suggestions:
:path: ".symlinks/plugins/flutter_siri_suggestions/ios" :path: ".symlinks/plugins/flutter_siri_suggestions/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
path_provider_ios: path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_ios/ios"
receive_sharing_intent: receive_sharing_intent:
@ -103,12 +108,13 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
@ -122,6 +128,6 @@ SPEC CHECKSUMS:
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: e4c97c7a9aacaeda4b952f7ef9ea29e47660f622 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.2 COCOAPODS: 1.11.3

View File

@ -549,7 +549,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -567,20 +567,23 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1; CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = QMWX3X2NF7; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.22; MARKETING_VERSION = 0.2.33;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -635,7 +638,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -684,7 +687,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -704,20 +707,23 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1; CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = QMWX3X2NF7; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.22; MARKETING_VERSION = 0.2.33;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -735,20 +741,23 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CURRENT_PROJECT_VERSION = 1; CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = QMWX3X2NF7; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.22; MARKETING_VERSION = 0.2.33;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -767,9 +776,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Share Extension/Info.plist"; INFOPLIST_FILE = "Share Extension/Info.plist";
@ -786,6 +797,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -806,9 +819,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Share Extension/Info.plist"; INFOPLIST_FILE = "Share Extension/Info.plist";
@ -824,6 +839,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki.Share-Extension";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -842,9 +859,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Share Extension/Info.plist"; INFOPLIST_FILE = "Share Extension/Info.plist";
@ -860,6 +879,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -880,9 +901,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Action Extension/Info.plist"; INFOPLIST_FILE = "Action Extension/Info.plist";
@ -899,6 +922,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -921,9 +946,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Action Extension/Info.plist"; INFOPLIST_FILE = "Action Extension/Info.plist";
@ -939,6 +966,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki.Action-Extension";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -959,9 +988,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Action Extension/Info.plist"; INFOPLIST_FILE = "Action Extension/Info.plist";
@ -977,6 +1008,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -7,10 +7,10 @@
<key>NSExtensionAttributes</key> <key>NSExtensionAttributes</key>
<dict> <dict>
<key>NSExtensionActivationRule</key> <key>NSExtensionActivationRule</key>
<dict> <dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key> <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer> <integer>1</integer>
</dict> </dict>
</dict> </dict>
<key>NSExtensionMainStoryboard</key> <key>NSExtensionMainStoryboard</key>
<string>MainInterface</string> <string>MainInterface</string>

8
ios/fastlane/Appfile Normal file
View File

@ -0,0 +1,8 @@
app_identifier("com.jiaqi.hacki") # The bundle identifier of your app
apple_id("georgefung78@Live.com") # Your Apple Developer Portal username
itc_team_id("120097292") # App Store Connect Team ID
team_id("QMWX3X2NF7") # Developer Portal Team ID
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile

96
ios/fastlane/Fastfile Normal file
View File

@ -0,0 +1,96 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
## Update these lines to match your project!
# Bundle Identifier used for the iOS App on the App Store Connect portal
APP_IDENTIFIER = 'com.jiaqi.hacki'
# Issuer ID from the Keys section of https://appstoreconnect.apple.com/access/users
APPSTORECONNECT_ISSUER_ID = '0b588ac9-5b3e-4420-867a-a33decce7b91'
# Key ID from the key matching the `APP_STORE_CONNECT_API_KEY_KEY` secret, found under the Keys section of https://appstoreconnect.apple.com/access/users
APPSTORECONNECT_KEY_ID = 'DPNP8R66QS'
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do |options|
setup_ci if ENV['CI']
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",
app_identifier: [APP_IDENTIFIER, "#{APP_IDENTIFIER}.Share-Extension", "#{APP_IDENTIFIER}.Action-Extension"],
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,
issuer_id: APPSTORECONNECT_ISSUER_ID
)
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",
"--release", "--no-pub", "--no-codesign",
"--build-number", new_build_number.to_s,
*extra_config_args
)
increment_version_number(
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc')
)
increment_build_number({
build_number: latest_testflight_build_number + 1
})
# Build & sign using Runner.xcworkspace
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
output_directory: "../build/ios/archive"
)
upload_to_testflight(
# This takes a long time, so don't waste GH runner minutes (but it means manually needing to
# set the build live for external testers).
skip_waiting_for_build_processing: true,
)
end
end

13
ios/fastlane/Matchfile Normal file
View File

@ -0,0 +1,13 @@
git_url("git@github.com:Livinglist/certificates.git")
storage_mode("git")
type("development") # The default type, can be: appstore, adhoc, enterprise or development
# app_identifier(["tools.fastlane.app", "tools.fastlane.app2"])
# username("user@fastlane.tools") # Your Apple Developer Portal username
# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options
# The docs are available on https://docs.fastlane.tools/actions/match

View File

@ -7,7 +7,9 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:logger/logger.dart';
import 'package:responsive_builder/responsive_builder.dart'; import 'package:responsive_builder/responsive_builder.dart';
import 'package:rxdart/rxdart.dart';
part 'stories_event.dart'; part 'stories_event.dart';
part 'stories_state.dart'; part 'stories_state.dart';
@ -15,15 +17,18 @@ part 'stories_state.dart';
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> { class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesBloc({ StoriesBloc({
required PreferenceCubit preferenceCubit, required PreferenceCubit preferenceCubit,
CacheRepository? cacheRepository, OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository, StoriesRepository? storiesRepository,
PreferenceRepository? preferenceRepository, PreferenceRepository? preferenceRepository,
Logger? logger,
}) : _preferenceCubit = preferenceCubit, }) : _preferenceCubit = preferenceCubit,
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(), _offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository = _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_preferenceRepository = _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(), preferenceRepository ?? locator.get<PreferenceRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(const StoriesState.init()) { super(const StoriesState.init()) {
on<StoriesInitialize>(onInitialize); on<StoriesInitialize>(onInitialize);
on<StoriesRefresh>(onRefresh); on<StoriesRefresh>(onRefresh);
@ -39,9 +44,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
} }
final PreferenceCubit _preferenceCubit; final PreferenceCubit _preferenceCubit;
final CacheRepository _cacheRepository; final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final Logger _logger;
DeviceScreenType? deviceScreenType; DeviceScreenType? deviceScreenType;
StreamSubscription<PreferenceState>? _streamSubscription; StreamSubscription<PreferenceState>? _streamSubscription;
static const int _smallPageSize = 10; static const int _smallPageSize = 10;
@ -71,7 +77,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
add(StoriesPageSizeChanged(pageSize: pageSize)); add(StoriesPageSizeChanged(pageSize: pageSize));
} }
}); });
final bool hasCachedStories = await _cacheRepository.hasCachedStories; final bool hasCachedStories = await _offlineRepository.hasCachedStories;
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile; final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
final int pageSize = _getPageSize(isComplexTile: isComplexTile); final int pageSize = _getPageSize(isComplexTile: isComplexTile);
emit( emit(
@ -90,13 +96,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
required Emitter<StoriesState> emit, required Emitter<StoriesState> emit,
}) async { }) async {
if (state.offlineReading) { if (state.offlineReading) {
final List<int> ids = await _cacheRepository.getCachedStoryIds(of: of); final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
emit( emit(
state state
.copyWithStoryIdsUpdated(of: of, to: ids) .copyWithStoryIdsUpdated(of: of, to: ids)
.copyWithCurrentPageUpdated(of: of, to: 0), .copyWithCurrentPageUpdated(of: of, to: 0),
); );
_cacheRepository _offlineRepository
.getCachedStoriesStream( .getCachedStoriesStream(
ids: ids.sublist(0, min(ids.length, state.currentPageSize)), ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
) )
@ -167,7 +173,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
} }
if (state.offlineReading) { if (state.offlineReading) {
_cacheRepository _offlineRepository
.getCachedStoriesStream( .getCachedStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist( ids: state.storyIdsByType[event.type]!.sublist(
lower, lower,
@ -241,9 +247,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
), ),
); );
await _cacheRepository.deleteAllStoryIds(); await _offlineRepository.deleteAllStoryIds();
await _cacheRepository.deleteAllStories(); await _offlineRepository.deleteAllStories();
await _cacheRepository.deleteAllComments(); await _offlineRepository.deleteAllComments();
final Set<int> prioritizedIds = <int>{}; final Set<int> prioritizedIds = <int>{};
final List<StoryType> prioritizedTypes = <StoryType>[...types] final List<StoryType> prioritizedTypes = <StoryType>[...types]
@ -251,7 +257,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
for (final StoryType type in prioritizedTypes) { for (final StoryType type in prioritizedTypes) {
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type); final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
await _cacheRepository.cacheStoryIds(of: type, ids: ids); await _offlineRepository.cacheStoryIds(of: type, ids: ids);
prioritizedIds.addAll(ids); prioritizedIds.addAll(ids);
} }
@ -273,7 +279,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final List<int> ids = await _storiesRepository.fetchStoryIds( final List<int> ids = await _storiesRepository.fetchStoryIds(
of: StoryType.latest, of: StoryType.latest,
); );
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: ids); await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
latestIds.addAll(ids); latestIds.addAll(ids);
await fetchAndCacheStories( await fetchAndCacheStories(
@ -281,12 +287,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
includingWebPage: event.includingWebPage, includingWebPage: event.includingWebPage,
isPrioritized: false, isPrioritized: false,
); );
emit(
state.copyWith(
downloadStatus: StoriesDownloadStatus.finished,
),
);
} catch (_) { } catch (_) {
emit( emit(
state.copyWith( state.copyWith(
@ -318,44 +318,53 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
continue; continue;
} }
await _cacheRepository.cacheStory(story: story); await _offlineRepository.cacheStory(story: story);
if (story.url.isNotEmpty && includingWebPage) { if (story.url.isNotEmpty && includingWebPage) {
await _cacheRepository.cacheUrl(url: story.url); _logger.i('downloading ${story.url}');
await _offlineRepository.cacheUrl(url: story.url);
} }
final Completer<void> completer = Completer<void>();
_storiesRepository _storiesRepository
.fetchAllChildrenComments(ids: story.kids) .fetchAllChildrenComments(ids: story.kids)
.listen((Comment? comment) async { .whereType<Comment>()
if (comment != null) { .listen(
await _cacheRepository.cacheComment(comment: comment); (Comment comment) => unawaited(
} _offlineRepository.cacheComment(comment: comment),
}).onDone(() { ),
completer.complete(); )
add(StoryDownloaded(skipped: false)); .onDone(() => add(StoryDownloaded(skipped: false)));
});
await completer.future;
} }
} }
void onStoryDownloaded(StoryDownloaded event, Emitter<StoriesState> emit) { void onStoryDownloaded(StoryDownloaded event, Emitter<StoriesState> emit) {
if (event.skipped) { if (event.skipped) {
final int updatedStoriesToBeDownloaded = state.storiesToBeDownloaded - 1;
emit( emit(
state.copyWith( state.copyWith(
storiesToBeDownloaded: state.storiesToBeDownloaded - 1, storiesToBeDownloaded: updatedStoriesToBeDownloaded,
downloadStatus:
state.storiesDownloaded == updatedStoriesToBeDownloaded
? StoriesDownloadStatus.finished
: null,
), ),
); );
} else { } else {
final int updatedStoriesDownloaded = state.storiesDownloaded + 1; final int updatedStoriesDownloaded = state.storiesDownloaded + 1;
final int updatedStoriesToBeDownloaded =
updatedStoriesDownloaded > state.storiesToBeDownloaded
? state.storiesToBeDownloaded + 1
: state.storiesToBeDownloaded;
emit( emit(
state.copyWith( state.copyWith(
storiesDownloaded: updatedStoriesDownloaded, storiesDownloaded: updatedStoriesDownloaded,
storiesToBeDownloaded: storiesToBeDownloaded: updatedStoriesToBeDownloaded,
updatedStoriesDownloaded > state.storiesToBeDownloaded downloadStatus:
? state.storiesToBeDownloaded + 1 updatedStoriesDownloaded == updatedStoriesToBeDownloaded
: state.storiesToBeDownloaded, ? StoriesDownloadStatus.finished
: null,
), ),
); );
} }
@ -373,10 +382,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesExitOffline event, StoriesExitOffline event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
) async { ) async {
await _cacheRepository.deleteAllStoryIds(); await _offlineRepository.deleteAllStoryIds();
await _cacheRepository.deleteAllStories(); await _offlineRepository.deleteAllStories();
await _cacheRepository.deleteAllComments(); await _offlineRepository.deleteAllComments();
await _cacheRepository.deleteAllWebPages(); await _offlineRepository.deleteAllWebPages();
emit(state.copyWith(offlineReading: false)); emit(state.copyWith(offlineReading: false));
add(StoriesInitialize()); add(StoriesInitialize());
} }

View File

@ -0,0 +1,30 @@
import 'package:logger/logger.dart';
class CustomLogFilter extends LogFilter {
@override
Level? get level => Level.verbose;
/// The minimal level allowed in production.
static const Level _minimalLevel = Level.info;
@override
bool shouldLog(LogEvent event) {
bool shouldLog = false;
if (event.level.index >= _minimalLevel.index) {
return true;
}
assert(
() {
if (event.level.index >= level!.index) {
shouldLog = true;
}
return true;
}(),
'',
);
return shouldLog;
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.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/repositories/repositories.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
@ -10,16 +11,17 @@ final GetIt locator = GetIt.instance;
/// Set up [GetIt] locator. /// Set up [GetIt] locator.
Future<void> setUpLocator() async { Future<void> setUpLocator() async {
locator locator
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()))
..registerSingleton<StoriesRepository>(StoriesRepository()) ..registerSingleton<StoriesRepository>(StoriesRepository())
..registerSingleton<PreferenceRepository>(PreferenceRepository()) ..registerSingleton<PreferenceRepository>(PreferenceRepository())
..registerSingleton<SearchRepository>(SearchRepository()) ..registerSingleton<SearchRepository>(SearchRepository())
..registerSingleton<AuthRepository>(AuthRepository()) ..registerSingleton<AuthRepository>(AuthRepository())
..registerSingleton<PostRepository>(PostRepository()) ..registerSingleton<PostRepository>(PostRepository())
..registerSingleton<SembastRepository>(SembastRepository()) ..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<CacheRepository>(CacheRepository()) ..registerSingleton<OfflineRepository>(OfflineRepository())
..registerSingleton<CacheService>(CacheService()) ..registerSingleton<DraftCache>(DraftCache())
..registerSingleton<CommentCache>(CommentCache())
..registerSingleton<LocalNotification>(LocalNotification()) ..registerSingleton<LocalNotification>(LocalNotification())
..registerSingleton<Logger>(Logger())
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>( ..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
RouteObserver<ModalRoute<dynamic>>(), RouteObserver<ModalRoute<dynamic>>(),
); );

View File

@ -10,31 +10,31 @@ part 'collapse_state.dart';
class CollapseCubit extends Cubit<CollapseState> { class CollapseCubit extends Cubit<CollapseState> {
CollapseCubit({ CollapseCubit({
required int commentId, required int commentId,
CacheService? cacheService, CollapseCache? collapseCache,
}) : _commentId = commentId, }) : _commentId = commentId,
_cacheService = cacheService ?? locator.get<CacheService>(), _collapseCache = collapseCache ?? locator.get<CollapseCache>(),
super(const CollapseState.init()); super(const CollapseState.init());
final int _commentId; final int _commentId;
final CacheService _cacheService; final CollapseCache _collapseCache;
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription; late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
void init() { void init() {
_streamSubscription = _streamSubscription =
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener); _collapseCache.hiddenComments.listen(hiddenCommentsStreamListener);
emit( emit(
state.copyWith( state.copyWith(
collapsedCount: _cacheService.totalHidden(_commentId), collapsedCount: _collapseCache.totalHidden(_commentId),
collapsed: _cacheService.isCollapsed(_commentId), collapsed: _collapseCache.isCollapsed(_commentId),
hidden: _cacheService.isHidden(_commentId), hidden: _collapseCache.isHidden(_commentId),
), ),
); );
} }
void collapse() { void collapse() {
if (state.collapsed) { if (state.collapsed) {
_cacheService.uncollapse(_commentId); _collapseCache.uncollapse(_commentId);
emit( emit(
state.copyWith( state.copyWith(
@ -43,7 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
), ),
); );
} else { } else {
final int count = _cacheService.collapse(_commentId); final int count = _collapseCache.collapse(_commentId);
emit( emit(
state.copyWith( state.copyWith(

View File

@ -11,31 +11,56 @@ import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
part 'comments_state.dart'; part 'comments_state.dart';
class CommentsCubit extends Cubit<CommentsState> { class CommentsCubit extends Cubit<CommentsState> {
CommentsCubit({ CommentsCubit({
CacheService? cacheService, required CollapseCache collapseCache,
CacheRepository? cacheRepository, CommentCache? commentCache,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository, StoriesRepository? storiesRepository,
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
Logger? logger,
required bool offlineReading, required bool offlineReading,
required Item item, required Item item,
}) : _cacheService = cacheService ?? locator.get<CacheService>(), required FetchMode defaultFetchMode,
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(), required CommentsOrder defaultCommentsOrder,
}) : _collapseCache = collapseCache,
_commentCache = commentCache ?? locator.get<CommentCache>(),
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository = _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
super(CommentsState.init(offlineReading: offlineReading, item: item)); _logger = logger ?? locator.get<Logger>(),
super(
CommentsState.init(
offlineReading: offlineReading,
item: item,
fetchMode: defaultFetchMode,
order: defaultCommentsOrder,
),
);
final CacheService _cacheService; final CollapseCache _collapseCache;
final CacheRepository _cacheRepository; final CommentCache _commentCache;
final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
final Logger _logger;
/// The [StreamSubscription] for stream (both lazy or eager)
/// fetching comments posted directly to the story.
StreamSubscription<Comment>? _streamSubscription; StreamSubscription<Comment>? _streamSubscription;
/// The map of [StreamSubscription] for streams
/// fetching comments lazily. [int] is the id of parent comment.
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
<int, StreamSubscription<Comment>>{};
static const int _pageSize = 20; static const int _pageSize = 20;
@override @override
@ -47,6 +72,7 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> init({ Future<void> init({
bool onlyShowTargetComment = false, bool onlyShowTargetComment = false,
bool useCommentCache = false,
List<Comment>? targetParents, List<Comment>? targetParents,
}) async { }) async {
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) { if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
@ -59,7 +85,7 @@ class CommentsCubit extends Cubit<CommentsState> {
); );
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchCommentsStream( .fetchAllCommentsRecursivelyStream(
ids: targetParents!.last.kids, ids: targetParents!.last.kids,
level: targetParents.last.level + 1, level: targetParents.last.level + 1,
) )
@ -69,7 +95,13 @@ class CommentsCubit extends Cubit<CommentsState> {
return; return;
} }
emit(state.copyWith(status: CommentsStatus.loading)); emit(
state.copyWith(
status: CommentsStatus.loading,
comments: <Comment>[],
currentPage: 0,
),
);
final Item item = state.item; final Item item = state.item;
final Item updatedItem = state.offlineReading final Item updatedItem = state.offlineReading
@ -80,15 +112,31 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(state.copyWith(item: updatedItem)); emit(state.copyWith(item: updatedItem));
if (state.offlineReading) { if (state.offlineReading) {
_streamSubscription = _cacheRepository _streamSubscription = _offlineRepository
.getCachedCommentsStream(ids: kids) .getCachedCommentsStream(ids: kids)
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} else { } else {
_streamSubscription = _storiesRepository switch (state.fetchMode) {
.fetchCommentsStream(ids: kids) case FetchMode.lazy:
.listen(_onCommentFetched) _streamSubscription = _storiesRepository
..onDone(_onDone); .fetchCommentsStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
)
.listen(_onCommentFetched)
..onDone(_onDone);
break;
case FetchMode.eager:
_streamSubscription = _storiesRepository
.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
)
.listen(_onCommentFetched)
..onDone(_onDone);
break;
}
} }
} }
@ -102,28 +150,47 @@ class CommentsCubit extends Cubit<CommentsState> {
return; return;
} }
_cacheService
..resetComments()
..resetCollapsedComments();
emit( emit(
state.copyWith( state.copyWith(
status: CommentsStatus.loading, status: CommentsStatus.loading,
comments: <Comment>[],
), ),
); );
_collapseCache.resetCollapsedComments();
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();
for (final int id in _streamSubscriptions.keys) {
await _streamSubscriptions[id]?.cancel();
}
_streamSubscriptions.clear();
emit(
state.copyWith(
comments: <Comment>[],
currentPage: 0,
),
);
final Item item = state.item; final Item item = state.item;
final Item updatedItem = final Item updatedItem =
await _storiesRepository.fetchItemBy(id: item.id) ?? item; await _storiesRepository.fetchItemBy(id: item.id) ?? item;
final List<int> kids = sortKids(updatedItem.kids); final List<int> kids = sortKids(updatedItem.kids);
_streamSubscription = _storiesRepository if (state.fetchMode == FetchMode.lazy) {
.fetchCommentsStream(ids: kids) _streamSubscription = _storiesRepository
.listen(_onCommentFetched) .fetchCommentsStream(
..onDone(_onDone); ids: kids,
)
.listen(_onCommentFetched)
..onDone(_onDone);
} else {
_streamSubscription = _storiesRepository
.fetchAllCommentsRecursivelyStream(
ids: kids,
)
.listen(_onCommentFetched)
..onDone(_onDone);
}
emit( emit(
state.copyWith( state.copyWith(
@ -138,17 +205,67 @@ class CommentsCubit extends Cubit<CommentsState> {
emit( emit(
state.copyWith( state.copyWith(
onlyShowTargetComment: false, onlyShowTargetComment: false,
comments: <Comment>[],
item: story, item: story,
), ),
); );
init(); init();
} }
void loadMore() { /// [comment] is only used for lazy fetching.
if (_streamSubscription != null) { void loadMore({Comment? comment}) {
emit(state.copyWith(status: CommentsStatus.loading)); switch (state.fetchMode) {
_streamSubscription?.resume(); case FetchMode.lazy:
if (comment == null) return;
if (_streamSubscriptions.containsKey(comment.id)) return;
final int level = comment.level + 1;
int offset = 0;
/// Ignoring because the subscription will be cancelled in close()
// ignore: cancel_subscriptions
final StreamSubscription<Comment> streamSubscription =
_storiesRepository
.fetchCommentsStream(ids: comment.kids)
.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),
),
),
);
offset++;
})
..onDone(() {
_streamSubscriptions[comment.id]?.cancel();
_streamSubscriptions.remove(comment.id);
})
..onError((dynamic error) {
_logger.e(error);
_streamSubscriptions[comment.id]?.cancel();
_streamSubscriptions.remove(comment.id);
});
_streamSubscriptions[comment.id] = streamSubscription;
break;
case FetchMode.eager:
if (_streamSubscription != null) {
emit(state.copyWith(status: CommentsStatus.loading));
_streamSubscription?.resume();
}
break;
} }
} }
@ -175,11 +292,30 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
void onOrderChanged(CommentsOrder? order) { void onOrderChanged(CommentsOrder? order) {
HapticFeedback.selectionClick();
if (order == null) return; if (order == null) return;
if (state.order == order) return;
HapticFeedback.selectionClick();
_streamSubscription?.cancel(); _streamSubscription?.cancel();
emit(state.copyWith(order: order, comments: <Comment>[])); for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
init(); s.cancel();
}
_streamSubscriptions.clear();
emit(state.copyWith(order: order));
init(useCommentCache: true);
}
void onFetchModeChanged(FetchMode? fetchMode) {
if (fetchMode == null) return;
if (state.fetchMode == fetchMode) return;
_collapseCache.resetCollapsedComments();
HapticFeedback.selectionClick();
_streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
}
_streamSubscriptions.clear();
emit(state.copyWith(fetchMode: fetchMode));
init(useCommentCache: true);
} }
List<int> sortKids(List<int> kids) { List<int> sortKids(List<int> kids) {
@ -205,9 +341,8 @@ class CommentsCubit extends Cubit<CommentsState> {
void _onCommentFetched(Comment? comment) { void _onCommentFetched(Comment? comment) {
if (comment != null) { if (comment != null) {
_cacheService _collapseCache.addKid(comment.id, to: comment.parent);
..addKid(comment.id, to: comment.parent) _commentCache.cacheComment(comment);
..cacheComment(comment);
_sembastRepository.cacheComment(comment); _sembastRepository.cacheComment(comment);
final List<LinkifyElement> elements = _linkify( final List<LinkifyElement> elements = _linkify(
@ -224,21 +359,24 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(state.copyWith(comments: updatedComments)); emit(state.copyWith(comments: updatedComments));
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage && if (state.fetchMode == FetchMode.eager) {
updatedComments.length <= if (updatedComments.length >=
_pageSize * 2 + _pageSize * state.currentPage) { _pageSize + _pageSize * state.currentPage &&
final bool isHidden = _cacheService.isHidden(comment.id); updatedComments.length <=
_pageSize * 2 + _pageSize * state.currentPage) {
final bool isHidden = _collapseCache.isHidden(comment.id);
if (!isHidden) { if (!isHidden) {
_streamSubscription?.pause(); _streamSubscription?.pause();
}
emit(
state.copyWith(
currentPage: state.currentPage + 1,
status: CommentsStatus.loaded,
),
);
} }
emit(
state.copyWith(
currentPage: state.currentPage + 1,
status: CommentsStatus.loaded,
),
);
} }
} }
} }
@ -271,6 +409,9 @@ class CommentsCubit extends Cubit<CommentsState> {
@override @override
Future<void> close() async { Future<void> close() async {
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
await s.cancel();
}
await super.close(); await super.close();
} }
} }

View File

@ -8,12 +8,6 @@ enum CommentsStatus {
failure, failure,
} }
enum CommentsOrder {
natural,
newestFirst,
oldestFirst,
}
class CommentsState extends Equatable { class CommentsState extends Equatable {
const CommentsState({ const CommentsState({
required this.item, required this.item,
@ -21,6 +15,7 @@ class CommentsState extends Equatable {
required this.status, required this.status,
required this.fetchParentStatus, required this.fetchParentStatus,
required this.order, required this.order,
required this.fetchMode,
required this.onlyShowTargetComment, required this.onlyShowTargetComment,
required this.offlineReading, required this.offlineReading,
required this.currentPage, required this.currentPage,
@ -29,10 +24,11 @@ class CommentsState extends Equatable {
CommentsState.init({ CommentsState.init({
required this.offlineReading, required this.offlineReading,
required this.item, required this.item,
required this.fetchMode,
required this.order,
}) : comments = <Comment>[], }) : comments = <Comment>[],
status = CommentsStatus.init, status = CommentsStatus.init,
fetchParentStatus = CommentsStatus.init, fetchParentStatus = CommentsStatus.init,
order = CommentsOrder.natural,
onlyShowTargetComment = false, onlyShowTargetComment = false,
currentPage = 0; currentPage = 0;
@ -41,6 +37,7 @@ class CommentsState extends Equatable {
final CommentsStatus status; final CommentsStatus status;
final CommentsStatus fetchParentStatus; final CommentsStatus fetchParentStatus;
final CommentsOrder order; final CommentsOrder order;
final FetchMode fetchMode;
final bool onlyShowTargetComment; final bool onlyShowTargetComment;
final bool offlineReading; final bool offlineReading;
final int currentPage; final int currentPage;
@ -51,6 +48,7 @@ class CommentsState extends Equatable {
CommentsStatus? status, CommentsStatus? status,
CommentsStatus? fetchParentStatus, CommentsStatus? fetchParentStatus,
CommentsOrder? order, CommentsOrder? order,
FetchMode? fetchMode,
bool? onlyShowTargetComment, bool? onlyShowTargetComment,
bool? offlineReading, bool? offlineReading,
int? currentPage, int? currentPage,
@ -61,6 +59,7 @@ class CommentsState extends Equatable {
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus, fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
status: status ?? this.status, status: status ?? this.status,
order: order ?? this.order, order: order ?? this.order,
fetchMode: fetchMode ?? this.fetchMode,
onlyShowTargetComment: onlyShowTargetComment:
onlyShowTargetComment ?? this.onlyShowTargetComment, onlyShowTargetComment ?? this.onlyShowTargetComment,
offlineReading: offlineReading ?? this.offlineReading, offlineReading: offlineReading ?? this.offlineReading,
@ -68,6 +67,8 @@ class CommentsState extends Equatable {
); );
} }
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
item, item,
@ -75,6 +76,7 @@ class CommentsState extends Equatable {
status, status,
fetchParentStatus, fetchParentStatus,
order, order,
fetchMode,
onlyShowTargetComment, onlyShowTargetComment,
offlineReading, offlineReading,
currentPage, currentPage,

View File

@ -1,26 +1,27 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/utils/debouncer.dart'; import 'package:hacki/utils/debouncer.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
part 'edit_state.dart'; part 'edit_state.dart';
class EditCubit extends Cubit<EditState> { class EditCubit extends HydratedCubit<EditState> {
EditCubit({CacheService? cacheService}) EditCubit({DraftCache? draftCache})
: _cacheService = cacheService ?? locator.get<CacheService>(), : _draftCache = draftCache ?? locator.get<DraftCache>(),
_debouncer = Debouncer(delay: const Duration(seconds: 1)), _debouncer = Debouncer(delay: const Duration(seconds: 1)),
super(const EditState.init()); super(const EditState.init());
final CacheService _cacheService; final DraftCache _draftCache;
final Debouncer _debouncer; final Debouncer _debouncer;
void onReplyTapped(Item item) { void onReplyTapped(Item item) {
emit( emit(
EditState( EditState(
replyingTo: item, replyingTo: item,
text: _cacheService.getDraft(replyingTo: item.id), text: _draftCache.getDraft(replyingTo: item.id),
), ),
); );
} }
@ -44,9 +45,10 @@ class EditCubit extends Cubit<EditState> {
void onReplySubmittedSuccessfully() { void onReplySubmittedSuccessfully() {
if (state.replyingTo != null) { if (state.replyingTo != null) {
_cacheService.removeDraft(replyingTo: state.replyingTo!.id); _draftCache.removeDraft(replyingTo: state.replyingTo!.id);
} }
emit(const EditState.init()); emit(const EditState.init());
clear();
} }
void onTextChanged(String text) { void onTextChanged(String text) {
@ -54,11 +56,61 @@ class EditCubit extends Cubit<EditState> {
if (state.replyingTo != null) { if (state.replyingTo != null) {
final int? id = state.replyingTo?.id; final int? id = state.replyingTo?.id;
_debouncer.run(() { _debouncer.run(() {
_cacheService.cacheDraft( _draftCache.cacheDraft(
text: text, text: text,
replyingTo: id!, replyingTo: id!,
); );
}); });
} }
} }
void deleteDraft() => clear();
bool called = false;
@override
EditState? fromJson(Map<String, dynamic> json) {
final String text = json['text'] as String? ?? '';
final Map<String, dynamic>? itemJson =
json['item'] as Map<String, dynamic>?;
final Item? replyingTo = itemJson == null ? null : Item.fromJson(itemJson);
if (replyingTo != null && text.isNotEmpty) {
_draftCache.cacheDraft(text: text, replyingTo: replyingTo.id);
final EditState state = EditState(
text: text,
replyingTo: replyingTo,
);
_cachedState = state;
return state;
}
return state;
}
@override
Map<String, dynamic>? toJson(EditState state) {
EditState selected = state;
if (state.replyingTo == null ||
(state.replyingTo?.id != _cachedState.replyingTo?.id &&
state.text.isNullOrEmpty)) {
selected = _cachedState;
}
if (selected.text.isNullOrEmpty) {
clear();
return null;
}
return <String, dynamic>{
'text': selected.text ?? '',
'item': selected.replyingTo?.toJson(),
};
}
static EditState _cachedState = const EditState.init();
} }

View File

@ -30,12 +30,9 @@ class NotificationCubit extends Cubit<NotificationState> {
_authBloc.stream.listen((AuthState authState) { _authBloc.stream.listen((AuthState authState) {
if (authState.isLoggedIn && authState.username != _username) { if (authState.isLoggedIn && authState.username != _username) {
// Get the user setting. // Get the user setting.
_preferenceRepository.shouldShowNotification if (_preferenceCubit.state.showNotification) {
.then((bool showNotification) { Future<void>.delayed(const Duration(seconds: 2), init);
if (showNotification) { }
init();
}
});
// Listen for setting changes in the future. // Listen for setting changes in the future.
_preferenceCubit.stream.listen((PreferenceState prefState) { _preferenceCubit.stream.listen((PreferenceState prefState) {

View File

@ -1,6 +1,8 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
part 'preference_state.dart'; part 'preference_state.dart';
@ -9,69 +11,67 @@ class PreferenceCubit extends Cubit<PreferenceState> {
PreferenceCubit({PreferenceRepository? storageRepository}) PreferenceCubit({PreferenceRepository? storageRepository})
: _preferenceRepository = : _preferenceRepository =
storageRepository ?? locator.get<PreferenceRepository>(), storageRepository ?? locator.get<PreferenceRepository>(),
super(const PreferenceState.init()) { super(PreferenceState.init()) {
init(); init();
} }
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
void init() { void init() {
_preferenceRepository.shouldShowNotification for (final BooleanPreference p
.then((bool value) => emit(state.copyWith(showNotification: value))); in Preference.allPreferences.whereType<BooleanPreference>()) {
_preferenceRepository.shouldShowComplexStoryTile.then( initPreference<bool>(p).then<bool?>((bool? value) {
(bool value) => emit(state.copyWith(showComplexStoryTile: value)), final Preference<dynamic> updatedPreference = p.copyWith(val: value);
); emit(state.copyWithPreference(updatedPreference));
_preferenceRepository.shouldShowWebFirst return null;
.then((bool value) => emit(state.copyWith(showWebFirst: value))); });
_preferenceRepository.shouldShowEyeCandy }
.then((bool value) => emit(state.copyWith(showEyeCandy: value)));
_preferenceRepository.trueDarkMode for (final IntPreference p
.then((bool value) => emit(state.copyWith(useTrueDark: value))); in Preference.allPreferences.whereType<IntPreference>()) {
_preferenceRepository.readerMode initPreference<int>(p).then<int?>((int? value) {
.then((bool value) => emit(state.copyWith(useReader: value))); final Preference<dynamic> updatedPreference = p.copyWith(val: value);
_preferenceRepository.markReadStories emit(state.copyWithPreference(updatedPreference));
.then((bool value) => emit(state.copyWith(markReadStories: value))); return null;
_preferenceRepository.shouldShowMetadata });
.then((bool value) => emit(state.copyWith(showMetadata: value))); }
} }
void toggleNotificationMode() { Future<T?> initPreference<T>(Preference<T> preference) async {
emit(state.copyWith(showNotification: !state.showNotification)); switch (T) {
_preferenceRepository.toggleNotificationMode(); 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() { void toggle(BooleanPreference preference) {
emit(state.copyWith(showComplexStoryTile: !state.showComplexStoryTile)); final BooleanPreference updatedPreference =
_preferenceRepository.toggleDisplayMode(); preference.copyWith(val: !preference.val) as BooleanPreference;
emit(state.copyWithPreference(updatedPreference));
_preferenceRepository.setBool(preference.key, !preference.val);
} }
void toggleNavigationMode() { void update<T>(Preference<T> preference, {required T to}) {
emit(state.copyWith(showWebFirst: !state.showWebFirst)); final T value = to;
_preferenceRepository.toggleNavigationMode(); final Preference<T> updatedPreference = preference.copyWith(val: value);
}
void toggleEyeCandyMode() { emit(state.copyWithPreference(updatedPreference));
emit(state.copyWith(showEyeCandy: !state.showEyeCandy));
_preferenceRepository.toggleEyeCandyMode();
}
void toggleTrueDarkMode() { switch (T) {
emit(state.copyWith(useTrueDark: !state.useTrueDark)); case int:
_preferenceRepository.toggleTrueDarkMode(); _preferenceRepository.setInt(preference.key, value as int);
} break;
case bool:
void toggleReaderMode() { _preferenceRepository.setBool(preference.key, value as bool);
emit(state.copyWith(useReader: !state.useReader)); break;
_preferenceRepository.toggleReaderMode(); default:
} throw UnimplementedError();
}
void toggleMarkReadStoriesMode() {
emit(state.copyWith(markReadStories: !state.markReadStories));
_preferenceRepository.toggleMarkReadStoriesMode();
}
void toggleMetadataMode() {
emit(state.copyWith(showMetadata: !state.showMetadata));
_preferenceRepository.toggleMetadataMode();
} }
} }

View File

@ -2,66 +2,81 @@ part of 'preference_cubit.dart';
class PreferenceState extends Equatable { class PreferenceState extends Equatable {
const PreferenceState({ const PreferenceState({
required this.showNotification, required this.preferences,
required this.showComplexStoryTile,
required this.showWebFirst,
required this.showEyeCandy,
required this.useTrueDark,
required this.useReader,
required this.markReadStories,
required this.showMetadata,
}); });
const PreferenceState.init() PreferenceState.init()
: showNotification = false, : preferences = <Preference<dynamic>>{...Preference.allPreferences};
showComplexStoryTile = false,
showWebFirst = false,
showEyeCandy = false,
useTrueDark = false,
useReader = false,
markReadStories = false,
showMetadata = false;
final bool showNotification; final Set<Preference<dynamic>> preferences;
final bool showComplexStoryTile;
final bool showWebFirst;
final bool showEyeCandy;
final bool useTrueDark;
final bool useReader;
final bool markReadStories;
final bool showMetadata;
PreferenceState copyWith({ PreferenceState copyWith({
bool? showNotification, Set<Preference<dynamic>>? preferences,
bool? showComplexStoryTile,
bool? showWebFirst,
bool? showEyeCandy,
bool? useTrueDark,
bool? useReader,
bool? markReadStories,
bool? showMetadata,
}) { }) {
return PreferenceState( return PreferenceState(
showNotification: showNotification ?? this.showNotification, preferences: preferences ?? this.preferences,
showComplexStoryTile: showComplexStoryTile ?? this.showComplexStoryTile,
showWebFirst: showWebFirst ?? this.showWebFirst,
showEyeCandy: showEyeCandy ?? this.showEyeCandy,
useTrueDark: useTrueDark ?? this.useTrueDark,
useReader: useReader ?? this.useReader,
markReadStories: markReadStories ?? this.markReadStories,
showMetadata: showMetadata ?? this.showMetadata,
); );
} }
PreferenceState copyWithPreference<T extends Preference<dynamic>>(
T preference,
) {
return PreferenceState(
preferences: <Preference<dynamic>>{
...preferences.toList()
..remove(preference)
..insert(Preference.allPreferences.indexOf(preference), preference),
},
);
}
bool isOn<T extends BooleanPreference>(T preference) {
return preferences
.whereType<BooleanPreference>()
.singleWhere(
(BooleanPreference e) => e.runtimeType == preference.runtimeType,
)
.val;
}
bool _isOn<T extends BooleanPreference>() {
return preferences
.whereType<BooleanPreference>()
.singleWhere(
(BooleanPreference e) => e.runtimeType == T,
)
.val;
}
bool get showNotification => _isOn<NotificationModePreference>();
bool get showComplexStoryTile => _isOn<DisplayModePreference>();
bool get showWebFirst => _isOn<NavigationModePreference>();
bool get showEyeCandy => _isOn<EyeCandyModePreference>();
bool get useTrueDark => _isOn<TrueDarkModePreference>();
bool get useReader => _isOn<ReaderModePreference>();
bool get markReadStories => _isOn<MarkReadStoriesModePreference>();
bool get showMetadata => _isOn<MetadataModePreference>();
bool get tapAnywhereToCollapse => _isOn<CollapseModePreference>();
FetchMode get fetchMode => FetchMode.values
.elementAt(preferences.singleWhereType<FetchModePreference>().val);
CommentsOrder get order => CommentsOrder.values
.elementAt(preferences.singleWhereType<CommentsOrderPreference>().val);
FontSize get fontSize => FontSize.values
.elementAt(preferences.singleWhereType<FontSizePreference>().val);
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
showNotification, ...preferences.map<dynamic>((Preference<dynamic> e) => e.val),
showComplexStoryTile,
showWebFirst,
showEyeCandy,
useTrueDark,
useReader,
markReadStories,
showMetadata,
]; ];
} }

View File

@ -23,73 +23,71 @@ class SearchCubit extends Cubit<SearchState> {
state.copyWith( state.copyWith(
results: <Story>[], results: <Story>[],
status: SearchStatus.loading, status: SearchStatus.loading,
searchFilters: state.searchFilters.copyWith(query: query, page: 0), params: state.params.copyWith(query: query, page: 0),
), ),
); );
streamSubscription = _searchRepository streamSubscription =
.search(filters: state.searchFilters) _searchRepository.search(params: state.params).listen(_onStoryFetched)
.listen(_onStoryFetched) ..onDone(() {
..onDone(() { emit(state.copyWith(status: SearchStatus.loaded));
emit(state.copyWith(status: SearchStatus.loaded)); });
});
} }
void loadMore() { void loadMore() {
if (state.status != SearchStatus.loading) { if (state.status != SearchStatus.loading) {
final int updatedPage = state.searchFilters.page + 1; final int updatedPage = state.params.page + 1;
emit( emit(
state.copyWith( state.copyWith(
status: SearchStatus.loadingMore, status: SearchStatus.loadingMore,
searchFilters: state.searchFilters.copyWith(page: updatedPage), params: state.params.copyWith(page: updatedPage),
), ),
); );
streamSubscription = _searchRepository streamSubscription =
.search(filters: state.searchFilters) _searchRepository.search(params: state.params).listen(_onStoryFetched)
.listen(_onStoryFetched) ..onDone(() {
..onDone(() { emit(state.copyWith(status: SearchStatus.loaded));
emit(state.copyWith(status: SearchStatus.loaded)); });
});
} }
} }
void addFilter<T extends SearchFilter>(T filter) { void addFilter<T extends SearchFilter>(T filter) {
if (state.searchFilters.contains<T>()) { if (state.params.contains<T>()) {
emit( emit(
state.copyWith( state.copyWith(
searchFilters: state.searchFilters.copyWithFilterRemoved<T>(), params: state.params.copyWithFilterRemoved<T>(),
), ),
); );
} }
emit( emit(
state.copyWith( state.copyWith(
searchFilters: state.searchFilters.copyWithFilterAdded(filter), params: state.params.copyWithFilterAdded(filter),
), ),
); );
search(state.searchFilters.query); search(state.params.query);
} }
void removeFilter<T extends SearchFilter>() { void removeFilter<T extends SearchFilter>() {
emit( emit(
state.copyWith( state.copyWith(
searchFilters: state.searchFilters.copyWithFilterRemoved<T>(), params: state.params.copyWithFilterRemoved<T>(),
), ),
); );
search(state.searchFilters.query); search(state.params.query);
} }
void onSortToggled() { void onSortToggled() {
emit( emit(
state.copyWith( state.copyWith(
searchFilters: state.searchFilters.copyWith( params: state.params.copyWith(
sorted: !state.searchFilters.sorted, sorted: !state.params.sorted,
), ),
), ),
); );
search(state.searchFilters.query); search(state.params.query);
} }
void _onStoryFetched(Story story) { void _onStoryFetched(Story story) {

View File

@ -11,27 +11,27 @@ class SearchState extends Equatable {
const SearchState({ const SearchState({
required this.status, required this.status,
required this.results, required this.results,
required this.searchFilters, required this.params,
}); });
SearchState.init() SearchState.init()
: status = SearchStatus.initial, : status = SearchStatus.initial,
results = <Story>[], results = <Story>[],
searchFilters = SearchFilters.init(); params = SearchParams.init();
final List<Story> results; final List<Story> results;
final SearchStatus status; final SearchStatus status;
final SearchFilters searchFilters; final SearchParams params;
SearchState copyWith({ SearchState copyWith({
List<Story>? results, List<Story>? results,
SearchStatus? status, SearchStatus? status,
SearchFilters? searchFilters, SearchParams? params,
}) { }) {
return SearchState( return SearchState(
results: results ?? this.results, results: results ?? this.results,
status: status ?? this.status, status: status ?? this.status,
searchFilters: searchFilters ?? this.searchFilters, params: params ?? this.params,
); );
} }
@ -39,6 +39,6 @@ class SearchState extends Equatable {
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
status, status,
results, results,
searchFilters, params,
]; ];
} }

View File

@ -3,18 +3,24 @@ import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
part 'split_view_state.dart'; part 'split_view_state.dart';
class SplitViewCubit extends Cubit<SplitViewState> { class SplitViewCubit extends Cubit<SplitViewState> {
SplitViewCubit({CacheService? cacheService}) SplitViewCubit({
: _cacheService = cacheService ?? locator.get<CacheService>(), CommentCache? commentCache,
Logger? logger,
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
_logger = logger ?? locator.get<Logger>(),
super(const SplitViewState.init()); super(const SplitViewState.init());
final CacheService _cacheService; final Logger _logger;
final CommentCache _commentCache;
void updateItemScreenArgs(ItemScreenArgs args) { void updateItemScreenArgs(ItemScreenArgs args) {
_cacheService.resetCollapsedComments(); _logger.i('resetting comments in CommentCache');
_commentCache.resetComments();
emit(state.copyWith(itemScreenArgs: args)); emit(state.copyWith(itemScreenArgs: args));
} }

View File

@ -3,34 +3,34 @@ import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart' show Comment; import 'package:hacki/models/models.dart' show Comment;
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/services/cache_service.dart'; import 'package:hacki/services/services.dart';
part 'time_machine_state.dart'; part 'time_machine_state.dart';
class TimeMachineCubit extends Cubit<TimeMachineState> { class TimeMachineCubit extends Cubit<TimeMachineState> {
TimeMachineCubit({ TimeMachineCubit({
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
CacheService? cacheService, CommentCache? commentCache,
}) : _sembastRepository = }) : _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
_cacheService = cacheService ?? locator.get<CacheService>(), _commentCache = commentCache ?? locator.get<CommentCache>(),
super(TimeMachineState.init()); super(TimeMachineState.init());
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
final CacheService _cacheService; final CommentCache _commentCache;
Future<void> activateTimeMachine(Comment comment) async { Future<void> activateTimeMachine(Comment comment) async {
emit(state.copyWith(parents: <Comment>[])); emit(state.copyWith(parents: <Comment>[]));
final List<Comment> parents = <Comment>[]; final List<Comment> parents = <Comment>[];
Comment? parent = _cacheService.getComment(comment.parent); Comment? parent = _commentCache.getComment(comment.parent);
parent ??= await _sembastRepository.getCachedComment(id: comment.parent); parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
while (parent != null) { while (parent != null) {
parents.insert(0, parent); parents.insert(0, parent);
final int parentId = parent.parent; final int parentId = parent.parent;
parent = _cacheService.getComment(parentId); parent = _commentCache.getComment(parentId);
parent ??= await _sembastRepository.getCachedComment(id: parentId); parent ??= await _sembastRepository.getCachedComment(id: parentId);
} }

View File

@ -0,0 +1,58 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
extension ContextExtension on BuildContext {
T? tryRead<T>() {
try {
return read<T>();
} catch (_) {
return null;
}
}
Rect? get rect {
final RenderBox? box = findRenderObject() as RenderBox?;
final Rect? rect =
box == null ? null : box.localToGlobal(Offset.zero) & box.size;
return rect;
}
static double _screenWidth = 0;
static double _storyTileHeight = 0;
static int _storyTileMaxLines = 4;
static const double _screenWidthLowerBound = 430,
_screenWidthUpperBound = 850,
_picHeightLowerBound = 110,
_picHeightUpperBound = 128,
_smallPicHeight = 100,
_picHeightFactor = 0.3;
double get storyTileHeight {
final double screenWidth =
min(MediaQuery.of(this).size.height, MediaQuery.of(this).size.width);
if (screenWidth == _screenWidth) {
return _storyTileHeight;
} else {
_screenWidth = screenWidth;
}
final bool showSmallerPreviewPic = screenWidth > _screenWidthLowerBound &&
screenWidth < _screenWidthUpperBound;
final double height = showSmallerPreviewPic
? _smallPicHeight
: (screenWidth * _picHeightFactor)
.clamp(_picHeightLowerBound, _picHeightUpperBound);
final int maxLines = height == _smallPicHeight ? 3 : 4;
_storyTileMaxLines = maxLines;
_storyTileHeight = height;
return height;
}
int get storyTileMaxLines {
return _storyTileMaxLines;
}
}

View File

@ -7,10 +7,8 @@ extension DateTimeExtension on DateTime {
return '$gap year${gap == 1 ? '' : 's'} ago'; return '$gap year${gap == 1 ? '' : 's'} ago';
} else if (diff.inDays > 30) { } else if (diff.inDays > 30) {
int gap = now.month - month; int gap = now.month - month;
if (gap == 0) { if (gap <= 0) {
gap = 1; gap = now.month + 12 - month;
} else if (gap < 0) {
gap = now.month + (12 - month);
} }
return '$gap month${gap == 1 ? '' : 's'} ago'; return '$gap month${gap == 1 ? '' : 's'} ago';
} else if (diff.inDays >= 1) { } else if (diff.inDays >= 1) {

View File

@ -1,7 +1,9 @@
export 'context_extension.dart';
export 'date_time_extension.dart'; export 'date_time_extension.dart';
export 'int_extension.dart'; export 'int_extension.dart';
export 'list_extension.dart'; export 'list_extension.dart';
export 'object_extension.dart'; export 'object_extension.dart';
export 'set_extension.dart';
export 'state_extension.dart'; export 'state_extension.dart';
export 'string_extension.dart'; export 'string_extension.dart';
export 'widget_extension.dart'; export 'widget_extension.dart';

View File

@ -0,0 +1,13 @@
extension SetExtension<E> on Set<E> {
void removeWhereType<T extends E>() {
return removeWhere((E e) => e is T);
}
bool hasType<T extends E>() {
return whereType<T>().isNotEmpty;
}
T singleWhereType<T extends E>() {
return whereType<T>().single;
}
}

View File

@ -1,11 +1,13 @@
extension StringExtension on String { extension StringExtension on String {
int? getItemId() { int? get itemId {
final RegExp regex = RegExp(r'\d+$'); final RegExp regex = RegExp(r'\d+$');
final RegExp exception = RegExp(r'\)|].*$'); final RegExp exception = RegExp(r'\)|].*$');
final String match = regex.stringMatch(replaceAll(exception, '')) ?? ''; final String match = regex.stringMatch(replaceAll(exception, '')) ?? '';
return int.tryParse(match); return int.tryParse(match);
} }
bool get isStoryLink => contains('news.ycombinator.com/item');
String removeAllEmojis() { String removeAllEmojis() {
final RegExp regex = RegExp( final RegExp regex = RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])', r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])',
@ -13,3 +15,12 @@ extension StringExtension on String {
return replaceAllMapped(regex, (_) => ''); return replaceAllMapped(regex, (_) => '');
} }
} }
extension OptionalStringExtension on String? {
bool get isNullOrEmpty {
if (this == null) return true;
return this!.trim().isEmpty;
}
bool get isNotNullOrEmpty => !isNullOrEmpty;
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension WidgetModifier on Widget { extension WidgetModifier on Widget {
Widget padding([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) { Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
return Padding( return Padding(
padding: value, padding: value,
child: this, child: this,

View File

@ -3,8 +3,10 @@ import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:feature_discovery/feature_discovery.dart'; import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart'; import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
@ -12,11 +14,13 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/custom_router.dart'; import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/custom_bloc_observer.dart';
import 'package:hacki/services/fetcher.dart'; import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject; import 'package:rxdart/rxdart.dart' show BehaviorSubject;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -30,9 +34,19 @@ final BehaviorSubject<String?> selectNotificationSubject =
final BehaviorSubject<String?> siriSuggestionSubject = final BehaviorSubject<String?> siriSuggestionSubject =
BehaviorSubject<String?>(); BehaviorSubject<String?>();
Future<void> main() async { late final bool isTesting;
Future<void> main({bool testing = false}) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
isTesting = testing;
final HydratedStorage storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
if (Platform.isIOS) { if (Platform.isIOS) {
unawaited( unawaited(
Workmanager().initialize( Workmanager().initialize(
@ -73,6 +87,19 @@ Future<void> main() async {
siriSuggestionSubject.add(storyId); siriSuggestionSubject.add(storyId);
}, },
); );
} else if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
),
);
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge,
overlays: <SystemUiOverlay>[SystemUiOverlay.top],
);
} }
final Directory tempDir = await getTemporaryDirectory(); final Directory tempDir = await getTemporaryDirectory();
@ -84,20 +111,10 @@ Future<void> main() async {
final AdaptiveThemeMode? savedThemeMode = await AdaptiveTheme.getThemeMode(); final AdaptiveThemeMode? savedThemeMode = await AdaptiveTheme.getThemeMode();
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
final bool trueDarkMode = final bool trueDarkMode =
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false; prefs.getBool(const TrueDarkModePreference().key) ?? false;
// Uncomment code below for running with logging. Bloc.observer = CustomBlocObserver();
// BlocOverrides.runZoned( HydratedBloc.storage = storage;
// () {
// runApp(
// HackiApp(
// savedThemeMode: savedThemeMode,
// trueDarkMode: trueDarkMode,
// ),
// );
// },
// blocObserver: CustomBlocObserver(),
// );
runApp( runApp(
HackiApp( HackiApp(
@ -180,6 +197,10 @@ class HackiApp extends StatelessWidget {
lazy: false, lazy: false,
create: (BuildContext context) => PostCubit(), create: (BuildContext context) => PostCubit(),
), ),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
], ],
child: AdaptiveTheme( child: AdaptiveTheme(
light: ThemeData( light: ThemeData(

View File

@ -59,6 +59,7 @@ class Comment extends Item {
); );
} }
@override
Map<String, dynamic> toJson() => <String, dynamic>{ Map<String, dynamic> toJson() => <String, dynamic>{
'id': id, 'id': id,
'time': time, 'time': time,

View File

@ -0,0 +1,9 @@
enum CommentsOrder {
natural('Natural'),
newestFirst('Newest first'),
oldestFirst('Oldest first');
const CommentsOrder(this.description);
final String description;
}

View File

@ -0,0 +1,9 @@
mixin SettingsDisplayable {
String get title;
String get subtitle => '';
/// Whether or not this should be displayed
/// in settings.
bool get isDisplayable => true;
}

View File

@ -0,0 +1,8 @@
enum FetchMode {
lazy('Lazy'),
eager('Eager');
const FetchMode(this.description);
final String description;
}

12
lib/models/font_size.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:hacki/styles/styles.dart';
enum FontSize {
regular('Regular', TextDimens.pt15),
large('Large', TextDimens.pt16),
xlarge('XLarge', TextDimens.pt18);
const FontSize(this.description, this.fontSize);
final String description;
final double fontSize;
}

View File

@ -1,7 +1,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/extensions/date_time_extension.dart'; import 'package:hacki/extensions/date_time_extension.dart';
abstract class Item extends Equatable { class Item extends Equatable {
const Item({ const Item({
required this.id, required this.id,
required this.deleted, required this.deleted,
@ -35,6 +35,22 @@ abstract class Item extends Equatable {
text = '', text = '',
type = ''; type = '';
Item.fromJson(Map<String, dynamic> json)
: id = json['id'] as int? ?? 0,
score = json['score'] as int? ?? 0,
descendants = json['descendants'] as int? ?? 0,
time = json['time'] as int? ?? 0,
by = json['by'] as String? ?? '',
title = json['title'] as String? ?? '',
text = json['text'] as String? ?? '',
url = json['url'] as String? ?? '',
kids = <int>[],
dead = json['dead'] as bool? ?? false,
deleted = json['deleted'] as bool? ?? false,
parent = json['parent'] as int? ?? 0,
parts = <int>[],
type = json['type'] as String? ?? '';
final int id; final int id;
final int time; final int time;
final int score; final int score;
@ -65,4 +81,40 @@ abstract class Item extends Equatable {
bool get isJob => type == 'job'; bool get isJob => type == 'job';
bool get isComment => type == 'comment'; bool get isComment => type == 'comment';
Map<String, dynamic> toJson() {
return <String, dynamic>{
'descendants': descendants,
'id': id,
'score': score,
'time': time,
'by': by,
'title': title,
'url': url,
'kids': kids,
'text': text,
'dead': dead,
'deleted': deleted,
'type': type,
'parts': parts,
};
}
@override
List<Object?> get props => <Object?>[
id,
deleted,
by,
time,
text,
dead,
parent,
kids,
url,
score,
title,
type,
parts,
descendants,
];
} }

View File

@ -1,8 +1,12 @@
export 'buildable_comment.dart'; export 'buildable_comment.dart';
export 'comment.dart'; export 'comment.dart';
export 'comments_order.dart';
export 'fetch_mode.dart';
export 'font_size.dart';
export 'item.dart'; export 'item.dart';
export 'poll_option.dart'; export 'poll_option.dart';
export 'post_data.dart'; export 'post_data.dart';
export 'search_filters.dart'; export 'preference.dart';
export 'search_params.dart';
export 'story.dart'; export 'story.dart';
export 'user.dart'; export 'user.dart';

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