Compare commits

..

6 Commits

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

* tap anywhere to collapse.

* bump version.

* add feedback.

* refactor preference.

* renaming.

* bump version.

* nit.

* cleanup.

* bump version.

* add feedback.

* nit.

* nit.

* fix android icon.

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

* fixed comment tile overflow.

* added github action. (#73)

* updated provisioning profile.

* updated profile.

* updated profile.

* updated profile

* updated profile.

* updated profile.

* updated fastfile.

* updated actions

* updated action.

* updated fastlane.

* revert actions.

* fixed action.

* avoid running on tag creation.

* updated action.

* bump version of Flutter.

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

* bumped version.

* bumped version

* bumped version.

* downgrade flutter version.

* Use transparent color for navigation bar (#70)

* Use transparent color for navigation bar

* Fixed whitespace

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

* bumped ios version.

* bumped versions.

* updated min screen width.

* removed print.

* fixed stack overflow in EditCubit.

* added integration test.

* bumped version.

Co-authored-by: wnhrt <github@weinhart.net>
Co-authored-by: Niklas Weinhart <git@weinhart.net>
2022-09-16 22:37:16 -07:00
69 changed files with 2497 additions and 1664 deletions

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

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

View File

@ -1,32 +0,0 @@
name: Releases
on:
push:
# tags:
# - '*'
jobs:
releases:
name: release apk
runs-on: ubuntu-latest
env:
JAVA_VERSION: "11.0"
FLUTTER_VERSION: "3.0.5"
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.5'
channel: 'stable'
- run: flutter pub get
- run: flutter analyze
# - run: flutter test
# - run: flutter build apk --release
# - uses: ncipollo/release-action@v1
# with:
# artifacts: "build/app/outputs/flutter-apk/*.apk"
# token: ${{ secrets.GITHUB_TOKEN }}

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

@ -0,0 +1,54 @@
name: Publish (iOS)
on:
# Allow manual builds of this workflow
workflow_dispatch: {}
# Run the workflow whenever a new tag named 'v*' is pushed
push:
branches:
- "!*"
tags:
- "v*"
jobs:
build_and_publish:
runs-on: macos-latest
env:
# Point the `ruby/setup-ruby` action at this Gemfile, so it
# caches dependencies for us.
BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile
steps:
- name: Check out from git
uses: actions/checkout@v2
# Configure ruby according to our .ruby-version
- name: Setup ruby & Bundler
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
# Set up flutter (feel free to adjust the version below)
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
cache: true
flutter-version: 3.3.10
- run: flutter pub get
- run: flutter format --set-exit-if-changed .
- run: flutter analyze
# Start an ssh-agent that will provide the SSH key from the
# SSH_PRIVATE_KEY secret to `fastlane match`
- name: Setup SSH key
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}"
- name: Download dependencies
run: flutter pub get
- name: Build & Publish to TestFlight with Fastlane
env:
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: cd ios && bundle exec fastlane beta "build_name:${{ github.ref_name }}"

1
.ruby-version Normal file
View File

@ -0,0 +1 @@
2.7.5

View File

@ -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
}
@ -79,3 +79,14 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

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

View File

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

View File

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

View File

@ -1,46 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hacki/main.dart' as app;
import 'package:hacki/screens/widgets/story_tile.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('performance test', () {
testWidgets('scrolling performance on ItemScreen',
(WidgetTester tester) async {
await app.main(testing: true);
await tester.pump();
final Finder bestStoryTabFinder = find.text('BEST');
await tester.tap(bestStoryTabFinder);
await tester.pumpAndSettle(const Duration(seconds: 3));
final Finder storyTileFinder = find.byType(StoryTile);
await tester.tap(storyTileFinder.first);
await tester.pumpAndSettle(const Duration(seconds: 3));
TestGesture gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -300));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -300));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -300));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, 900));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -900));
await tester.pump();
});
});
}

View File

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

View File

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

4
ios/Gemfile Normal file
View File

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

285
ios/Gemfile.lock Normal file
View File

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

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
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

View File

@ -108,7 +108,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
@ -128,6 +128,6 @@ SPEC CHECKSUMS:
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: e4c97c7a9aacaeda4b952f7ef9ea29e47660f622
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.2
COCOAPODS: 1.11.3

View File

@ -549,7 +549,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_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 = 5;
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
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.30;
MARKETING_VERSION = 0.2.33;
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 = 5;
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
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.30;
MARKETING_VERSION = 0.2.33;
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 = 5;
DEVELOPMENT_TEAM = QMWX3X2NF7;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
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.30;
MARKETING_VERSION = 0.2.33;
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;

View File

@ -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
View File

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

96
ios/fastlane/Fastfile Normal file
View File

@ -0,0 +1,96 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
## Update these lines to match your project!
# Bundle Identifier used for the iOS App on the App Store Connect portal
APP_IDENTIFIER = 'com.jiaqi.hacki'
# Issuer ID from the Keys section of https://appstoreconnect.apple.com/access/users
APPSTORECONNECT_ISSUER_ID = '0b588ac9-5b3e-4420-867a-a33decce7b91'
# Key ID from the key matching the `APP_STORE_CONNECT_API_KEY_KEY` secret, found under the Keys section of https://appstoreconnect.apple.com/access/users
APPSTORECONNECT_KEY_ID = 'DPNP8R66QS'
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do |options|
setup_ci if ENV['CI']
is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example'
if !is_example_repo && APP_IDENTIFIER == 'no.tjer.HelloWorld' then
UI.user_error! "You need to update your Fastfile to use your own `APP_IDENTIFIER`"
end
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
sync_code_signing(
type: "appstore",
app_identifier: [APP_IDENTIFIER, "#{APP_IDENTIFIER}.Share-Extension", "#{APP_IDENTIFIER}.Action-Extension"],
readonly: true
)
if !is_example_repo then
if APPSTORECONNECT_ISSUER_ID == '69a6de83-feb7-47e3-e053-5b8c7c11a4d1' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_ISSUER_ID`"
end
if APPSTORECONNECT_KEY_ID == 'YRQDJRKMR9' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_KEY_ID`"
end
end
# We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH
app_store_connect_api_key(
key_id: APPSTORECONNECT_KEY_ID,
issuer_id: APPSTORECONNECT_ISSUER_ID
)
latest_testflight_build_number
# Figure out the build number (and optionally build name)
new_build_number = ( + 1)
extra_config_args = []
if options.key?(:build_name) then
extra_config_args = ["--build-name", options[:build_name].delete_prefix('v')]
end
# Prep the xcodeproject from Flutter without building (`--config-only`)
sh(
"flutter", "build", "ios", "--config-only",
"--release", "--no-pub", "--no-codesign",
"--build-number", new_build_number.to_s,
*extra_config_args
)
increment_version_number(
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc')
)
increment_build_number({
build_number: latest_testflight_build_number + 1
})
# Build & sign using Runner.xcworkspace
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
output_directory: "../build/ios/archive"
)
upload_to_testflight(
# This takes a long time, so don't waste GH runner minutes (but it means manually needing to
# set the build live for external testers).
skip_waiting_for_build_processing: true,
)
end
end

13
ios/fastlane/Matchfile Normal file
View File

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

View File

@ -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;

View File

@ -8,17 +8,6 @@ enum CommentsStatus {
failure,
}
enum CommentsOrder {
natural,
newestFirst,
oldestFirst,
}
enum FetchMode {
lazy,
eager,
}
class CommentsState extends Equatable {
const CommentsState({
required this.item,

View File

@ -66,6 +66,8 @@ class EditCubit extends HydratedCubit<EditState> {
void deleteDraft() => clear();
bool called = false;
@override
EditState? fromJson(Map<String, dynamic> json) {
final String text = json['text'] as String? ?? '';
@ -81,7 +83,7 @@ class EditCubit extends HydratedCubit<EditState> {
replyingTo: replyingTo,
);
cachedState = state;
_cachedState = state;
return state;
}
@ -94,16 +96,21 @@ class EditCubit extends HydratedCubit<EditState> {
EditState selected = state;
if (state.replyingTo == null ||
(state.replyingTo?.id != cachedState.replyingTo?.id &&
(state.replyingTo?.id != _cachedState.replyingTo?.id &&
state.text.isNullOrEmpty)) {
selected = cachedState;
selected = _cachedState;
}
if (selected.text.isNullOrEmpty) {
clear();
return null;
}
return <String, dynamic>{
'text': selected.text,
'text': selected.text ?? '',
'item': selected.replyingTo?.toJson(),
};
}
static EditState cachedState = const EditState.init();
static EditState _cachedState = const EditState.init();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ extension ContextExtension on BuildContext {
static double _screenWidth = 0;
static double _storyTileHeight = 0;
static int _storyTileMaxLines = 4;
static const double _screenWidthLowerBound = 428,
static const double _screenWidthLowerBound = 430,
_screenWidthUpperBound = 850,
_picHeightLowerBound = 110,
_picHeightUpperBound = 128,

View File

@ -3,6 +3,7 @@ export 'date_time_extension.dart';
export 'int_extension.dart';
export 'list_extension.dart';
export 'object_extension.dart';
export 'set_extension.dart';
export 'state_extension.dart';
export 'string_extension.dart';
export 'widget_extension.dart';

View File

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

View File

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

View File

@ -6,6 +6,7 @@ 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';
@ -13,7 +14,7 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/custom_bloc_observer.dart';
import 'package:hacki/services/fetcher.dart';
@ -86,6 +87,19 @@ Future<void> main({bool testing = false}) 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();
@ -97,34 +111,17 @@ Future<void> main({bool testing = false}) async {
final AdaptiveThemeMode? savedThemeMode = await AdaptiveTheme.getThemeMode();
final SharedPreferences prefs = await SharedPreferences.getInstance();
final bool trueDarkMode =
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
prefs.getBool(const TrueDarkModePreference().key) ?? false;
if (kReleaseMode) {
HydratedBlocOverrides.runZoned(
() => runApp(
HackiApp(
savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode,
),
),
storage: storage,
);
} else {
BlocOverrides.runZoned(
() {
HydratedBlocOverrides.runZoned(
() => runApp(
HackiApp(
savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode,
),
),
storage: storage,
);
},
blocObserver: CustomBlocObserver(),
);
}
Bloc.observer = CustomBlocObserver();
HydratedBloc.storage = storage;
runApp(
HackiApp(
savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode,
),
);
}
class HackiApp extends StatelessWidget {

View File

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

View File

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

View File

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

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

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

View File

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

285
lib/models/preference.dart Normal file
View File

@ -0,0 +1,285 @@
import 'dart:io';
import 'package:equatable/equatable.dart';
import 'package:hacki/models/displayable.dart';
import 'package:hacki/models/models.dart';
abstract class Preference<T> extends Equatable with SettingsDisplayable {
const Preference({required this.val});
final T val;
String get key;
Preference<T> copyWith({required T? val});
static List<Preference<dynamic>> allPreferences = <Preference<dynamic>>[
FetchModePreference(),
CommentsOrderPreference(),
FontSizePreference(),
// order here reflects the order on settings screen.
const NotificationModePreference(),
const CollapseModePreference(),
const DisplayModePreference(),
const MetadataModePreference(),
NavigationModePreference(),
const ReaderModePreference(),
const MarkReadStoriesModePreference(),
const EyeCandyModePreference(),
const TrueDarkModePreference(),
];
@override
List<Object?> get props => <Object?>[key];
}
abstract class BooleanPreference extends Preference<bool> {
const BooleanPreference({required super.val});
}
abstract class IntPreference extends Preference<int> {
const IntPreference({required super.val});
}
const bool _notificationModeDefaultValue = true;
const bool _displayModeDefaultValue = true;
const bool _navigationModeDefaultValueIOS = true;
const bool _navigationModeDefaultValueAndroid = false;
const bool _eyeCandyModeDefaultValue = false;
const bool _trueDarkModeDefaultValue = false;
const bool _readerModeDefaultValue = true;
const bool _markReadStoriesModeDefaultValue = true;
const bool _metadataModeDefaultValue = true;
const bool _collapseModeDefaultValue = false;
final int _fetchModeDefaultValue = FetchMode.eager.index;
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
final int _fontSizeDefaultValue = FontSize.regular.index;
class NotificationModePreference extends BooleanPreference {
const NotificationModePreference({bool? val})
: super(val: val ?? _notificationModeDefaultValue);
@override
NotificationModePreference copyWith({required bool? val}) {
return NotificationModePreference(val: val);
}
@override
String get key => 'notificationMode';
@override
String get title => 'Notification on New Reply';
@override
String get subtitle =>
'''Hacki scans for new replies to your 15 most recent comments or stories every 5 minutes while the app is running in the foreground.''';
}
class CollapseModePreference extends BooleanPreference {
const CollapseModePreference({bool? val})
: super(val: val ?? _collapseModeDefaultValue);
@override
CollapseModePreference copyWith({required bool? val}) {
return CollapseModePreference(val: val);
}
@override
String get key => 'collapseMode';
@override
String get title => 'Tap Anywhere to Collapse';
}
/// The value deciding whether or not the story
/// tile should display link preview. Defaults to true.
class DisplayModePreference extends BooleanPreference {
const DisplayModePreference({bool? val})
: super(val: val ?? _displayModeDefaultValue);
@override
DisplayModePreference copyWith({required bool? val}) {
return DisplayModePreference(val: val);
}
@override
String get key => 'displayMode';
@override
String get title => 'Complex Story Tile';
@override
String get subtitle => 'show web preview in story tile.';
}
class MetadataModePreference extends BooleanPreference {
const MetadataModePreference({bool? val})
: super(val: val ?? _metadataModeDefaultValue);
@override
MetadataModePreference copyWith({required bool? val}) {
return MetadataModePreference(val: val);
}
@override
String get key => 'metadataMode';
@override
String get title => 'Show Metadata';
@override
String get subtitle =>
'''show number of comments and post date in story tile.''';
}
/// The value deciding whether or not user should be
/// navigated to web view first. Defaults to false.
class NavigationModePreference extends BooleanPreference {
NavigationModePreference({bool? val})
: super(
val: val ??
(Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS),
);
@override
NavigationModePreference copyWith({required bool? val}) {
return NavigationModePreference(val: val);
}
@override
String get key => 'navigationMode';
@override
String get title => 'Show Web Page First';
@override
String get subtitle => ''''show web page first after tapping on story.''';
}
class ReaderModePreference extends BooleanPreference {
const ReaderModePreference({bool? val})
: super(val: val ?? _readerModeDefaultValue);
@override
ReaderModePreference copyWith({required bool? val}) {
return ReaderModePreference(val: val);
}
@override
String get key => 'readerMode';
@override
String get title => 'Use Reader';
@override
String get subtitle =>
'''enter reader mode in Safari directly when it is available.''';
@override
bool get isDisplayable => Platform.isIOS;
}
class MarkReadStoriesModePreference extends BooleanPreference {
const MarkReadStoriesModePreference({bool? val})
: super(val: val ?? _markReadStoriesModeDefaultValue);
@override
MarkReadStoriesModePreference copyWith({required bool? val}) {
return MarkReadStoriesModePreference(val: val);
}
@override
String get key => 'markReadStoriesMode';
@override
String get title => 'Mark Read Stories';
@override
String get subtitle => 'grey out stories you have read.';
}
class EyeCandyModePreference extends BooleanPreference {
const EyeCandyModePreference({bool? val})
: super(val: val ?? _eyeCandyModeDefaultValue);
@override
EyeCandyModePreference copyWith({required bool? val}) {
return EyeCandyModePreference(val: val);
}
@override
String get key => 'eyeCandyMode';
@override
String get title => 'Eye Candy';
@override
String get subtitle => 'some sort of magic.';
}
class TrueDarkModePreference extends BooleanPreference {
const TrueDarkModePreference({bool? val})
: super(val: val ?? _trueDarkModeDefaultValue);
@override
TrueDarkModePreference copyWith({required bool? val}) {
return TrueDarkModePreference(val: val);
}
@override
String get key => 'trueDarkMode';
@override
String get title => 'True Dark Mode';
@override
String get subtitle => 'you might need to restart the app.';
}
class FetchModePreference extends IntPreference {
FetchModePreference({int? val}) : super(val: val ?? _fetchModeDefaultValue);
@override
FetchModePreference copyWith({required int? val}) {
return FetchModePreference(val: val);
}
@override
String get key => 'fetchMode';
@override
String get title => 'Default fetch mode';
}
class CommentsOrderPreference extends IntPreference {
CommentsOrderPreference({int? val})
: super(val: val ?? _commentsOrderDefaultValue);
@override
CommentsOrderPreference copyWith({required int? val}) {
return CommentsOrderPreference(val: val);
}
@override
String get key => 'commentsOrder';
@override
String get title => 'Default comments order';
}
class FontSizePreference extends IntPreference {
FontSizePreference({int? val}) : super(val: val ?? _fontSizeDefaultValue);
@override
FontSizePreference copyWith({required int? val}) {
return FontSizePreference(val: val);
}
@override
String get key => 'fontSize';
@override
String get title => 'Default font size';
}

View File

@ -1,4 +1,4 @@
part of 'search_filters.dart';
part of 'search_params.dart';
abstract class SearchFilter {
String get query;

View File

@ -3,15 +3,15 @@ import 'package:equatable/equatable.dart';
part 'search_filter.dart';
class SearchFilters extends Equatable {
const SearchFilters({
class SearchParams extends Equatable {
const SearchParams({
required this.filters,
required this.query,
required this.page,
this.sorted = false,
});
SearchFilters.init()
SearchParams.init()
: filters = <SearchFilter>{},
query = '',
page = 0,
@ -22,13 +22,13 @@ class SearchFilters extends Equatable {
final int page;
final bool sorted;
SearchFilters copyWith({
SearchParams copyWith({
Set<SearchFilter>? filters,
String? query,
int? page,
bool? sorted,
}) {
return SearchFilters(
return SearchParams(
filters: filters ?? this.filters,
query: query ?? this.query,
page: page ?? this.page,
@ -36,8 +36,8 @@ class SearchFilters extends Equatable {
);
}
SearchFilters copyWithFilterRemoved<T extends SearchFilter>() {
return SearchFilters(
SearchParams copyWithFilterRemoved<T extends SearchFilter>() {
return SearchParams(
filters: <SearchFilter>{...filters}
..removeWhere((SearchFilter e) => e is T),
query: query,
@ -46,10 +46,10 @@ class SearchFilters extends Equatable {
);
}
SearchFilters copyWithFilterAdded(
SearchParams copyWithFilterAdded(
SearchFilter filter,
) {
return SearchFilters(
return SearchParams(
filters: <SearchFilter>{...filters, filter},
query: query,
page: page,

View File

@ -3,7 +3,6 @@ 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';
@ -25,39 +24,6 @@ class PreferenceRepository {
static const String _pinnedStoriesIdsKey = 'pinnedStoriesIds';
static const String _unreadCommentsIdsKey = 'unreadCommentsIds';
static const String _lastReadStoryIdKey = 'lastReadStoryId';
static const String _isFirstLaunchKey = 'isFirstLaunch';
static const String _metadataModeKey = 'metadataMode';
static const String _notificationModeKey = 'notificationMode';
static const String _readerModeKey = 'readerMode';
/// Exposing this val for main func.
static const String trueDarkModeKey = 'trueDarkMode';
/// The key of a boolean value deciding whether or not the story
/// tile should display link preview. Defaults to true.
static const String _displayModeKey = 'displayMode';
/// The key of a boolean value deciding whether or not user should be
/// navigated to web view first. Defaults to false.
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 _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;
@ -70,70 +36,21 @@ class PreferenceRepository {
Future<String?> get password async => _secureStorage.read(key: _passwordKey);
Future<bool> get isFirstLaunch async {
final SharedPreferences prefs = await _prefs;
final bool val =
prefs.getBool(_isFirstLaunchKey) ?? _isFirstLaunchKeyDefaultValue;
await prefs.setBool(_isFirstLaunchKey, false);
return val;
}
Future<bool> get shouldShowNotification async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_notificationModeKey) ??
_notificationModeDefaultValue,
Future<bool?> getBool(String key) => _prefs.then(
(SharedPreferences prefs) => prefs.getBool(key),
);
Future<bool> get shouldShowComplexStoryTile async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_displayModeKey) ?? _displayModeDefaultValue,
Future<int?> getInt(String key) => _prefs.then(
(SharedPreferences prefs) => prefs.getInt(key),
);
Future<bool> get shouldShowWebFirst async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_navigationModeKey) ??
(Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS),
//ignore: avoid_positional_boolean_parameters
void setBool(String key, bool val) => _prefs.then(
(SharedPreferences prefs) => prefs.setBool(key, val),
);
Future<bool> get shouldShowEyeCandy async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_eyeCandyModeKey) ?? _eyeCandyModeDefaultValue,
);
Future<bool> get shouldShowMetadata async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_metadataModeKey) ?? _metadataModeDefaultValue,
);
Future<bool> get trueDarkMode async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(trueDarkModeKey) ?? _trueDarkModeDefaultValue,
);
Future<bool> get readerMode async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_readerModeKey) ?? _readerModeDefaultValue,
);
Future<bool> get markReadStories async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_markReadStoriesModeKey) ??
_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,
),
void setInt(String key, int val) => _prefs.then(
(SharedPreferences prefs) => prefs.setInt(key, val),
);
Future<bool> hasPushed(int commentId) async =>
@ -195,76 +112,6 @@ class PreferenceRepository {
await _secureStorage.delete(key: _passwordKey);
}
Future<void> toggleNotificationMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode =
prefs.getBool(_notificationModeKey) ?? _notificationModeDefaultValue;
await prefs.setBool(_notificationModeKey, !currentMode);
}
Future<void> toggleDisplayMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode =
prefs.getBool(_displayModeKey) ?? _displayModeDefaultValue;
await prefs.setBool(_displayModeKey, !currentMode);
}
Future<void> toggleNavigationMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode = prefs.getBool(_navigationModeKey) ??
(Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS);
await prefs.setBool(_navigationModeKey, !currentMode);
}
Future<void> toggleEyeCandyMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode =
prefs.getBool(_eyeCandyModeKey) ?? _eyeCandyModeDefaultValue;
await prefs.setBool(_eyeCandyModeKey, !currentMode);
}
Future<void> toggleTrueDarkMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode =
prefs.getBool(trueDarkModeKey) ?? _trueDarkModeDefaultValue;
await prefs.setBool(trueDarkModeKey, !currentMode);
}
Future<void> toggleReaderMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode =
prefs.getBool(_readerModeKey) ?? _readerModeDefaultValue;
await prefs.setBool(_readerModeKey, !currentMode);
}
Future<void> toggleMarkReadStoriesMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode = prefs.getBool(_markReadStoriesModeKey) ??
_markReadStoriesModeDefaultValue;
await prefs.setBool(_markReadStoriesModeKey, !currentMode);
}
Future<void> toggleMetadataMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode =
prefs.getBool(_metadataModeKey) ?? _metadataModeDefaultValue;
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 {

View File

@ -11,9 +11,9 @@ class SearchRepository {
final Dio _dio;
Stream<Story> search({
required SearchFilters filters,
required SearchParams params,
}) async* {
final String url = '$_baseUrl${filters.filteredQuery}';
final String url = '$_baseUrl${params.filteredQuery}';
final Response<Map<String, dynamic>> response =
await _dio.get<Map<String, dynamic>>(url);
final Map<String, dynamic>? data = response.data;

View File

@ -1,5 +1,3 @@
// ignore_for_file: lines_longer_than_80_chars
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@ -446,7 +444,7 @@ class _HomeScreenState extends State<HomeScreen>
void onShareExtensionTapped(String? event) {
if (event == null) return;
final int? id = event.getItemId();
final int? id = event.itemId;
if (id != null) {
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
enum MenuAction {
upvote,
downvote,
share,
block,
flag,
cancel,
}

View File

@ -0,0 +1 @@
export 'menu_action.dart';

View File

@ -13,6 +13,8 @@ class CustomAppBar extends AppBar {
required Color backgroundColor,
required Future<bool> Function() onBackgroundTap,
required Future<bool> Function() onDismiss,
required VoidCallback onFontSizeTap,
required GlobalKey fontSizeIconButtonKey,
bool splitViewEnabled = false,
VoidCallback? onZoomTap,
bool? expanded,
@ -39,6 +41,13 @@ class CustomAppBar extends AppBar {
ScrollUpIconButton(
scrollController: scrollController,
),
IconButton(
key: fontSizeIconButtonKey,
icon: const Icon(
Icons.format_size,
),
onPressed: onFontSizeTap,
),
if (item is Story)
PinIconButton(
story: item,

View File

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
class LoginDialog extends StatelessWidget {
const LoginDialog({
Key? key,
required this.usernameController,
required this.passwordController,
required this.showSnackBar,
}) : super(key: key);
final TextEditingController usernameController;
final TextEditingController passwordController;
final Function({
required String content,
VoidCallback? action,
String? label,
}) showSnackBar;
@override
Widget build(BuildContext context) {
return BlocConsumer<AuthBloc, AuthState>(
listener: (BuildContext context, AuthState state) {
if (state.isLoggedIn) {
final String happyFace = Constants.happyFaces.pickRandomly()!;
Navigator.pop(context);
showSnackBar(content: 'Logged in successfully! $happyFace');
}
},
builder: (BuildContext context, AuthState state) {
return SimpleDialog(
children: <Widget>[
if (state.status == AuthStatus.loading)
const SizedBox(
height: Dimens.pt36,
width: Dimens.pt36,
child: Center(
child: CircularProgressIndicator(
color: Palette.orange,
),
),
)
else if (!state.isLoggedIn) ...<Widget>[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt18,
),
child: TextField(
controller: usernameController,
cursorColor: Palette.orange,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'Username',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
),
),
),
),
const SizedBox(
height: Dimens.pt16,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt18,
),
child: TextField(
controller: passwordController,
cursorColor: Palette.orange,
obscureText: true,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'Password',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
),
),
),
),
const SizedBox(
height: Dimens.pt16,
),
if (state.status == AuthStatus.failure)
const Padding(
padding: EdgeInsets.only(
left: Dimens.pt18,
),
child: Text(
'Something went wrong...',
style: TextStyle(
color: Palette.grey,
fontSize: TextDimens.pt12,
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(
state.agreedToEULA
? Icons.check_box
: Icons.check_box_outline_blank,
color: state.agreedToEULA
? Palette.deepOrange
: Palette.grey,
),
onPressed: () =>
context.read<AuthBloc>().add(AuthToggleAgreeToEULA()),
),
Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(
text: 'I agree to ',
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
WidgetSpan(
child: Transform.translate(
offset: const Offset(0, 1),
child: TapDownWrapper(
onTap: () => LinkUtil.launch(
Constants.endUserAgreementLink,
),
child: const Text(
'End User Agreement',
style: TextStyle(
color: Palette.deepOrange,
decoration: TextDecoration.underline,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
)
],
),
Padding(
padding: const EdgeInsets.only(
right: Dimens.pt12,
),
child: ButtonBar(
children: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
context.read<AuthBloc>().add(AuthInitialize());
},
child: const Text(
'Cancel',
),
),
ElevatedButton(
onPressed: () {
if (state.agreedToEULA) {
final String username = usernameController.text;
final String password = passwordController.text;
if (username.isNotEmpty && password.isNotEmpty) {
context.read<AuthBloc>().add(
AuthLogin(
username: username,
password: password,
),
);
}
}
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
state.agreedToEULA
? Palette.deepOrange
: Palette.grey,
),
),
child: const Text(
'Log in',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Palette.white,
),
),
),
],
),
),
],
],
);
},
);
}
}

View File

@ -0,0 +1,444 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/item/widgets/widgets.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class MainView extends StatelessWidget {
const MainView({
Key? key,
required this.scrollController,
required this.refreshController,
required this.commentEditingController,
required this.authState,
required this.state,
required this.focusNode,
required this.topPadding,
required this.splitViewEnabled,
required this.onMoreTapped,
required this.onStoryLinkTapped,
required this.onLoginTapped,
required this.onRightMoreTapped,
}) : super(key: key);
final ScrollController scrollController;
final RefreshController refreshController;
final TextEditingController commentEditingController;
final AuthState authState;
final CommentsState state;
final FocusNode focusNode;
final double topPadding;
final bool splitViewEnabled;
final Function(Item item, Rect? rect) onMoreTapped;
final ValueChanged<String> onStoryLinkTapped;
final VoidCallback onLoginTapped;
final ValueChanged<Comment> onRightMoreTapped;
@override
Widget build(BuildContext context) {
return SmartRefresher(
scrollController: scrollController,
enablePullUp: !state.onlyShowTargetComment,
enablePullDown: !state.onlyShowTargetComment,
header: WaterDropMaterialHeader(
backgroundColor: Palette.orange,
offset: topPadding,
),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
builder: (BuildContext context, LoadStatus? mode) {
const double height = 55;
late final Widget body;
if (mode == LoadStatus.idle) {
body = const Text('');
} else if (mode == LoadStatus.loading) {
body = const Text('');
} else if (mode == LoadStatus.failed) {
body = const Text(
'',
);
} else if (mode == LoadStatus.canLoading) {
body = const Text(
'',
);
} else {
body = const Text('');
}
return SizedBox(
height: height,
child: Center(child: body),
);
},
),
controller: refreshController,
onRefresh: () {
HapticFeedback.lightImpact();
if (context.read<StoriesBloc>().state.offlineReading) {
refreshController.refreshCompleted();
} else {
context.read<CommentsCubit>().refresh();
if (state.item.isPoll) {
context.read<PollCubit>().refresh();
}
}
},
onLoading: () {
if (state.fetchMode == FetchMode.eager) {
context.read<CommentsCubit>().loadMore();
} else {
refreshController.loadComplete();
}
},
child: ListView.builder(
primary: false,
itemCount: state.comments.length + 2,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return Column(
children: <Widget>[
SizedBox(
height: topPadding,
),
if (!splitViewEnabled)
const Padding(
padding: EdgeInsets.only(bottom: Dimens.pt6),
child: OfflineBanner(),
),
Slidable(
startActionPane: ActionPane(
motion: const BehindMotion(),
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,
),
SlidableAction(
onPressed: (BuildContext context) =>
onMoreTapped(state.item, context.rect),
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,
),
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,
),
),
],
),
),
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)
BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (
PreferenceState previous,
PreferenceState current,
) =>
previous.fontSize != current.fontSize,
builder: (
BuildContext context,
PreferenceState prefState,
) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt10,
),
child: SelectableLinkify(
text: state.item.text,
style: TextStyle(
fontSize:
MediaQuery.of(context).textScaleFactor *
context
.read<PreferenceCubit>()
.state
.fontSize
.fontSize,
),
linkStyle: TextStyle(
fontSize:
MediaQuery.of(context).textScaleFactor *
context
.read<PreferenceCubit>()
.state
.fontSize
.fontSize,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
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,
),
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: FetchMode.values
.map(
(FetchMode val) => DropdownMenuItem<FetchMode>(
value: val,
child: Text(
val.description,
style: const TextStyle(
fontSize: TextDimens.pt13,
),
),
),
)
.toList(),
onChanged:
context.read<CommentsCubit>().onFetchModeChanged,
),
const SizedBox(
width: Dimens.pt6,
),
DropdownButton<CommentsOrder>(
value: state.order,
underline: const SizedBox.shrink(),
items: CommentsOrder.values
.map(
(CommentsOrder val) =>
DropdownMenuItem<CommentsOrder>(
value: val,
child: Text(
val.description,
style: const TextStyle(
fontSize: TextDimens.pt13,
),
),
),
)
.toList(),
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),
),
),
],
],
);
} 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(Constants.happyFaces.pickRandomly()!),
),
);
} else {
return const SizedBox.shrink();
}
}
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;
}
if (cmt.id != context.read<EditCubit>().state.replyingTo?.id) {
commentEditingController.clear();
}
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,
),
);
},
),
);
}
}

View File

@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
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/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/item/models/models.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
class MorePopupMenu extends StatelessWidget {
const MorePopupMenu({
Key? key,
required this.item,
required this.isBlocked,
required this.showSnackBar,
required this.onStoryLinkTapped,
required this.onLoginTapped,
}) : super(key: key);
final Item item;
final bool isBlocked;
final Function({
required String content,
VoidCallback? action,
String? label,
}) showSnackBar;
final ValueChanged<String> onStoryLinkTapped;
final VoidCallback onLoginTapped;
@override
Widget build(BuildContext context) {
return BlocProvider<VoteCubit>(
create: (BuildContext context) => VoteCubit(
item: item,
authBloc: context.read<AuthBloc>(),
),
child: BlocConsumer<VoteCubit, VoteState>(
listenWhen: (VoteState previous, VoteState current) {
return previous.status != current.status;
},
listener: (BuildContext context, VoteState voteState) {
if (voteState.status == VoteStatus.submitted) {
showSnackBar(content: 'Vote submitted successfully.');
} else if (voteState.status == VoteStatus.canceled) {
showSnackBar(content: 'Vote canceled.');
} else if (voteState.status == VoteStatus.failure) {
showSnackBar(content: 'Something went wrong...');
} else if (voteState.status ==
VoteStatus.failureKarmaBelowThreshold) {
showSnackBar(
content: "You can't downvote because you are karmaly broke.",
);
} else if (voteState.status == VoteStatus.failureNotLoggedIn) {
showSnackBar(
content: 'Not logged in, no voting! (;O´)o',
action: onLoginTapped,
label: 'Log in',
);
} else if (voteState.status == VoteStatus.failureBeHumble) {
showSnackBar(content: 'No voting on your own post! (;O´)o');
}
Navigator.pop(
context,
MenuAction.upvote,
);
},
builder: (BuildContext context, VoteState voteState) {
final bool upvoted = voteState.vote == Vote.up;
final bool downvoted = voteState.vote == Vote.down;
return Container(
height: item is Comment ? 430 : 450,
color: Theme.of(context).canvasColor,
child: Material(
color: Palette.transparent,
child: Column(
children: <Widget>[
BlocProvider<UserCubit>(
create: (BuildContext context) =>
UserCubit()..init(userId: item.by),
child: BlocBuilder<UserCubit, UserState>(
builder: (BuildContext context, UserState state) {
return ListTile(
leading: const Icon(
Icons.account_circle,
),
title: Text(item.by),
subtitle: Text(
state.user.description,
),
onTap: () {
showDialog<void>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: Text('About ${state.user.id}'),
content: state.user.about.isEmpty
? Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: const <Widget>[
Text(
'empty',
style: TextStyle(
color: Palette.grey,
),
),
],
)
: SelectableLinkify(
text: HtmlUtil.parseHtml(
state.user.about,
),
linkStyle: const TextStyle(
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'Okay',
),
),
],
),
);
},
);
},
),
),
ListTile(
leading: Icon(
FeatherIcons.chevronUp,
color: upvoted ? Palette.orange : null,
),
title: Text(
upvoted ? 'Upvoted' : 'Upvote',
style: upvoted
? const TextStyle(color: Palette.orange)
: null,
),
subtitle:
item is Story ? Text(item.score.toString()) : null,
onTap: context.read<VoteCubit>().upvote,
),
ListTile(
leading: Icon(
FeatherIcons.chevronDown,
color: downvoted ? Palette.orange : null,
),
title: Text(
downvoted ? 'Downvoted' : 'Downvote',
style: downvoted
? const TextStyle(color: Palette.orange)
: null,
),
onTap: context.read<VoteCubit>().downvote,
),
ListTile(
leading: const Icon(FeatherIcons.share),
title: const Text(
'Share',
),
onTap: () => Navigator.pop(
context,
MenuAction.share,
),
),
ListTile(
leading: const Icon(Icons.local_police),
title: const Text(
'Flag',
),
onTap: () => Navigator.pop(
context,
MenuAction.flag,
),
),
ListTile(
leading: Icon(
isBlocked ? Icons.visibility : Icons.visibility_off,
),
title: Text(
isBlocked ? 'Unblock' : 'Block',
),
onTap: () => Navigator.pop(
context,
MenuAction.block,
),
),
ListTile(
leading: const Icon(Icons.close),
title: const Text(
'Cancel',
),
onTap: () => Navigator.pop(
context,
MenuAction.cancel,
),
),
],
),
),
);
},
),
);
}
}

View File

@ -257,7 +257,6 @@ class _ReplyBoxState extends State<ReplyBox> {
showDialog<void>(
context: context,
barrierDismissible: true,
builder: (_) {
return AlertDialog(
insetPadding: const EdgeInsets.symmetric(

View File

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:responsive_builder/responsive_builder.dart';
class TimeMachineDialog extends StatelessWidget {
const TimeMachineDialog({
Key? key,
required this.comment,
required this.size,
required this.deviceType,
required this.widthFactor,
required this.onStoryLinkTapped,
}) : super(key: key);
final Comment comment;
final Size size;
final DeviceScreenType deviceType;
final double widthFactor;
final Function(String) onStoryLinkTapped;
@override
Widget build(BuildContext context) {
return BlocProvider<TimeMachineCubit>.value(
value: TimeMachineCubit()..activateTimeMachine(comment),
child: BlocBuilder<TimeMachineCubit, TimeMachineState>(
builder: (BuildContext context, TimeMachineState state) {
return Center(
child: Material(
color: Theme.of(context).canvasColor,
borderRadius: const BorderRadius.all(
Radius.circular(
Dimens.pt4,
),
),
child: SizedBox(
height: size.height * 0.8,
width: size.width * widthFactor,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt8,
vertical: Dimens.pt12,
),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
const SizedBox(
width: Dimens.pt8,
),
const Text('Parents:'),
const Spacer(),
IconButton(
icon: const Icon(
Icons.close,
size: Dimens.pt16,
),
onPressed: () => Navigator.pop(context),
padding: EdgeInsets.zero,
),
],
),
Expanded(
child: ListView(
children: <Widget>[
for (final Comment c in state.parents) ...<Widget>[
CommentTile(
comment: c,
myUsername:
context.read<AuthBloc>().state.username,
onStoryLinkTapped: onStoryLinkTapped,
actionable: false,
fetchMode: FetchMode.eager,
),
const Divider(
height: Dimens.zero,
),
],
],
),
),
],
),
),
),
),
);
},
),
);
}
}

View File

@ -1,7 +1,11 @@
export 'custom_app_bar.dart';
export 'fav_icon_button.dart';
export 'link_icon_button.dart';
export 'login_dialog.dart';
export 'main_view.dart';
export 'more_popup_menu.dart';
export 'pin_icon_button.dart';
export 'poll_view.dart';
export 'reply_box.dart';
export 'scroll_up_icon_button.dart';
export 'time_machine_dialog.dart';

View File

@ -266,23 +266,6 @@ class _ProfileScreenState extends State<ProfileScreen>
},
),
const OfflineListTile(),
SwitchListTile(
title: const Text('Notification on New Reply'),
subtitle: const Text(
'Hacki scans for new replies to your 15 '
'most recent comments or stories '
'every 5 minutes while the app is '
'running in the foreground.',
),
value: preferenceState.showNotification,
onChanged: (bool val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleNotificationMode();
},
activeColor: Palette.orange,
),
const SizedBox(
height: Dimens.pt8,
),
@ -324,30 +307,30 @@ class _ProfileScreenState extends State<ProfileScreen>
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,
items: FetchMode.values
.map(
(FetchMode val) =>
DropdownMenuItem<FetchMode>(
value: val,
child: Text(
val.description,
style: const TextStyle(
fontSize: TextDimens.pt16,
),
),
),
),
),
DropdownMenuItem<FetchMode>(
value: FetchMode.eager,
child: Text(
'Eager',
style: TextStyle(
fontSize: TextDimens.pt16,
),
),
),
],
onChanged: context
.read<PreferenceCubit>()
.selectFetchMode,
)
.toList(),
onChanged: (FetchMode? fetchMode) {
if (fetchMode != null) {
context
.read<PreferenceCubit>()
.update(
FetchModePreference(),
to: fetchMode.index,
);
}
},
),
const Spacer(),
],
@ -359,39 +342,31 @@ class _ProfileScreenState extends State<ProfileScreen>
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,
items: CommentsOrder.values
.map(
(CommentsOrder val) =>
DropdownMenuItem<
CommentsOrder>(
value: val,
child: Text(
val.description,
style: const 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,
)
.toList(),
onChanged: (CommentsOrder? order) {
if (order != null) {
context
.read<PreferenceCubit>()
.update(
CommentsOrderPreference(),
to: order.index,
);
}
},
),
const Spacer(),
],
@ -399,113 +374,38 @@ class _ProfileScreenState extends State<ProfileScreen>
),
],
),
SwitchListTile(
title: const Text('Complex Story Tile'),
subtitle: const Text(
'show web preview in story tile.',
),
value: preferenceState.showComplexStoryTile,
onChanged: (bool val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleDisplayMode();
},
activeColor: Palette.orange,
),
SwitchListTile(
title: const Text('Show Metadata'),
subtitle: const Text(
'show number of comments and post date'
' in story tile.',
),
value: preferenceState.showMetadata,
onChanged: (bool val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleMetadataMode();
},
activeColor: Palette.orange,
),
SwitchListTile(
title: const Text('Show Web Page First'),
subtitle: const Text(
'show web page first after tapping'
' on story.',
),
value: preferenceState.showWebFirst,
onChanged: (bool val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleNavigationMode();
},
activeColor: Palette.orange,
),
if (Platform.isIOS)
for (final Preference<dynamic> preference
in preferenceState.preferences
.whereType<BooleanPreference>()
.where(
(Preference<dynamic> e) =>
e.isDisplayable,
))
SwitchListTile(
title: const Text('Use Reader'),
subtitle: const Text(
'enter reader mode in Safari directly'
' when it is available.',
title: Text(preference.title),
subtitle: preference.subtitle.isNotEmpty
? Text(preference.subtitle)
: null,
value: preferenceState.isOn(
preference as BooleanPreference,
),
value: preferenceState.useReader,
onChanged: (bool val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleReaderMode();
.update(preference, to: val);
if (preference
is MarkReadStoriesModePreference &&
val == false) {
context
.read<StoriesBloc>()
.add(ClearAllReadStories());
}
},
activeColor: Palette.orange,
),
SwitchListTile(
title: const Text('Mark Read Stories'),
subtitle: const Text(
'grey out stories you have read.',
),
value: preferenceState.markReadStories,
onChanged: (bool val) {
HapticFeedback.lightImpact();
if (!val) {
context
.read<StoriesBloc>()
.add(ClearAllReadStories());
}
context
.read<PreferenceCubit>()
.toggleMarkReadStoriesMode();
},
activeColor: Palette.orange,
),
SwitchListTile(
title: const Text('Eye Candy'),
subtitle: const Text('some sort of magic.'),
value: preferenceState.showEyeCandy,
onChanged: (bool val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleEyeCandyMode();
},
activeColor: Palette.orange,
),
SwitchListTile(
title: const Text('True Dark Mode'),
subtitle: const Text(
'you might need to restart the app.',
),
value: preferenceState.useTrueDark,
onChanged: (bool val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleTrueDarkMode();
},
activeColor: Palette.orange,
),
ListTile(
title: const Text(
'Theme',
@ -526,7 +426,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog(
context: context,
applicationName: 'Hacki',
applicationVersion: 'v0.2.30',
applicationVersion: 'v0.2.33',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(

View File

@ -71,8 +71,7 @@ class _SearchScreenState extends State<SearchScreen> {
width: 8,
),
DateTimeRangeFilterChip(
filter:
state.searchFilters.get<DateTimeRangeFilter>(),
filter: state.params.get<DateTimeRangeFilter>(),
onDateTimeRangeUpdated:
(DateTime start, DateTime end) =>
context.read<SearchCubit>().addFilter(
@ -91,7 +90,7 @@ class _SearchScreenState extends State<SearchScreen> {
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onSortToggled(),
selected: state.searchFilters.sorted,
selected: state.params.sorted,
label: '''newest first''',
),
const SizedBox(

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hacki/models/search_filters.dart';
import 'package:hacki/models/search_params.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:intl/intl.dart';

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hacki/models/search_filters.dart';
import 'package:hacki/models/search_params.dart';
import 'package:hacki/screens/widgets/widgets.dart';
class PostedByFilterChip extends StatelessWidget {

View File

@ -232,22 +232,22 @@ class CommentTile extends StatelessWidget {
(comment as BuildableComment)
.elements,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
TextDimens.pt15,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
TextDimens.pt15,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
decoration:
TextDecoration.underline,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.contains(
'news.ycombinator.com/item',
)) {
if (link.url.isStoryLink) {
onStoryLinkTapped
.call(link.url);
} else {
@ -255,6 +255,7 @@ class CommentTile extends StatelessWidget {
}
},
),
onTap: () => onTextTapped(context),
)
: SelectableLinkify(
key: ValueKey<int>(comment.id),
@ -262,24 +263,23 @@ class CommentTile extends StatelessWidget {
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
TextDimens.pt15,
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
TextDimens.pt15,
prefState.fontSize.fontSize,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.contains(
'news.ycombinator.com/item',
)) {
if (link.url.isStoryLink) {
onStoryLinkTapped
.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
onTap: () => onTextTapped(context),
),
),
if (!state.collapsed &&
@ -309,7 +309,9 @@ class CommentTile extends StatelessWidget {
HapticFeedback.selectionClick();
context
.read<CommentsCubit>()
.loadMore(comment: comment);
.loadMore(
comment: comment,
);
},
child: Text(
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
@ -357,6 +359,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,
),
@ -415,4 +418,11 @@ class CommentTile extends StatelessWidget {
_colors[initialLevel] = color;
return color;
}
void onTextTapped(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapse) {
HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse();
}
}
}

View File

@ -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,
),

View File

@ -1,11 +1,16 @@
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) {
locator.get<Logger>().v('$bloc created');
if (bloc is! CollapseCubit) {
locator.get<Logger>().v('$bloc created');
}
super.onCreate(bloc);
}
@ -14,7 +19,10 @@ class CustomBlocObserver extends BlocObserver {
Bloc<dynamic, dynamic> bloc,
Object? event,
) {
locator.get<Logger>().v(event);
if (event is! StoriesEvent) {
locator.get<Logger>().v(event);
}
super.onEvent(bloc, event);
}
@ -23,7 +31,10 @@ class CustomBlocObserver extends BlocObserver {
Bloc<dynamic, dynamic> bloc,
Transition<dynamic, dynamic> transition,
) {
locator.get<Logger>().v(transition);
if (bloc is! StoriesBloc) {
locator.get<Logger>().v(transition);
}
super.onTransition(bloc, transition);
}
@ -34,6 +45,8 @@ class CustomBlocObserver extends BlocObserver {
StackTrace stackTrace,
) {
locator.get<Logger>().e(error);
locator.get<Logger>().e(stackTrace);
super.onError(bloc, error, stackTrace);
}
}

View File

@ -7,28 +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.1.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.1.11"
version: "3.3.0"
args:
dependency: transitive
description:
@ -42,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:
@ -56,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:
@ -77,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:
@ -119,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:
@ -133,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:
@ -161,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:
@ -182,14 +175,14 @@ packages:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.5.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.0.2"
csslib:
dependency: transitive
description:
@ -203,7 +196,7 @@ packages:
name: dbus
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.6"
version: "0.7.8"
diff_match_patch:
dependency: transitive
description:
@ -224,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:
@ -273,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:
@ -313,7 +306,7 @@ packages:
name: flutter_inappwebview
url: "https://pub.dartlang.org"
source: hosted
version: "5.4.3+7"
version: "5.7.2+3"
flutter_linkify:
dependency: "direct main"
description:
@ -327,14 +320,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:
@ -348,21 +341,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:
@ -397,7 +390,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
@ -454,7 +447,7 @@ packages:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.2"
version: "2.2.3"
html:
dependency: "direct main"
description:
@ -475,7 +468,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:
@ -496,7 +489,7 @@ packages:
name: hydrated_bloc
url: "https://pub.dartlang.org"
source: hosted
version: "8.1.0"
version: "9.0.0-dev.3"
integration_test:
dependency: "direct dev"
description: flutter
@ -550,21 +543,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:
@ -620,7 +613,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:
@ -634,14 +627,14 @@ packages:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.16"
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:
@ -669,7 +662,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:
@ -755,28 +748,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.10"
version: "4.1.0"
share_plus_linux:
dependency: transitive
description:
@ -825,7 +818,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:
@ -853,7 +846,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:
@ -874,7 +867,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:
@ -928,14 +921,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:
@ -963,14 +956,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.0"
version: "0.3.1"
synced_shared_preferences:
dependency: "direct main"
description:
@ -984,35 +977,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:
@ -1033,7 +1026,7 @@ packages:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
universal_platform:
dependency: "direct main"
description:
@ -1047,14 +1040,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:
@ -1089,7 +1082,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:
@ -1124,14 +1117,14 @@ packages:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "8.2.2"
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:
@ -1159,7 +1152,7 @@ packages:
name: wakelock_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.1"
watcher:
dependency: transitive
description:
@ -1187,7 +1180,7 @@ packages:
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:
@ -1201,28 +1194,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:
@ -1236,7 +1229,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:
@ -1252,5 +1245,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.17.0 <3.0.0"
flutter: ">=3.0.5"
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.3.10"

View File

@ -1,22 +1,22 @@
name: hacki
description: A Hacker News reader.
version: 0.2.30+73
version: 0.2.33+76
publish_to: none
environment:
sdk: ">=2.17.0 <3.0.0"
flutter: "3.0.5"
flutter: "3.3.10"
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:
@ -25,16 +25,16 @@ dependencies:
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.7.2+3
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
@ -42,7 +42,7 @@ dependencies:
html: ^0.15.0
html_unescape: ^2.0.0
http: ^0.13.3
hydrated_bloc: ^8.1.0
hydrated_bloc: ^9.0.0-dev.3
intl: ^0.17.0
logger: ^1.1.0
path: ^1.8.0
@ -73,7 +73,7 @@ dependencies:
workmanager: ^0.5.0
dev_dependencies:
bloc_test: ^9.0.3
bloc_test: ^9.1.0
flutter_test:
sdk: flutter
integration_test:

View 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,
);
}
},
);
}