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:
branches:
- "**"
- '!master'
jobs:
releases:
@ -19,4 +20,5 @@ jobs:
channel: 'stable'
- run: flutter pub get
- 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
push:
branches:
- "!*"
- master
tags:
- "v*"
@ -51,4 +51,4 @@ jobs:
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: cd ios && bundle exec fastlane beta "build_name:${{ github.ref_name }}"
run: cd ios && bundle exec fastlane beta

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

View File

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

View File

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

View File

@ -31,10 +31,6 @@ platform :ios do
is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example'
if !is_example_repo && APP_IDENTIFIER == 'no.tjer.HelloWorld' then
UI.user_error! "You need to update your Fastfile to use your own `APP_IDENTIFIER`"
end
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
sync_code_signing(
type: "appstore",
@ -42,15 +38,6 @@ platform :ios do
readonly: true
)
if !is_example_repo then
if APPSTORECONNECT_ISSUER_ID == '69a6de83-feb7-47e3-e053-5b8c7c11a4d1' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_ISSUER_ID`"
end
if APPSTORECONNECT_KEY_ID == 'YRQDJRKMR9' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_KEY_ID`"
end
end
# We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH
app_store_connect_api_key(
key_id: APPSTORECONNECT_KEY_ID,
@ -59,21 +46,18 @@ platform :ios do
latest_testflight_build_number
# Figure out the build number (and optionally build name)
new_build_number = ( + 1)
extra_config_args = []
if options.key?(:build_name) then
extra_config_args = ["--build-name", options[:build_name].delete_prefix('v')]
end
# Prep the xcodeproject from Flutter without building (`--config-only`)
sh(
"flutter", "build", "ios", "--config-only",
"--release", "--no-pub", "--no-codesign",
"--build-number", new_build_number.to_s,
*extra_config_args
"--build-number", new_build_number.to_s
)
version = get_version_number(xcodeproj: "Runner.xcodeproj", target: 'Runner')
increment_version_number(
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc')
version_number: version
)
increment_build_number({
@ -93,4 +77,4 @@ latest_testflight_build_number
skip_waiting_for_build_processing: true,
)
end
end
end

View File

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

View File

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

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:equatable/equatable.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/services/services.dart';
part 'collapse_state.dart';
@ -10,13 +11,16 @@ part 'collapse_state.dart';
class CollapseCubit extends Cubit<CollapseState> {
CollapseCubit({
required int commentId,
required CommentsCubit commentsCubit,
CollapseCache? collapseCache,
}) : _commentId = commentId,
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
_commentsCubit = commentsCubit,
super(const CollapseState.init());
final int _commentId;
final CollapseCache _collapseCache;
final CommentsCubit _commentsCubit;
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
void init() {
@ -43,12 +47,19 @@ class CollapseCubit extends Cubit<CollapseState> {
),
);
} 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(
state.copyWith(
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.
void loadMore({Comment? comment}) {
if (comment == null && state.status == CommentsStatus.loading) return;
switch (state.fetchMode) {
case FetchMode.lazy:
if (comment == null) return;

View File

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

View File

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

View File

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

View File

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

View File

@ -155,7 +155,7 @@ class NavigationModePreference extends BooleanPreference {
String get title => 'Show Web Page First';
@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 {

View File

@ -1,7 +1,8 @@
import 'package:equatable/equatable.dart';
import 'package:intl/intl.dart';
class User {
User({
class User extends Equatable {
const User({
required this.about,
required this.created,
required this.delay,
@ -9,7 +10,7 @@ class User {
required this.karma,
});
User.empty()
const User.empty()
: about = '',
created = 0,
delay = 0,
@ -39,4 +40,13 @@ class User {
String toString() {
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)
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 *
BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (
PreferenceState previous,
PreferenceState current,
) =>
previous.fontSize != current.fontSize,
builder: (
BuildContext context,
PreferenceState prefState,
) {
return Column(
children: <Widget>[
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,
fontSize: MediaQuery.of(context)
.textScaleFactor *
prefState.fontSize.fontSize,
color: state.item.url.isNotEmpty
? Palette.orange
: null,
),
),
),
)
else
const SizedBox(
height: Dimens.pt6,
),
if (state.item.text.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt10,
),
child: SelectableLinkify(
text: state.item.text,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
context
.read<PreferenceCubit>()
.state
.fontSize
.fontSize,
),
linkStyle: TextStyle(
fontSize:
MediaQuery.of(context).textScaleFactor *
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
context
.read<PreferenceCubit>()
.state
.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)
BlocProvider<PollCubit>(
create: (BuildContext context) =>

View File

@ -426,7 +426,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog(
context: context,
applicationName: 'Hacki',
applicationVersion: 'v0.2.33',
applicationVersion: 'v1.0.0',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(
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/extensions/extensions.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/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
@ -46,344 +47,329 @@ class CommentTile extends StatelessWidget {
lazy: false,
create: (_) => CollapseCubit(
commentId: comment.id,
commentsCubit: context.read<CommentsCubit>(),
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
)..init(),
child: BlocBuilder<CollapseCubit, CollapseState>(
builder: (BuildContext context, CollapseState state) {
child: BlocBuilder3<CollapseCubit, CollapseState, PreferenceCubit,
PreferenceState, BlocklistCubit, BlocklistState>(
builder: (
BuildContext context,
CollapseState state,
PreferenceState prefState,
BlocklistState blocklistState,
) {
if (actionable && state.hidden) return const SizedBox.shrink();
return BlocBuilder<PreferenceCubit, PreferenceState>(
builder: (BuildContext context, PreferenceState prefState) {
return BlocBuilder<BlocklistCubit, BlocklistState>(
builder: (BuildContext context, BlocklistState blocklistState) {
const Color orange = Color.fromRGBO(255, 152, 0, 1);
final Color color = _getColor(level);
const Color orange = Color.fromRGBO(255, 152, 0, 1);
final Color color = _getColor(level);
final Padding child = Padding(
padding: EdgeInsets.zero,
final Padding child = Padding(
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(
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(
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,
),
),
],
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,
),
),
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,
const Spacer(),
Text(
comment.postedDate,
style: const TextStyle(
color: Palette.grey,
),
],
),
],
),
),
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,
link: story.url,
offlineReading: context.read<StoriesBloc>().state.offlineReading,
placeholderWidget: 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,
),
],
),
),
)
],
),
),
),
placeholderWidget: _LinkPreviewPlaceholder(
height: height,
),
errorImage: Constants.hackerNewsLogoLink,
backgroundColor: Palette.transparent,
@ -136,7 +54,7 @@ class StoryTile extends StatelessWidget {
titleStyle: TextStyle(
color: hasRead
? Palette.grey[500]
: Theme.of(context).textTheme.subtitle1!.color,
: Theme.of(context).textTheme.subtitle1?.color,
fontWeight: FontWeight.bold,
),
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 'comment_tile.dart';
export 'countdown_reminder.dart';

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
name: hacki
description: A Hacker News reader.
version: 0.2.33+76
version: 1.0.0+77
publish_to: none
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);
});
}