Compare commits

..

15 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
c2b66d29c3 add sharing option. (#131) 2023-02-04 18:46:04 -08:00
e0a53e44b2 bump flutter to 3.7.1 (#129) 2023-02-01 15:19:06 -08:00
4cf8379db0 fix Story model. (#128) 2023-01-31 22:02:17 -08:00
c1c26bf0e0 fix preference model. (#127) 2023-01-31 18:19:34 -08:00
36 changed files with 395 additions and 383 deletions

View File

@ -11,15 +11,13 @@ jobs:
name: Check commit
runs-on: ubuntu-latest
timeout-minutes: 30
env:
FLUTTER_VERSION: "3.7.0"
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
- name: checkout all the submodules
uses: actions/checkout@v3
with:
flutter-version: '3.7.0'
channel: 'stable'
- run: flutter pub get
- run: flutter format --set-exit-if-changed .
- run: flutter analyze
- run: flutter test
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

View File

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

View File

@ -0,0 +1,3 @@
- Customization of tab bar.
- Option to enable swipe gesture for switching between tabs.
- Access to action menu from home screen.

View File

@ -49,7 +49,7 @@ latest_testflight_build_number
# Prep the xcodeproject from Flutter without building (`--config-only`)
sh(
"flutter", "build", "ios", "--config-only",
"/Users/runner/work/Hacki/Hacki/submodules/flutter/bin/flutter", "build", "ios", "--config-only",
"--release", "--no-pub", "--no-codesign",
"--build-number", new_build_number.to_s
)

View File

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

View File

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

View File

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

View File

@ -2,6 +2,8 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/styles/styles.dart';
extension ContextExtension on BuildContext {
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 {
final RenderBox? box = findRenderObject() as RenderBox?;
final Rect? rect =

View File

@ -21,22 +21,15 @@ extension StateExtension on State {
VoidCallback? action,
String? label,
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Palette.deepOrange,
content: Text(content),
action: action != null && label != null
? SnackBarAction(
context.showSnackBar(
content: content,
action: action,
label: label,
onPressed: action,
textColor: Theme.of(context).textTheme.bodyLarge?.color,
)
: null,
behavior: SnackBarBehavior.floating,
),
);
}
void showErrorSnackBar() => context.showErrorSnackBar();
Future<void>? goToItemScreen({
required ItemScreenArgs args,
bool forceNewScreen = false,
@ -70,7 +63,6 @@ extension StateExtension on State {
return MorePopupMenu(
item: item,
isBlocked: isBlocked,
showSnackBar: showSnackBar,
onStoryLinkTapped: onStoryLinkTapped,
onLoginTapped: onLoginTapped,
);
@ -119,12 +111,46 @@ extension StateExtension on State {
}
}
void onShareTapped(Item item, Rect? rect) {
Share.share(
Future<void> onShareTapped(Item item, Rect? rect) async {
late final String? linkToShare;
if (item.url.isNotEmpty) {
linkToShare = await showModalBottomSheet<String>(
context: context,
builder: (BuildContext context) {
return Container(
height: 140,
color: Theme.of(context).canvasColor,
child: Material(
child: Column(
children: <Widget>[
ListTile(
onTap: () => Navigator.pop(context, item.url),
title: const Text('Link to article'),
),
ListTile(
onTap: () => Navigator.pop(
context,
'https://news.ycombinator.com/item?id=${item.id}',
),
title: const Text('Link to HN'),
),
],
),
),
);
},
);
} else {
linkToShare = 'https://news.ycombinator.com/item?id=${item.id}';
}
if (linkToShare != null) {
await Share.share(
linkToShare,
sharePositionOrigin: rect,
);
}
}
void onFlagTapped(Item item) {
showDialog<bool>(

View File

@ -9,4 +9,5 @@ export 'post_data.dart';
export 'preference.dart';
export 'search_params.dart';
export 'story.dart';
export 'story_type.dart';
export 'user.dart';

View File

@ -24,22 +24,7 @@ class PollOption extends Item {
PollOption.empty()
: ratio = 0,
super(
id: 0,
score: 0,
descendants: 0,
time: 0,
by: '',
title: '',
url: '',
kids: <int>[],
dead: false,
parts: <int>[],
deleted: false,
parent: 0,
text: '',
type: '',
);
super.empty();
PollOption.fromJson(super.json)
: ratio = 0,

View File

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:io';
import 'package:equatable/equatable.dart';
@ -13,8 +14,10 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
Preference<T> copyWith({required T? val});
static List<Preference<dynamic>> allPreferences = <Preference<dynamic>>[
// Order of these first three preferences does not matter.
static final List<Preference<dynamic>> allPreferences =
UnmodifiableListView<Preference<dynamic>>(
<Preference<dynamic>>[
// Order of these first four preferences does not matter.
FetchModePreference(),
CommentsOrderPreference(),
FontSizePreference(),
@ -32,7 +35,8 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
const MarkReadStoriesModePreference(),
const EyeCandyModePreference(),
const TrueDarkModePreference(),
];
],
);
@override
List<Object?> get props => <Object?>[key];
@ -81,7 +85,7 @@ class SwipeGesturePreference extends BooleanPreference {
@override
String get subtitle =>
'''Enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
}
class NotificationModePreference extends BooleanPreference {
@ -118,6 +122,10 @@ class CollapseModePreference extends BooleanPreference {
@override
String get title => 'Tap Anywhere to Collapse';
@override
String get subtitle =>
'''if disabled, tap on the top of comment tile to collapse.''';
}
/// The value deciding whether or not the story

View File

@ -1,41 +1,6 @@
import 'package:hacki/config/constants.dart';
import 'package:hacki/models/item.dart';
enum StoryType {
top('topstories'),
best('beststories'),
latest('newstories'),
ask('askstories'),
show('showstories');
const StoryType(this.path);
final String path;
String get label {
switch (this) {
case StoryType.top:
return 'TOP';
case StoryType.best:
return 'BEST';
case StoryType.latest:
return 'NEW';
case StoryType.ask:
return 'ASK';
case StoryType.show:
return 'SHOW';
}
}
static int convertToSettingsValue(List<StoryType> tabs) {
return int.parse(
tabs
.map((StoryType e) => e.index.toString())
.reduce((String value, String element) => '$value$element'),
);
}
}
class Story extends Item {
const Story({
required super.descendants,
@ -55,23 +20,7 @@ class Story extends Item {
parent: 0,
);
Story.empty()
: super(
id: 0,
score: 0,
descendants: 0,
time: 0,
by: '',
title: '',
url: '',
kids: <int>[],
dead: false,
parts: <int>[],
deleted: false,
parent: 0,
text: '',
type: '',
);
Story.empty() : super.empty();
Story.placeholder()
: super(

View File

@ -0,0 +1,34 @@
enum StoryType {
top('topstories'),
best('beststories'),
latest('newstories'),
ask('askstories'),
show('showstories');
const StoryType(this.path);
final String path;
String get label {
switch (this) {
case StoryType.top:
return 'TOP';
case StoryType.best:
return 'BEST';
case StoryType.latest:
return 'NEW';
case StoryType.ask:
return 'ASK';
case StoryType.show:
return 'SHOW';
}
}
static int convertToSettingsValue(List<StoryType> tabs) {
return int.parse(
tabs
.map((StoryType e) => e.index.toString())
.reduce((String value, String element) => '$value$element'),
);
}
}

View File

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

View File

@ -14,15 +14,13 @@ class PostRepository extends PostableRepository {
final PreferenceRepository _preferenceRepository;
static const String _authority = 'news.ycombinator.com';
Future<bool> comment({
required int parentId,
required String text,
}) async {
final String? username = await _preferenceRepository.username;
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) {
return false;
@ -54,7 +52,7 @@ class PostRepository extends PostableRepository {
return false;
}
final Response<List<int>> formResponse = await _getFormResponse(
final Response<List<int>> formResponse = await getFormResponse(
username: username,
password: password,
path: 'submitlink',
@ -69,7 +67,7 @@ class PostRepository extends PostableRepository {
final String? cookie =
formResponse.headers.value(HttpHeaders.setCookieHeader);
final Uri uri = Uri.https(_authority, 'r');
final Uri uri = Uri.https(authority, 'r');
final PostDataMixin data = SubmitPostData(
fnid: formValues['fnid']!,
fnop: formValues['fnop']!,
@ -97,7 +95,7 @@ class PostRepository extends PostableRepository {
return false;
}
final Response<List<int>> formResponse = await _getFormResponse(
final Response<List<int>> formResponse = await getFormResponse(
username: username,
password: password,
id: id,
@ -113,7 +111,7 @@ class PostRepository extends PostableRepository {
final String? cookie =
formResponse.headers.value(HttpHeaders.setCookieHeader);
final Uri uri = Uri.https(_authority, 'xedit');
final Uri uri = Uri.https(authority, 'xedit');
final PostDataMixin data = EditPostData(
hmac: formValues['hmac']!,
id: id,
@ -126,28 +124,4 @@ class PostRepository extends PostableRepository {
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 {
PostableRepository({
Dio? dio,
this.authority = 'news.ycombinator.com',
}) : _dio = dio ?? Dio();
final Dio _dio;
@protected
final String authority;
@protected
Future<bool> performDefaultPost(
Uri uri,
@ -60,4 +64,29 @@ class PostableRepository {
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)
.then((Story? story) {
if (story == null) {
showSnackBar(content: 'Something went wrong...');
showErrorSnackBar();
return;
}
final ItemScreenArgs args = ItemScreenArgs(item: story);
@ -326,7 +326,7 @@ class _HomeScreenState extends State<HomeScreen>
.fetchStoryBy(storyId)
.then((Story? story) {
if (story == null) {
showSnackBar(content: 'Something went wrong...');
showErrorSnackBar();
return;
}
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<PostCubit>().reset();
} else if (postState.status == PostStatus.failure) {
showSnackBar(
content: 'Something went wrong...'
'${Constants.sadFace}',
label: 'Okay',
action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
);
showErrorSnackBar();
context.read<PostCubit>().reset();
}
},

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/blocs/auth/auth_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/context_extension.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/styles/styles.dart';
@ -61,36 +62,29 @@ class PollView extends StatelessWidget {
listener: (BuildContext context, VoteState voteState) {
ScaffoldMessenger.of(context).clearSnackBars();
if (voteState.status == VoteStatus.submitted) {
showSnackBar(
context,
context.showSnackBar(
content: 'Vote submitted successfully.',
);
} else if (voteState.status == VoteStatus.canceled) {
showSnackBar(context, content: 'Vote canceled.');
context.showSnackBar(content: 'Vote canceled.');
} else if (voteState.status == VoteStatus.failure) {
showSnackBar(
context,
content: 'Something went wrong...',
);
context.showErrorSnackBar();
} else if (voteState.status ==
VoteStatus.failureKarmaBelowThreshold) {
showSnackBar(
context,
context.showSnackBar(
content: "You can't downvote because"
' you are karmaly broke.',
);
} else if (voteState.status ==
VoteStatus.failureNotLoggedIn) {
showSnackBar(
context,
context.showSnackBar(
content: 'Not logged in, no voting! (;O´)o',
action: onLoginTapped,
label: 'Log in',
);
} else if (voteState.status ==
VoteStatus.failureBeHumble) {
showSnackBar(
context,
context.showSnackBar(
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,
),
if (state.status == AuthStatus.failure)
const Padding(
padding: EdgeInsets.only(
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt18,
),
child: Text(
'Something went wrong...',
style: TextStyle(
Constants.errorMessage,
style: const TextStyle(
color: Palette.grey,
fontSize: TextDimens.pt12,
),

View File

@ -50,9 +50,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
content: 'Post submitted successfully.',
);
} else if (state.status == SubmitStatus.failure) {
showSnackBar(
content: 'Something went wrong...',
);
showErrorSnackBar();
}
},
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/models/models.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/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
@ -40,6 +41,8 @@ class CommentTile extends StatelessWidget {
final void Function(String) onStoryLinkTapped;
final FetchMode fetchMode;
static final Map<int, Color> _colors = <int, Color>{};
@override
Widget build(BuildContext context) {
return BlocProvider<CollapseCubit>(
@ -157,63 +160,24 @@ class CommentTile extends StatelessWidget {
],
),
),
AnimatedSize(
duration: const Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (actionable && state.collapsed)
Center(
child: Padding(
padding: const EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text(
'collapsed '
'(${state.collapsedCount + 1})',
style: const TextStyle(
CenteredText(
text:
'''collapsed (${state.collapsedCount + 1})''',
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,
),
),
),
)
const CenteredText.deleted()
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,
),
),
),
)
const CenteredText.dead()
else if (blocklistState.blocklist
.contains(comment.by))
const CenteredText.blocked()
else
Padding(
padding: const EdgeInsets.only(
@ -226,7 +190,8 @@ class CommentTile extends StatelessWidget {
? SelectableText.rich(
key: ValueKey<int>(comment.id),
buildTextSpan(
(comment as BuildableComment).elements,
(comment as BuildableComment)
.elements,
style: TextStyle(
fontSize: MediaQuery.of(
context,
@ -238,12 +203,14 @@ class CommentTile extends StatelessWidget {
context,
).textScaleFactor *
prefState.fontSize.fontSize,
decoration: TextDecoration.underline,
decoration:
TextDecoration.underline,
color: Palette.orange,
),
onOpen: (LinkableElement link) {
if (link.url.isStoryLink) {
onStoryLinkTapped.call(link.url);
onStoryLinkTapped
.call(link.url);
} else {
LinkUtil.launch(link.url);
}
@ -275,18 +242,10 @@ class CommentTile extends StatelessWidget {
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)
],
),
),
if (_shouldShowLoadButton(context))
Center(
child: Padding(
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) {
final int initialLevel = level;
@ -406,10 +370,13 @@ class CommentTile extends StatelessWidget {
return color;
}
void onTextTapped(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse();
}
bool _shouldShowLoadButton(BuildContext context) {
final CollapseState collapseState = context.read<CollapseCubit>().state;
final CommentsState commentsState = context.read<CommentsCubit>().state;
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!)
.then((Story? story) {
if (story == null) {
showSnackBar(content: 'Something went wrong...');
showErrorSnackBar();
return;
}
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/models/models.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:url_launcher/url_launcher.dart';
@ -119,7 +119,7 @@ class _LinkPreviewState extends State<LinkPreview> {
@override
void initState() {
_errorTitle = widget.errorTitle ?? 'Something went wrong!';
_errorTitle = widget.errorTitle ?? Constants.errorMessage;
_errorBody = widget.errorBody ??
'Oops! Unable to parse the url. We have '
'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:hacki/blocs/blocs.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';
class OfflineBanner extends StatelessWidget {

View File

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

View File

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

View File

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

View File

@ -1358,5 +1358,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.0 <4.0.0"
flutter: ">=3.7.0"
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.7.3"

View File

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