Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
b3e994269c | |||
a2c66a0075 | |||
5f43fd6968 | |||
d83381a7fd | |||
764ff09345 | |||
ab449adce2 | |||
2ec41b26f2 | |||
19f2107d95 | |||
c9b2d82dfc | |||
56e442b09f | |||
9069efcced | |||
bf6a5667dc | |||
cff73a010b |
22
.github/workflows/commit_check.yml
vendored
Normal 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.4"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.3.4'
|
||||
channel: 'stable'
|
||||
- run: flutter pub get
|
||||
- run: flutter format --set-exit-if-changed .
|
||||
- run: flutter analyze
|
32
.github/workflows/github-actions.yml
vendored
@ -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
@ -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.4
|
||||
- run: flutter pub get
|
||||
- run: flutter format --set-exit-if-changed .
|
||||
- run: flutter analyze
|
||||
# Start an ssh-agent that will provide the SSH key from the
|
||||
# SSH_PRIVATE_KEY secret to `fastlane match`
|
||||
- name: Setup SSH key
|
||||
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
@ -0,0 +1 @@
|
||||
2.7.5
|
@ -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
|
||||
|
||||
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.
|
||||
|
||||
[](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
|
||||
[](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
|
||||
[](https://github.com/Livinglist/Hacki/releases/latest)
|
||||
[](https://badges.pufler.dev)
|
||||
[](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
|
||||
[](https://pub.dev/packages/effective_dart)
|
||||
[](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/)
|
||||
|
||||
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion 32
|
||||
compileSdkVersion 33
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -51,7 +51,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.jiaqifeng.hacki"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 32
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 406 KiB |
2
fastlane/metadata/android/en-US/changelogs/67.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/68.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/69.txt
Normal file
@ -0,0 +1,3 @@
|
||||
- Lazy loading.
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/70.txt
Normal file
@ -0,0 +1,3 @@
|
||||
- Lazy loading.
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/71.txt
Normal file
@ -0,0 +1,3 @@
|
||||
- Lazy loading.
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/72.txt
Normal file
@ -0,0 +1,3 @@
|
||||
- Lazy loading.
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/73.txt
Normal file
@ -0,0 +1,3 @@
|
||||
- Lazy loading.
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/74.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Bumped Flutter version.
|
||||
- Updated navigation bar background color.
|
2
fastlane/metadata/android/en-US/changelogs/75.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Bumped Flutter version.
|
||||
- Updated navigation bar background color.
|
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 406 KiB |
68
integration_test/scrolling_test.dart
Normal 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',
|
||||
);
|
||||
});
|
||||
}
|
@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>9.0</string>
|
||||
<string>11.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
4
ios/Gemfile
Normal file
@ -0,0 +1,4 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
gem "cocoapods"
|
285
ios/Gemfile.lock
Normal 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
|
@ -1,5 +1,5 @@
|
||||
# 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.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@ -37,8 +37,5 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -19,6 +19,8 @@ PODS:
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (5.0.0)
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
@ -50,6 +52,7 @@ DEPENDENCIES:
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
@ -80,6 +83,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_siri_suggestions:
|
||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
receive_sharing_intent:
|
||||
@ -103,12 +108,13 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
@ -122,6 +128,6 @@ SPEC CHECKSUMS:
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: e4c97c7a9aacaeda4b952f7ef9ea29e47660f622
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
@ -549,7 +549,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@ -567,20 +567,23 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.24;
|
||||
MARKETING_VERSION = 0.2.32;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@ -635,7 +638,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@ -684,7 +687,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@ -704,20 +707,23 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.24;
|
||||
MARKETING_VERSION = 0.2.32;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -735,20 +741,23 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.24;
|
||||
MARKETING_VERSION = 0.2.32;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@ -767,9 +776,11 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -786,6 +797,8 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -806,9 +819,11 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -824,6 +839,8 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki.Share-Extension";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -842,9 +859,11 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
@ -860,6 +879,8 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -880,9 +901,11 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -899,6 +922,8 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@ -921,9 +946,11 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -939,6 +966,8 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki.Action-Extension";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -959,9 +988,11 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Action Extension/Info.plist";
|
||||
@ -977,6 +1008,8 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -7,10 +7,10 @@
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
|
8
ios/fastlane/Appfile
Normal 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
@ -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
@ -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
|
@ -17,15 +17,18 @@ part 'stories_state.dart';
|
||||
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesBloc({
|
||||
required PreferenceCubit preferenceCubit,
|
||||
CacheRepository? cacheRepository,
|
||||
OfflineRepository? offlineRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceCubit = preferenceCubit,
|
||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
_storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const StoriesState.init()) {
|
||||
on<StoriesInitialize>(onInitialize);
|
||||
on<StoriesRefresh>(onRefresh);
|
||||
@ -41,9 +44,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
final PreferenceCubit _preferenceCubit;
|
||||
final CacheRepository _cacheRepository;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
DeviceScreenType? deviceScreenType;
|
||||
StreamSubscription<PreferenceState>? _streamSubscription;
|
||||
static const int _smallPageSize = 10;
|
||||
@ -73,7 +77,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
add(StoriesPageSizeChanged(pageSize: pageSize));
|
||||
}
|
||||
});
|
||||
final bool hasCachedStories = await _cacheRepository.hasCachedStories;
|
||||
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
|
||||
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
|
||||
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
||||
emit(
|
||||
@ -92,13 +96,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
required Emitter<StoriesState> emit,
|
||||
}) async {
|
||||
if (state.offlineReading) {
|
||||
final List<int> ids = await _cacheRepository.getCachedStoryIds(of: of);
|
||||
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(of: of, to: ids)
|
||||
.copyWithCurrentPageUpdated(of: of, to: 0),
|
||||
);
|
||||
_cacheRepository
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||
)
|
||||
@ -169,7 +173,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
if (state.offlineReading) {
|
||||
_cacheRepository
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
@ -243,9 +247,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
),
|
||||
);
|
||||
|
||||
await _cacheRepository.deleteAllStoryIds();
|
||||
await _cacheRepository.deleteAllStories();
|
||||
await _cacheRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
|
||||
final Set<int> prioritizedIds = <int>{};
|
||||
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
||||
@ -253,7 +257,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
for (final StoryType type in prioritizedTypes) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -275,7 +279,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||
of: StoryType.latest,
|
||||
);
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||
latestIds.addAll(ids);
|
||||
|
||||
await fetchAndCacheStories(
|
||||
@ -314,11 +318,11 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
continue;
|
||||
}
|
||||
|
||||
await _cacheRepository.cacheStory(story: story);
|
||||
await _offlineRepository.cacheStory(story: story);
|
||||
|
||||
if (story.url.isNotEmpty && includingWebPage) {
|
||||
locator.get<Logger>().i('downloading ${story.url}');
|
||||
await _cacheRepository.cacheUrl(url: story.url);
|
||||
_logger.i('downloading ${story.url}');
|
||||
await _offlineRepository.cacheUrl(url: story.url);
|
||||
}
|
||||
|
||||
_storiesRepository
|
||||
@ -326,7 +330,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
.whereType<Comment>()
|
||||
.listen(
|
||||
(Comment comment) => unawaited(
|
||||
_cacheRepository.cacheComment(comment: comment),
|
||||
_offlineRepository.cacheComment(comment: comment),
|
||||
),
|
||||
)
|
||||
.onDone(() => add(StoryDownloaded(skipped: false)));
|
||||
@ -378,10 +382,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesExitOffline event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
await _cacheRepository.deleteAllStoryIds();
|
||||
await _cacheRepository.deleteAllStories();
|
||||
await _cacheRepository.deleteAllComments();
|
||||
await _cacheRepository.deleteAllWebPages();
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllWebPages();
|
||||
emit(state.copyWith(offlineReading: false));
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ import 'package:logger/logger.dart';
|
||||
|
||||
class CustomLogFilter extends LogFilter {
|
||||
@override
|
||||
// ignore: overridden_fields
|
||||
Level? level = Level.verbose;
|
||||
Level? get level => Level.verbose;
|
||||
|
||||
/// The minimal level allowed in production.
|
||||
static const Level _minimalLevel = Level.info;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:hacki/config/custom_log_filter.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
@ -10,14 +11,18 @@ final GetIt locator = GetIt.instance;
|
||||
/// Set up [GetIt] locator.
|
||||
Future<void> setUpLocator() async {
|
||||
locator
|
||||
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()))
|
||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||
..registerSingleton<SearchRepository>(SearchRepository())
|
||||
..registerSingleton<AuthRepository>(AuthRepository())
|
||||
..registerSingleton<PostRepository>(PostRepository())
|
||||
..registerSingleton<SembastRepository>(SembastRepository())
|
||||
..registerSingleton<CacheRepository>(CacheRepository())
|
||||
..registerSingleton<CacheService>(CacheService())
|
||||
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||
..registerSingleton<DraftCache>(DraftCache())
|
||||
..registerSingleton<CommentCache>(CommentCache())
|
||||
..registerSingleton<LocalNotification>(LocalNotification())
|
||||
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()));
|
||||
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
);
|
||||
}
|
||||
|
@ -10,31 +10,31 @@ part 'collapse_state.dart';
|
||||
class CollapseCubit extends Cubit<CollapseState> {
|
||||
CollapseCubit({
|
||||
required int commentId,
|
||||
CacheService? cacheService,
|
||||
CollapseCache? collapseCache,
|
||||
}) : _commentId = commentId,
|
||||
_cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
||||
super(const CollapseState.init());
|
||||
|
||||
final int _commentId;
|
||||
final CacheService _cacheService;
|
||||
final CollapseCache _collapseCache;
|
||||
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||
|
||||
void init() {
|
||||
_streamSubscription =
|
||||
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener);
|
||||
_collapseCache.hiddenComments.listen(hiddenCommentsStreamListener);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
collapsedCount: _cacheService.totalHidden(_commentId),
|
||||
collapsed: _cacheService.isCollapsed(_commentId),
|
||||
hidden: _cacheService.isHidden(_commentId),
|
||||
collapsedCount: _collapseCache.totalHidden(_commentId),
|
||||
collapsed: _collapseCache.isCollapsed(_commentId),
|
||||
hidden: _collapseCache.isHidden(_commentId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void collapse() {
|
||||
if (state.collapsed) {
|
||||
_cacheService.uncollapse(_commentId);
|
||||
_collapseCache.uncollapse(_commentId);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -43,7 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final int count = _cacheService.collapse(_commentId);
|
||||
final int count = _collapseCache.collapse(_commentId);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -11,31 +11,56 @@ import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'comments_state.dart';
|
||||
|
||||
class CommentsCubit extends Cubit<CommentsState> {
|
||||
CommentsCubit({
|
||||
CacheService? cacheService,
|
||||
CacheRepository? cacheRepository,
|
||||
required CollapseCache collapseCache,
|
||||
CommentCache? commentCache,
|
||||
OfflineRepository? offlineRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
Logger? logger,
|
||||
required bool offlineReading,
|
||||
required Item item,
|
||||
}) : _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
}) : _collapseCache = collapseCache,
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
_storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_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 CacheRepository _cacheRepository;
|
||||
final CollapseCache _collapseCache;
|
||||
final CommentCache _commentCache;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
final SembastRepository _sembastRepository;
|
||||
final Logger _logger;
|
||||
|
||||
/// The [StreamSubscription] for stream (both lazy or eager)
|
||||
/// fetching comments posted directly to the story.
|
||||
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;
|
||||
|
||||
@override
|
||||
@ -47,6 +72,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
Future<void> init({
|
||||
bool onlyShowTargetComment = false,
|
||||
bool useCommentCache = false,
|
||||
List<Comment>? targetParents,
|
||||
}) async {
|
||||
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
|
||||
@ -59,7 +85,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
);
|
||||
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(
|
||||
.fetchAllCommentsRecursivelyStream(
|
||||
ids: targetParents!.last.kids,
|
||||
level: targetParents.last.level + 1,
|
||||
)
|
||||
@ -69,7 +95,13 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: CommentsStatus.loading));
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
comments: <Comment>[],
|
||||
currentPage: 0,
|
||||
),
|
||||
);
|
||||
|
||||
final Item item = state.item;
|
||||
final Item updatedItem = state.offlineReading
|
||||
@ -80,15 +112,31 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
emit(state.copyWith(item: updatedItem));
|
||||
|
||||
if (state.offlineReading) {
|
||||
_streamSubscription = _cacheRepository
|
||||
_streamSubscription = _offlineRepository
|
||||
.getCachedCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
} else {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
switch (state.fetchMode) {
|
||||
case FetchMode.lazy:
|
||||
_streamSubscription = _storiesRepository
|
||||
.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;
|
||||
}
|
||||
|
||||
_cacheService
|
||||
..resetComments()
|
||||
..resetCollapsedComments();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
comments: <Comment>[],
|
||||
),
|
||||
);
|
||||
|
||||
_collapseCache.resetCollapsedComments();
|
||||
|
||||
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 updatedItem =
|
||||
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
if (state.fetchMode == FetchMode.lazy) {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(
|
||||
ids: kids,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
} else {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -138,17 +205,67 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
onlyShowTargetComment: false,
|
||||
comments: <Comment>[],
|
||||
item: story,
|
||||
),
|
||||
);
|
||||
init();
|
||||
}
|
||||
|
||||
void loadMore() {
|
||||
if (_streamSubscription != null) {
|
||||
emit(state.copyWith(status: CommentsStatus.loading));
|
||||
_streamSubscription?.resume();
|
||||
/// [comment] is only used for lazy fetching.
|
||||
void loadMore({Comment? comment}) {
|
||||
switch (state.fetchMode) {
|
||||
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) {
|
||||
HapticFeedback.selectionClick();
|
||||
if (order == null) return;
|
||||
if (state.order == order) return;
|
||||
HapticFeedback.selectionClick();
|
||||
_streamSubscription?.cancel();
|
||||
emit(state.copyWith(order: order, comments: <Comment>[]));
|
||||
init();
|
||||
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||
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) {
|
||||
@ -205,9 +341,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
void _onCommentFetched(Comment? comment) {
|
||||
if (comment != null) {
|
||||
_cacheService
|
||||
..addKid(comment.id, to: comment.parent)
|
||||
..cacheComment(comment);
|
||||
_collapseCache.addKid(comment.id, to: comment.parent);
|
||||
_commentCache.cacheComment(comment);
|
||||
_sembastRepository.cacheComment(comment);
|
||||
|
||||
final List<LinkifyElement> elements = _linkify(
|
||||
@ -224,21 +359,24 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
emit(state.copyWith(comments: updatedComments));
|
||||
|
||||
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
|
||||
updatedComments.length <=
|
||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||
final bool isHidden = _cacheService.isHidden(comment.id);
|
||||
if (state.fetchMode == FetchMode.eager) {
|
||||
if (updatedComments.length >=
|
||||
_pageSize + _pageSize * state.currentPage &&
|
||||
updatedComments.length <=
|
||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||
final bool isHidden = _collapseCache.isHidden(comment.id);
|
||||
|
||||
if (!isHidden) {
|
||||
_streamSubscription?.pause();
|
||||
if (!isHidden) {
|
||||
_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
|
||||
Future<void> close() async {
|
||||
await _streamSubscription?.cancel();
|
||||
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||
await s.cancel();
|
||||
}
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,11 @@ enum CommentsOrder {
|
||||
oldestFirst,
|
||||
}
|
||||
|
||||
enum FetchMode {
|
||||
lazy,
|
||||
eager,
|
||||
}
|
||||
|
||||
class CommentsState extends Equatable {
|
||||
const CommentsState({
|
||||
required this.item,
|
||||
@ -21,6 +26,7 @@ class CommentsState extends Equatable {
|
||||
required this.status,
|
||||
required this.fetchParentStatus,
|
||||
required this.order,
|
||||
required this.fetchMode,
|
||||
required this.onlyShowTargetComment,
|
||||
required this.offlineReading,
|
||||
required this.currentPage,
|
||||
@ -29,10 +35,11 @@ class CommentsState extends Equatable {
|
||||
CommentsState.init({
|
||||
required this.offlineReading,
|
||||
required this.item,
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
}) : comments = <Comment>[],
|
||||
status = CommentsStatus.init,
|
||||
fetchParentStatus = CommentsStatus.init,
|
||||
order = CommentsOrder.natural,
|
||||
onlyShowTargetComment = false,
|
||||
currentPage = 0;
|
||||
|
||||
@ -41,6 +48,7 @@ class CommentsState extends Equatable {
|
||||
final CommentsStatus status;
|
||||
final CommentsStatus fetchParentStatus;
|
||||
final CommentsOrder order;
|
||||
final FetchMode fetchMode;
|
||||
final bool onlyShowTargetComment;
|
||||
final bool offlineReading;
|
||||
final int currentPage;
|
||||
@ -51,6 +59,7 @@ class CommentsState extends Equatable {
|
||||
CommentsStatus? status,
|
||||
CommentsStatus? fetchParentStatus,
|
||||
CommentsOrder? order,
|
||||
FetchMode? fetchMode,
|
||||
bool? onlyShowTargetComment,
|
||||
bool? offlineReading,
|
||||
int? currentPage,
|
||||
@ -61,6 +70,7 @@ class CommentsState extends Equatable {
|
||||
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
||||
status: status ?? this.status,
|
||||
order: order ?? this.order,
|
||||
fetchMode: fetchMode ?? this.fetchMode,
|
||||
onlyShowTargetComment:
|
||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
@ -68,6 +78,8 @@ class CommentsState extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
item,
|
||||
@ -75,6 +87,7 @@ class CommentsState extends Equatable {
|
||||
status,
|
||||
fetchParentStatus,
|
||||
order,
|
||||
fetchMode,
|
||||
onlyShowTargetComment,
|
||||
offlineReading,
|
||||
currentPage,
|
||||
|
@ -1,26 +1,27 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/utils/debouncer.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
part 'edit_state.dart';
|
||||
|
||||
class EditCubit extends Cubit<EditState> {
|
||||
EditCubit({CacheService? cacheService})
|
||||
: _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
class EditCubit extends HydratedCubit<EditState> {
|
||||
EditCubit({DraftCache? draftCache})
|
||||
: _draftCache = draftCache ?? locator.get<DraftCache>(),
|
||||
_debouncer = Debouncer(delay: const Duration(seconds: 1)),
|
||||
super(const EditState.init());
|
||||
|
||||
final CacheService _cacheService;
|
||||
final DraftCache _draftCache;
|
||||
final Debouncer _debouncer;
|
||||
|
||||
void onReplyTapped(Item item) {
|
||||
emit(
|
||||
EditState(
|
||||
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() {
|
||||
if (state.replyingTo != null) {
|
||||
_cacheService.removeDraft(replyingTo: state.replyingTo!.id);
|
||||
_draftCache.removeDraft(replyingTo: state.replyingTo!.id);
|
||||
}
|
||||
emit(const EditState.init());
|
||||
clear();
|
||||
}
|
||||
|
||||
void onTextChanged(String text) {
|
||||
@ -54,11 +56,61 @@ class EditCubit extends Cubit<EditState> {
|
||||
if (state.replyingTo != null) {
|
||||
final int? id = state.replyingTo?.id;
|
||||
_debouncer.run(() {
|
||||
_cacheService.cacheDraft(
|
||||
_draftCache.cacheDraft(
|
||||
text: text,
|
||||
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();
|
||||
}
|
||||
|
@ -33,7 +33,8 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
_preferenceRepository.shouldShowNotification
|
||||
.then((bool showNotification) {
|
||||
if (showNotification) {
|
||||
init();
|
||||
// Delaying the initialization to prevent janks in home screen.
|
||||
Future<void>.delayed(const Duration(seconds: 2), init);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
|
||||
part 'preference_state.dart';
|
||||
@ -33,6 +35,10 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
||||
_preferenceRepository.shouldShowMetadata
|
||||
.then((bool value) => emit(state.copyWith(showMetadata: value)));
|
||||
_preferenceRepository.fetchMode
|
||||
.then((FetchMode value) => emit(state.copyWith(fetchMode: value)));
|
||||
_preferenceRepository.commentsOrder
|
||||
.then((CommentsOrder value) => emit(state.copyWith(order: value)));
|
||||
}
|
||||
|
||||
void toggleNotificationMode() {
|
||||
@ -74,4 +80,18 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
emit(state.copyWith(showMetadata: !state.showMetadata));
|
||||
_preferenceRepository.toggleMetadataMode();
|
||||
}
|
||||
|
||||
void selectFetchMode(FetchMode? fetchMode) {
|
||||
if (fetchMode == null || state.fetchMode == fetchMode) return;
|
||||
HapticFeedback.lightImpact();
|
||||
emit(state.copyWith(fetchMode: fetchMode));
|
||||
_preferenceRepository.selectFetchMode(fetchMode);
|
||||
}
|
||||
|
||||
void selectCommentsOrder(CommentsOrder? order) {
|
||||
if (order == null || state.order == order) return;
|
||||
HapticFeedback.lightImpact();
|
||||
emit(state.copyWith(order: order));
|
||||
_preferenceRepository.selectCommentsOrder(order);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ class PreferenceState extends Equatable {
|
||||
required this.useReader,
|
||||
required this.markReadStories,
|
||||
required this.showMetadata,
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
});
|
||||
|
||||
const PreferenceState.init()
|
||||
@ -20,7 +22,9 @@ class PreferenceState extends Equatable {
|
||||
useTrueDark = false,
|
||||
useReader = false,
|
||||
markReadStories = false,
|
||||
showMetadata = false;
|
||||
showMetadata = false,
|
||||
fetchMode = FetchMode.eager,
|
||||
order = CommentsOrder.natural;
|
||||
|
||||
final bool showNotification;
|
||||
final bool showComplexStoryTile;
|
||||
@ -30,6 +34,8 @@ class PreferenceState extends Equatable {
|
||||
final bool useReader;
|
||||
final bool markReadStories;
|
||||
final bool showMetadata;
|
||||
final FetchMode fetchMode;
|
||||
final CommentsOrder order;
|
||||
|
||||
PreferenceState copyWith({
|
||||
bool? showNotification,
|
||||
@ -40,6 +46,8 @@ class PreferenceState extends Equatable {
|
||||
bool? useReader,
|
||||
bool? markReadStories,
|
||||
bool? showMetadata,
|
||||
FetchMode? fetchMode,
|
||||
CommentsOrder? order,
|
||||
}) {
|
||||
return PreferenceState(
|
||||
showNotification: showNotification ?? this.showNotification,
|
||||
@ -50,6 +58,8 @@ class PreferenceState extends Equatable {
|
||||
useReader: useReader ?? this.useReader,
|
||||
markReadStories: markReadStories ?? this.markReadStories,
|
||||
showMetadata: showMetadata ?? this.showMetadata,
|
||||
fetchMode: fetchMode ?? this.fetchMode,
|
||||
order: order ?? this.order,
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,5 +73,7 @@ class PreferenceState extends Equatable {
|
||||
useReader,
|
||||
markReadStories,
|
||||
showMetadata,
|
||||
fetchMode,
|
||||
order,
|
||||
];
|
||||
}
|
||||
|
@ -3,18 +3,24 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'split_view_state.dart';
|
||||
|
||||
class SplitViewCubit extends Cubit<SplitViewState> {
|
||||
SplitViewCubit({CacheService? cacheService})
|
||||
: _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
SplitViewCubit({
|
||||
CommentCache? commentCache,
|
||||
Logger? logger,
|
||||
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const SplitViewState.init());
|
||||
|
||||
final CacheService _cacheService;
|
||||
final Logger _logger;
|
||||
final CommentCache _commentCache;
|
||||
|
||||
void updateItemScreenArgs(ItemScreenArgs args) {
|
||||
_cacheService.resetCollapsedComments();
|
||||
_logger.i('resetting comments in CommentCache');
|
||||
_commentCache.resetComments();
|
||||
emit(state.copyWith(itemScreenArgs: args));
|
||||
}
|
||||
|
||||
|
@ -3,34 +3,34 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart' show Comment;
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/services/cache_service.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
|
||||
part 'time_machine_state.dart';
|
||||
|
||||
class TimeMachineCubit extends Cubit<TimeMachineState> {
|
||||
TimeMachineCubit({
|
||||
SembastRepository? sembastRepository,
|
||||
CacheService? cacheService,
|
||||
CommentCache? commentCache,
|
||||
}) : _sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
_cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
super(TimeMachineState.init());
|
||||
|
||||
final SembastRepository _sembastRepository;
|
||||
final CacheService _cacheService;
|
||||
final CommentCache _commentCache;
|
||||
|
||||
Future<void> activateTimeMachine(Comment comment) async {
|
||||
emit(state.copyWith(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);
|
||||
|
||||
while (parent != null) {
|
||||
parents.insert(0, parent);
|
||||
|
||||
final int parentId = parent.parent;
|
||||
parent = _cacheService.getComment(parentId);
|
||||
parent = _commentCache.getComment(parentId);
|
||||
parent ??= await _sembastRepository.getCachedComment(id: parentId);
|
||||
}
|
||||
|
||||
|
58
lib/extensions/context_extension.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -7,10 +7,8 @@ extension DateTimeExtension on DateTime {
|
||||
return '$gap year${gap == 1 ? '' : 's'} ago';
|
||||
} else if (diff.inDays > 30) {
|
||||
int gap = now.month - month;
|
||||
if (gap == 0) {
|
||||
gap = 1;
|
||||
} else if (gap < 0) {
|
||||
gap = now.month + (12 - month);
|
||||
if (gap <= 0) {
|
||||
gap = now.month + 12 - month;
|
||||
}
|
||||
return '$gap month${gap == 1 ? '' : 's'} ago';
|
||||
} else if (diff.inDays >= 1) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'context_extension.dart';
|
||||
export 'date_time_extension.dart';
|
||||
export 'int_extension.dart';
|
||||
export 'list_extension.dart';
|
||||
|
@ -13,3 +13,12 @@ extension StringExtension on String {
|
||||
return replaceAllMapped(regex, (_) => '');
|
||||
}
|
||||
}
|
||||
|
||||
extension OptionalStringExtension on String? {
|
||||
bool get isNullOrEmpty {
|
||||
if (this == null) return true;
|
||||
return this!.trim().isEmpty;
|
||||
}
|
||||
|
||||
bool get isNotNullOrEmpty => !isNullOrEmpty;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension WidgetModifier on Widget {
|
||||
Widget padding([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
||||
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
||||
return Padding(
|
||||
padding: value,
|
||||
child: this,
|
||||
|
@ -3,8 +3,10 @@ import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
||||
@ -14,9 +16,11 @@ import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/custom_bloc_observer.dart';
|
||||
import 'package:hacki/services/fetcher.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -30,9 +34,19 @@ final BehaviorSubject<String?> selectNotificationSubject =
|
||||
final BehaviorSubject<String?> siriSuggestionSubject =
|
||||
BehaviorSubject<String?>();
|
||||
|
||||
Future<void> main() async {
|
||||
late final bool isTesting;
|
||||
|
||||
Future<void> main({bool testing = false}) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
isTesting = testing;
|
||||
|
||||
final HydratedStorage storage = await HydratedStorage.build(
|
||||
storageDirectory: kIsWeb
|
||||
? HydratedStorage.webStorageDirectory
|
||||
: await getTemporaryDirectory(),
|
||||
);
|
||||
|
||||
if (Platform.isIOS) {
|
||||
unawaited(
|
||||
Workmanager().initialize(
|
||||
@ -73,6 +87,19 @@ Future<void> main() async {
|
||||
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();
|
||||
@ -86,18 +113,8 @@ Future<void> main() async {
|
||||
final bool trueDarkMode =
|
||||
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
|
||||
|
||||
// Uncomment code below for running with logging.
|
||||
// BlocOverrides.runZoned(
|
||||
// () {
|
||||
// runApp(
|
||||
// HackiApp(
|
||||
// savedThemeMode: savedThemeMode,
|
||||
// trueDarkMode: trueDarkMode,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// blocObserver: CustomBlocObserver(),
|
||||
// );
|
||||
Bloc.observer = CustomBlocObserver();
|
||||
HydratedBloc.storage = storage;
|
||||
|
||||
runApp(
|
||||
HackiApp(
|
||||
@ -180,6 +197,10 @@ class HackiApp extends StatelessWidget {
|
||||
lazy: false,
|
||||
create: (BuildContext context) => PostCubit(),
|
||||
),
|
||||
BlocProvider<EditCubit>(
|
||||
lazy: false,
|
||||
create: (BuildContext context) => EditCubit(),
|
||||
),
|
||||
],
|
||||
child: AdaptiveTheme(
|
||||
light: ThemeData(
|
||||
@ -221,6 +242,9 @@ class HackiApp extends StatelessWidget {
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: useTrueDark ? trueDarkTheme : theme,
|
||||
navigatorKey: navigatorKey,
|
||||
navigatorObservers: <NavigatorObserver>[
|
||||
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||
],
|
||||
onGenerateRoute: CustomRouter.onGenerateRoute,
|
||||
initialRoute: HomeScreen.routeName,
|
||||
),
|
||||
|
@ -59,6 +59,7 @@ class Comment extends Item {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'id': id,
|
||||
'time': time,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/extensions/date_time_extension.dart';
|
||||
|
||||
abstract class Item extends Equatable {
|
||||
class Item extends Equatable {
|
||||
const Item({
|
||||
required this.id,
|
||||
required this.deleted,
|
||||
@ -35,6 +35,22 @@ abstract class Item extends Equatable {
|
||||
text = '',
|
||||
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 time;
|
||||
final int score;
|
||||
@ -65,4 +81,40 @@ abstract class Item extends Equatable {
|
||||
bool get isJob => type == 'job';
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
@ -79,6 +79,7 @@ class PollOption extends Item {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'descendants': descendants,
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
enum StoryType {
|
||||
@ -93,6 +91,7 @@ class Story extends Item {
|
||||
String get simpleMetadata =>
|
||||
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'descendants': descendants,
|
||||
@ -113,9 +112,10 @@ class Story extends Item {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String prettyString =
|
||||
const JsonEncoder.withIndent(' ').convert(this);
|
||||
return 'Story $prettyString';
|
||||
// final String prettyString =
|
||||
// const JsonEncoder.withIndent(' ').convert(this);
|
||||
// return 'Story $prettyString';
|
||||
return 'Story $id';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class User {
|
||||
@ -39,8 +37,6 @@ class User {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String prettyString =
|
||||
const JsonEncoder.withIndent(' ').convert(this);
|
||||
return 'User $prettyString';
|
||||
return 'User $about, $created, $delay, $id, $karma';
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,14 @@ class AuthRepository extends PostableRepository {
|
||||
AuthRepository({
|
||||
Dio? dio,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(dio: dio);
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
|
||||
static const String _authority = 'news.ycombinator.com';
|
||||
|
||||
@ -45,7 +48,7 @@ class AuthRepository extends PostableRepository {
|
||||
password: password,
|
||||
);
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,22 @@ import 'package:hive/hive.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
/// [CacheRepository] is for storing stories and comments for offline reading.
|
||||
/// [OfflineRepository] is for storing stories and comments for offline reading.
|
||||
/// It's using [Hive] as its database which is being stored in temp directory.
|
||||
class CacheRepository {
|
||||
CacheRepository({
|
||||
class OfflineRepository {
|
||||
OfflineRepository({
|
||||
Future<Box<List<int>>>? storyIdBox,
|
||||
Future<Box<Map<dynamic, dynamic>>>? storyBox,
|
||||
Future<LazyBox<String>>? webPageBox,
|
||||
Future<LazyBox<Map<dynamic, dynamic>>>? commentBox,
|
||||
Logger? logger,
|
||||
}) : _storyIdBox = storyIdBox ?? Hive.openBox<List<int>>(_storyIdBoxName),
|
||||
_storyBox =
|
||||
storyBox ?? Hive.openBox<Map<dynamic, dynamic>>(_storyBoxName),
|
||||
_webPageBox = webPageBox ?? Hive.openLazyBox<String>(_webPageBoxName),
|
||||
_commentBox = commentBox ??
|
||||
Hive.openLazyBox<Map<dynamic, dynamic>>(_commentBoxName);
|
||||
Hive.openLazyBox<Map<dynamic, dynamic>>(_commentBoxName),
|
||||
_logger = logger ?? locator.get<Logger>();
|
||||
|
||||
static const String _storyIdBoxName = 'storyIdBox';
|
||||
static const String _storyBoxName = 'storyBox';
|
||||
@ -28,6 +30,7 @@ class CacheRepository {
|
||||
final Future<Box<Map<dynamic, dynamic>>> _storyBox;
|
||||
final Future<LazyBox<Map<dynamic, dynamic>>> _commentBox;
|
||||
final Future<LazyBox<String>> _webPageBox;
|
||||
final Logger _logger;
|
||||
|
||||
Future<bool> get hasCachedStories =>
|
||||
_storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty);
|
||||
@ -41,7 +44,7 @@ class CacheRepository {
|
||||
try {
|
||||
box = await _storyIdBox;
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_storyIdBoxName);
|
||||
box = await _storyIdBox;
|
||||
}
|
||||
@ -55,7 +58,7 @@ class CacheRepository {
|
||||
try {
|
||||
box = await _storyBox;
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||
box = await _storyBox;
|
||||
}
|
||||
@ -69,7 +72,7 @@ class CacheRepository {
|
||||
try {
|
||||
box = await _webPageBox;
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_webPageBoxName);
|
||||
box = await _webPageBox;
|
||||
}
|
||||
@ -83,7 +86,7 @@ class CacheRepository {
|
||||
final LazyBox<String> box = await _webPageBox;
|
||||
return box.get(url);
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_webPageBoxName);
|
||||
return null;
|
||||
}
|
||||
@ -94,7 +97,7 @@ class CacheRepository {
|
||||
final LazyBox<String> box = await _webPageBox;
|
||||
return box.containsKey(url);
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_webPageBoxName);
|
||||
return false;
|
||||
}
|
||||
@ -106,7 +109,7 @@ class CacheRepository {
|
||||
final List<int>? ids = box.get(of.name);
|
||||
return ids ?? <int>[];
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_storyIdBoxName);
|
||||
return <int>[];
|
||||
}
|
||||
@ -118,7 +121,7 @@ class CacheRepository {
|
||||
try {
|
||||
box = await _storyBox;
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||
return;
|
||||
}
|
||||
@ -143,7 +146,7 @@ class CacheRepository {
|
||||
try {
|
||||
box = await _storyBox;
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||
return null;
|
||||
}
|
||||
@ -162,7 +165,7 @@ class CacheRepository {
|
||||
try {
|
||||
box = await _commentBox;
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_commentBoxName);
|
||||
box = await _commentBox;
|
||||
}
|
||||
@ -180,7 +183,7 @@ class CacheRepository {
|
||||
final Comment comment = Comment.fromJson(json.cast<String, dynamic>());
|
||||
return comment;
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_commentBoxName);
|
||||
return null;
|
||||
}
|
||||
@ -210,7 +213,7 @@ class CacheRepository {
|
||||
final Box<List<int>> box = await _storyIdBox;
|
||||
return box.clear();
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_storyIdBoxName);
|
||||
return 0;
|
||||
}
|
||||
@ -221,7 +224,7 @@ class CacheRepository {
|
||||
final Box<Map<dynamic, dynamic>> box = await _storyBox;
|
||||
return box.clear();
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||
return 0;
|
||||
}
|
||||
@ -232,7 +235,7 @@ class CacheRepository {
|
||||
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
|
||||
return box.clear();
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_commentBoxName);
|
||||
return 0;
|
||||
}
|
||||
@ -243,7 +246,7 @@ class CacheRepository {
|
||||
final LazyBox<String> box = await _webPageBox;
|
||||
return box.clear();
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
await Hive.deleteBoxFromDisk(_webPageBoxName);
|
||||
return 0;
|
||||
}
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
|
||||
@ -12,9 +13,11 @@ class PreferenceRepository {
|
||||
SyncedSharedPreferences? syncedPrefs,
|
||||
Future<SharedPreferences>? prefs,
|
||||
FlutterSecureStorage? secureStorage,
|
||||
Logger? logger,
|
||||
}) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance,
|
||||
_prefs = prefs ?? SharedPreferences.getInstance(),
|
||||
_secureStorage = secureStorage ?? const FlutterSecureStorage();
|
||||
_secureStorage = secureStorage ?? const FlutterSecureStorage(),
|
||||
_logger = logger ?? locator.get<Logger>();
|
||||
|
||||
static const String _usernameKey = 'username';
|
||||
static const String _passwordKey = 'password';
|
||||
@ -40,20 +43,26 @@ class PreferenceRepository {
|
||||
static const String _navigationModeKey = 'navigationMode';
|
||||
static const String _eyeCandyModeKey = 'eyeCandyMode';
|
||||
static const String _markReadStoriesModeKey = 'markReadStoriesMode';
|
||||
static const String _fetchModeKey = 'fetchMode';
|
||||
static const String _commentsOrderKey = 'commentsOrder';
|
||||
|
||||
static const bool _notificationModeDefaultValue = true;
|
||||
static const bool _displayModeDefaultValue = true;
|
||||
static const bool _navigationModeDefaultValue = true;
|
||||
static const bool _navigationModeDefaultValueIOS = true;
|
||||
static const bool _navigationModeDefaultValueAndroid = false;
|
||||
static const bool _eyeCandyModeDefaultValue = false;
|
||||
static const bool _trueDarkModeDefaultValue = false;
|
||||
static const bool _readerModeDefaultValue = true;
|
||||
static const bool _markReadStoriesModeDefaultValue = true;
|
||||
static const bool _isFirstLaunchKeyDefaultValue = true;
|
||||
static const bool _metadataModeDefaultValue = true;
|
||||
static final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||
static final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||
|
||||
final SyncedSharedPreferences _syncedPrefs;
|
||||
final Future<SharedPreferences> _prefs;
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
final Logger _logger;
|
||||
|
||||
Future<bool> get loggedIn async => await username != null;
|
||||
|
||||
@ -84,7 +93,10 @@ class PreferenceRepository {
|
||||
|
||||
Future<bool> get shouldShowWebFirst async => _prefs.then(
|
||||
(SharedPreferences prefs) =>
|
||||
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue,
|
||||
prefs.getBool(_navigationModeKey) ??
|
||||
(Platform.isAndroid
|
||||
? _navigationModeDefaultValueAndroid
|
||||
: _navigationModeDefaultValueIOS),
|
||||
);
|
||||
|
||||
Future<bool> get shouldShowEyeCandy async => _prefs.then(
|
||||
@ -113,6 +125,17 @@ class PreferenceRepository {
|
||||
_markReadStoriesModeDefaultValue,
|
||||
);
|
||||
|
||||
Future<FetchMode> get fetchMode async => _prefs.then(
|
||||
(SharedPreferences prefs) => FetchMode.values
|
||||
.elementAt(prefs.getInt(_fetchModeKey) ?? _fetchModeDefaultValue),
|
||||
);
|
||||
|
||||
Future<CommentsOrder> get commentsOrder async => _prefs.then(
|
||||
(SharedPreferences prefs) => CommentsOrder.values.elementAt(
|
||||
prefs.getInt(_commentsOrderKey) ?? _commentsOrderDefaultValue,
|
||||
),
|
||||
);
|
||||
|
||||
Future<bool> hasPushed(int commentId) async =>
|
||||
_prefs.then((SharedPreferences prefs) {
|
||||
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
|
||||
@ -160,7 +183,7 @@ class PreferenceRepository {
|
||||
aOptions: androidOptions,
|
||||
);
|
||||
} catch (_) {
|
||||
locator.get<Logger>().e(_);
|
||||
_logger.e(_);
|
||||
}
|
||||
|
||||
rethrow;
|
||||
@ -188,8 +211,10 @@ class PreferenceRepository {
|
||||
|
||||
Future<void> toggleNavigationMode() async {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
final bool currentMode =
|
||||
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue;
|
||||
final bool currentMode = prefs.getBool(_navigationModeKey) ??
|
||||
(Platform.isAndroid
|
||||
? _navigationModeDefaultValueAndroid
|
||||
: _navigationModeDefaultValueIOS);
|
||||
await prefs.setBool(_navigationModeKey, !currentMode);
|
||||
}
|
||||
|
||||
@ -228,6 +253,18 @@ class PreferenceRepository {
|
||||
await prefs.setBool(_metadataModeKey, !currentMode);
|
||||
}
|
||||
|
||||
Future<void> selectFetchMode(FetchMode fetchMode) async {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
final int index = fetchMode.index;
|
||||
await prefs.setInt(_fetchModeKey, index);
|
||||
}
|
||||
|
||||
Future<void> selectCommentsOrder(CommentsOrder order) async {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
final int index = order.index;
|
||||
await prefs.setInt(_commentsOrderKey, index);
|
||||
}
|
||||
|
||||
//#region fav
|
||||
|
||||
Future<List<int>> favList({required String of}) async {
|
||||
|
@ -1,5 +1,5 @@
|
||||
export 'auth_repository.dart';
|
||||
export 'cache_repository.dart';
|
||||
export 'offline_repository.dart';
|
||||
export 'post_repository.dart';
|
||||
export 'preference_repository.dart';
|
||||
export 'search_repository.dart';
|
||||
|
@ -51,9 +51,37 @@ class StoriesRepository {
|
||||
Stream<Comment> fetchCommentsStream({
|
||||
required List<int> ids,
|
||||
int level = 0,
|
||||
Comment? Function(int)? getFromCache,
|
||||
}) async* {
|
||||
for (final int id in ids) {
|
||||
final Comment? comment = await _firebaseClient
|
||||
Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
|
||||
|
||||
comment ??= await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||
.then((Map<String, dynamic>? json) async {
|
||||
if (json == null) return null;
|
||||
|
||||
final Comment comment = Comment.fromJson(json, level: level);
|
||||
return comment;
|
||||
});
|
||||
|
||||
if (comment != null) {
|
||||
yield comment;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Stream<Comment> fetchAllCommentsRecursivelyStream({
|
||||
required List<int> ids,
|
||||
int level = 0,
|
||||
Comment? Function(int)? getFromCache,
|
||||
}) async* {
|
||||
for (final int id in ids) {
|
||||
Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
|
||||
|
||||
comment ??= await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||
.then((Map<String, dynamic>? json) async {
|
||||
@ -66,9 +94,10 @@ class StoriesRepository {
|
||||
if (comment != null) {
|
||||
yield comment;
|
||||
|
||||
yield* fetchCommentsStream(
|
||||
yield* fetchAllCommentsRecursivelyStream(
|
||||
ids: comment.kids,
|
||||
level: level + 1,
|
||||
getFromCache: getFromCache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
@ -46,8 +47,7 @@ class HomeScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final CacheService cacheService = locator.get<CacheService>();
|
||||
with SingleTickerProviderStateMixin, RouteAware {
|
||||
final Throttle featureDiscoveryDismissThrottle = Throttle(
|
||||
delay: _throttleDelay,
|
||||
);
|
||||
@ -61,6 +61,19 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
static const Duration _throttleDelay = Duration(seconds: 1);
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
super.didPopNext();
|
||||
if (context.read<StoriesBloc>().deviceScreenType ==
|
||||
DeviceScreenType.mobile) {
|
||||
locator.get<Logger>().i('resetting comments in CommentCache');
|
||||
Future<void>.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
locator.get<CommentCache>().resetComments,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -88,14 +101,26 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
siriSuggestionSubject.stream.listen(onSiriSuggestionTapped);
|
||||
}
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
const <String>{
|
||||
Constants.featureLogIn,
|
||||
},
|
||||
);
|
||||
});
|
||||
SchedulerBinding.instance
|
||||
..addPostFrameCallback((_) {
|
||||
if (!isTesting) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
const <String>{
|
||||
Constants.featureLogIn,
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
..addPostFrameCallback((_) {
|
||||
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||
|
||||
if (route == null) return;
|
||||
|
||||
locator
|
||||
.get<RouteObserver<ModalRoute<dynamic>>>()
|
||||
.subscribe(this, route);
|
||||
});
|
||||
|
||||
tabController = TabController(vsync: this, length: 6)
|
||||
..addListener(() {
|
||||
@ -427,7 +452,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
|
||||
if (mounted) {
|
||||
if (item != null) {
|
||||
goToItemScreen(args: ItemScreenArgs(item: item));
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(item: item),
|
||||
forceNewScreen: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
// ignore_for_file: comment_references
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -38,6 +40,7 @@ class ItemScreenArgs extends Equatable {
|
||||
const ItemScreenArgs({
|
||||
required this.item,
|
||||
this.onlyShowTargetComment = false,
|
||||
this.useCommentCache = false,
|
||||
this.targetComments,
|
||||
});
|
||||
|
||||
@ -45,11 +48,17 @@ class ItemScreenArgs extends Equatable {
|
||||
final bool onlyShowTargetComment;
|
||||
final List<Comment>? targetComments;
|
||||
|
||||
/// when a user is trying to view a sub-thread from a main thread, we don't
|
||||
/// need to fetch comments from [StoryRepository] since we have some, if not
|
||||
/// all, comments cached in [CommentCache].
|
||||
final bool useCommentCache;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
item,
|
||||
onlyShowTargetComment,
|
||||
targetComments,
|
||||
useCommentCache,
|
||||
];
|
||||
}
|
||||
|
||||
@ -66,8 +75,8 @@ class ItemScreen extends StatefulWidget {
|
||||
static Route<dynamic> route(ItemScreenArgs args) {
|
||||
return MaterialPageRoute<ItemScreen>(
|
||||
settings: const RouteSettings(name: routeName),
|
||||
builder: (BuildContext context) => RepositoryProvider<CacheService>(
|
||||
create: (BuildContext context) => CacheService(),
|
||||
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
|
||||
create: (BuildContext context) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
@ -76,16 +85,17 @@ class ItemScreen extends StatefulWidget {
|
||||
offlineReading:
|
||||
context.read<StoriesBloc>().state.offlineReading,
|
||||
item: args.item,
|
||||
cacheService: context.read<CacheService>(),
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode:
|
||||
context.read<PreferenceCubit>().state.fetchMode,
|
||||
defaultCommentsOrder:
|
||||
context.read<PreferenceCubit>().state.order,
|
||||
)..init(
|
||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||
targetParents: args.targetComments,
|
||||
useCommentCache: args.useCommentCache,
|
||||
),
|
||||
),
|
||||
BlocProvider<EditCubit>(
|
||||
lazy: false,
|
||||
create: (BuildContext context) => EditCubit(),
|
||||
),
|
||||
],
|
||||
child: ItemScreen(
|
||||
item: args.item,
|
||||
@ -106,8 +116,9 @@ class ItemScreen extends StatefulWidget {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
child: RepositoryProvider<CacheService>(
|
||||
create: (BuildContext context) => CacheService(),
|
||||
child: RepositoryProvider<CollapseCache>(
|
||||
create: (BuildContext context) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
key: ValueKey<ItemScreenArgs>(args),
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
@ -116,16 +127,16 @@ class ItemScreen extends StatefulWidget {
|
||||
offlineReading:
|
||||
context.read<StoriesBloc>().state.offlineReading,
|
||||
item: args.item,
|
||||
cacheService: context.read<CacheService>(),
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode:
|
||||
context.read<PreferenceCubit>().state.fetchMode,
|
||||
defaultCommentsOrder:
|
||||
context.read<PreferenceCubit>().state.order,
|
||||
)..init(
|
||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||
targetParents: args.targetComments,
|
||||
),
|
||||
),
|
||||
BlocProvider<EditCubit>(
|
||||
lazy: false,
|
||||
create: (BuildContext context) => EditCubit(),
|
||||
),
|
||||
],
|
||||
child: ItemScreen(
|
||||
item: args.item,
|
||||
@ -145,7 +156,7 @@ class ItemScreen extends StatefulWidget {
|
||||
_ItemScreenState createState() => _ItemScreenState();
|
||||
}
|
||||
|
||||
class _ItemScreenState extends State<ItemScreen> {
|
||||
class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
final TextEditingController commentEditingController =
|
||||
TextEditingController();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
@ -166,20 +177,40 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
static const Duration _featureDiscoveryDismissThrottleDelay =
|
||||
Duration(seconds: 1);
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
super.didPop();
|
||||
if (context.read<EditCubit>().state.text.isNullOrEmpty) {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
const <String>{
|
||||
Constants.featurePinToTop,
|
||||
Constants.featureAddStoryToFavList,
|
||||
Constants.featureOpenStoryInWebView,
|
||||
},
|
||||
);
|
||||
});
|
||||
SchedulerBinding.instance
|
||||
..addPostFrameCallback((_) {
|
||||
if (!isTesting) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
const <String>{
|
||||
Constants.featurePinToTop,
|
||||
Constants.featureAddStoryToFavList,
|
||||
Constants.featureOpenStoryInWebView,
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
..addPostFrameCallback((_) {
|
||||
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||
|
||||
if (route == null) return;
|
||||
|
||||
locator
|
||||
.get<RouteObserver<ModalRoute<dynamic>>>()
|
||||
.subscribe(this, route);
|
||||
});
|
||||
|
||||
scrollController.addListener(() {
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
@ -187,11 +218,12 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
context.read<EditCubit>().onScrolled();
|
||||
}
|
||||
});
|
||||
|
||||
commentEditingController.text = context.read<EditCubit>().state.text ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
locator.get<CacheService>().resetComments();
|
||||
refreshController.dispose();
|
||||
commentEditingController.dispose();
|
||||
scrollController.dispose();
|
||||
@ -295,316 +327,378 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoading: context.read<CommentsCubit>().loadMore,
|
||||
child: ListView(
|
||||
onLoading: () {
|
||||
if (state.fetchMode == FetchMode.eager) {
|
||||
context.read<CommentsCubit>().loadMore();
|
||||
} else {
|
||||
refreshController.loadComplete();
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
primary: false,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
if (!widget.splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
itemCount: state.comments.length + 2,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (state.item.id !=
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.state
|
||||
.replyingTo
|
||||
?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.onReplyTapped(state.item);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) => onMoreTapped(state.item),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
if (!widget.splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
child: Row(
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (state.item.id !=
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.state
|
||||
.replyingTo
|
||||
?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.onReplyTapped(state.item);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.useReader,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.offlineReading,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
state.item.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.useReader,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.offlineReading,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
state.item.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
TextDimens.pt15,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
TextDimens.pt15,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.contains(
|
||||
'news.ycombinator.com/item',
|
||||
)) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)
|
||||
..init(),
|
||||
child: PollView(
|
||||
onLoginTapped: onLoginTapped,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
MediaQuery.of(context).textScaleFactor *
|
||||
TextDimens.pt15,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize:
|
||||
MediaQuery.of(context).textScaleFactor *
|
||||
TextDimens.pt15,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.contains(
|
||||
'news.ycombinator.com/item',
|
||||
)) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: PollView(
|
||||
onLoginTapped: onLoginTapped,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => context
|
||||
.read<CommentsCubit>()
|
||||
.loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => context
|
||||
.read<CommentsCubit>()
|
||||
.loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: context
|
||||
.read<CommentsCubit>()
|
||||
.loadParentThread,
|
||||
child: state.fetchParentStatus ==
|
||||
CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: context
|
||||
.read<CommentsCubit>()
|
||||
.loadParentThread,
|
||||
child: state.fetchParentStatus ==
|
||||
CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child:
|
||||
CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent thread',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.offlineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: const <DropdownMenuItem<FetchMode>>[
|
||||
DropdownMenuItem<FetchMode>(
|
||||
value: FetchMode.lazy,
|
||||
child: Text(
|
||||
'Lazy',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text('View parent thread'),
|
||||
DropdownMenuItem<FetchMode>(
|
||||
value: FetchMode.eager,
|
||||
child: Text(
|
||||
'Eager',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: context
|
||||
.read<CommentsCubit>()
|
||||
.onFetchModeChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: const <
|
||||
DropdownMenuItem<CommentsOrder>>[
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.natural,
|
||||
child: Text(
|
||||
'Natural',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.newestFirst,
|
||||
child: Text(
|
||||
'Newest first',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.oldestFirst,
|
||||
child: Text(
|
||||
'Oldest first',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: context
|
||||
.read<CommentsCubit>()
|
||||
.onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: const <DropdownMenuItem<CommentsOrder>>[
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.natural,
|
||||
child: Text(
|
||||
'Natural',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt14,
|
||||
),
|
||||
),
|
||||
if (state.comments.isEmpty &&
|
||||
state.status ==
|
||||
CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.newestFirst,
|
||||
child: Text(
|
||||
'Newest first',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt14,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.oldestFirst,
|
||||
child: Text(
|
||||
'Oldest first',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
context.read<CommentsCubit>().onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
for (final Comment comment in state.comments)
|
||||
FadeIn(
|
||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||
child: CommentTile(
|
||||
comment: comment,
|
||||
level: comment.level,
|
||||
myUsername:
|
||||
authState.isLoggedIn ? authState.username : null,
|
||||
opUsername: state.item.by,
|
||||
onReplyTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
);
|
||||
} else if (index == state.comments.length + 1) {
|
||||
if ((state.status == CommentsStatus.allLoaded &&
|
||||
state.comments.isNotEmpty) ||
|
||||
state.onlyShowTargetComment) {
|
||||
return SizedBox(
|
||||
height: 240,
|
||||
child: Center(
|
||||
child: Text(happyFace),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
if (cmt.id !=
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.state
|
||||
.replyingTo
|
||||
?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
index = index - 1;
|
||||
final Comment comment = state.comments.elementAt(index);
|
||||
return FadeIn(
|
||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||
child: CommentTile(
|
||||
comment: comment,
|
||||
level: comment.level,
|
||||
myUsername:
|
||||
authState.isLoggedIn ? authState.username : null,
|
||||
opUsername: state.item.by,
|
||||
fetchMode: state.fetchMode,
|
||||
onReplyTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<EditCubit>().onReplyTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onEditTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
if (cmt.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
context.read<EditCubit>().onEditTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
),
|
||||
}
|
||||
|
||||
context.read<EditCubit>().onReplyTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onEditTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
commentEditingController.clear();
|
||||
context.read<EditCubit>().onEditTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
),
|
||||
if ((state.status == CommentsStatus.allLoaded &&
|
||||
state.comments.isNotEmpty) ||
|
||||
state.onlyShowTargetComment)
|
||||
SizedBox(
|
||||
height: 240,
|
||||
child: Center(
|
||||
child: Text(happyFace),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return BlocListener<EditCubit, EditState>(
|
||||
listenWhen: (EditState previous, EditState current) {
|
||||
return previous.replyingTo != current.replyingTo ||
|
||||
previous.itemBeingEdited != current.itemBeingEdited;
|
||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||
commentEditingController.text != current.text;
|
||||
},
|
||||
listener: (BuildContext context, EditState editState) {
|
||||
if (editState.replyingTo != null ||
|
||||
@ -753,7 +847,10 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(item: comment),
|
||||
args: ItemScreenArgs(
|
||||
item: comment,
|
||||
useCommentCache: true,
|
||||
),
|
||||
forceNewScreen: true,
|
||||
);
|
||||
},
|
||||
@ -825,6 +922,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
context.read<AuthBloc>().state.username,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
actionable: false,
|
||||
fetchMode: FetchMode.eager,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
@ -866,7 +964,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void onMoreTapped(Item item) {
|
||||
void onMoreTapped(Item item, Rect? rect) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (item.dead || item.deleted) {
|
||||
@ -1075,7 +1173,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
case _MenuAction.downvote:
|
||||
break;
|
||||
case _MenuAction.share:
|
||||
onShareTapped(item);
|
||||
onShareTapped(item, rect);
|
||||
break;
|
||||
case _MenuAction.flag:
|
||||
onFlagTapped(item);
|
||||
@ -1090,8 +1188,12 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void onShareTapped(Item item) =>
|
||||
Share.share('https://news.ycombinator.com/item?id=${item.id}');
|
||||
void onShareTapped(Item item, Rect? rect) {
|
||||
Share.share(
|
||||
'https://news.ycombinator.com/item?id=${item.id}',
|
||||
sharePositionOrigin: rect,
|
||||
);
|
||||
}
|
||||
|
||||
void onFlagTapped(Item item) {
|
||||
showDialog<bool>(
|
||||
|
@ -5,7 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/link_util.dart';
|
||||
|
||||
@ -145,6 +147,39 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
color: Palette.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
final EditState state =
|
||||
context.read<EditCubit>().state;
|
||||
if (state.replyingTo != null &&
|
||||
state.text.isNotNullOrEmpty) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
AlertDialog(
|
||||
title: const Text('Save draft?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.deleteDraft();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'No',
|
||||
style: TextStyle(
|
||||
color: Palette.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(context),
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
widget.onCloseTapped();
|
||||
expanded = false;
|
||||
},
|
||||
@ -222,7 +257,6 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
@ -249,8 +283,31 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
style: const TextStyle(color: Palette.grey),
|
||||
),
|
||||
const Spacer(),
|
||||
if (replyingTo != null)
|
||||
TextButton(
|
||||
child: const Text('View thread'),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
expanded = false;
|
||||
});
|
||||
Navigator.popUntil(
|
||||
context,
|
||||
(Route<dynamic> route) =>
|
||||
route.settings.name == ItemScreen.routeName ||
|
||||
route.isFirst,
|
||||
);
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(
|
||||
item: replyingTo,
|
||||
useCommentCache: true,
|
||||
),
|
||||
forceNewScreen: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Copy All'),
|
||||
child: const Text('Copy all'),
|
||||
onPressed: () => FlutterClipboard.copy(
|
||||
replyingTo?.text ?? '',
|
||||
).then((_) => HapticFeedback.selectionClick()),
|
||||
|
@ -20,7 +20,7 @@ class _ScrollUpIconButtonState extends State<ScrollUpIconButton> {
|
||||
super.initState();
|
||||
|
||||
widget.scrollController.addListener(() {
|
||||
if (widget.scrollController.offset <= 1000) {
|
||||
if (widget.scrollController.offset <= 1000 && mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
|
@ -283,6 +283,122 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
SizedBox(
|
||||
width: Dimens.pt16,
|
||||
),
|
||||
Text('Default fetch mode'),
|
||||
Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Text('Default comments order'),
|
||||
Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt16,
|
||||
),
|
||||
DropdownButton<FetchMode>(
|
||||
value: preferenceState.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: const <
|
||||
DropdownMenuItem<FetchMode>>[
|
||||
DropdownMenuItem<FetchMode>(
|
||||
value: FetchMode.lazy,
|
||||
child: Text(
|
||||
'Lazy',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<FetchMode>(
|
||||
value: FetchMode.eager,
|
||||
child: Text(
|
||||
'Eager',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: context
|
||||
.read<PreferenceCubit>()
|
||||
.selectFetchMode,
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: preferenceState.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: const <
|
||||
DropdownMenuItem<CommentsOrder>>[
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.natural,
|
||||
child: Text(
|
||||
'Natural',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.newestFirst,
|
||||
child: Text(
|
||||
'Newest first',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: CommentsOrder.oldestFirst,
|
||||
child: Text(
|
||||
'Oldest first',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: context
|
||||
.read<PreferenceCubit>()
|
||||
.selectCommentsOrder,
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Complex Story Tile'),
|
||||
subtitle: const Text(
|
||||
@ -410,7 +526,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Hacki',
|
||||
applicationVersion: 'v0.2.24',
|
||||
applicationVersion: 'v0.2.32',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(
|
||||
@ -680,7 +796,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.get<SembastRepository>()
|
||||
.deleteAllCachedComments()
|
||||
.whenComplete(
|
||||
locator.get<CacheRepository>().deleteAll,
|
||||
locator.get<OfflineRepository>().deleteAll,
|
||||
)
|
||||
.whenComplete(
|
||||
locator.get<PreferenceRepository>().clearAllReadStories,
|
||||
|
@ -34,7 +34,7 @@ class _WebViewScreenState extends State<WebViewScreen> {
|
||||
),
|
||||
body: WebView(
|
||||
onWebViewCreated: (WebViewController controller) async {
|
||||
final String? html = await locator.get<CacheRepository>().getHtml(
|
||||
final String? html = await locator.get<OfflineRepository>().getHtml(
|
||||
url: widget.url,
|
||||
);
|
||||
|
||||
|
@ -17,6 +17,7 @@ class CommentTile extends StatelessWidget {
|
||||
required this.myUsername,
|
||||
required this.comment,
|
||||
required this.onStoryLinkTapped,
|
||||
required this.fetchMode,
|
||||
this.onReplyTapped,
|
||||
this.onMoreTapped,
|
||||
this.onEditTapped,
|
||||
@ -32,10 +33,11 @@ class CommentTile extends StatelessWidget {
|
||||
final int level;
|
||||
final bool actionable;
|
||||
final Function(Comment)? onReplyTapped;
|
||||
final Function(Comment)? onMoreTapped;
|
||||
final Function(Comment, Rect?)? onMoreTapped;
|
||||
final Function(Comment)? onEditTapped;
|
||||
final Function(Comment)? onRightMoreTapped;
|
||||
final Function(String) onStoryLinkTapped;
|
||||
final FetchMode fetchMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -44,7 +46,7 @@ class CommentTile extends StatelessWidget {
|
||||
lazy: false,
|
||||
create: (_) => CollapseCubit(
|
||||
commentId: comment.id,
|
||||
cacheService: context.read<CacheService>(),
|
||||
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
|
||||
)..init(),
|
||||
child: BlocBuilder<CollapseCubit, CollapseState>(
|
||||
builder: (BuildContext context, CollapseState state) {
|
||||
@ -88,8 +90,11 @@ class CommentTile extends StatelessWidget {
|
||||
icon: Icons.edit,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onMoreTapped?.call(comment),
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped?.call(
|
||||
comment,
|
||||
context.rect,
|
||||
),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
@ -277,6 +282,47 @@ class CommentTile extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!state.collapsed &&
|
||||
fetchMode == FetchMode.lazy &&
|
||||
comment.kids.isNotEmpty &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.commentIds
|
||||
.contains(comment.kids.first) &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.onlyShowTargetComment)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
context
|
||||
.read<CommentsCubit>()
|
||||
.loadMore(comment: comment);
|
||||
},
|
||||
child: Text(
|
||||
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
@ -298,7 +344,7 @@ class CommentTile extends StatelessWidget {
|
||||
: Palette.transparent;
|
||||
final bool isMyComment = myUsername == comment.by;
|
||||
|
||||
Widget? wrapper = child;
|
||||
Widget wrapper = child;
|
||||
|
||||
if (isMyComment && level == 0) {
|
||||
return Container(
|
||||
@ -311,6 +357,7 @@ class CommentTile extends StatelessWidget {
|
||||
final Color wrapperBorderColor = _getColor(i);
|
||||
final bool shouldHighlight = isMyComment && i == level;
|
||||
wrapper = Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: const EdgeInsets.only(
|
||||
left: Dimens.pt12,
|
||||
),
|
||||
@ -330,7 +377,7 @@ class CommentTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return wrapper!;
|
||||
return wrapper;
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -340,7 +387,12 @@ class CommentTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static final Map<int, Color> _colors = <int, Color>{};
|
||||
|
||||
Color _getColor(int level) {
|
||||
final int initialLevel = level;
|
||||
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
|
||||
|
||||
while (level >= 10) {
|
||||
level = level - 10;
|
||||
}
|
||||
@ -361,6 +413,7 @@ class CommentTile extends StatelessWidget {
|
||||
1,
|
||||
);
|
||||
|
||||
_colors[initialLevel] = color;
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class _CountDownReminderState extends State<CountdownReminder>
|
||||
bool isVisible = false;
|
||||
|
||||
static const Duration countdownDuration = Duration(seconds: 8);
|
||||
static const Duration visibilityCountdownDuration = Duration(seconds: 3);
|
||||
static const Duration visibilityCountdownDuration = Duration.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/link_view.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
||||
@ -199,23 +200,9 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double screenWidthLowerBound = 428,
|
||||
screenWidthUpperBound = 850,
|
||||
picHeightLowerBound = 118,
|
||||
picHeightUpperBound = 140,
|
||||
smallPicHeight = 100,
|
||||
picHeightFactor = 0.14;
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final bool showSmallerPreviewPic = screenWidth > screenWidthLowerBound &&
|
||||
screenWidth < screenWidthUpperBound;
|
||||
final double height = showSmallerPreviewPic
|
||||
? smallPicHeight
|
||||
: (MediaQuery.of(context).size.height * picHeightFactor)
|
||||
.clamp(picHeightLowerBound, picHeightUpperBound);
|
||||
|
||||
final Widget loadingWidget = widget.placeholderWidget ??
|
||||
Container(
|
||||
height: height,
|
||||
height: context.storyTileHeight,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
@ -232,13 +219,13 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
final WebInfo? info = _info as WebInfo?;
|
||||
loadedWidget = _info == null
|
||||
? _buildLinkContainer(
|
||||
height,
|
||||
context.storyTileHeight,
|
||||
title: _errorTitle,
|
||||
desc: _errorBody,
|
||||
imageUri: null,
|
||||
)
|
||||
: _buildLinkContainer(
|
||||
height,
|
||||
context.storyTileHeight,
|
||||
title: _errorTitle,
|
||||
desc: WebAnalyzer.isNotEmpty(info!.description)
|
||||
? info.description
|
||||
|
@ -147,7 +147,7 @@ class LinkView extends StatelessWidget {
|
||||
|
||||
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 1),
|
||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@ -168,7 +168,7 @@ class LinkView extends StatelessWidget {
|
||||
return Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(5, 3, 5, 0),
|
||||
padding: const EdgeInsets.fromLTRB(5, 2, 5, 0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (showMetadata)
|
||||
|
@ -140,7 +140,7 @@ class WebAnalyzer {
|
||||
|
||||
while (comment == null && index < story.kids.length) {
|
||||
comment = await locator
|
||||
.get<CacheRepository>()
|
||||
.get<OfflineRepository>()
|
||||
.getCachedComment(id: story.kids.elementAt(index));
|
||||
index++;
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ class _OnboardingViewState extends State<OnboardingView> {
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
primary: Palette.orange,
|
||||
backgroundColor: Palette.orange,
|
||||
padding: const EdgeInsets.all(
|
||||
Dimens.pt18,
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
@ -29,20 +30,7 @@ class StoryTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (showWebPreview) {
|
||||
const double screenWidthLowerBound = 428,
|
||||
screenWidthUpperBound = 850,
|
||||
picHeightLowerBound = 118,
|
||||
picHeightUpperBound = 140,
|
||||
smallPicHeight = 100,
|
||||
picHeightFactor = 0.14;
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final bool showSmallerPreviewPic = screenWidth > screenWidthLowerBound &&
|
||||
screenWidth < screenWidthUpperBound;
|
||||
final double height = showSmallerPreviewPic
|
||||
? smallPicHeight
|
||||
: (MediaQuery.of(context).size.height * picHeightFactor)
|
||||
.clamp(picHeightLowerBound, picHeightUpperBound);
|
||||
|
||||
final double height = context.storyTileHeight;
|
||||
return TapDownWrapper(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
@ -143,7 +131,7 @@ class StoryTile extends StatelessWidget {
|
||||
backgroundColor: Palette.transparent,
|
||||
borderRadius: Dimens.zero,
|
||||
removeElevation: true,
|
||||
bodyMaxLines: height == smallPicHeight ? 3 : 4,
|
||||
bodyMaxLines: context.storyTileMaxLines,
|
||||
errorTitle: story.title,
|
||||
titleStyle: TextStyle(
|
||||
color: hasRead
|
||||
|
3
lib/services/caches/caches.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'collapse_cache.dart';
|
||||
export 'comment_cache.dart';
|
||||
export 'draft_cache.dart';
|
@ -1,9 +1,6 @@
|
||||
import 'package:hacki/models/models.dart' show Comment;
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
class CacheService {
|
||||
final Map<int, Comment> _comments = <int, Comment>{};
|
||||
final Map<int, String> _drafts = <int, String>{};
|
||||
class CollapseCache {
|
||||
final Map<int, Set<int>> _kids = <int, Set<int>>{};
|
||||
final Set<int> _collapsed = <int>{};
|
||||
final Map<int, Set<int>> _hidden = <int, Set<int>>{};
|
||||
@ -76,19 +73,4 @@ class CacheService {
|
||||
bool isCollapsed(int commentId) => _collapsed.contains(commentId);
|
||||
|
||||
int totalHidden(int commentId) => _hidden[commentId]?.length ?? 0;
|
||||
|
||||
void cacheComment(Comment comment) => _comments[comment.id] = comment;
|
||||
|
||||
Comment? getComment(int id) => _comments[id];
|
||||
|
||||
void resetComments() {
|
||||
_comments.clear();
|
||||
}
|
||||
|
||||
void removeDraft({required int replyingTo}) => _drafts.remove(replyingTo);
|
||||
|
||||
void cacheDraft({required String text, required int replyingTo}) =>
|
||||
_drafts[replyingTo] = text;
|
||||
|
||||
String? getDraft({required int replyingTo}) => _drafts[replyingTo];
|
||||
}
|
13
lib/services/caches/comment_cache.dart
Normal file
@ -0,0 +1,13 @@
|
||||
import 'package:hacki/models/models.dart' show Comment;
|
||||
|
||||
class CommentCache {
|
||||
static final Map<int, Comment> _comments = <int, Comment>{};
|
||||
|
||||
void cacheComment(Comment comment) => _comments[comment.id] = comment;
|
||||
|
||||
Comment? getComment(int id) => _comments[id];
|
||||
|
||||
void resetComments() {
|
||||
_comments.clear();
|
||||
}
|
||||
}
|
10
lib/services/caches/draft_cache.dart
Normal file
@ -0,0 +1,10 @@
|
||||
class DraftCache {
|
||||
static final Map<int, String> _drafts = <int, String>{};
|
||||
|
||||
void removeDraft({required int replyingTo}) => _drafts.remove(replyingTo);
|
||||
|
||||
void cacheDraft({required String text, required int replyingTo}) =>
|
||||
_drafts[replyingTo] = text;
|
||||
|
||||
String? getDraft({required int replyingTo}) => _drafts[replyingTo];
|
||||
}
|
@ -1,17 +1,43 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
class CustomBlocObserver extends BlocObserver {
|
||||
@override
|
||||
void onCreate(BlocBase<dynamic> bloc) {
|
||||
if (bloc is! CollapseCubit) {
|
||||
locator.get<Logger>().v('$bloc created');
|
||||
}
|
||||
|
||||
super.onCreate(bloc);
|
||||
}
|
||||
|
||||
@override
|
||||
void onEvent(
|
||||
Bloc<dynamic, dynamic> bloc,
|
||||
Object? event,
|
||||
) {
|
||||
locator.get<Logger>().d(event);
|
||||
if (event is! StoriesEvent) {
|
||||
locator.get<Logger>().v(event);
|
||||
}
|
||||
|
||||
super.onEvent(bloc, event);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTransition(
|
||||
Bloc<dynamic, dynamic> bloc,
|
||||
Transition<dynamic, dynamic> transition,
|
||||
) {
|
||||
if (bloc is! StoriesBloc) {
|
||||
locator.get<Logger>().v(transition);
|
||||
}
|
||||
|
||||
super.onTransition(bloc, transition);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(
|
||||
BlocBase<dynamic> bloc,
|
||||
@ -19,6 +45,8 @@ class CustomBlocObserver extends BlocObserver {
|
||||
StackTrace stackTrace,
|
||||
) {
|
||||
locator.get<Logger>().e(error);
|
||||
locator.get<Logger>().e(stackTrace);
|
||||
|
||||
super.onError(bloc, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export 'cache_service.dart';
|
||||
export 'caches/caches.dart';
|
||||
export 'custom_bloc_observer.dart';
|
||||
export 'fetcher.dart';
|
||||
export 'firebase_client.dart';
|
||||
|
@ -27,6 +27,7 @@ abstract class TextDimens {
|
||||
static const double pt8 = 8;
|
||||
static const double pt10 = 10;
|
||||
static const double pt12 = 12;
|
||||
static const double pt13 = 13;
|
||||
static const double pt14 = 14;
|
||||
static const double pt15 = 15;
|
||||
static const double pt16 = 16;
|
||||
|
@ -19,7 +19,7 @@ abstract class LinkUtil {
|
||||
}) {
|
||||
if (offlineReading) {
|
||||
locator
|
||||
.get<CacheRepository>()
|
||||
.get<OfflineRepository>()
|
||||
.hasCachedWebPage(url: link)
|
||||
.then((bool cached) {
|
||||
if (cached) {
|
||||
|
182
pubspec.lock
@ -7,21 +7,28 @@ packages:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "40.0.0"
|
||||
version: "47.0.0"
|
||||
adaptive_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: adaptive_theme
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.1.1"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.7.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -35,7 +42,7 @@ packages:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
version: "2.9.0"
|
||||
badges:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -49,14 +56,14 @@ packages:
|
||||
name: bloc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.3"
|
||||
version: "8.1.0"
|
||||
bloc_test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: bloc_test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.0.3"
|
||||
version: "9.1.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -70,35 +77,28 @@ packages:
|
||||
name: cached_network_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "2.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.0.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.2.1"
|
||||
clipboard:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -112,7 +112,7 @@ packages:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -126,7 +126,7 @@ packages:
|
||||
name: connectivity_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.3.7"
|
||||
connectivity_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -154,7 +154,7 @@ packages:
|
||||
name: connectivity_plus_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.2.3"
|
||||
connectivity_plus_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -175,7 +175,7 @@ packages:
|
||||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.5.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -196,7 +196,7 @@ packages:
|
||||
name: dbus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
version: "0.7.8"
|
||||
diff_match_patch:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -217,14 +217,14 @@ packages:
|
||||
name: equatable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.5"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.3.1"
|
||||
fast_gbk:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -266,7 +266,7 @@ packages:
|
||||
name: flutter_bloc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.1"
|
||||
version: "8.1.1"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -281,6 +281,11 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_fadein:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -298,9 +303,11 @@ packages:
|
||||
flutter_inappwebview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_inappwebview
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: e81d9b76aa8cbbdec65429a84a9edaf370715e90
|
||||
url: "https://github.com/vocsyinfotech/flutter_inappwebview"
|
||||
source: git
|
||||
version: "5.4.3+7"
|
||||
flutter_linkify:
|
||||
dependency: "direct main"
|
||||
@ -315,14 +322,14 @@ packages:
|
||||
name: flutter_local_notifications
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.6.1"
|
||||
version: "9.9.1"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0+1"
|
||||
version: "0.5.1"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -336,21 +343,21 @@ packages:
|
||||
name: flutter_secure_storage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "6.0.0"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -385,7 +392,7 @@ packages:
|
||||
name: flutter_slidable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "2.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -410,6 +417,11 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
gbk_codec:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -437,7 +449,7 @@ packages:
|
||||
name: hive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -458,7 +470,7 @@ packages:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.4"
|
||||
version: "0.13.5"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -473,6 +485,18 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
hydrated_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hydrated_bloc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.0.0-dev.3"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -521,21 +545,21 @@ packages:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
version: "0.12.12"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.8.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -591,7 +615,7 @@ packages:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
version: "1.8.2"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -605,14 +629,14 @@ packages:
|
||||
name: path_provider_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.0.20"
|
||||
path_provider_ios:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.10"
|
||||
version: "2.0.11"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -640,7 +664,7 @@ packages:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.3"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -726,28 +750,28 @@ packages:
|
||||
name: responsive_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
version: "0.4.3"
|
||||
rxdart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rxdart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.27.4"
|
||||
version: "0.27.5"
|
||||
sembast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sembast
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0+1"
|
||||
version: "3.3.0"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
version: "4.1.0"
|
||||
share_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -796,7 +820,7 @@ packages:
|
||||
name: shared_preferences_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.12"
|
||||
version: "2.0.13"
|
||||
shared_preferences_ios:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -824,7 +848,7 @@ packages:
|
||||
name: shared_preferences_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -845,7 +869,7 @@ packages:
|
||||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -899,14 +923,14 @@ packages:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
version: "1.9.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2+1"
|
||||
version: "2.0.3+1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -934,7 +958,14 @@ packages:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
synced_shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -948,35 +979,35 @@ packages:
|
||||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0+2"
|
||||
version: "3.0.0+3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.21.1"
|
||||
version: "1.21.4"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.9"
|
||||
version: "0.4.12"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.13"
|
||||
version: "0.4.16"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1011,14 +1042,14 @@ packages:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "6.1.5"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.17"
|
||||
version: "6.0.19"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1053,7 +1084,7 @@ packages:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.12"
|
||||
version: "2.0.13"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1088,14 +1119,14 @@ packages:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
version: "9.0.0"
|
||||
wakelock:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.1+2"
|
||||
version: "0.6.2"
|
||||
wakelock_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1123,7 +1154,7 @@ packages:
|
||||
name: wakelock_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.2.1"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1138,13 +1169,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
webview_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1158,28 +1196,28 @@ packages:
|
||||
name: webview_flutter_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.14"
|
||||
version: "2.10.1"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.9.3"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.1"
|
||||
version: "2.9.4"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
version: "3.0.0"
|
||||
workmanager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1193,7 +1231,7 @@ packages:
|
||||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+1"
|
||||
version: "0.2.0+2"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1209,5 +1247,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.17.0 <3.0.0"
|
||||
flutter: ">=3.0.0"
|
||||
dart: ">=2.18.0 <3.0.0"
|
||||
flutter: ">=3.3.2"
|
||||
|
32
pubspec.yaml
@ -1,39 +1,44 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 0.2.24+66
|
||||
version: 0.2.32+75
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
flutter: "3.3.4"
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.0.0
|
||||
badges: ^2.0.2
|
||||
bloc: ^8.0.3
|
||||
bloc: ^8.1.0
|
||||
cached_network_image: ^3.2.1
|
||||
clipboard: ^0.1.3
|
||||
collection: ^1.16.0
|
||||
connectivity_plus: ^2.2.1
|
||||
connectivity_plus: ^2.3.7
|
||||
dio: ^4.0.4
|
||||
equatable: 2.0.3
|
||||
equatable: ^2.0.5
|
||||
fast_gbk: ^1.0.0
|
||||
# feature_discovery: ^0.14.0
|
||||
# feature_discovery: ^0.14.0
|
||||
feature_discovery:
|
||||
git:
|
||||
url: https://github.com/livinglist/feature_discovery
|
||||
ref: flutter3_compatibility
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_bloc: ^8.1.1
|
||||
flutter_cache_manager: ^3.3.0
|
||||
flutter_fadein: ^2.0.0
|
||||
flutter_feather_icons: 2.0.0+1
|
||||
flutter_inappwebview: ^5.4.3+4
|
||||
# flutter_inappwebview: ^5.4.3+7
|
||||
flutter_inappwebview:
|
||||
git:
|
||||
url: https://github.com/vocsyinfotech/flutter_inappwebview
|
||||
ref: master
|
||||
flutter_linkify: ^5.0.2
|
||||
flutter_local_notifications: ^9.5.0
|
||||
flutter_secure_storage: ^5.0.2
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_siri_suggestions: ^2.1.0
|
||||
flutter_slidable: ^1.2.1
|
||||
flutter_slidable: ^2.0.0
|
||||
font_awesome_flutter: ^9.2.0
|
||||
gbk_codec: ^0.4.0
|
||||
get_it: 7.2.0
|
||||
@ -41,13 +46,14 @@ dependencies:
|
||||
html: ^0.15.0
|
||||
html_unescape: ^2.0.0
|
||||
http: ^0.13.3
|
||||
hydrated_bloc: ^9.0.0-dev.3
|
||||
intl: ^0.17.0
|
||||
logger: ^1.1.0
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.8
|
||||
path_provider_android: ^2.0.8
|
||||
path_provider_ios: ^2.0.8
|
||||
# pull_to_refresh: ^2.0.0
|
||||
# pull_to_refresh: ^2.0.0
|
||||
pull_to_refresh:
|
||||
git:
|
||||
url: https://github.com/livinglist/flutter_pulltorefresh
|
||||
@ -71,11 +77,13 @@ dependencies:
|
||||
workmanager: ^0.5.0
|
||||
|
||||
dev_dependencies:
|
||||
bloc_test: ^9.0.3
|
||||
bloc_test: ^9.1.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
mocktail: ^0.3.0
|
||||
very_good_analysis: ^2.3.0
|
||||
very_good_analysis: ^2.4.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
29
test_driver/perf_driver.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:flutter_driver/flutter_driver.dart' as driver;
|
||||
import 'package:integration_test/integration_test_driver.dart';
|
||||
|
||||
Future<void> main() {
|
||||
return integrationDriver(
|
||||
responseDataCallback: (Map<String, dynamic>? data) async {
|
||||
if (data != null) {
|
||||
final driver.Timeline timeline = driver.Timeline.fromJson(
|
||||
data['scrolling_timeline'] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
// Convert the Timeline into a TimelineSummary that's easier to
|
||||
// read and understand.
|
||||
final driver.TimelineSummary summary =
|
||||
driver.TimelineSummary.summarize(timeline);
|
||||
|
||||
// Then, write the entire timeline to disk in a json format.
|
||||
// This file can be opened in the Chrome browser's tracing tools
|
||||
// found by navigating to chrome://tracing.
|
||||
// Optionally, save the summary to disk by setting includeSummary
|
||||
// to true
|
||||
await summary.writeTimelineToFile(
|
||||
'scrolling_timeline',
|
||||
pretty: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|