mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
fe162208ca |
@ -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(
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -62,8 +62,7 @@ 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) {
|
||||
@ -85,8 +84,7 @@ class PollView extends StatelessWidget {
|
||||
);
|
||||
} else if (voteState.status ==
|
||||
VoteStatus.failureBeHumble) {
|
||||
showSnackBar(
|
||||
context,
|
||||
context.showSnackBar(
|
||||
content: 'No voting on your own post! (;`O´)o',
|
||||
);
|
||||
}
|
||||
@ -149,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
48
lib/screens/widgets/centered_text.dart
Normal file
48
lib/screens/widgets/centered_text.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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>(
|
||||
@ -163,63 +166,18 @@ class CommentTile extends StatelessWidget {
|
||||
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(
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
const CenteredText.dead()
|
||||
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.blocked()
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
@ -284,55 +242,41 @@ 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)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
context
|
||||
.read<CommentsCubit>()
|
||||
.loadMore(
|
||||
comment: comment,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
if (_shouldShowLoadButton(context))
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -391,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;
|
||||
@ -421,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;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -1,6 +1,6 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 1.0.9+87
|
||||
version: 1.0.10+88
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
Reference in New Issue
Block a user