Compare commits

..

19 Commits

Author SHA1 Message Date
a1b491cf0d fix regex for getting item id. (#91) 2022-12-27 00:00:10 -08:00
edf0c82040 Improve loading mechanism. (#90)
* load more comments when user folds the last comment.

* improvements.

* improve loading experience.
2022-12-26 22:55:50 -08:00
946a3c5a9a Improve loading mechanism. (#89)
* load more comments when user folds the last comment.

* improvements.
2022-12-26 22:09:31 -08:00
d8bc60c071 Add tests. (#88)
* update fontsize.

* fix title.

* fix info list.

* add small.

* nit.

* nit.

* test.

* add tests.

* update github action.
2022-12-25 20:12:11 -08:00
48477cd5c8 Fix comment tile. (#87)
* fix comment not correctly collapsing.

* fix comment tile overflow.

* bumped version to 1.0.0
2022-12-25 00:58:14 -08:00
38df6293fe update comment tile. (#86) 2022-12-22 19:32:57 -08:00
a5fe9e45fc fix NavigationModePreference 2022-12-21 11:09:59 -08:00
9de5baa77a bumped version. (#85) 2022-12-20 23:34:45 -08:00
2daccd64e8 update fontsize. (#84)
* update fontsize.

* fix title.

* fix info list.

* add small.

* nit.

* nit.
2022-12-20 22:49:33 -08:00
d0c68f9419 update Fastfile. 2022-12-20 22:10:49 -08:00
5f1dbfc510 update Fastfile 2022-12-20 21:58:45 -08:00
90eee37c17 update Fastfile 2022-12-20 21:25:21 -08:00
5630e61a74 update Fastfile 2022-12-20 21:02:34 -08:00
eaad4b01dd fix ci. (#83)
* fix ci.

* update project.

* update github checks.

* update github checks.

* nit.

* nit.

* update fastfile.

* fix info.plist

* nit.

* nit.

* nit.

* nit.

* nit.

* nit.

* nit.

* update publish_ios.yml
2022-12-20 20:37:49 -08:00
3ab172f3d3 update publish_ios.yml 2022-12-19 13:51:57 -08:00
5450eba64b fix RegExp. (#82)
* fix regexp.

* bump version.
2022-12-19 13:51:10 -08:00
e2d6bb44d0 update publish_ios.yml 2022-12-19 13:46:00 -08:00
ffbd3a2449 add flutter as submodule (#80)
* add flutter as submodule

* move flutter to submodules.

* removed unused file.

* nit.
2022-12-18 18:33:46 -08:00
2405a6d30c update publish_ios.yml 2022-12-17 18:48:16 -08:00
33 changed files with 834 additions and 593 deletions

View File

@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- "**" - "**"
- '!master'
jobs: jobs:
releases: releases:
@ -19,4 +20,5 @@ jobs:
channel: 'stable' channel: 'stable'
- run: flutter pub get - run: flutter pub get
- run: flutter format --set-exit-if-changed . - run: flutter format --set-exit-if-changed .
- run: flutter analyze - run: flutter analyze
- run: flutter test

View File

@ -6,7 +6,7 @@ on:
# Run the workflow whenever a new tag named 'v*' is pushed # Run the workflow whenever a new tag named 'v*' is pushed
push: push:
branches: branches:
- "!*" - master
tags: tags:
- "v*" - "v*"
@ -51,4 +51,4 @@ jobs:
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: cd ios && bundle exec fastlane beta "build_name:${{ github.ref_name }}" run: cd ios && bundle exec fastlane beta

3
.gitmodules vendored Normal file
View File

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

View File

@ -6,4 +6,8 @@ linter:
library_private_types_in_public_api: false library_private_types_in_public_api: false
omit_local_variable_types: false omit_local_variable_types: false
one_member_abstracts: false one_member_abstracts: false
always_specify_types: true always_specify_types: true
analyzer:
exclude:
- "submodules/**"

View File

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

View File

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

View File

@ -56,7 +56,7 @@
}; };
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = { E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 8;
dstPath = ""; dstPath = "";
dstSubfolderSpec = 13; dstSubfolderSpec = 13;
files = ( files = (
@ -64,7 +64,7 @@
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */, E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
); );
name = "Embed App Extensions"; name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 1;
}; };
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
@ -569,17 +569,19 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.33; MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -709,17 +711,19 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.33; MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -743,17 +747,19 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.33; MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -778,7 +784,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -821,7 +827,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -861,7 +867,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -903,7 +909,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -948,7 +954,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -990,7 +996,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;

View File

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

View File

@ -31,10 +31,6 @@ platform :ios do
is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example' 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) # Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
sync_code_signing( sync_code_signing(
type: "appstore", type: "appstore",
@ -42,15 +38,6 @@ platform :ios do
readonly: true 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 # We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH
app_store_connect_api_key( app_store_connect_api_key(
key_id: APPSTORECONNECT_KEY_ID, key_id: APPSTORECONNECT_KEY_ID,
@ -59,21 +46,18 @@ platform :ios do
latest_testflight_build_number latest_testflight_build_number
# Figure out the build number (and optionally build name) # Figure out the build number (and optionally build name)
new_build_number = ( + 1) 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`) # Prep the xcodeproject from Flutter without building (`--config-only`)
sh( sh(
"flutter", "build", "ios", "--config-only", "flutter", "build", "ios", "--config-only",
"--release", "--no-pub", "--no-codesign", "--release", "--no-pub", "--no-codesign",
"--build-number", new_build_number.to_s, "--build-number", new_build_number.to_s
*extra_config_args
) )
version = get_version_number(xcodeproj: "Runner.xcodeproj", target: 'Runner')
increment_version_number( increment_version_number(
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc') version_number: version
) )
increment_build_number({ increment_build_number({
@ -93,4 +77,4 @@ latest_testflight_build_number
skip_waiting_for_build_processing: true, skip_waiting_for_build_processing: true,
) )
end end
end end

View File

@ -20,7 +20,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
super(AuthState.init()) { super(const AuthState.init()) {
on<AuthInitialize>(onInitialize); on<AuthInitialize>(onInitialize);
on<AuthLogin>(onLogin); on<AuthLogin>(onLogin);
on<AuthLogout>(onLogout); on<AuthLogout>(onLogout);
@ -101,7 +101,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async { Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async {
emit( emit(
state.copyWith( state.copyWith(
user: User.empty(), user: const User.empty(),
isLoggedIn: false, isLoggedIn: false,
agreedToEULA: false, agreedToEULA: false,
), ),

View File

@ -14,8 +14,8 @@ class AuthState extends Equatable {
required this.agreedToEULA, required this.agreedToEULA,
}); });
AuthState.init() const AuthState.init()
: user = User.empty(), : user = const User.empty(),
isLoggedIn = false, isLoggedIn = false,
status = AuthStatus.loaded, status = AuthStatus.loaded,
agreedToEULA = false; agreedToEULA = false;

View File

@ -48,3 +48,8 @@ abstract class Constants {
'(ㆆ_ㆆ)', '(ㆆ_ㆆ)',
]; ];
} }
abstract class RegExpConstants {
static const String linkSuffix = r'(\)|])(.)*$';
static const String number = '[0-9]+';
}

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
part 'collapse_state.dart'; part 'collapse_state.dart';
@ -10,13 +11,16 @@ part 'collapse_state.dart';
class CollapseCubit extends Cubit<CollapseState> { class CollapseCubit extends Cubit<CollapseState> {
CollapseCubit({ CollapseCubit({
required int commentId, required int commentId,
required CommentsCubit commentsCubit,
CollapseCache? collapseCache, CollapseCache? collapseCache,
}) : _commentId = commentId, }) : _commentId = commentId,
_collapseCache = collapseCache ?? locator.get<CollapseCache>(), _collapseCache = collapseCache ?? locator.get<CollapseCache>(),
_commentsCubit = commentsCubit,
super(const CollapseState.init()); super(const CollapseState.init());
final int _commentId; final int _commentId;
final CollapseCache _collapseCache; final CollapseCache _collapseCache;
final CommentsCubit _commentsCubit;
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription; late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
void init() { void init() {
@ -43,12 +47,19 @@ class CollapseCubit extends Cubit<CollapseState> {
), ),
); );
} else { } else {
final int count = _collapseCache.collapse(_commentId); final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
final int lastCommentId = _commentsCubit.state.comments.last.id;
final bool shouldLoadMore = _commentId == lastCommentId ||
collapsedCommentIds.contains(lastCommentId);
if (shouldLoadMore) {
_commentsCubit.loadMore();
}
emit( emit(
state.copyWith( state.copyWith(
collapsed: true, collapsed: true,
collapsedCount: state.collapsed ? 0 : count, collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
), ),
); );
} }

View File

@ -213,6 +213,8 @@ class CommentsCubit extends Cubit<CommentsState> {
/// [comment] is only used for lazy fetching. /// [comment] is only used for lazy fetching.
void loadMore({Comment? comment}) { void loadMore({Comment? comment}) {
if (comment == null && state.status == CommentsStatus.loading) return;
switch (state.fetchMode) { switch (state.fetchMode) {
case FetchMode.lazy: case FetchMode.lazy:
if (comment == null) return; if (comment == null) return;

View File

@ -10,7 +10,7 @@ class UserCubit extends Cubit<UserState> {
UserCubit({StoriesRepository? storiesRepository}) UserCubit({StoriesRepository? storiesRepository})
: _storiesRepository = : _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
super(UserState.init()); super(const UserState.init());
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;

View File

@ -13,8 +13,8 @@ class UserState extends Equatable {
required this.status, required this.status,
}); });
UserState.init() const UserState.init()
: user = User.empty(), : user = const User.empty(),
status = UserStatus.initial; status = UserStatus.initial;
final User user; final User user;

View File

@ -1,7 +1,9 @@
import 'package:hacki/config/constants.dart';
extension StringExtension on String { extension StringExtension on String {
int? get itemId { int? get itemId {
final RegExp regex = RegExp(r'\d+$'); final RegExp regex = RegExp(RegExpConstants.number);
final RegExp exception = RegExp(r'\)|].*$'); final RegExp exception = RegExp(RegExpConstants.linkSuffix);
final String match = regex.stringMatch(replaceAll(exception, '')) ?? ''; final String match = regex.stringMatch(replaceAll(exception, '')) ?? '';
return int.tryParse(match); return int.tryParse(match);
} }

View File

@ -1,8 +1,9 @@
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
enum FontSize { enum FontSize {
regular('Regular', TextDimens.pt15), small('Small', TextDimens.pt15),
large('Large', TextDimens.pt16), regular('Regular', TextDimens.pt16),
large('Large', TextDimens.pt17),
xlarge('XLarge', TextDimens.pt18); xlarge('XLarge', TextDimens.pt18);
const FontSize(this.description, this.fontSize); const FontSize(this.description, this.fontSize);

View File

@ -155,7 +155,7 @@ class NavigationModePreference extends BooleanPreference {
String get title => 'Show Web Page First'; String get title => 'Show Web Page First';
@override @override
String get subtitle => ''''show web page first after tapping on story.'''; String get subtitle => '''show web page first after tapping on story.''';
} }
class ReaderModePreference extends BooleanPreference { class ReaderModePreference extends BooleanPreference {

View File

@ -1,7 +1,8 @@
import 'package:equatable/equatable.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class User { class User extends Equatable {
User({ const User({
required this.about, required this.about,
required this.created, required this.created,
required this.delay, required this.delay,
@ -9,7 +10,7 @@ class User {
required this.karma, required this.karma,
}); });
User.empty() const User.empty()
: about = '', : about = '',
created = 0, created = 0,
delay = 0, delay = 0,
@ -39,4 +40,13 @@ class User {
String toString() { String toString() {
return 'User $about, $created, $delay, $id, $karma'; return 'User $about, $created, $delay, $id, $karma';
} }
@override
List<Object?> get props => <Object?>[
about,
created,
delay,
id,
karma,
];
} }

View File

@ -171,87 +171,96 @@ class MainView extends StatelessWidget {
], ],
), ),
), ),
if (state.item is Story) BlocBuilder<PreferenceCubit, PreferenceState>(
InkWell( buildWhen: (
onTap: () => LinkUtil.launch( PreferenceState previous,
state.item.url, PreferenceState current,
useReader: ) =>
context.read<PreferenceCubit>().state.useReader, previous.fontSize != current.fontSize,
offlineReading: context builder: (
.read<StoriesBloc>() BuildContext context,
.state PreferenceState prefState,
.offlineReading, ) {
), return Column(
child: Padding( children: <Widget>[
padding: const EdgeInsets.only( if (state.item is Story)
left: Dimens.pt6, InkWell(
right: Dimens.pt6, onTap: () => LinkUtil.launch(
bottom: Dimens.pt12, state.item.url,
top: Dimens.pt12, useReader: context
), .read<PreferenceCubit>()
child: Text( .state
state.item.title, .useReader,
textAlign: TextAlign.center, offlineReading: context
style: TextStyle( .read<StoriesBloc>()
fontWeight: FontWeight.bold, .state
color: state.item.url.isNotEmpty .offlineReading,
? Palette.orange ),
: null, child: Padding(
), padding: const EdgeInsets.only(
), left: Dimens.pt6,
), right: Dimens.pt6,
) bottom: Dimens.pt12,
else top: Dimens.pt12,
const SizedBox( ),
height: Dimens.pt6, child: Text(
), state.item.title,
if (state.item.text.isNotEmpty) textAlign: TextAlign.center,
BlocBuilder<PreferenceCubit, PreferenceState>( style: TextStyle(
buildWhen: ( fontWeight: FontWeight.bold,
PreferenceState previous, fontSize: MediaQuery.of(context)
PreferenceState current, .textScaleFactor *
) => prefState.fontSize.fontSize,
previous.fontSize != current.fontSize, color: state.item.url.isNotEmpty
builder: ( ? Palette.orange
BuildContext context, : null,
PreferenceState prefState, ),
) { ),
return Padding( ),
padding: const EdgeInsets.symmetric( )
horizontal: Dimens.pt10, else
), const SizedBox(
child: SelectableLinkify( height: Dimens.pt6,
text: state.item.text, ),
style: TextStyle( if (state.item.text.isNotEmpty)
fontSize: Padding(
MediaQuery.of(context).textScaleFactor * padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt10,
),
child: SelectableLinkify(
text: state.item.text,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
context context
.read<PreferenceCubit>() .read<PreferenceCubit>()
.state .state
.fontSize .fontSize
.fontSize, .fontSize,
), ),
linkStyle: TextStyle( linkStyle: TextStyle(
fontSize: fontSize: MediaQuery.of(context)
MediaQuery.of(context).textScaleFactor * .textScaleFactor *
context context
.read<PreferenceCubit>() .read<PreferenceCubit>()
.state .state
.fontSize .fontSize
.fontSize, .fontSize,
color: Palette.orange, color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
), ),
onOpen: (LinkableElement link) { ],
if (link.url.isStoryLink) { );
onStoryLinkTapped(link.url); },
} else { ),
LinkUtil.launch(link.url);
}
},
),
);
},
),
if (state.item.isPoll) if (state.item.isPoll)
BlocProvider<PollCubit>( BlocProvider<PollCubit>(
create: (BuildContext context) => create: (BuildContext context) =>

View File

@ -426,7 +426,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Hacki', applicationName: 'Hacki',
applicationVersion: 'v0.2.33', applicationVersion: 'v1.0.0',
applicationIcon: ClipRRect( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular( Radius.circular(

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
typedef BlocBuilderCondition<S> = bool Function(S previous, S current);
typedef BlocWidgetBuilder3<StateA, StateB, StateC> = Widget Function(
BuildContext,
StateA,
StateB,
StateC,
);
class BlocBuilder3<
BlocA extends StateStreamable<BlocAState>,
BlocAState,
BlocB extends StateStreamable<BlocBState>,
BlocBState,
BlocC extends StateStreamable<BlocCState>,
BlocCState> extends StatelessWidget {
const BlocBuilder3({
Key? key,
required this.builder,
this.blocA,
this.blocB,
this.blocC,
this.buildWhenA,
this.buildWhenB,
this.buildWhenC,
}) : super(key: key);
final BlocWidgetBuilder3<BlocAState, BlocBState, BlocCState> builder;
final BlocA? blocA;
final BlocB? blocB;
final BlocC? blocC;
final BlocBuilderCondition<BlocAState>? buildWhenA;
final BlocBuilderCondition<BlocBState>? buildWhenB;
final BlocBuilderCondition<BlocCState>? buildWhenC;
@override
Widget build(BuildContext context) {
return BlocBuilder<BlocA, BlocAState>(
bloc: blocA,
buildWhen: buildWhenA,
builder: (BuildContext context, BlocAState blocAState) {
return BlocBuilder<BlocB, BlocBState>(
bloc: blocB,
buildWhen: buildWhenB,
builder: (BuildContext context, BlocBState blocBState) {
return BlocBuilder<BlocC, BlocCState>(
bloc: blocC,
buildWhen: buildWhenC,
builder: (BuildContext context, BlocCState blocCState) {
return builder(context, blocAState, blocBState, blocCState);
},
);
},
);
},
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/bloc_builder_3.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
@ -46,344 +47,329 @@ class CommentTile extends StatelessWidget {
lazy: false, lazy: false,
create: (_) => CollapseCubit( create: (_) => CollapseCubit(
commentId: comment.id, commentId: comment.id,
commentsCubit: context.read<CommentsCubit>(),
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(), collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
)..init(), )..init(),
child: BlocBuilder<CollapseCubit, CollapseState>( child: BlocBuilder3<CollapseCubit, CollapseState, PreferenceCubit,
builder: (BuildContext context, CollapseState state) { PreferenceState, BlocklistCubit, BlocklistState>(
builder: (
BuildContext context,
CollapseState state,
PreferenceState prefState,
BlocklistState blocklistState,
) {
if (actionable && state.hidden) return const SizedBox.shrink(); if (actionable && state.hidden) return const SizedBox.shrink();
return BlocBuilder<PreferenceCubit, PreferenceState>( const Color orange = Color.fromRGBO(255, 152, 0, 1);
builder: (BuildContext context, PreferenceState prefState) { final Color color = _getColor(level);
return BlocBuilder<BlocklistCubit, BlocklistState>(
builder: (BuildContext context, BlocklistState blocklistState) {
const Color orange = Color.fromRGBO(255, 152, 0, 1);
final Color color = _getColor(level);
final Padding child = Padding( final Padding child = Padding(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Slidable(
startActionPane: actionable
? ActionPane(
motion: const StretchMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) => onReplyTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.message,
),
if (context.read<AuthBloc>().state.user.id ==
comment.by)
SlidableAction(
onPressed: (_) => onEditTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.edit,
),
SlidableAction(
onPressed: (BuildContext context) =>
onMoreTapped?.call(
comment,
context.rect,
),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.more_horiz,
),
],
)
: null,
endActionPane: actionable
? ActionPane(
motion: const StretchMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) =>
onRightMoreTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.av_timer,
),
],
)
: null,
child: InkWell(
onTap: () {
if (actionable) {
HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse();
}
},
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Slidable( Padding(
startActionPane: actionable padding: const EdgeInsets.only(
? ActionPane( left: Dimens.pt6,
motion: const StretchMotion(), right: Dimens.pt6,
children: <Widget>[ top: Dimens.pt6,
SlidableAction( ),
onPressed: (_) => child: Row(
onReplyTapped?.call(comment), children: <Widget>[
backgroundColor: Palette.orange, Text(
foregroundColor: Palette.white, comment.by,
icon: Icons.message, style: TextStyle(
), color:
if (context prefState.showEyeCandy ? orange : color,
.read<AuthBloc>() ),
.state ),
.user if (comment.by == opUsername)
.id == const Text(
comment.by) ' - OP',
SlidableAction( style: TextStyle(
onPressed: (_) => color: orange,
onEditTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.edit,
),
SlidableAction(
onPressed: (BuildContext context) =>
onMoreTapped?.call(
comment,
context.rect,
),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.more_horiz,
),
],
)
: null,
endActionPane: actionable
? ActionPane(
motion: const StretchMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) =>
onRightMoreTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.av_timer,
),
],
)
: null,
child: InkWell(
onTap: () {
if (actionable) {
HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse();
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt6,
right: Dimens.pt6,
top: Dimens.pt6,
),
child: Row(
children: <Widget>[
Text(
comment.by,
style: TextStyle(
color: prefState.showEyeCandy
? orange
: color,
),
),
if (comment.by == opUsername)
const Text(
' - OP',
style: TextStyle(
color: orange,
),
),
const Spacer(),
Text(
comment.postedDate,
style: const TextStyle(
color: Palette.grey,
),
),
],
), ),
), ),
if (actionable && state.collapsed) const Spacer(),
Center( Text(
child: Padding( comment.postedDate,
padding: const EdgeInsets.only( style: const TextStyle(
bottom: Dimens.pt12, color: Palette.grey,
),
child: Text(
'collapsed '
'(${state.collapsedCount + 1})',
style: const TextStyle(
color: Palette.orangeAccent,
),
),
),
)
else if (comment.deleted)
const Center(
child: Padding(
padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'deleted',
style: TextStyle(
color: Palette.grey,
),
),
),
)
else if (comment.dead)
const Center(
child: Padding(
padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'dead',
style: TextStyle(
color: Palette.grey,
),
),
),
)
else if (blocklistState.blocklist
.contains(comment.by))
const Center(
child: Padding(
padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'blocked',
style: TextStyle(
color: Palette.grey,
),
),
),
)
else
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt8,
right: Dimens.pt8,
top: Dimens.pt6,
bottom: Dimens.pt12,
),
child: comment is BuildableComment
? SelectableText.rich(
key: ValueKey<int>(comment.id),
buildTextSpan(
(comment as BuildableComment)
.elements,
style: TextStyle(
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
decoration:
TextDecoration.underline,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped
.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
onTap: () => onTextTapped(context),
)
: SelectableLinkify(
key: ValueKey<int>(comment.id),
text: comment.text,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
prefState.fontSize.fontSize,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped
.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
onTap: () => onTextTapped(context),
),
),
if (!state.collapsed &&
fetchMode == FetchMode.lazy &&
comment.kids.isNotEmpty &&
!context
.read<CommentsCubit>()
.state
.commentIds
.contains(comment.kids.first) &&
!context
.read<CommentsCubit>()
.state
.onlyShowTargetComment)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextButton(
onPressed: () {
HapticFeedback.selectionClick();
context
.read<CommentsCubit>()
.loadMore(
comment: comment,
);
},
child: Text(
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
style: const TextStyle(
fontSize: TextDimens.pt12,
),
),
),
),
],
),
),
),
const Divider(
height: Dimens.zero,
), ),
], ),
],
),
),
if (actionable && state.collapsed)
Center(
child: Padding(
padding: const EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'collapsed '
'(${state.collapsedCount + 1})',
style: const TextStyle(
color: Palette.orangeAccent,
),
),
),
)
else if (comment.deleted)
const Center(
child: Padding(
padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'deleted',
style: TextStyle(
color: Palette.grey,
),
),
),
)
else if (comment.dead)
const Center(
child: Padding(
padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'dead',
style: TextStyle(
color: Palette.grey,
),
),
),
)
else if (blocklistState.blocklist.contains(comment.by))
const Center(
child: Padding(
padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'blocked',
style: TextStyle(
color: Palette.grey,
),
),
),
)
else
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt8,
right: Dimens.pt8,
top: Dimens.pt6,
bottom: Dimens.pt12,
),
child: comment is BuildableComment
? SelectableText.rich(
key: ValueKey<int>(comment.id),
buildTextSpan(
(comment as BuildableComment).elements,
style: TextStyle(
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
decoration: TextDecoration.underline,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
onTap: () => onTextTapped(context),
)
: SelectableLinkify(
key: ValueKey<int>(comment.id),
text: comment.text,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
prefState.fontSize.fontSize,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
prefState.fontSize.fontSize,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
onTap: () => onTextTapped(context),
),
),
if (!state.collapsed &&
fetchMode == FetchMode.lazy &&
comment.kids.isNotEmpty &&
!context
.read<CommentsCubit>()
.state
.commentIds
.contains(comment.kids.first) &&
!context
.read<CommentsCubit>()
.state
.onlyShowTargetComment)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextButton(
onPressed: () {
HapticFeedback.selectionClick();
context.read<CommentsCubit>().loadMore(
comment: comment,
);
},
child: Text(
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
style: const TextStyle(
fontSize: TextDimens.pt12,
),
),
),
),
],
),
), ),
), ),
const Divider(
height: Dimens.zero,
), ),
], ],
), ),
); ),
),
final double commentBackgroundColorOpacity = ],
Theme.of(context).brightness == Brightness.dark ),
? 0.03
: 0.15;
final Color commentColor = prefState.showEyeCandy
? color.withOpacity(commentBackgroundColorOpacity)
: Palette.transparent;
final bool isMyComment = myUsername == comment.by;
Widget wrapper = child;
if (isMyComment && level == 0) {
return Container(
color: Palette.orange.withOpacity(0.2),
child: wrapper,
);
}
for (final int i in level.to(0, inclusive: false)) {
final Color wrapperBorderColor = _getColor(i);
final bool shouldHighlight = isMyComment && i == level;
wrapper = Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: Dimens.pt12,
),
decoration: BoxDecoration(
border: i != 0
? Border(
left: BorderSide(
color: wrapperBorderColor,
),
)
: null,
color: shouldHighlight
? Palette.orange.withOpacity(0.2)
: commentColor,
),
child: wrapper,
);
}
return wrapper;
},
);
},
); );
final double commentBackgroundColorOpacity =
Theme.of(context).brightness == Brightness.dark ? 0.03 : 0.15;
final Color commentColor = prefState.showEyeCandy
? color.withOpacity(commentBackgroundColorOpacity)
: Palette.transparent;
final bool isMyComment = myUsername == comment.by;
Widget wrapper = child;
if (isMyComment && level == 0) {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Palette.orange.withOpacity(0.2),
),
child: wrapper,
);
}
for (final int i in level.to(0, inclusive: false)) {
final Color wrapperBorderColor = _getColor(i);
final bool shouldHighlight = isMyComment && i == level;
wrapper = Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: Dimens.pt8,
),
decoration: BoxDecoration(
border: i != 0
? Border(
left: BorderSide(
color: wrapperBorderColor,
),
)
: null,
color: shouldHighlight
? Palette.orange.withOpacity(0.2)
: commentColor,
),
child: wrapper,
);
}
return wrapper;
}, },
), ),
); );

View File

@ -42,90 +42,8 @@ class StoryTile extends StatelessWidget {
story: story, story: story,
link: story.url, link: story.url,
offlineReading: context.read<StoriesBloc>().state.offlineReading, offlineReading: context.read<StoriesBloc>().state.offlineReading,
placeholderWidget: FadeIn( placeholderWidget: _LinkPreviewPlaceholder(
child: SizedBox( height: height,
height: height,
child: Shimmer.fromColors(
baseColor: Palette.orange,
highlightColor: Palette.orangeAccent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
right: Dimens.pt5,
bottom: Dimens.pt5,
top: Dimens.pt5,
),
child: Container(
height: height,
width: height,
color: Palette.white,
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.only(
left: Dimens.pt4,
top: Dimens.pt6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: double.infinity,
height: Dimens.pt14,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt4,
),
),
Container(
width: double.infinity,
height: Dimens.pt10,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt3,
),
),
Container(
width: double.infinity,
height: Dimens.pt10,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt3,
),
),
Container(
width: double.infinity,
height: Dimens.pt10,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt3,
),
),
Container(
width: Dimens.pt40,
height: Dimens.pt10,
color: Palette.white,
),
],
),
),
)
],
),
),
),
), ),
errorImage: Constants.hackerNewsLogoLink, errorImage: Constants.hackerNewsLogoLink,
backgroundColor: Palette.transparent, backgroundColor: Palette.transparent,
@ -136,7 +54,7 @@ class StoryTile extends StatelessWidget {
titleStyle: TextStyle( titleStyle: TextStyle(
color: hasRead color: hasRead
? Palette.grey[500] ? Palette.grey[500]
: Theme.of(context).textTheme.subtitle1!.color, : Theme.of(context).textTheme.subtitle1?.color,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
showMetadata: showMetadata, showMetadata: showMetadata,
@ -193,3 +111,101 @@ class StoryTile extends StatelessWidget {
} }
} }
} }
class _LinkPreviewPlaceholder extends StatelessWidget {
const _LinkPreviewPlaceholder({
Key? key,
required this.height,
}) : super(key: key);
final double height;
@override
Widget build(BuildContext context) {
return FadeIn(
child: SizedBox(
height: height,
child: Shimmer.fromColors(
baseColor: Palette.orange,
highlightColor: Palette.orangeAccent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
right: Dimens.pt5,
bottom: Dimens.pt5,
top: Dimens.pt5,
),
child: Container(
height: height,
width: height,
color: Palette.white,
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.only(
left: Dimens.pt4,
top: Dimens.pt6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: double.infinity,
height: Dimens.pt14,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt4,
),
),
Container(
width: double.infinity,
height: Dimens.pt10,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt3,
),
),
Container(
width: double.infinity,
height: Dimens.pt10,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt3,
),
),
Container(
width: double.infinity,
height: Dimens.pt10,
color: Palette.white,
),
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt3,
),
),
Container(
width: Dimens.pt40,
height: Dimens.pt10,
color: Palette.white,
),
],
),
),
)
],
),
),
),
);
}
}

View File

@ -1,3 +1,4 @@
export 'bloc_builder_3.dart';
export 'circle_tab_indicator.dart'; export 'circle_tab_indicator.dart';
export 'comment_tile.dart'; export 'comment_tile.dart';
export 'countdown_reminder.dart'; export 'countdown_reminder.dart';

View File

@ -15,7 +15,7 @@ class CollapseCache {
addIfParentIsHiddenOrCollapsed(commentId, to); addIfParentIsHiddenOrCollapsed(commentId, to);
} }
int collapse(int commentId) { Set<int> collapse(int commentId) {
_collapsed.add(commentId); _collapsed.add(commentId);
Set<int> findHiddenComments(int commentId) { Set<int> findHiddenComments(int commentId) {
@ -35,7 +35,7 @@ class CollapseCache {
_hiddenCommentsSubject.add(_hidden); _hiddenCommentsSubject.add(_hidden);
return hiddenComments.length; return hiddenComments;
} }
void uncollapse(int commentId) { void uncollapse(int commentId) {

View File

@ -31,6 +31,7 @@ abstract class TextDimens {
static const double pt14 = 14; static const double pt14 = 14;
static const double pt15 = 15; static const double pt15 = 15;
static const double pt16 = 16; static const double pt16 = 16;
static const double pt17 = 17;
static const double pt18 = 18; static const double pt18 = 18;
static const double pt20 = 20; static const double pt20 = 20;
static const double pt24 = 24; static const double pt24 = 24;

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
@ -35,7 +36,7 @@ abstract class LinkUtil {
} }
Uri rinseLink(String link) { Uri rinseLink(String link) {
final RegExp regex = RegExp(r'\)|].*$'); final RegExp regex = RegExp(RegExpConstants.linkSuffix);
if (!link.contains('en.wikipedia.org') && link.contains(regex)) { if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
final String match = regex.stringMatch(link) ?? ''; final String match = regex.stringMatch(link) ?? '';
return Uri.parse(link.replaceAll(match, '')); return Uri.parse(link.replaceAll(match, ''));

View File

@ -1,6 +1,6 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 0.2.33+76 version: 1.0.0+77
publish_to: none publish_to: none
environment: environment:

1
submodules/flutter Submodule

Submodule submodules/flutter added at 135454af32

View File

@ -0,0 +1,158 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:mocktail/mocktail.dart';
class MockAuthRepository extends Mock implements AuthRepository {}
class MockPreferenceRepository extends Mock implements PreferenceRepository {}
class MockStoriesRepository extends Mock implements StoriesRepository {}
class MockSembastRepository extends Mock implements SembastRepository {}
void main() {
final MockAuthRepository mockAuthRepository = MockAuthRepository();
final MockPreferenceRepository mockPreferenceRepository =
MockPreferenceRepository();
final MockStoriesRepository mockStoriesRepository = MockStoriesRepository();
final MockSembastRepository mockSembastRepository = MockSembastRepository();
const int created = 0, delay = 1, karma = 2;
const String about = 'about', id = 'id';
const User tUser = User(
about: about,
created: created,
delay: delay,
id: id,
karma: karma,
);
group(
'AuthBloc',
() {
setUp(() {
when(() => mockAuthRepository.loggedIn)
.thenAnswer((_) => Future<bool>.value(false));
});
test(
'initial state is AuthState.init',
() {
expect(
AuthBloc(
authRepository: mockAuthRepository,
preferenceRepository: mockPreferenceRepository,
storiesRepository: mockStoriesRepository,
sembastRepository: mockSembastRepository,
).state,
equals(const AuthState.init()),
);
},
);
},
);
group('AuthAppStarted', () {
const String username = 'username', password = 'password';
setUp(() {
when(() => mockAuthRepository.username)
.thenAnswer((_) => Future<String?>.value(username));
when(() => mockAuthRepository.password)
.thenAnswer((_) => Future<String>.value(password));
when(() => mockStoriesRepository.fetchUserBy(userId: username))
.thenAnswer((_) => Future<User>.value(tUser));
when(() => mockAuthRepository.loggedIn)
.thenAnswer((_) => Future<bool>.value(false));
});
blocTest<AuthBloc, AuthState>(
'initialize',
build: () {
return AuthBloc(
authRepository: mockAuthRepository,
preferenceRepository: mockPreferenceRepository,
storiesRepository: mockStoriesRepository,
sembastRepository: mockSembastRepository,
);
},
expect: () => <AuthState>[
const AuthState.init().copyWith(
status: AuthStatus.loaded,
),
],
verify: (_) {
verify(() => mockAuthRepository.loggedIn).called(2);
verifyNever(() => mockAuthRepository.username);
verifyNever(() => mockStoriesRepository.fetchUserBy(userId: username));
},
);
blocTest<AuthBloc, AuthState>(
'sign in',
build: () {
when(
() => mockAuthRepository.login(
username: username,
password: password,
),
).thenAnswer((_) => Future<bool>.value(true));
return AuthBloc(
authRepository: mockAuthRepository,
preferenceRepository: mockPreferenceRepository,
storiesRepository: mockStoriesRepository,
sembastRepository: mockSembastRepository,
);
},
act: (AuthBloc bloc) => bloc
..add(
AuthToggleAgreeToEULA(),
)
..add(
AuthLogin(
username: username,
password: password,
),
),
expect: () => <AuthState>[
const AuthState(
user: User.empty(),
isLoggedIn: false,
status: AuthStatus.loaded,
agreedToEULA: false,
),
const AuthState(
user: User.empty(),
isLoggedIn: false,
status: AuthStatus.loaded,
agreedToEULA: true,
),
const AuthState(
user: User.empty(),
isLoggedIn: false,
status: AuthStatus.loading,
agreedToEULA: true,
),
const AuthState(
user: tUser,
isLoggedIn: true,
status: AuthStatus.loaded,
agreedToEULA: true,
),
],
verify: (_) {
verify(
() => mockAuthRepository.login(
username: username,
password: password,
),
).called(1);
verify(() => mockStoriesRepository.fetchUserBy(userId: username))
.called(1);
},
);
});
}

View File

@ -1,28 +0,0 @@
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hacki/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(
const HackiApp(
savedThemeMode: AdaptiveThemeMode.light,
trueDarkMode: false,
),
);
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}