Compare commits

...

11 Commits

Author SHA1 Message Date
fe162208ca fix expand animation. (#142) 2023-02-10 14:08:31 -08:00
58139ba7a3 update commit_check.yml (#141) 2023-02-09 15:42:08 -08:00
33a31acbe2 update Fastfile. 2023-02-09 15:20:19 -08:00
0fcfcbb7e3 update Fastfile. (#140) 2023-02-09 15:12:00 -08:00
a98f52c90b update publish_ios.yml 2023-02-09 14:37:11 -08:00
8e8e48c44a update GitHub action. (#139) 2023-02-09 14:28:46 -08:00
603b7cc939 bump flutter to 3.7.3 (#138) 2023-02-09 11:27:03 -08:00
649fa33df3 fix err msg. (#137) 2023-02-09 00:19:34 -08:00
81d4a0f2df banner cleanup. (#136) 2023-02-08 23:44:15 -08:00
24112a471e add collapse/expand animation to comment tile. (#135) 2023-02-08 23:08:09 -08:00
c7824eaef3 bump flutter to 3.7.2 (#134) 2023-02-08 17:43:23 -08:00
30 changed files with 286 additions and 288 deletions

View File

@ -11,15 +11,13 @@ jobs:
name: Check commit name: Check commit
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
env:
FLUTTER_VERSION: "3.7.1"
steps: steps:
- uses: actions/checkout@v2 - name: checkout all the submodules
- uses: subosito/flutter-action@v2 uses: actions/checkout@v3
with: with:
flutter-version: '3.7.1' submodules: recursive
channel: 'stable' - run: submodules/flutter/bin/flutter doctor
- run: flutter pub get - run: submodules/flutter/bin/flutter pub get
- run: flutter format --set-exit-if-changed . - run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
- run: flutter analyze - run: submodules/flutter/bin/flutter analyze lib test integration_test
- run: flutter test - run: submodules/flutter/bin/flutter test

View File

@ -20,21 +20,21 @@ jobs:
steps: steps:
- name: Check out from git - name: Check out from git
uses: actions/checkout@v2 uses: actions/checkout@v3
with:
submodules: recursive
- run: submodules/flutter/bin/flutter doctor
- run: submodules/flutter/bin/flutter pub get
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
- run: submodules/flutter/bin/flutter analyze lib test integration_test
- run: submodules/flutter/bin/flutter test
# Configure ruby according to our .ruby-version # Configure ruby according to our .ruby-version
- name: Setup ruby & Bundler - name: Setup ruby & Bundler
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true bundler-cache: true
# Set up flutter (feel free to adjust the version below)
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
cache: true
flutter-version: 3.7.1
- run: flutter pub get
- run: flutter format --set-exit-if-changed .
- run: flutter analyze
# Start an ssh-agent that will provide the SSH key from the # Start an ssh-agent that will provide the SSH key from the
# SSH_PRIVATE_KEY secret to `fastlane match` # SSH_PRIVATE_KEY secret to `fastlane match`
- name: Setup SSH key - name: Setup SSH key
@ -43,8 +43,7 @@ jobs:
run: | run: |
ssh-agent -a $SSH_AUTH_SOCK > /dev/null ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}" ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}"
- name: Download dependencies
run: flutter pub get
- name: Build & Publish to TestFlight with Fastlane - name: Build & Publish to TestFlight with Fastlane
env: env:
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 }}

View File

@ -49,7 +49,7 @@ latest_testflight_build_number
# 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", "/Users/runner/work/Hacki/Hacki/submodules/flutter/bin/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
) )

View File

@ -56,6 +56,8 @@ abstract class Constants {
'ʕ•́ᴥ•̀ʔっ', 'ʕ•́ᴥ•̀ʔっ',
'(ㆆ_ㆆ)', '(ㆆ_ㆆ)',
].pickRandomly()!; ].pickRandomly()!;
static final String errorMessage = 'Something went wrong...$sadFace';
} }
abstract class RegExpConstants { abstract class RegExpConstants {

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
/// Custom router. /// Custom router.
@ -39,8 +40,8 @@ class CustomRouter {
appBar: AppBar( appBar: AppBar(
title: const Text('Error'), title: const Text('Error'),
), ),
body: const Center( body: Center(
child: Text('Something went wrong!'), child: Text(Constants.errorMessage),
), ),
), ),
); );

View File

@ -20,7 +20,7 @@ Future<void> setUpLocator() async {
Logger( Logger(
filter: CustomLogFilter(), filter: CustomLogFilter(),
printer: LogUtil.logPrinter, printer: LogUtil.logPrinter,
output: LogUtil.getLogOutput(logOutputFile), output: LogUtil.logOutput(logOutputFile),
), ),
) )
..registerSingleton<StoriesRepository>(StoriesRepository()) ..registerSingleton<StoriesRepository>(StoriesRepository())

View File

@ -2,6 +2,8 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/styles/styles.dart';
extension ContextExtension on BuildContext { extension ContextExtension on BuildContext {
T? tryRead<T>() { T? tryRead<T>() {
@ -12,6 +14,31 @@ extension ContextExtension on BuildContext {
} }
} }
void showSnackBar({
required String content,
VoidCallback? action,
String? label,
}) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
backgroundColor: Palette.deepOrange,
content: Text(content),
action: action != null && label != null
? SnackBarAction(
label: label,
onPressed: action,
textColor: Theme.of(this).textTheme.bodyLarge?.color,
)
: null,
behavior: SnackBarBehavior.floating,
),
);
}
void showErrorSnackBar() => showSnackBar(
content: Constants.errorMessage,
);
Rect? get rect { Rect? get rect {
final RenderBox? box = findRenderObject() as RenderBox?; final RenderBox? box = findRenderObject() as RenderBox?;
final Rect? rect = final Rect? rect =

View File

@ -21,22 +21,15 @@ extension StateExtension on State {
VoidCallback? action, VoidCallback? action,
String? label, String? label,
}) { }) {
ScaffoldMessenger.of(context).showSnackBar( context.showSnackBar(
SnackBar( content: content,
backgroundColor: Palette.deepOrange, action: action,
content: Text(content), label: label,
action: action != null && label != null
? SnackBarAction(
label: label,
onPressed: action,
textColor: Theme.of(context).textTheme.bodyLarge?.color,
)
: null,
behavior: SnackBarBehavior.floating,
),
); );
} }
void showErrorSnackBar() => context.showErrorSnackBar();
Future<void>? goToItemScreen({ Future<void>? goToItemScreen({
required ItemScreenArgs args, required ItemScreenArgs args,
bool forceNewScreen = false, bool forceNewScreen = false,
@ -70,7 +63,6 @@ extension StateExtension on State {
return MorePopupMenu( return MorePopupMenu(
item: item, item: item,
isBlocked: isBlocked, isBlocked: isBlocked,
showSnackBar: showSnackBar,
onStoryLinkTapped: onStoryLinkTapped, onStoryLinkTapped: onStoryLinkTapped,
onLoginTapped: onLoginTapped, onLoginTapped: onLoginTapped,
); );

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/postable_repository.dart'; import 'package:hacki/repositories/postable_repository.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/preference_repository.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
class AuthRepository extends PostableRepository { class AuthRepository extends PostableRepository {
@ -18,8 +18,6 @@ class AuthRepository extends PostableRepository {
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final Logger _logger; final Logger _logger;
static const String _authority = 'news.ycombinator.com';
Future<bool> get loggedIn async => _preferenceRepository.loggedIn; Future<bool> get loggedIn async => _preferenceRepository.loggedIn;
Future<String?> get username async => _preferenceRepository.username; Future<String?> get username async => _preferenceRepository.username;
@ -30,7 +28,7 @@ class AuthRepository extends PostableRepository {
required String username, required String username,
required String password, required String password,
}) async { }) async {
final Uri uri = Uri.https(_authority, 'login'); final Uri uri = Uri.https(authority, 'login');
final PostDataMixin data = LoginPostData( final PostDataMixin data = LoginPostData(
acct: username, acct: username,
pw: password, pw: password,
@ -64,7 +62,7 @@ class AuthRepository extends PostableRepository {
required int id, required int id,
required bool flag, required bool flag,
}) async { }) async {
final Uri uri = Uri.https(_authority, 'flag'); final Uri uri = Uri.https(authority, 'flag');
final String? username = await _preferenceRepository.username; final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password; final String? password = await _preferenceRepository.password;
final PostDataMixin data = FlagPostData( final PostDataMixin data = FlagPostData(
@ -81,7 +79,7 @@ class AuthRepository extends PostableRepository {
required int id, required int id,
required bool favorite, required bool favorite,
}) async { }) async {
final Uri uri = Uri.https(_authority, 'fave'); final Uri uri = Uri.https(authority, 'fave');
final String? username = await _preferenceRepository.username; final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password; final String? password = await _preferenceRepository.password;
final PostDataMixin data = FavoritePostData( final PostDataMixin data = FavoritePostData(
@ -98,7 +96,7 @@ class AuthRepository extends PostableRepository {
required int id, required int id,
required bool upvote, required bool upvote,
}) async { }) async {
final Uri uri = Uri.https(_authority, 'vote'); final Uri uri = Uri.https(authority, 'vote');
final String? username = await _preferenceRepository.username; final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password; final String? password = await _preferenceRepository.password;
final PostDataMixin data = VotePostData( final PostDataMixin data = VotePostData(
@ -115,7 +113,7 @@ class AuthRepository extends PostableRepository {
required int id, required int id,
required bool downvote, required bool downvote,
}) async { }) async {
final Uri uri = Uri.https(_authority, 'vote'); final Uri uri = Uri.https(authority, 'vote');
final String? username = await _preferenceRepository.username; final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password; final String? password = await _preferenceRepository.password;
final PostDataMixin data = VotePostData( final PostDataMixin data = VotePostData(

View File

@ -14,15 +14,13 @@ class PostRepository extends PostableRepository {
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
static const String _authority = 'news.ycombinator.com';
Future<bool> comment({ Future<bool> comment({
required int parentId, required int parentId,
required String text, required String text,
}) async { }) async {
final String? username = await _preferenceRepository.username; final String? username = await _preferenceRepository.username;
final String? password = await _preferenceRepository.password; final String? password = await _preferenceRepository.password;
final Uri uri = Uri.https(_authority, 'comment'); final Uri uri = Uri.https(authority, 'comment');
if (username == null || password == null) { if (username == null || password == null) {
return false; return false;
@ -54,7 +52,7 @@ class PostRepository extends PostableRepository {
return false; return false;
} }
final Response<List<int>> formResponse = await _getFormResponse( final Response<List<int>> formResponse = await getFormResponse(
username: username, username: username,
password: password, password: password,
path: 'submitlink', path: 'submitlink',
@ -69,7 +67,7 @@ class PostRepository extends PostableRepository {
final String? cookie = final String? cookie =
formResponse.headers.value(HttpHeaders.setCookieHeader); formResponse.headers.value(HttpHeaders.setCookieHeader);
final Uri uri = Uri.https(_authority, 'r'); final Uri uri = Uri.https(authority, 'r');
final PostDataMixin data = SubmitPostData( final PostDataMixin data = SubmitPostData(
fnid: formValues['fnid']!, fnid: formValues['fnid']!,
fnop: formValues['fnop']!, fnop: formValues['fnop']!,
@ -97,7 +95,7 @@ class PostRepository extends PostableRepository {
return false; return false;
} }
final Response<List<int>> formResponse = await _getFormResponse( final Response<List<int>> formResponse = await getFormResponse(
username: username, username: username,
password: password, password: password,
id: id, id: id,
@ -113,7 +111,7 @@ class PostRepository extends PostableRepository {
final String? cookie = final String? cookie =
formResponse.headers.value(HttpHeaders.setCookieHeader); formResponse.headers.value(HttpHeaders.setCookieHeader);
final Uri uri = Uri.https(_authority, 'xedit'); final Uri uri = Uri.https(authority, 'xedit');
final PostDataMixin data = EditPostData( final PostDataMixin data = EditPostData(
hmac: formValues['hmac']!, hmac: formValues['hmac']!,
id: id, id: id,
@ -126,28 +124,4 @@ class PostRepository extends PostableRepository {
cookie: cookie, cookie: cookie,
); );
} }
Future<Response<List<int>>> _getFormResponse({
required String username,
required String password,
required String path,
int? id,
}) async {
final Uri uri = Uri.https(
_authority,
path,
<String, dynamic>{if (id != null) 'id': id.toString()},
);
final PostDataMixin data = FormPostData(
acct: username,
pw: password,
id: id,
);
return performPost(
uri,
data,
responseType: ResponseType.bytes,
validateStatus: (int? status) => status == HttpStatus.ok,
);
}
} }

View File

@ -8,10 +8,14 @@ import 'package:hacki/utils/service_exception.dart';
class PostableRepository { class PostableRepository {
PostableRepository({ PostableRepository({
Dio? dio, Dio? dio,
this.authority = 'news.ycombinator.com',
}) : _dio = dio ?? Dio(); }) : _dio = dio ?? Dio();
final Dio _dio; final Dio _dio;
@protected
final String authority;
@protected @protected
Future<bool> performDefaultPost( Future<bool> performDefaultPost(
Uri uri, Uri uri,
@ -60,4 +64,29 @@ class PostableRepository {
throw ServiceException(e.message); throw ServiceException(e.message);
} }
} }
@protected
Future<Response<List<int>>> getFormResponse({
required String username,
required String password,
required String path,
int? id,
}) async {
final Uri uri = Uri.https(
authority,
path,
<String, dynamic>{if (id != null) 'id': id.toString()},
);
final PostDataMixin data = FormPostData(
acct: username,
pw: password,
id: id,
);
return performPost(
uri,
data,
responseType: ResponseType.bytes,
validateStatus: (int? status) => status == HttpStatus.ok,
);
}
} }

View File

@ -301,7 +301,7 @@ class _HomeScreenState extends State<HomeScreen>
.fetchStoryBy(storyId) .fetchStoryBy(storyId)
.then((Story? story) { .then((Story? story) {
if (story == null) { if (story == null) {
showSnackBar(content: 'Something went wrong...'); showErrorSnackBar();
return; return;
} }
final ItemScreenArgs args = ItemScreenArgs(item: story); final ItemScreenArgs args = ItemScreenArgs(item: story);
@ -326,7 +326,7 @@ class _HomeScreenState extends State<HomeScreen>
.fetchStoryBy(storyId) .fetchStoryBy(storyId)
.then((Story? story) { .then((Story? story) {
if (story == null) { if (story == null) {
showSnackBar(content: 'Something went wrong...'); showErrorSnackBar();
return; return;
} }
final ItemScreenArgs args = ItemScreenArgs(item: story); final ItemScreenArgs args = ItemScreenArgs(item: story);

View File

@ -239,12 +239,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
context.read<EditCubit>().onReplySubmittedSuccessfully(); context.read<EditCubit>().onReplySubmittedSuccessfully();
context.read<PostCubit>().reset(); context.read<PostCubit>().reset();
} else if (postState.status == PostStatus.failure) { } else if (postState.status == PostStatus.failure) {
showSnackBar( showErrorSnackBar();
content: 'Something went wrong...'
'${Constants.sadFace}',
label: 'Okay',
action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
);
context.read<PostCubit>().reset(); context.read<PostCubit>().reset();
} }
}, },

View File

@ -87,13 +87,13 @@ class LoginDialog extends StatelessWidget {
height: Dimens.pt16, height: Dimens.pt16,
), ),
if (state.status == AuthStatus.failure) if (state.status == AuthStatus.failure)
const Padding( Padding(
padding: EdgeInsets.only( padding: const EdgeInsets.only(
left: Dimens.pt18, left: Dimens.pt18,
), ),
child: Text( child: Text(
'Something went wrong...', Constants.errorMessage,
style: TextStyle( style: const TextStyle(
color: Palette.grey, color: Palette.grey,
fontSize: TextDimens.pt12, fontSize: TextDimens.pt12,
), ),

View File

@ -15,18 +15,12 @@ class MorePopupMenu extends StatelessWidget {
super.key, super.key,
required this.item, required this.item,
required this.isBlocked, required this.isBlocked,
required this.showSnackBar,
required this.onStoryLinkTapped, required this.onStoryLinkTapped,
required this.onLoginTapped, required this.onLoginTapped,
}); });
final Item item; final Item item;
final bool isBlocked; final bool isBlocked;
final void Function({
required String content,
VoidCallback? action,
String? label,
}) showSnackBar;
final ValueChanged<String> onStoryLinkTapped; final ValueChanged<String> onStoryLinkTapped;
final VoidCallback onLoginTapped; final VoidCallback onLoginTapped;
@ -43,24 +37,26 @@ class MorePopupMenu extends StatelessWidget {
}, },
listener: (BuildContext context, VoteState voteState) { listener: (BuildContext context, VoteState voteState) {
if (voteState.status == VoteStatus.submitted) { if (voteState.status == VoteStatus.submitted) {
showSnackBar(content: 'Vote submitted successfully.'); context.showSnackBar(content: 'Vote submitted successfully.');
} else if (voteState.status == VoteStatus.canceled) { } else if (voteState.status == VoteStatus.canceled) {
showSnackBar(content: 'Vote canceled.'); context.showSnackBar(content: 'Vote canceled.');
} else if (voteState.status == VoteStatus.failure) { } else if (voteState.status == VoteStatus.failure) {
showSnackBar(content: 'Something went wrong...'); context.showErrorSnackBar();
} else if (voteState.status == } else if (voteState.status ==
VoteStatus.failureKarmaBelowThreshold) { VoteStatus.failureKarmaBelowThreshold) {
showSnackBar( context.showSnackBar(
content: "You can't downvote because you are karmaly broke.", content: "You can't downvote because you are karmaly broke.",
); );
} else if (voteState.status == VoteStatus.failureNotLoggedIn) { } else if (voteState.status == VoteStatus.failureNotLoggedIn) {
showSnackBar( context.showSnackBar(
content: 'Not logged in, no voting! (;O´)o', content: 'Not logged in, no voting! (;O´)o',
action: onLoginTapped, action: onLoginTapped,
label: 'Log in', label: 'Log in',
); );
} else if (voteState.status == VoteStatus.failureBeHumble) { } else if (voteState.status == VoteStatus.failureBeHumble) {
showSnackBar(content: 'No voting on your own post! (;O´)o'); context.showSnackBar(
content: 'No voting on your own post! (;O´)o',
);
} }
Navigator.pop( Navigator.pop(

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/blocs/auth/auth_bloc.dart'; import 'package:hacki/blocs/auth/auth_bloc.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/context_extension.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
@ -61,36 +62,29 @@ class PollView extends StatelessWidget {
listener: (BuildContext context, VoteState voteState) { listener: (BuildContext context, VoteState voteState) {
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
if (voteState.status == VoteStatus.submitted) { if (voteState.status == VoteStatus.submitted) {
showSnackBar( context.showSnackBar(
context,
content: 'Vote submitted successfully.', content: 'Vote submitted successfully.',
); );
} else if (voteState.status == VoteStatus.canceled) { } else if (voteState.status == VoteStatus.canceled) {
showSnackBar(context, content: 'Vote canceled.'); context.showSnackBar(content: 'Vote canceled.');
} else if (voteState.status == VoteStatus.failure) { } else if (voteState.status == VoteStatus.failure) {
showSnackBar( context.showErrorSnackBar();
context,
content: 'Something went wrong...',
);
} else if (voteState.status == } else if (voteState.status ==
VoteStatus.failureKarmaBelowThreshold) { VoteStatus.failureKarmaBelowThreshold) {
showSnackBar( context.showSnackBar(
context,
content: "You can't downvote because" content: "You can't downvote because"
' you are karmaly broke.', ' you are karmaly broke.',
); );
} else if (voteState.status == } else if (voteState.status ==
VoteStatus.failureNotLoggedIn) { VoteStatus.failureNotLoggedIn) {
showSnackBar( context.showSnackBar(
context,
content: 'Not logged in, no voting! (;O´)o', content: 'Not logged in, no voting! (;O´)o',
action: onLoginTapped, action: onLoginTapped,
label: 'Log in', label: 'Log in',
); );
} else if (voteState.status == } else if (voteState.status ==
VoteStatus.failureBeHumble) { VoteStatus.failureBeHumble) {
showSnackBar( context.showSnackBar(
context,
content: 'No voting on your own post! (;O´)o', content: 'No voting on your own post! (;O´)o',
); );
} }
@ -153,26 +147,4 @@ class PollView extends StatelessWidget {
}, },
); );
} }
void showSnackBar(
BuildContext context, {
required String content,
VoidCallback? action,
String? label,
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Palette.deepOrange,
content: Text(content),
action: action != null && label != null
? SnackBarAction(
label: label,
onPressed: action,
textColor: Theme.of(context).textTheme.bodyLarge?.color,
)
: null,
behavior: SnackBarBehavior.floating,
),
);
}
} }

View File

@ -453,13 +453,13 @@ class _ProfileScreenState extends State<ProfileScreen>
height: Dimens.pt16, height: Dimens.pt16,
), ),
if (state.status == AuthStatus.failure) if (state.status == AuthStatus.failure)
const Padding( Padding(
padding: EdgeInsets.only( padding: const EdgeInsets.only(
left: Dimens.pt18, left: Dimens.pt18,
), ),
child: Text( child: Text(
'Something went wrong...', Constants.errorMessage,
style: TextStyle( style: const TextStyle(
color: Palette.grey, color: Palette.grey,
fontSize: TextDimens.pt12, fontSize: TextDimens.pt12,
), ),

View File

@ -50,9 +50,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
content: 'Post submitted successfully.', content: 'Post submitted successfully.',
); );
} else if (state.status == SubmitStatus.failure) { } else if (state.status == SubmitStatus.failure) {
showSnackBar( showErrorSnackBar();
content: 'Something went wrong...',
);
} }
}, },
builder: (BuildContext context, SubmitState state) { builder: (BuildContext context, SubmitState state) {

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:hacki/styles/styles.dart';
class CenteredText extends StatelessWidget {
const CenteredText({
super.key,
required this.text,
this.color = Palette.grey,
});
const CenteredText.deleted({Key? key})
: this(
key: key,
text: 'deleted',
);
const CenteredText.dead({Key? key})
: this(
key: key,
text: 'dead',
);
const CenteredText.blocked({Key? key})
: this(
key: key,
text: 'blocked',
);
final String text;
final Color color;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
text,
style: TextStyle(
color: color,
),
),
),
);
}
}

View File

@ -8,6 +8,7 @@ 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/screens/widgets/bloc_builder_3.dart';
import 'package:hacki/screens/widgets/centered_text.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';
@ -40,6 +41,8 @@ class CommentTile extends StatelessWidget {
final void Function(String) onStoryLinkTapped; final void Function(String) onStoryLinkTapped;
final FetchMode fetchMode; final FetchMode fetchMode;
static final Map<int, Color> _colors = <int, Color>{};
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<CollapseCubit>( return BlocProvider<CollapseCubit>(
@ -157,136 +160,92 @@ class CommentTile extends StatelessWidget {
], ],
), ),
), ),
if (actionable && state.collapsed) AnimatedSize(
Center( duration: const Duration(milliseconds: 200),
child: Padding( child: Column(
padding: const EdgeInsets.only( crossAxisAlignment: CrossAxisAlignment.start,
bottom: Dimens.pt12, children: <Widget>[
), if (actionable && state.collapsed)
child: Text( CenteredText(
'collapsed ' text:
'(${state.collapsedCount + 1})', '''collapsed (${state.collapsedCount + 1})''',
style: const TextStyle(
color: Palette.orangeAccent, color: Palette.orangeAccent,
), )
), else if (comment.deleted)
), const CenteredText.deleted()
) else if (comment.dead)
else if (comment.deleted) const CenteredText.dead()
const Center( else if (blocklistState.blocklist
child: Padding( .contains(comment.by))
padding: EdgeInsets.only( const CenteredText.blocked()
bottom: Dimens.pt12, else
), Padding(
child: Text( padding: const EdgeInsets.only(
'deleted', left: Dimens.pt8,
style: TextStyle( right: Dimens.pt8,
color: Palette.grey, top: Dimens.pt6,
), bottom: Dimens.pt12,
),
),
)
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),
), ),
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 && if (_shouldShowLoadButton(context))
comment.kids.isNotEmpty &&
!context
.read<CommentsCubit>()
.state
.commentIds
.contains(comment.kids.first) &&
!context
.read<CommentsCubit>()
.state
.onlyShowTargetComment)
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -376,7 +335,12 @@ class CommentTile extends StatelessWidget {
); );
} }
static final Map<int, Color> _colors = <int, Color>{}; void onTextTapped(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse();
}
}
Color _getColor(int level) { Color _getColor(int level) {
final int initialLevel = level; final int initialLevel = level;
@ -406,10 +370,13 @@ class CommentTile extends StatelessWidget {
return color; return color;
} }
void onTextTapped(BuildContext context) { bool _shouldShowLoadButton(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) { final CollapseState collapseState = context.read<CollapseCubit>().state;
HapticFeedback.selectionClick(); final CommentsState commentsState = context.read<CommentsCubit>().state;
context.read<CollapseCubit>().collapse(); return fetchMode == FetchMode.lazy &&
} comment.kids.isNotEmpty &&
collapseState.collapsed == false &&
commentsState.commentIds.contains(comment.kids.first) == false &&
commentsState.onlyShowTargetComment == false;
} }
} }

View File

@ -111,7 +111,7 @@ class _CountDownReminderState extends State<CountdownReminder>
.fetchStoryBy(state.storyId!) .fetchStoryBy(state.storyId!)
.then((Story? story) { .then((Story? story) {
if (story == null) { if (story == null) {
showSnackBar(content: 'Something went wrong...'); showErrorSnackBar();
return; return;
} }
final ItemScreenArgs args = ItemScreenArgs(item: story); final ItemScreenArgs args = ItemScreenArgs(item: story);

View File

@ -5,7 +5,7 @@ import 'package:hacki/config/constants.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/link_preview/link_view.dart'; import 'package:hacki/screens/widgets/link_preview/link_view.dart';
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -119,7 +119,7 @@ class _LinkPreviewState extends State<LinkPreview> {
@override @override
void initState() { void initState() {
_errorTitle = widget.errorTitle ?? 'Something went wrong!'; _errorTitle = widget.errorTitle ?? Constants.errorMessage;
_errorBody = widget.errorBody ?? _errorBody = widget.errorBody ??
'Oops! Unable to parse the url. We have ' 'Oops! Unable to parse the url. We have '
'sent feedback to our developers & ' 'sent feedback to our developers & '

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
class OfflineBanner extends StatelessWidget { class OfflineBanner extends StatelessWidget {

View File

@ -1,4 +1,5 @@
export 'bloc_builder_3.dart'; export 'bloc_builder_3.dart';
export 'centered_text.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

@ -3,3 +3,4 @@ export 'custom_bloc_observer.dart';
export 'fetcher.dart'; export 'fetcher.dart';
export 'firebase_client.dart'; export 'firebase_client.dart';
export 'local_notification.dart'; export 'local_notification.dart';
export 'web_analyzer.dart';

View File

@ -14,7 +14,7 @@ abstract class LogUtil {
colors: false, colors: false,
); );
static LogOutput getLogOutput(File outputFile) => MultiOutput( static LogOutput logOutput(File outputFile) => MultiOutput(
<LogOutput>[ <LogOutput>[
ConsoleOutput(), ConsoleOutput(),
CustomFileOutput( CustomFileOutput(
@ -43,7 +43,7 @@ abstract class LogUtil {
final Uint8List fileContent = await currentSessionLog.readAsBytes(); final Uint8List fileContent = await currentSessionLog.readAsBytes();
await previousSessionLog.writeAsString( await previousSessionLog.writeAsString(
'Current session logs:', 'Current session logs:\n',
mode: FileMode.append, mode: FileMode.append,
); );
return previousSessionLog.writeAsBytes( return previousSessionLog.writeAsBytes(

View File

@ -1359,4 +1359,4 @@ packages:
version: "3.1.1" version: "3.1.1"
sdks: sdks:
dart: ">=2.18.0 <3.0.0" dart: ">=2.18.0 <3.0.0"
flutter: ">=3.7.1" flutter: ">=3.7.3"

View File

@ -1,11 +1,11 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 1.0.7+85 version: 1.0.10+88
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: "3.7.1" flutter: "3.7.3"
dependencies: dependencies:
adaptive_theme: ^3.0.0 adaptive_theme: ^3.0.0