mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
c375def289 | |||
3469543c7b | |||
ab755581fd | |||
6b75eb8549 | |||
36ded8a8e3 | |||
582ac7b0be |
@ -19,6 +19,8 @@ import flutter_local_notifications
|
|||||||
|
|
||||||
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
|
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
|
||||||
|
|
||||||
|
WorkmanagerPlugin.registerTask(withIdentifier: "workmanager.background.task")
|
||||||
|
|
||||||
if #available(iOS 10.0, *) {
|
if #available(iOS 10.0, *) {
|
||||||
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
||||||
}
|
}
|
||||||
|
@ -47,13 +47,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
user: user,
|
user: user,
|
||||||
|
status: AuthStatus.loaded,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: AuthStatus.loaded,
|
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
|
status: AuthStatus.loaded,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,15 @@ abstract class Constants {
|
|||||||
'(ㆆ_ㆆ)',
|
'(ㆆ_ㆆ)',
|
||||||
].pickRandomly()!;
|
].pickRandomly()!;
|
||||||
|
|
||||||
|
static final String magicWord = <String>[
|
||||||
|
'to be over the rainbow!',
|
||||||
|
'to infinity and beyond!',
|
||||||
|
'to see the future.',
|
||||||
|
].pickRandomly()!;
|
||||||
|
|
||||||
static final String errorMessage = 'Something went wrong...$sadFace';
|
static final String errorMessage = 'Something went wrong...$sadFace';
|
||||||
|
static final String loginErrorMessage =
|
||||||
|
'''Failed to log in $sadFace, this could happen if your account requires a CAPTCHA, please try logging in inside a browser to see if this is the case, if so, you may try logging in here again later after CAPTCHA is no longer needed.''';
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class RegExpConstants {
|
abstract class RegExpConstants {
|
||||||
|
@ -76,12 +76,12 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
Future<void> init({
|
Future<void> init({
|
||||||
bool onlyShowTargetComment = false,
|
bool onlyShowTargetComment = false,
|
||||||
bool useCommentCache = false,
|
bool useCommentCache = false,
|
||||||
List<Comment>? targetParents,
|
List<Comment>? targetAncestors,
|
||||||
}) async {
|
}) async {
|
||||||
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
|
if (onlyShowTargetComment && (targetAncestors?.isNotEmpty ?? false)) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
comments: targetParents,
|
comments: targetAncestors,
|
||||||
onlyShowTargetComment: true,
|
onlyShowTargetComment: true,
|
||||||
status: CommentsStatus.allLoaded,
|
status: CommentsStatus.allLoaded,
|
||||||
),
|
),
|
||||||
@ -89,8 +89,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
_streamSubscription = _storiesRepository
|
_streamSubscription = _storiesRepository
|
||||||
.fetchAllCommentsRecursivelyStream(
|
.fetchAllCommentsRecursivelyStream(
|
||||||
ids: targetParents!.last.kids,
|
ids: targetAncestors!.last.kids,
|
||||||
level: targetParents.last.level + 1,
|
level: targetAncestors.last.level + 1,
|
||||||
)
|
)
|
||||||
.asyncMap(_toBuildableComment)
|
.asyncMap(_toBuildableComment)
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
@ -111,7 +111,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
final Item item = state.item;
|
final Item item = state.item;
|
||||||
final Item updatedItem = state.offlineReading
|
final Item updatedItem = state.offlineReading
|
||||||
? item
|
? item
|
||||||
: await _storiesRepository.fetchItem(id: item.id) ?? item;
|
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
|
||||||
|
item;
|
||||||
final List<int> kids = sortKids(updatedItem.kids);
|
final List<int> kids = sortKids(updatedItem.kids);
|
||||||
|
|
||||||
emit(state.copyWith(item: updatedItem));
|
emit(state.copyWith(item: updatedItem));
|
||||||
@ -273,8 +274,9 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
Future<void> loadParentThread() async {
|
Future<void> loadParentThread() async {
|
||||||
unawaited(HapticFeedback.lightImpact());
|
unawaited(HapticFeedback.lightImpact());
|
||||||
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
|
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
|
||||||
final Story? parent =
|
final Story? parent = await _storiesRepository
|
||||||
await _storiesRepository.fetchParentStory(id: state.item.id);
|
.fetchParentStory(id: state.item.id)
|
||||||
|
.then(_toBuildableStory);
|
||||||
|
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
return;
|
return;
|
||||||
@ -380,6 +382,19 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Item?> _toBuildable(Item? item) async {
|
||||||
|
if (item == null) return null;
|
||||||
|
|
||||||
|
switch (item.runtimeType) {
|
||||||
|
case Comment:
|
||||||
|
return _toBuildableComment(item as Comment);
|
||||||
|
case Story:
|
||||||
|
return _toBuildableStory(item as Story);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<BuildableComment?> _toBuildableComment(Comment? comment) async {
|
static Future<BuildableComment?> _toBuildableComment(Comment? comment) async {
|
||||||
if (comment == null) return null;
|
if (comment == null) return null;
|
||||||
|
|
||||||
@ -395,6 +410,25 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return buildableComment;
|
return buildableComment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<BuildableStory?> _toBuildableStory(Story? story) async {
|
||||||
|
if (story == null) {
|
||||||
|
return null;
|
||||||
|
} else if (story.text.isEmpty) {
|
||||||
|
return BuildableStory.fromTitleOnlyStory(story);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LinkifyElement> elements =
|
||||||
|
await compute<String, List<LinkifyElement>>(
|
||||||
|
LinkifierUtil.linkify,
|
||||||
|
story.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildableStory buildableStory =
|
||||||
|
BuildableStory.fromStory(story, elements: elements);
|
||||||
|
|
||||||
|
return buildableStory;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _streamSubscription?.cancel();
|
await _streamSubscription?.cancel();
|
||||||
|
@ -15,19 +15,19 @@ class SearchCubit extends Cubit<SearchState> {
|
|||||||
|
|
||||||
final SearchRepository _searchRepository;
|
final SearchRepository _searchRepository;
|
||||||
|
|
||||||
StreamSubscription<Story>? streamSubscription;
|
StreamSubscription<Item>? streamSubscription;
|
||||||
|
|
||||||
void search(String query) {
|
void search(String query) {
|
||||||
streamSubscription?.cancel();
|
streamSubscription?.cancel();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
results: <Story>[],
|
results: <Item>[],
|
||||||
status: SearchStatus.loading,
|
status: SearchStatus.loading,
|
||||||
params: state.params.copyWith(query: query, page: 0),
|
params: state.params.copyWith(query: query, page: 0),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
streamSubscription =
|
streamSubscription =
|
||||||
_searchRepository.search(params: state.params).listen(_onStoryFetched)
|
_searchRepository.search(params: state.params).listen(_onItemFetched)
|
||||||
..onDone(() {
|
..onDone(() {
|
||||||
emit(state.copyWith(status: SearchStatus.loaded));
|
emit(state.copyWith(status: SearchStatus.loaded));
|
||||||
});
|
});
|
||||||
@ -43,7 +43,7 @@ class SearchCubit extends Cubit<SearchState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
streamSubscription =
|
streamSubscription =
|
||||||
_searchRepository.search(params: state.params).listen(_onStoryFetched)
|
_searchRepository.search(params: state.params).listen(_onItemFetched)
|
||||||
..onDone(() {
|
..onDone(() {
|
||||||
emit(state.copyWith(status: SearchStatus.loaded));
|
emit(state.copyWith(status: SearchStatus.loaded));
|
||||||
});
|
});
|
||||||
@ -69,6 +69,8 @@ class SearchCubit extends Cubit<SearchState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void removeFilter<T extends SearchFilter>() {
|
void removeFilter<T extends SearchFilter>() {
|
||||||
|
if (state.params.contains<T>() == false) return;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
params: state.params.copyWithFilterRemoved<T>(),
|
params: state.params.copyWithFilterRemoved<T>(),
|
||||||
@ -78,6 +80,16 @@ class SearchCubit extends Cubit<SearchState> {
|
|||||||
search(state.params.query);
|
search(state.params.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onToggled(TypeTagFilter filter) {
|
||||||
|
if (state.params.contains<TypeTagFilter>() &&
|
||||||
|
state.params.get<TypeTagFilter>() == filter) {
|
||||||
|
removeFilter<TypeTagFilter>();
|
||||||
|
} else {
|
||||||
|
removeFilter<TypeTagFilter>();
|
||||||
|
addFilter<TypeTagFilter>(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onSortToggled() {
|
void onSortToggled() {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -90,10 +102,44 @@ class SearchCubit extends Cubit<SearchState> {
|
|||||||
search(state.params.query);
|
search(state.params.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStoryFetched(Story story) {
|
void onDateTimeRangeUpdated(DateTime start, DateTime end) {
|
||||||
|
final DateTime updatedStart = start.copyWith(
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
microsecond: 0,
|
||||||
|
);
|
||||||
|
final DateTime updatedEnd = end.copyWith(
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
microsecond: 0,
|
||||||
|
);
|
||||||
|
final DateTime? existingStart =
|
||||||
|
state.params.get<DateTimeRangeFilter>()?.startTime;
|
||||||
|
final DateTime? existingEnd =
|
||||||
|
state.params.get<DateTimeRangeFilter>()?.endTime;
|
||||||
|
|
||||||
|
if (existingStart == updatedStart && existingEnd == updatedEnd) return;
|
||||||
|
|
||||||
|
addFilter(
|
||||||
|
DateTimeRangeFilter(
|
||||||
|
startTime: updatedStart,
|
||||||
|
endTime: updatedEnd,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPostedByChanged(String? username) {
|
||||||
|
if (username == null) {
|
||||||
|
removeFilter<PostedByFilter>();
|
||||||
|
} else {
|
||||||
|
addFilter(PostedByFilter(author: username));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemFetched(Item item) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
results: List<Story>.from(state.results)..add(story),
|
results: List<Item>.from(state.results)..add(item),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,15 +16,15 @@ class SearchState extends Equatable {
|
|||||||
|
|
||||||
SearchState.init()
|
SearchState.init()
|
||||||
: status = SearchStatus.initial,
|
: status = SearchStatus.initial,
|
||||||
results = <Story>[],
|
results = <Item>[],
|
||||||
params = SearchParams.init();
|
params = SearchParams.init();
|
||||||
|
|
||||||
final List<Story> results;
|
final List<Item> results;
|
||||||
final SearchStatus status;
|
final SearchStatus status;
|
||||||
final SearchParams params;
|
final SearchParams params;
|
||||||
|
|
||||||
SearchState copyWith({
|
SearchState copyWith({
|
||||||
List<Story>? results,
|
List<Item>? results,
|
||||||
SearchStatus? status,
|
SearchStatus? status,
|
||||||
SearchParams? params,
|
SearchParams? params,
|
||||||
}) {
|
}) {
|
||||||
|
@ -20,20 +20,20 @@ class TimeMachineCubit extends Cubit<TimeMachineState> {
|
|||||||
final CommentCache _commentCache;
|
final CommentCache _commentCache;
|
||||||
|
|
||||||
Future<void> activateTimeMachine(Comment comment) async {
|
Future<void> activateTimeMachine(Comment comment) async {
|
||||||
emit(state.copyWith(parents: <Comment>[]));
|
emit(state.copyWith(ancestors: <Comment>[]));
|
||||||
|
|
||||||
final List<Comment> parents = <Comment>[];
|
final List<Comment> ancestors = <Comment>[];
|
||||||
Comment? parent = _commentCache.getComment(comment.parent);
|
Comment? parent = _commentCache.getComment(comment.parent);
|
||||||
parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
|
parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
|
||||||
|
|
||||||
while (parent != null) {
|
while (parent != null) {
|
||||||
parents.insert(0, parent);
|
ancestors.insert(0, parent);
|
||||||
|
|
||||||
final int parentId = parent.parent;
|
final int parentId = parent.parent;
|
||||||
parent = _commentCache.getComment(parentId);
|
parent = _commentCache.getComment(parentId);
|
||||||
parent ??= await _sembastRepository.getCachedComment(id: parentId);
|
parent ??= await _sembastRepository.getCachedComment(id: parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(parents: parents));
|
emit(state.copyWith(ancestors: ancestors));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
part of 'time_machine_cubit.dart';
|
part of 'time_machine_cubit.dart';
|
||||||
|
|
||||||
class TimeMachineState extends Equatable {
|
class TimeMachineState extends Equatable {
|
||||||
const TimeMachineState({required this.parents});
|
const TimeMachineState({required this.ancestors});
|
||||||
|
|
||||||
TimeMachineState.init() : parents = <Comment>[];
|
TimeMachineState.init() : ancestors = <Comment>[];
|
||||||
|
|
||||||
final List<Comment> parents;
|
final List<Comment> ancestors;
|
||||||
|
|
||||||
TimeMachineState copyWith({
|
TimeMachineState copyWith({
|
||||||
List<Comment>? parents,
|
List<Comment>? ancestors,
|
||||||
}) {
|
}) {
|
||||||
return TimeMachineState(parents: parents ?? this.parents);
|
return TimeMachineState(ancestors: ancestors ?? this.ancestors);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[parents];
|
List<Object?> get props => <Object?>[ancestors];
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
|
||||||
import 'package:hacki/screens/item/models/models.dart';
|
import 'package:hacki/screens/item/models/models.dart';
|
||||||
import 'package:hacki/screens/item/widgets/widgets.dart';
|
import 'package:hacki/screens/item/widgets/widgets.dart';
|
||||||
import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs;
|
import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs;
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
extension StateExtension on State {
|
extension StateExtension on State {
|
||||||
@ -59,11 +56,11 @@ extension StateExtension on State {
|
|||||||
context.read<BlocklistCubit>().state.blocklist.contains(item.by);
|
context.read<BlocklistCubit>().state.blocklist.contains(item.by);
|
||||||
showModalBottomSheet<MenuAction>(
|
showModalBottomSheet<MenuAction>(
|
||||||
context: context,
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return MorePopupMenu(
|
return MorePopupMenu(
|
||||||
item: item,
|
item: item,
|
||||||
isBlocked: isBlocked,
|
isBlocked: isBlocked,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
onLoginTapped: onLoginTapped,
|
onLoginTapped: onLoginTapped,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -74,6 +71,9 @@ extension StateExtension on State {
|
|||||||
break;
|
break;
|
||||||
case MenuAction.downvote:
|
case MenuAction.downvote:
|
||||||
break;
|
break;
|
||||||
|
case MenuAction.fav:
|
||||||
|
onFavTapped(item);
|
||||||
|
break;
|
||||||
case MenuAction.share:
|
case MenuAction.share:
|
||||||
onShareTapped(item, rect);
|
onShareTapped(item, rect);
|
||||||
break;
|
break;
|
||||||
@ -90,24 +90,13 @@ extension StateExtension on State {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onStoryLinkTapped(String link) async {
|
void onFavTapped(Item item) {
|
||||||
final int? id = link.itemId;
|
final FavCubit favCubit = context.read<FavCubit>();
|
||||||
if (id != null) {
|
final bool isFav = favCubit.state.favIds.contains(item.id);
|
||||||
await locator
|
if (isFav) {
|
||||||
.get<StoriesRepository>()
|
favCubit.removeFav(item.id);
|
||||||
.fetchItem(id: id)
|
|
||||||
.then((Item? item) {
|
|
||||||
if (mounted) {
|
|
||||||
if (item != null) {
|
|
||||||
HackiApp.navigatorKey.currentState!.pushNamed(
|
|
||||||
ItemScreen.routeName,
|
|
||||||
arguments: ItemScreenArgs(item: item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
LinkUtil.launch(link);
|
favCubit.addFav(item.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,17 +220,11 @@ extension StateExtension on State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onLoginTapped() {
|
void onLoginTapped() {
|
||||||
final TextEditingController usernameController = TextEditingController();
|
|
||||||
final TextEditingController passwordController = TextEditingController();
|
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return LoginDialog(
|
return const LoginDialog();
|
||||||
usernameController: usernameController,
|
|
||||||
passwordController: passwordController,
|
|
||||||
showSnackBar: showSnackBar,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,8 @@ extension WidgetModifier on Widget {
|
|||||||
Widget contextMenuBuilder(
|
Widget contextMenuBuilder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
EditableTextState editableTextState, {
|
EditableTextState editableTextState, {
|
||||||
required BuildableComment comment,
|
required Item item,
|
||||||
}) {
|
}) {
|
||||||
final Iterable<EmphasisElement> emphasisElements =
|
|
||||||
comment.elements.whereType<EmphasisElement>();
|
|
||||||
final int start = editableTextState.textEditingValue.selection.base.offset;
|
final int start = editableTextState.textEditingValue.selection.base.offset;
|
||||||
final int end = editableTextState.textEditingValue.selection.end;
|
final int end = editableTextState.textEditingValue.selection.end;
|
||||||
|
|
||||||
@ -27,23 +25,28 @@ extension WidgetModifier on Widget {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (start != -1 && end != -1) {
|
if (start != -1 && end != -1) {
|
||||||
String selectedText = comment.text.substring(start, end);
|
String selectedText = item.text.substring(start, end);
|
||||||
|
|
||||||
|
if (item is Buildable) {
|
||||||
|
final Iterable<EmphasisElement> emphasisElements =
|
||||||
|
(item as Buildable).elements.whereType<EmphasisElement>();
|
||||||
|
|
||||||
int count = 1;
|
int count = 1;
|
||||||
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
||||||
final int s = (start + count * 2).clamp(0, comment.text.length);
|
final int s = (start + count * 2).clamp(0, item.text.length);
|
||||||
final int e = (end + count * 2).clamp(0, comment.text.length);
|
final int e = (end + count * 2).clamp(0, item.text.length);
|
||||||
selectedText = comment.text.substring(s, e);
|
selectedText = item.text.substring(s, e);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
count = 1;
|
count = 1;
|
||||||
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
||||||
final int s = (start - count * 2).clamp(0, comment.text.length);
|
final int s = (start - count * 2).clamp(0, item.text.length);
|
||||||
final int e = (end - count * 2).clamp(0, comment.text.length);
|
final int e = (end - count * 2).clamp(0, item.text.length);
|
||||||
selectedText = comment.text.substring(s, e);
|
selectedText = item.text.substring(s, e);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.addAll(<ContextMenuButtonItem>[
|
items.addAll(<ContextMenuButtonItem>[
|
||||||
ContextMenuButtonItem(
|
ContextMenuButtonItem(
|
||||||
|
5
lib/models/item/buildable.dart
Normal file
5
lib/models/item/buildable.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
|
||||||
|
|
||||||
|
mixin Buildable {
|
||||||
|
List<LinkifyElement> get elements;
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:hacki/models/comment.dart';
|
import 'package:hacki/models/item/buildable.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/item/comment.dart';
|
||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
/// [BuildableComment] is a subtype of [Comment] which stores
|
/// [BuildableComment] is a subtype of [Comment] which stores
|
||||||
/// the corresponding [LinkifyElement] for faster widget building.
|
/// the corresponding [LinkifyElement] for faster widget building.
|
||||||
class BuildableComment extends Comment {
|
class BuildableComment extends Comment with Buildable {
|
||||||
BuildableComment({
|
BuildableComment({
|
||||||
required super.id,
|
required super.id,
|
||||||
required super.time,
|
required super.time,
|
||||||
@ -33,5 +33,6 @@ class BuildableComment extends Comment {
|
|||||||
level: comment.level,
|
level: comment.level,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
final List<LinkifyElement> elements;
|
final List<LinkifyElement> elements;
|
||||||
}
|
}
|
46
lib/models/item/buildable_story.dart
Normal file
46
lib/models/item/buildable_story.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:hacki/models/item/buildable.dart';
|
||||||
|
import 'package:hacki/models/item/story.dart';
|
||||||
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
|
/// [BuildableStory] is a subtype of [Story] which stores
|
||||||
|
/// the corresponding [LinkifyElement] for faster widget building.
|
||||||
|
class BuildableStory extends Story with Buildable {
|
||||||
|
const BuildableStory({
|
||||||
|
required super.id,
|
||||||
|
required super.time,
|
||||||
|
required super.score,
|
||||||
|
required super.by,
|
||||||
|
required super.text,
|
||||||
|
required super.kids,
|
||||||
|
required super.descendants,
|
||||||
|
required super.title,
|
||||||
|
required super.type,
|
||||||
|
required super.url,
|
||||||
|
required super.parts,
|
||||||
|
required this.elements,
|
||||||
|
});
|
||||||
|
|
||||||
|
BuildableStory.fromStory(Story story, {required this.elements})
|
||||||
|
: super(
|
||||||
|
id: story.id,
|
||||||
|
time: story.time,
|
||||||
|
score: story.score,
|
||||||
|
by: story.by,
|
||||||
|
text: story.text,
|
||||||
|
kids: story.kids,
|
||||||
|
descendants: story.descendants,
|
||||||
|
title: story.title,
|
||||||
|
type: story.type,
|
||||||
|
url: story.url,
|
||||||
|
parts: story.parts,
|
||||||
|
);
|
||||||
|
|
||||||
|
BuildableStory.fromTitleOnlyStory(Story story)
|
||||||
|
: this.fromStory(
|
||||||
|
story,
|
||||||
|
elements: const <LinkifyElement>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final List<LinkifyElement> elements;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item/item.dart';
|
||||||
|
|
||||||
class Comment extends Item {
|
class Comment extends Item {
|
||||||
Comment({
|
Comment({
|
@ -1,8 +1,15 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:hacki/extensions/date_time_extension.dart';
|
import 'package:hacki/extensions/date_time_extension.dart';
|
||||||
import 'package:hacki/models/comment.dart';
|
import 'package:hacki/models/item/comment.dart';
|
||||||
import 'package:hacki/models/poll_option.dart';
|
import 'package:hacki/models/item/poll_option.dart';
|
||||||
import 'package:hacki/models/story.dart';
|
import 'package:hacki/models/item/story.dart';
|
||||||
|
|
||||||
|
export 'buildable.dart';
|
||||||
|
export 'buildable_comment.dart';
|
||||||
|
export 'buildable_story.dart';
|
||||||
|
export 'comment.dart';
|
||||||
|
export 'poll_option.dart';
|
||||||
|
export 'story.dart';
|
||||||
|
|
||||||
/// [Item] is the base type of [Story], [Comment] and [PollOption].
|
/// [Item] is the base type of [Story], [Comment] and [PollOption].
|
||||||
class Item extends Equatable {
|
class Item extends Equatable {
|
@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item/item.dart';
|
||||||
|
|
||||||
class PollOption extends Item {
|
class PollOption extends Item {
|
||||||
const PollOption({
|
const PollOption({
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item/item.dart';
|
||||||
|
|
||||||
class Story extends Item {
|
class Story extends Item {
|
||||||
const Story({
|
const Story({
|
@ -1,14 +1,10 @@
|
|||||||
export 'buildable_comment.dart';
|
|
||||||
export 'comment.dart';
|
|
||||||
export 'comments_order.dart';
|
export 'comments_order.dart';
|
||||||
export 'fetch_mode.dart';
|
export 'fetch_mode.dart';
|
||||||
export 'font.dart';
|
export 'font.dart';
|
||||||
export 'font_size.dart';
|
export 'font_size.dart';
|
||||||
export 'item.dart';
|
export 'item/item.dart';
|
||||||
export 'poll_option.dart';
|
|
||||||
export 'post_data.dart';
|
export 'post_data.dart';
|
||||||
export 'preference.dart';
|
export 'preference.dart';
|
||||||
export 'search_params.dart';
|
export 'search_params.dart';
|
||||||
export 'story.dart';
|
|
||||||
export 'story_type.dart';
|
export 'story_type.dart';
|
||||||
export 'user.dart';
|
export 'user.dart';
|
||||||
|
@ -8,8 +8,19 @@ abstract class NumericFilter extends SearchFilter {}
|
|||||||
|
|
||||||
abstract class TagFilter extends SearchFilter {}
|
abstract class TagFilter extends SearchFilter {}
|
||||||
|
|
||||||
|
abstract class TypeTagFilter extends TagFilter {
|
||||||
|
static List<TypeTagFilter> all = <TypeTagFilter>[
|
||||||
|
const StoryFilter(),
|
||||||
|
const PollFilter(),
|
||||||
|
const CommentFilter(),
|
||||||
|
const FrontPageFilter(),
|
||||||
|
const AskHnFilter(),
|
||||||
|
const ShowHnFilter(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
class DateTimeRangeFilter implements NumericFilter {
|
class DateTimeRangeFilter implements NumericFilter {
|
||||||
DateTimeRangeFilter({
|
const DateTimeRangeFilter({
|
||||||
this.startTime,
|
this.startTime,
|
||||||
this.endTime,
|
this.endTime,
|
||||||
});
|
});
|
||||||
@ -37,7 +48,7 @@ class DateTimeRangeFilter implements NumericFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PostedByFilter implements TagFilter {
|
class PostedByFilter implements TagFilter {
|
||||||
PostedByFilter({required this.author});
|
const PostedByFilter({required this.author});
|
||||||
|
|
||||||
final String author;
|
final String author;
|
||||||
|
|
||||||
@ -47,8 +58,8 @@ class PostedByFilter implements TagFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FrontPageFilter implements TagFilter {
|
class FrontPageFilter implements TypeTagFilter {
|
||||||
FrontPageFilter();
|
const FrontPageFilter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get query {
|
String get query {
|
||||||
@ -56,8 +67,8 @@ class FrontPageFilter implements TagFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShowHnFilter implements TagFilter {
|
class ShowHnFilter implements TypeTagFilter {
|
||||||
ShowHnFilter();
|
const ShowHnFilter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get query {
|
String get query {
|
||||||
@ -65,8 +76,8 @@ class ShowHnFilter implements TagFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AskHnFilter implements TagFilter {
|
class AskHnFilter implements TypeTagFilter {
|
||||||
AskHnFilter();
|
const AskHnFilter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get query {
|
String get query {
|
||||||
@ -74,8 +85,8 @@ class AskHnFilter implements TagFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PollFilter implements TagFilter {
|
class PollFilter implements TypeTagFilter {
|
||||||
PollFilter();
|
const PollFilter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get query {
|
String get query {
|
||||||
@ -83,8 +94,8 @@ class PollFilter implements TagFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoryFilter implements TagFilter {
|
class StoryFilter implements TypeTagFilter {
|
||||||
StoryFilter();
|
const StoryFilter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get query {
|
String get query {
|
||||||
@ -92,8 +103,17 @@ class StoryFilter implements TagFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CommentFilter implements TypeTagFilter {
|
||||||
|
const CommentFilter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get query {
|
||||||
|
return 'comment';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CombinedFilter implements TagFilter {
|
class CombinedFilter implements TagFilter {
|
||||||
CombinedFilter({required this.filters});
|
const CombinedFilter({required this.filters});
|
||||||
|
|
||||||
final List<TagFilter> filters;
|
final List<TagFilter> filters;
|
||||||
|
|
||||||
|
@ -70,7 +70,6 @@ class SearchParams extends Equatable {
|
|||||||
filters.whereType<NumericFilter>();
|
filters.whereType<NumericFilter>();
|
||||||
final List<TagFilter> tagFilters = <TagFilter>[
|
final List<TagFilter> tagFilters = <TagFilter>[
|
||||||
...filters.whereType<TagFilter>(),
|
...filters.whereType<TagFilter>(),
|
||||||
CombinedFilter(filters: <TagFilter>[StoryFilter(), PollFilter()]),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (numericFilters.isNotEmpty) {
|
if (numericFilters.isNotEmpty) {
|
||||||
|
@ -13,7 +13,7 @@ class SearchRepository {
|
|||||||
|
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
|
|
||||||
Stream<Story> search({
|
Stream<Item> search({
|
||||||
required SearchParams params,
|
required SearchParams params,
|
||||||
}) async* {
|
}) async* {
|
||||||
final String url = '$_baseUrl${params.filteredQuery}';
|
final String url = '$_baseUrl${params.filteredQuery}';
|
||||||
@ -36,22 +36,37 @@ class SearchRepository {
|
|||||||
final int score = hit['points'] as int? ?? 0;
|
final int score = hit['points'] as int? ?? 0;
|
||||||
final int descendants = hit['num_comments'] as int? ?? 0;
|
final int descendants = hit['num_comments'] as int? ?? 0;
|
||||||
|
|
||||||
// Getting rid of comments, only keeping stories for convenience.
|
|
||||||
// Don't judge me.
|
|
||||||
if (title.isEmpty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String url = hit['url'] as String? ?? '';
|
final String url = hit['url'] as String? ?? '';
|
||||||
final String type =
|
final String type =
|
||||||
title.toLowerCase().contains('poll:') ? 'poll' : 'story';
|
title.toLowerCase().contains('poll:') ? 'poll' : 'story';
|
||||||
|
final int id = int.parse(hit['objectID'] as String? ?? '0');
|
||||||
|
|
||||||
|
if (title.isEmpty) {
|
||||||
|
final String text = hit['comment_text'] as String? ?? '';
|
||||||
|
final String parsedText = await compute<String, String>(
|
||||||
|
HtmlUtil.parseHtml,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
final int parentId = hit['parent_id'] as int? ?? 0;
|
||||||
|
final Comment comment = Comment(
|
||||||
|
id: id,
|
||||||
|
score: score,
|
||||||
|
time: createdAt,
|
||||||
|
by: by,
|
||||||
|
text: parsedText,
|
||||||
|
kids: const <int>[],
|
||||||
|
parent: parentId,
|
||||||
|
dead: false,
|
||||||
|
deleted: false,
|
||||||
|
level: 0,
|
||||||
|
);
|
||||||
|
yield comment;
|
||||||
|
} else {
|
||||||
final String text = hit['story_text'] as String? ?? '';
|
final String text = hit['story_text'] as String? ?? '';
|
||||||
final String parsedText = await compute<String, String>(
|
final String parsedText = await compute<String, String>(
|
||||||
HtmlUtil.parseHtml,
|
HtmlUtil.parseHtml,
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
final int id = int.parse(hit['objectID'] as String? ?? '0');
|
|
||||||
|
|
||||||
final Story story = Story(
|
final Story story = Story(
|
||||||
descendants: descendants,
|
descendants: descendants,
|
||||||
id: id,
|
id: id,
|
||||||
@ -68,6 +83,7 @@ class SearchRepository {
|
|||||||
);
|
);
|
||||||
yield story;
|
yield story;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
context.read<PreferenceCubit>().state.order,
|
context.read<PreferenceCubit>().state.order,
|
||||||
)..init(
|
)..init(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||||
targetParents: args.targetComments,
|
targetAncestors: args.targetComments,
|
||||||
useCommentCache: args.useCommentCache,
|
useCommentCache: args.useCommentCache,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -116,7 +116,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
context.read<PreferenceCubit>().state.order,
|
context.read<PreferenceCubit>().state.order,
|
||||||
)..init(
|
)..init(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||||
targetParents: args.targetComments,
|
targetAncestors: args.targetComments,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -289,8 +289,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
topPadding: topPadding,
|
topPadding: topPadding,
|
||||||
splitViewEnabled: widget.splitViewEnabled,
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
onLoginTapped: onLoginTapped,
|
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -365,8 +363,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
topPadding: topPadding,
|
topPadding: topPadding,
|
||||||
splitViewEnabled: widget.splitViewEnabled,
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
onLoginTapped: onLoginTapped,
|
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
),
|
),
|
||||||
bottomSheet: ReplyBox(
|
bottomSheet: ReplyBox(
|
||||||
@ -453,7 +449,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.av_timer),
|
leading: const Icon(Icons.av_timer),
|
||||||
title: const Text('View parents'),
|
title: const Text('View ancestors'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
onTimeMachineActivated(comment);
|
onTimeMachineActivated(comment);
|
||||||
@ -497,7 +493,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
size: size,
|
size: size,
|
||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
widthFactor: widthFactor,
|
widthFactor: widthFactor,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
enum MenuAction {
|
enum MenuAction {
|
||||||
upvote,
|
upvote,
|
||||||
downvote,
|
downvote,
|
||||||
|
fav,
|
||||||
share,
|
share,
|
||||||
block,
|
block,
|
||||||
flag,
|
flag,
|
||||||
|
@ -2,25 +2,21 @@ 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/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.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';
|
||||||
|
|
||||||
class LoginDialog extends StatelessWidget {
|
class LoginDialog extends StatefulWidget {
|
||||||
const LoginDialog({
|
const LoginDialog({super.key});
|
||||||
super.key,
|
|
||||||
required this.usernameController,
|
|
||||||
required this.passwordController,
|
|
||||||
required this.showSnackBar,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TextEditingController usernameController;
|
@override
|
||||||
final TextEditingController passwordController;
|
State<LoginDialog> createState() => _LoginDialogState();
|
||||||
final void Function({
|
}
|
||||||
required String content,
|
|
||||||
VoidCallback? action,
|
class _LoginDialogState extends State<LoginDialog> {
|
||||||
String? label,
|
final TextEditingController usernameController = TextEditingController();
|
||||||
}) showSnackBar;
|
final TextEditingController passwordController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -90,9 +86,10 @@ class LoginDialog extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: Dimens.pt18,
|
left: Dimens.pt18,
|
||||||
|
right: Dimens.pt6,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
Constants.errorMessage,
|
Constants.loginErrorMessage,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
fontSize: TextDimens.pt12,
|
fontSize: TextDimens.pt12,
|
||||||
|
@ -25,8 +25,6 @@ class MainView extends StatelessWidget {
|
|||||||
required this.topPadding,
|
required this.topPadding,
|
||||||
required this.splitViewEnabled,
|
required this.splitViewEnabled,
|
||||||
required this.onMoreTapped,
|
required this.onMoreTapped,
|
||||||
required this.onStoryLinkTapped,
|
|
||||||
required this.onLoginTapped,
|
|
||||||
required this.onRightMoreTapped,
|
required this.onRightMoreTapped,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -38,8 +36,6 @@ class MainView extends StatelessWidget {
|
|||||||
final double topPadding;
|
final double topPadding;
|
||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||||
final ValueChanged<String> onStoryLinkTapped;
|
|
||||||
final VoidCallback onLoginTapped;
|
|
||||||
final ValueChanged<Comment> onRightMoreTapped;
|
final ValueChanged<Comment> onRightMoreTapped;
|
||||||
|
|
||||||
static const int _loadingIndicatorOpacityAnimationDuration = 300;
|
static const int _loadingIndicatorOpacityAnimationDuration = 300;
|
||||||
@ -123,8 +119,6 @@ class MainView extends StatelessWidget {
|
|||||||
topPadding: topPadding,
|
topPadding: topPadding,
|
||||||
splitViewEnabled: splitViewEnabled,
|
splitViewEnabled: splitViewEnabled,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
onLoginTapped: onLoginTapped,
|
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
);
|
);
|
||||||
} else if (index == state.comments.length + 1) {
|
} else if (index == state.comments.length + 1) {
|
||||||
@ -149,8 +143,6 @@ class MainView extends StatelessWidget {
|
|||||||
child: CommentTile(
|
child: CommentTile(
|
||||||
comment: comment,
|
comment: comment,
|
||||||
level: comment.level,
|
level: comment.level,
|
||||||
myUsername:
|
|
||||||
authState.isLoggedIn ? authState.username : null,
|
|
||||||
opUsername: state.item.by,
|
opUsername: state.item.by,
|
||||||
fetchMode: state.fetchMode,
|
fetchMode: state.fetchMode,
|
||||||
onReplyTapped: (Comment cmt) {
|
onReplyTapped: (Comment cmt) {
|
||||||
@ -177,7 +169,6 @@ class MainView extends StatelessWidget {
|
|||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
},
|
},
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -224,8 +215,6 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
required this.topPadding,
|
required this.topPadding,
|
||||||
required this.splitViewEnabled,
|
required this.splitViewEnabled,
|
||||||
required this.onMoreTapped,
|
required this.onMoreTapped,
|
||||||
required this.onStoryLinkTapped,
|
|
||||||
required this.onLoginTapped,
|
|
||||||
required this.onRightMoreTapped,
|
required this.onRightMoreTapped,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -238,8 +227,6 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
final double topPadding;
|
final double topPadding;
|
||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||||
final ValueChanged<String> onStoryLinkTapped;
|
|
||||||
final VoidCallback onLoginTapped;
|
|
||||||
final ValueChanged<Comment> onRightMoreTapped;
|
final ValueChanged<Comment> onRightMoreTapped;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -391,32 +378,8 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: Dimens.pt10,
|
horizontal: Dimens.pt10,
|
||||||
),
|
),
|
||||||
child: SelectableLinkify(
|
child: ItemText(
|
||||||
text: state.item.text,
|
item: state.item,
|
||||||
textScaleFactor:
|
|
||||||
MediaQuery.of(context).textScaleFactor,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: context
|
|
||||||
.read<PreferenceCubit>()
|
|
||||||
.state
|
|
||||||
.fontSize
|
|
||||||
.fontSize,
|
|
||||||
),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
fontSize: context
|
|
||||||
.read<PreferenceCubit>()
|
|
||||||
.state
|
|
||||||
.fontSize
|
|
||||||
.fontSize,
|
|
||||||
color: Palette.orange,
|
|
||||||
),
|
|
||||||
onOpen: (LinkableElement link) {
|
|
||||||
if (link.url.isStoryLink) {
|
|
||||||
onStoryLinkTapped(link.url);
|
|
||||||
} else {
|
|
||||||
LinkUtil.launch(link.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -428,9 +391,7 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
BlocProvider<PollCubit>(
|
BlocProvider<PollCubit>(
|
||||||
create: (BuildContext context) =>
|
create: (BuildContext context) =>
|
||||||
PollCubit(story: state.item as Story)..init(),
|
PollCubit(story: state.item as Story)..init(),
|
||||||
child: PollView(
|
child: const PollView(),
|
||||||
onLoginTapped: onLoginTapped,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
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:flutter_feather_icons/flutter_feather_icons.dart';
|
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||||
@ -6,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/item/models/models.dart';
|
import 'package:hacki/screens/item/models/models.dart';
|
||||||
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.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';
|
||||||
@ -15,15 +18,24 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.isBlocked,
|
required this.isBlocked,
|
||||||
required this.onStoryLinkTapped,
|
|
||||||
required this.onLoginTapped,
|
required this.onLoginTapped,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Item item;
|
final Item item;
|
||||||
final bool isBlocked;
|
final bool isBlocked;
|
||||||
final ValueChanged<String> onStoryLinkTapped;
|
|
||||||
final VoidCallback onLoginTapped;
|
final VoidCallback onLoginTapped;
|
||||||
|
|
||||||
|
static double? _cachedStoryHeight;
|
||||||
|
static double? _cachedCommentHeight;
|
||||||
|
|
||||||
|
static double get storyHeight {
|
||||||
|
return _cachedStoryHeight ??= Platform.isIOS ? 500 : 530;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double get commentHeight {
|
||||||
|
return _cachedCommentHeight ??= Platform.isIOS ? 480 : 520;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<VoteCubit>(
|
return BlocProvider<VoteCubit>(
|
||||||
@ -68,7 +80,7 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
final bool upvoted = voteState.vote == Vote.up;
|
final bool upvoted = voteState.vote == Vote.up;
|
||||||
final bool downvoted = voteState.vote == Vote.down;
|
final bool downvoted = voteState.vote == Vote.down;
|
||||||
return Container(
|
return Container(
|
||||||
height: item is Comment ? 430 : 450,
|
height: item is Comment ? commentHeight : storyHeight,
|
||||||
color: Theme.of(context).canvasColor,
|
color: Theme.of(context).canvasColor,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Palette.transparent,
|
color: Palette.transparent,
|
||||||
@ -88,6 +100,7 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
state.user.description,
|
state.user.description,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) => AlertDialog(
|
builder: (BuildContext context) => AlertDialog(
|
||||||
@ -112,15 +125,19 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
linkStyle: const TextStyle(
|
linkStyle: const TextStyle(
|
||||||
color: Palette.orange,
|
color: Palette.orange,
|
||||||
),
|
),
|
||||||
onOpen: (LinkableElement link) {
|
onOpen: (LinkableElement link) =>
|
||||||
if (link.url.isStoryLink) {
|
LinkUtil.launch,
|
||||||
onStoryLinkTapped.call(link.url);
|
|
||||||
} else {
|
|
||||||
LinkUtil.launch(link.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
onSearchUserTapped(context);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Search',
|
||||||
|
),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
@ -163,6 +180,24 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: context.read<VoteCubit>().downvote,
|
onTap: context.read<VoteCubit>().downvote,
|
||||||
),
|
),
|
||||||
|
BlocBuilder<FavCubit, FavState>(
|
||||||
|
builder: (BuildContext context, FavState state) {
|
||||||
|
final bool isFav = state.favIds.contains(item.id);
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isFav ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: isFav ? Palette.orange : null,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
isFav ? 'Unfavorite' : 'Favorite',
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.pop(
|
||||||
|
context,
|
||||||
|
MenuAction.fav,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(FeatherIcons.share),
|
leading: const Icon(FeatherIcons.share),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@ -213,4 +248,45 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onSearchUserTapped(BuildContext context) {
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return BlocProvider<SearchCubit>(
|
||||||
|
create: (_) => SearchCubit()
|
||||||
|
..addFilter(
|
||||||
|
PostedByFilter(
|
||||||
|
author: item.by,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
height: MediaQuery.of(context).size.height - Dimens.pt120,
|
||||||
|
color: Theme.of(context).canvasColor,
|
||||||
|
margin: const EdgeInsets.only(top: Dimens.pt12),
|
||||||
|
child: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
height: Dimens.pt4,
|
||||||
|
width: Dimens.pt24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Palette.grey,
|
||||||
|
borderRadius: BorderRadius.circular(Dimens.pt16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: SearchScreen(
|
||||||
|
fromUserDialog: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,18 @@ 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/extensions/extensions.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';
|
||||||
|
|
||||||
class PollView extends StatelessWidget {
|
class PollView extends StatefulWidget {
|
||||||
const PollView({
|
const PollView({super.key});
|
||||||
super.key,
|
|
||||||
required this.onLoginTapped,
|
|
||||||
});
|
|
||||||
|
|
||||||
final VoidCallback onLoginTapped;
|
@override
|
||||||
|
State<PollView> createState() => _PollViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PollViewState extends State<PollView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<PollCubit, PollState>(
|
return BlocBuilder<PollCubit, PollState>(
|
||||||
|
@ -5,11 +5,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item/item.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/link_util.dart';
|
|
||||||
|
|
||||||
class ReplyBox extends StatefulWidget {
|
class ReplyBox extends StatefulWidget {
|
||||||
const ReplyBox({
|
const ReplyBox({
|
||||||
@ -256,6 +255,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
void showTextPopup() {
|
void showTextPopup() {
|
||||||
final Item? replyingTo = context.read<EditCubit>().state.replyingTo;
|
final Item? replyingTo = context.read<EditCubit>().state.replyingTo;
|
||||||
|
|
||||||
|
if (replyingTo == null) return;
|
||||||
|
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
@ -280,13 +281,20 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
replyingTo?.by ?? '',
|
replyingTo.by,
|
||||||
style: const TextStyle(color: Palette.grey),
|
style: const TextStyle(
|
||||||
|
fontSize: TextDimens.pt14,
|
||||||
|
color: Palette.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (replyingTo != null)
|
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('View thread'),
|
child: const Text(
|
||||||
|
'View thread',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt14,
|
||||||
|
),
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -308,9 +316,14 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Copy all'),
|
child: const Text(
|
||||||
|
'Copy all',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt14,
|
||||||
|
),
|
||||||
|
),
|
||||||
onPressed: () => FlutterClipboard.copy(
|
onPressed: () => FlutterClipboard.copy(
|
||||||
replyingTo?.text ?? '',
|
replyingTo.text,
|
||||||
).then((_) => HapticFeedback.selectionClick()),
|
).then((_) => HapticFeedback.selectionClick()),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -334,17 +347,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
top: Dimens.pt6,
|
top: Dimens.pt6,
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: SelectableLinkify(
|
child: ItemText(
|
||||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
item: replyingTo,
|
||||||
textScaleFactor:
|
|
||||||
MediaQuery.of(context).textScaleFactor,
|
|
||||||
linkStyle: const TextStyle(
|
|
||||||
fontSize: TextDimens.pt15,
|
|
||||||
color: Palette.orange,
|
|
||||||
),
|
|
||||||
onOpen: (LinkableElement link) =>
|
|
||||||
LinkUtil.launch(link.url),
|
|
||||||
text: replyingTo?.text ?? '',
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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/blocs/blocs.dart';
|
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
@ -14,14 +13,12 @@ class TimeMachineDialog extends StatelessWidget {
|
|||||||
required this.size,
|
required this.size,
|
||||||
required this.deviceType,
|
required this.deviceType,
|
||||||
required this.widthFactor,
|
required this.widthFactor,
|
||||||
required this.onStoryLinkTapped,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Comment comment;
|
final Comment comment;
|
||||||
final Size size;
|
final Size size;
|
||||||
final DeviceScreenType deviceType;
|
final DeviceScreenType deviceType;
|
||||||
final double widthFactor;
|
final double widthFactor;
|
||||||
final void Function(String) onStoryLinkTapped;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -52,7 +49,7 @@ class TimeMachineDialog extends StatelessWidget {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: Dimens.pt8,
|
width: Dimens.pt8,
|
||||||
),
|
),
|
||||||
const Text('Parents:'),
|
const Text('Ancestors:'),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@ -67,12 +64,10 @@ class TimeMachineDialog extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
for (final Comment c in state.parents) ...<Widget>[
|
for (final Comment c
|
||||||
|
in state.ancestors) ...<Widget>[
|
||||||
CommentTile(
|
CommentTile(
|
||||||
comment: c,
|
comment: c,
|
||||||
myUsername:
|
|
||||||
context.read<AuthBloc>().state.username,
|
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
actionable: false,
|
actionable: false,
|
||||||
fetchMode: FetchMode.eager,
|
fetchMode: FetchMode.eager,
|
||||||
),
|
),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.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/config/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
@ -34,15 +35,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
|
|
||||||
PageType pageType = PageType.notification;
|
PageType pageType = PageType.notification;
|
||||||
|
|
||||||
final List<String> magicWords = <String>[
|
|
||||||
'to be a lord.',
|
|
||||||
'to conquer the world.',
|
|
||||||
'to be over the rainbow!',
|
|
||||||
'to bless humanity with long-lasting peace.',
|
|
||||||
'to save the world',
|
|
||||||
'to infinity and beyond!',
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -56,7 +48,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
final String magicWord = (magicWords..shuffle()).first;
|
|
||||||
return BlocBuilder<AuthBloc, AuthState>(
|
return BlocBuilder<AuthBloc, AuthState>(
|
||||||
builder: (BuildContext context, AuthState authState) {
|
builder: (BuildContext context, AuthState authState) {
|
||||||
return BlocConsumer<NotificationCubit, NotificationState>(
|
return BlocConsumer<NotificationCubit, NotificationState>(
|
||||||
@ -238,9 +229,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
),
|
),
|
||||||
Settings(
|
Settings(
|
||||||
authState: authState,
|
authState: authState,
|
||||||
magicWord: magicWord,
|
magicWord: Constants.magicWord,
|
||||||
pageType: pageType,
|
pageType: pageType,
|
||||||
onLoginTapped: onLoginTapped,
|
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
|
@ -32,13 +32,11 @@ class Settings extends StatefulWidget {
|
|||||||
required this.authState,
|
required this.authState,
|
||||||
required this.magicWord,
|
required this.magicWord,
|
||||||
required this.pageType,
|
required this.pageType,
|
||||||
required this.onLoginTapped,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final AuthState authState;
|
final AuthState authState;
|
||||||
final String magicWord;
|
final String magicWord;
|
||||||
final PageType pageType;
|
final PageType pageType;
|
||||||
final VoidCallback onLoginTapped;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Settings> createState() => _SettingsState();
|
State<Settings> createState() => _SettingsState();
|
||||||
@ -69,7 +67,7 @@ class _SettingsState extends State<Settings> {
|
|||||||
if (widget.authState.isLoggedIn) {
|
if (widget.authState.isLoggedIn) {
|
||||||
onLogoutTapped();
|
onLogoutTapped();
|
||||||
} else {
|
} else {
|
||||||
widget.onLoginTapped();
|
onLoginTapped();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -12,7 +12,15 @@ import 'package:hacki/utils/utils.dart';
|
|||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
class SearchScreen extends StatefulWidget {
|
class SearchScreen extends StatefulWidget {
|
||||||
const SearchScreen({super.key});
|
const SearchScreen({
|
||||||
|
super.key,
|
||||||
|
this.fromUserDialog = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// If user gets to [SearchScreen] from user dialog on Tablet,
|
||||||
|
/// we navigate to [ItemScreen] directly instead of injecting the
|
||||||
|
/// item into [SplitViewCubit].
|
||||||
|
final bool fromUserDialog;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SearchScreenState createState() => _SearchScreenState();
|
_SearchScreenState createState() => _SearchScreenState();
|
||||||
@ -37,6 +45,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: Column(
|
body: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@ -68,18 +77,13 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: Dimens.pt8,
|
||||||
),
|
),
|
||||||
DateTimeRangeFilterChip(
|
DateTimeRangeFilterChip(
|
||||||
filter: state.params.get<DateTimeRangeFilter>(),
|
filter: state.params.get<DateTimeRangeFilter>(),
|
||||||
onDateTimeRangeUpdated:
|
onDateTimeRangeUpdated: context
|
||||||
(DateTime start, DateTime end) =>
|
.read<SearchCubit>()
|
||||||
context.read<SearchCubit>().addFilter(
|
.onDateTimeRangeUpdated,
|
||||||
DateTimeRangeFilter(
|
|
||||||
startTime: start,
|
|
||||||
endTime: end,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onDateTimeRangeRemoved: context
|
onDateTimeRangeRemoved: context
|
||||||
.read<SearchCubit>()
|
.read<SearchCubit>()
|
||||||
.removeFilter<DateTimeRangeFilter>,
|
.removeFilter<DateTimeRangeFilter>,
|
||||||
@ -87,6 +91,14 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: Dimens.pt8,
|
width: Dimens.pt8,
|
||||||
),
|
),
|
||||||
|
PostedByFilterChip(
|
||||||
|
filter: state.params.get<PostedByFilter>(),
|
||||||
|
onChanged:
|
||||||
|
context.read<SearchCubit>().onPostedByChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
CustomChip(
|
CustomChip(
|
||||||
onSelected: (_) =>
|
onSelected: (_) =>
|
||||||
context.read<SearchCubit>().onSortToggled(),
|
context.read<SearchCubit>().onSortToggled(),
|
||||||
@ -100,13 +112,9 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
in CustomDateTimeRange.values) ...<Widget>[
|
in CustomDateTimeRange.values) ...<Widget>[
|
||||||
CustomRangeFilterChip(
|
CustomRangeFilterChip(
|
||||||
range: range,
|
range: range,
|
||||||
onTap: (DateTime start, DateTime end) =>
|
onTap: context
|
||||||
context.read<SearchCubit>().addFilter(
|
.read<SearchCubit>()
|
||||||
DateTimeRangeFilter(
|
.onDateTimeRangeUpdated,
|
||||||
startTime: start,
|
|
||||||
endTime: end,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: Dimens.pt8,
|
width: Dimens.pt8,
|
||||||
@ -115,12 +123,52 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
for (final TypeTagFilter filter
|
||||||
|
in TypeTagFilter.all) ...<Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt8,
|
||||||
|
),
|
||||||
|
CustomChip(
|
||||||
|
onSelected: (_) =>
|
||||||
|
context.read<SearchCubit>().onToggled(filter),
|
||||||
|
selected: context
|
||||||
|
.read<SearchCubit>()
|
||||||
|
.state
|
||||||
|
.params
|
||||||
|
.get<TypeTagFilter>() ==
|
||||||
|
filter,
|
||||||
|
label: filter.query,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
if (state.status == SearchStatus.loading &&
|
if (state.status == SearchStatus.loading &&
|
||||||
state.results.isEmpty) ...<Widget>[
|
state.results.isEmpty) ...<Widget>[
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: Dimens.pt100,
|
height: Dimens.pt100,
|
||||||
),
|
),
|
||||||
const CustomCircularProgressIndicator(),
|
const Center(
|
||||||
|
child: CustomCircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (state.status == SearchStatus.loaded &&
|
||||||
|
state.results.isEmpty) ...<Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt100,
|
||||||
|
),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Nothing found...',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SmartRefresher(
|
child: SmartRefresher(
|
||||||
@ -160,7 +208,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
...state.results
|
...state.results
|
||||||
.map(
|
.map(
|
||||||
(Story e) => <Widget>[
|
(Item e) => <Widget>[
|
||||||
|
if (e is Story)
|
||||||
FadeIn(
|
FadeIn(
|
||||||
child: StoryTile(
|
child: StoryTile(
|
||||||
showWebPreview:
|
showWebPreview:
|
||||||
@ -170,6 +219,19 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
story: e,
|
story: e,
|
||||||
onTap: () => goToItemScreen(
|
onTap: () => goToItemScreen(
|
||||||
args: ItemScreenArgs(item: e),
|
args: ItemScreenArgs(item: e),
|
||||||
|
forceNewScreen: widget.fromUserDialog,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (e is Comment)
|
||||||
|
FadeIn(
|
||||||
|
child: CommentTile(
|
||||||
|
actionable: false,
|
||||||
|
comment: e,
|
||||||
|
fetchMode: FetchMode.eager,
|
||||||
|
onTap: () => goToItemScreen(
|
||||||
|
args: ItemScreenArgs(item: e),
|
||||||
|
forceNewScreen: widget.fromUserDialog,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -35,7 +35,7 @@ class DateTimeRangeFilterChip extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
selected: filter != null,
|
selected: filter != null,
|
||||||
label:
|
label:
|
||||||
'''from ${_formatDateTime(filter?.startTime) ?? 'START DATE'} to ${_formatDateTime(filter?.endTime) ?? 'END DATE'}''',
|
'''from ${_formatDateTime(filter?.startTime) ?? 'X'} to ${_formatDateTime(filter?.endTime) ?? 'Y'}''',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,107 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hacki/models/search_params.dart';
|
import 'package:hacki/models/search_params.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
class PostedByFilterChip extends StatelessWidget {
|
class PostedByFilterChip extends StatelessWidget {
|
||||||
const PostedByFilterChip({
|
const PostedByFilterChip({
|
||||||
super.key,
|
super.key,
|
||||||
required this.filter,
|
required this.filter,
|
||||||
|
required this.onChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final PostedByFilter? filter;
|
final PostedByFilter? filter;
|
||||||
|
final ValueChanged<String?> onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomChip(
|
return CustomChip(
|
||||||
onSelected: (bool value) {},
|
onSelected: (_) async {
|
||||||
|
final String? username = await onChipTapped(context);
|
||||||
|
if (username == filter?.author) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChanged(username);
|
||||||
|
},
|
||||||
selected: filter != null,
|
selected: filter != null,
|
||||||
label: '''posted by ${filter?.author ?? ''}''',
|
label: '''posted by ${filter?.author ?? ''}'''.trimRight(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> onChipTapped(BuildContext context) async {
|
||||||
|
final TextEditingController usernameController = TextEditingController();
|
||||||
|
|
||||||
|
if (filter?.author != null) {
|
||||||
|
usernameController.text = filter!.author;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? username = await showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return SimpleDialog(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.pt18,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: usernameController,
|
||||||
|
cursorColor: Palette.orange,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Username',
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Palette.orange),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt16,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: Dimens.pt12,
|
||||||
|
),
|
||||||
|
child: ButtonBar(
|
||||||
|
children: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, filter?.author),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, null),
|
||||||
|
child: const Text(
|
||||||
|
'Clear',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
final String text = usernameController.text.trim();
|
||||||
|
Navigator.pop(context, text.isEmpty ? null : text);
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty.all(Palette.deepOrange),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Confirm',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Palette.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return username;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,11 @@ import 'package:hacki/models/models.dart';
|
|||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.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';
|
|
||||||
|
|
||||||
class CommentTile extends StatelessWidget {
|
class CommentTile extends StatelessWidget {
|
||||||
const CommentTile({
|
const CommentTile({
|
||||||
super.key,
|
super.key,
|
||||||
required this.myUsername,
|
|
||||||
required this.comment,
|
required this.comment,
|
||||||
required this.onStoryLinkTapped,
|
|
||||||
required this.fetchMode,
|
required this.fetchMode,
|
||||||
this.onReplyTapped,
|
this.onReplyTapped,
|
||||||
this.onMoreTapped,
|
this.onMoreTapped,
|
||||||
@ -25,19 +22,22 @@ class CommentTile extends StatelessWidget {
|
|||||||
this.opUsername,
|
this.opUsername,
|
||||||
this.actionable = true,
|
this.actionable = true,
|
||||||
this.level = 0,
|
this.level = 0,
|
||||||
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? myUsername;
|
|
||||||
final String? opUsername;
|
final String? opUsername;
|
||||||
final Comment comment;
|
final Comment comment;
|
||||||
final int level;
|
final int level;
|
||||||
final bool actionable;
|
final bool actionable;
|
||||||
|
final FetchMode fetchMode;
|
||||||
|
|
||||||
final void Function(Comment)? onReplyTapped;
|
final void Function(Comment)? onReplyTapped;
|
||||||
final void Function(Comment, Rect?)? onMoreTapped;
|
final void Function(Comment, Rect?)? onMoreTapped;
|
||||||
final void Function(Comment)? onEditTapped;
|
final void Function(Comment)? onEditTapped;
|
||||||
final void Function(Comment)? onRightMoreTapped;
|
final void Function(Comment)? onRightMoreTapped;
|
||||||
final void Function(String) onStoryLinkTapped;
|
|
||||||
final FetchMode fetchMode;
|
/// Override for search screen.
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
static final Map<int, Color> _colors = <int, Color>{};
|
static final Map<int, Color> _colors = <int, Color>{};
|
||||||
|
|
||||||
@ -120,6 +120,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
if (actionable) {
|
if (actionable) {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
context.read<CollapseCubit>().collapse();
|
context.read<CollapseCubit>().collapse();
|
||||||
|
} else {
|
||||||
|
onTap?.call();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -186,10 +188,16 @@ class CommentTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: _CommentText(
|
child: ItemText(
|
||||||
key: ValueKey<int>(comment.id),
|
key: ValueKey<int>(comment.id),
|
||||||
comment: comment,
|
item: comment,
|
||||||
onLinkTapped: _onLinkTapped,
|
onTap: () {
|
||||||
|
if (onTap == null) {
|
||||||
|
_onTextTapped(context);
|
||||||
|
} else {
|
||||||
|
onTap!.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -242,7 +250,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
final Color commentColor = prefState.eyeCandyEnabled
|
final Color commentColor = prefState.eyeCandyEnabled
|
||||||
? color.withOpacity(commentBackgroundColorOpacity)
|
? color.withOpacity(commentBackgroundColorOpacity)
|
||||||
: Palette.transparent;
|
: Palette.transparent;
|
||||||
final bool isMyComment = myUsername == comment.by;
|
final bool isMyComment = comment.deleted == false &&
|
||||||
|
context.read<AuthBloc>().state.username == comment.by;
|
||||||
|
|
||||||
Widget wrapper = child;
|
Widget wrapper = child;
|
||||||
|
|
||||||
@ -325,67 +334,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
commentsState?.onlyShowTargetComment == false;
|
commentsState?.onlyShowTargetComment == false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLinkTapped(LinkableElement link) {
|
void _onTextTapped(BuildContext context) {
|
||||||
if (link.url.isStoryLink) {
|
|
||||||
onStoryLinkTapped.call(link.url);
|
|
||||||
} else {
|
|
||||||
LinkUtil.launch(link.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CommentText extends StatelessWidget {
|
|
||||||
const _CommentText({
|
|
||||||
super.key,
|
|
||||||
required this.comment,
|
|
||||||
required this.onLinkTapped,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Comment comment;
|
|
||||||
final void Function(LinkableElement) onLinkTapped;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final PreferenceState prefState = context.read<PreferenceCubit>().state;
|
|
||||||
final TextStyle style = TextStyle(
|
|
||||||
fontSize: prefState.fontSize.fontSize,
|
|
||||||
);
|
|
||||||
final TextStyle linkStyle = TextStyle(
|
|
||||||
fontSize: prefState.fontSize.fontSize,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
color: Palette.orange,
|
|
||||||
);
|
|
||||||
if (comment is BuildableComment) {
|
|
||||||
return SelectableText.rich(
|
|
||||||
buildTextSpan(
|
|
||||||
(comment as BuildableComment).elements,
|
|
||||||
style: style,
|
|
||||||
linkStyle: linkStyle,
|
|
||||||
onOpen: onLinkTapped,
|
|
||||||
),
|
|
||||||
onTap: () => onTextTapped(context),
|
|
||||||
contextMenuBuilder: (
|
|
||||||
BuildContext context,
|
|
||||||
EditableTextState editableTextState,
|
|
||||||
) =>
|
|
||||||
contextMenuBuilder(
|
|
||||||
context,
|
|
||||||
editableTextState,
|
|
||||||
comment: comment as BuildableComment,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return SelectableLinkify(
|
|
||||||
text: comment.text,
|
|
||||||
style: style,
|
|
||||||
linkStyle: linkStyle,
|
|
||||||
onOpen: onLinkTapped,
|
|
||||||
onTap: () => onTextTapped(context),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTextTapped(BuildContext context) {
|
|
||||||
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
|
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
context.read<CollapseCubit>().collapse();
|
context.read<CollapseCubit>().collapse();
|
||||||
|
@ -4,7 +4,7 @@ import 'package:flutter_fadein/flutter_fadein.dart';
|
|||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart' show ReminderCubit, ReminderState;
|
import 'package:hacki/cubits/cubits.dart' show ReminderCubit, ReminderState;
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/story.dart';
|
import 'package:hacki/models/item/story.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
@ -181,6 +181,7 @@ class SelectableLinkify extends StatelessWidget {
|
|||||||
this.cursorHeight,
|
this.cursorHeight,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Text to be linkified
|
/// Text to be linkified
|
||||||
@ -273,6 +274,8 @@ class SelectableLinkify extends StatelessWidget {
|
|||||||
/// cursor location).
|
/// cursor location).
|
||||||
final SelectionChangedCallback? onSelectionChanged;
|
final SelectionChangedCallback? onSelectionChanged;
|
||||||
|
|
||||||
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<LinkifyElement> elements = LinkifierUtil.linkify(text);
|
final List<LinkifyElement> elements = LinkifierUtil.linkify(text);
|
||||||
@ -312,6 +315,16 @@ class SelectableLinkify extends StatelessWidget {
|
|||||||
cursorHeight: cursorHeight,
|
cursorHeight: cursorHeight,
|
||||||
selectionControls: selectionControls,
|
selectionControls: selectionControls,
|
||||||
onSelectionChanged: onSelectionChanged,
|
onSelectionChanged: onSelectionChanged,
|
||||||
|
contextMenuBuilder: contextMenuBuilder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) {
|
||||||
|
return AdaptiveTextSelectionToolbar.editableText(
|
||||||
|
editableTextState: editableTextState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
final RegExp _emphasisRegex = RegExp(
|
final RegExp _emphasisRegex = RegExp(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
final RegExp _quoteRegex = RegExp(
|
final RegExp _quoteRegex = RegExp(
|
||||||
|
71
lib/screens/widgets/item_text.dart
Normal file
71
lib/screens/widgets/item_text.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
|
||||||
|
class ItemText extends StatelessWidget {
|
||||||
|
const ItemText({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Item item;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final PreferenceState prefState = context.read<PreferenceCubit>().state;
|
||||||
|
final TextStyle style = TextStyle(
|
||||||
|
fontSize: prefState.fontSize.fontSize,
|
||||||
|
);
|
||||||
|
final TextStyle linkStyle = TextStyle(
|
||||||
|
fontSize: prefState.fontSize.fontSize,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
color: Palette.orange,
|
||||||
|
);
|
||||||
|
if (item is Buildable) {
|
||||||
|
return SelectableText.rich(
|
||||||
|
buildTextSpan(
|
||||||
|
(item as Buildable).elements,
|
||||||
|
style: style,
|
||||||
|
linkStyle: linkStyle,
|
||||||
|
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) =>
|
||||||
|
contextMenuBuilder(
|
||||||
|
context,
|
||||||
|
editableTextState,
|
||||||
|
item: item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SelectableLinkify(
|
||||||
|
text: item.text,
|
||||||
|
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||||
|
style: style,
|
||||||
|
linkStyle: linkStyle,
|
||||||
|
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||||
|
onTap: onTap,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) =>
|
||||||
|
contextMenuBuilder(
|
||||||
|
context,
|
||||||
|
editableTextState,
|
||||||
|
item: item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ export 'custom_circular_progress_indicator.dart';
|
|||||||
export 'custom_described_feature_overlay.dart';
|
export 'custom_described_feature_overlay.dart';
|
||||||
export 'custom_linkify/custom_linkify.dart';
|
export 'custom_linkify/custom_linkify.dart';
|
||||||
export 'custom_tab_bar.dart';
|
export 'custom_tab_bar.dart';
|
||||||
|
export 'item_text.dart';
|
||||||
export 'items_list_view.dart';
|
export 'items_list_view.dart';
|
||||||
export 'link_preview/link_preview.dart';
|
export 'link_preview/link_preview.dart';
|
||||||
export 'offline_banner.dart';
|
export 'offline_banner.dart';
|
||||||
|
@ -6,7 +6,8 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/utils/html_util.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:path_provider_android/path_provider_android.dart';
|
import 'package:path_provider_android/path_provider_android.dart';
|
||||||
import 'package:path_provider_foundation/path_provider_foundation.dart';
|
import 'package:path_provider_foundation/path_provider_foundation.dart';
|
||||||
import 'package:shared_preferences_android/shared_preferences_android.dart';
|
import 'package:shared_preferences_android/shared_preferences_android.dart';
|
||||||
@ -34,14 +35,21 @@ abstract class Fetcher {
|
|||||||
static const int _subscriptionUpperLimit = 15;
|
static const int _subscriptionUpperLimit = 15;
|
||||||
|
|
||||||
static Future<void> fetchReplies() async {
|
static Future<void> fetchReplies() async {
|
||||||
final PreferenceRepository preferenceRepository = PreferenceRepository();
|
final Logger logger = Logger();
|
||||||
|
final PreferenceRepository preferenceRepository =
|
||||||
|
PreferenceRepository(logger: logger);
|
||||||
|
|
||||||
final AuthRepository authRepository = AuthRepository(
|
final AuthRepository authRepository = AuthRepository(
|
||||||
preferenceRepository: preferenceRepository,
|
preferenceRepository: preferenceRepository,
|
||||||
|
logger: logger,
|
||||||
);
|
);
|
||||||
|
|
||||||
final StoriesRepository storiesRepository = StoriesRepository();
|
final StoriesRepository storiesRepository = StoriesRepository();
|
||||||
final SembastRepository sembastRepository = SembastRepository();
|
final SembastRepository sembastRepository = SembastRepository();
|
||||||
|
|
||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
final String? username = await authRepository.username;
|
final String? username = await authRepository.username;
|
||||||
final List<int> unreadIds = await preferenceRepository.unreadCommentsIds;
|
final List<int> unreadIds = await preferenceRepository.unreadCommentsIds;
|
||||||
|
|
||||||
|
@ -4,9 +4,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/screens/screens.dart' show WebViewScreen;
|
import 'package:hacki/screens/screens.dart'
|
||||||
|
show ItemScreen, ItemScreenArgs, WebViewScreen;
|
||||||
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';
|
||||||
|
|
||||||
@ -45,6 +48,11 @@ abstract class LinkUtil {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (link.isStoryLink) {
|
||||||
|
_onStoryLinkTapped(link);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Uri rinseLink(String link) {
|
Uri rinseLink(String link) {
|
||||||
final RegExp regex = RegExp(RegExpConstants.linkSuffix);
|
final RegExp regex = RegExp(RegExpConstants.linkSuffix);
|
||||||
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
|
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
|
||||||
@ -80,4 +88,23 @@ abstract class LinkUtil {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _onStoryLinkTapped(String link) async {
|
||||||
|
final int? id = link.itemId;
|
||||||
|
if (id != null) {
|
||||||
|
await locator
|
||||||
|
.get<StoriesRepository>()
|
||||||
|
.fetchItem(id: id)
|
||||||
|
.then((Item? item) {
|
||||||
|
if (item != null) {
|
||||||
|
HackiApp.navigatorKey.currentState!.pushNamed(
|
||||||
|
ItemScreen.routeName,
|
||||||
|
arguments: ItemScreenArgs(item: item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
launch(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 1.1.1+92
|
version: 1.2.1+94
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
Reference in New Issue
Block a user