mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
0b5329d050 | |||
c375def289 | |||
3469543c7b | |||
ab755581fd |
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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,22 +25,27 @@ 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);
|
||||||
|
|
||||||
int count = 1;
|
if (item is Buildable) {
|
||||||
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
final Iterable<EmphasisElement> emphasisElements =
|
||||||
final int s = (start + count * 2).clamp(0, comment.text.length);
|
(item as Buildable).elements.whereType<EmphasisElement>();
|
||||||
final int e = (end + count * 2).clamp(0, comment.text.length);
|
|
||||||
selectedText = comment.text.substring(s, e);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
||||||
|
final int s = (start - count * 2).clamp(0, item.text.length);
|
||||||
|
final int e = (end - count * 2).clamp(0, item.text.length);
|
||||||
|
selectedText = item.text.substring(s, e);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.addAll(<ContextMenuButtonItem>[
|
items.addAll(<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';
|
||||||
|
@ -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(
|
||||||
@ -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) {
|
||||||
|
@ -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';
|
||||||
@ -16,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>(
|
||||||
@ -69,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,
|
||||||
@ -114,13 +125,8 @@ 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(link.url),
|
||||||
onStoryLinkTapped.call(link.url);
|
|
||||||
} else {
|
|
||||||
LinkUtil.launch(link.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -174,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(
|
||||||
|
@ -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>(
|
||||||
@ -62,29 +62,29 @@ class PollView extends StatelessWidget {
|
|||||||
listener: (BuildContext context, VoteState voteState) {
|
listener: (BuildContext context, VoteState voteState) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
if (voteState.status == VoteStatus.submitted) {
|
if (voteState.status == VoteStatus.submitted) {
|
||||||
context.showSnackBar(
|
showSnackBar(
|
||||||
content: 'Vote submitted successfully.',
|
content: 'Vote submitted successfully.',
|
||||||
);
|
);
|
||||||
} else if (voteState.status == VoteStatus.canceled) {
|
} else if (voteState.status == VoteStatus.canceled) {
|
||||||
context.showSnackBar(content: 'Vote canceled.');
|
showSnackBar(content: 'Vote canceled.');
|
||||||
} else if (voteState.status == VoteStatus.failure) {
|
} else if (voteState.status == VoteStatus.failure) {
|
||||||
context.showErrorSnackBar();
|
showErrorSnackBar();
|
||||||
} else if (voteState.status ==
|
} else if (voteState.status ==
|
||||||
VoteStatus.failureKarmaBelowThreshold) {
|
VoteStatus.failureKarmaBelowThreshold) {
|
||||||
context.showSnackBar(
|
showSnackBar(
|
||||||
content: "You can't downvote because"
|
content: "You can't downvote because"
|
||||||
' you are karmaly broke.',
|
' you are karmaly broke.',
|
||||||
);
|
);
|
||||||
} else if (voteState.status ==
|
} else if (voteState.status ==
|
||||||
VoteStatus.failureNotLoggedIn) {
|
VoteStatus.failureNotLoggedIn) {
|
||||||
context.showSnackBar(
|
showSnackBar(
|
||||||
content: 'Not logged in, no voting! (;`O´)o',
|
content: 'Not logged in, no voting! (;`O´)o',
|
||||||
action: onLoginTapped,
|
action: onLoginTapped,
|
||||||
label: 'Log in',
|
label: 'Log in',
|
||||||
);
|
);
|
||||||
} else if (voteState.status ==
|
} else if (voteState.status ==
|
||||||
VoteStatus.failureBeHumble) {
|
VoteStatus.failureBeHumble) {
|
||||||
context.showSnackBar(
|
showSnackBar(
|
||||||
content: 'No voting on your own post! (;`O´)o',
|
content: 'No voting on your own post! (;`O´)o',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,37 +281,49 @@ 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(
|
|
||||||
child: const Text('View thread'),
|
|
||||||
onPressed: () {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
setState(() {
|
|
||||||
expanded = false;
|
|
||||||
});
|
|
||||||
Navigator.popUntil(
|
|
||||||
context,
|
|
||||||
(Route<dynamic> route) =>
|
|
||||||
route.settings.name == ItemScreen.routeName ||
|
|
||||||
route.isFirst,
|
|
||||||
);
|
|
||||||
goToItemScreen(
|
|
||||||
args: ItemScreenArgs(
|
|
||||||
item: replyingTo,
|
|
||||||
useCommentCache: true,
|
|
||||||
),
|
|
||||||
forceNewScreen: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Copy all'),
|
child: const Text(
|
||||||
|
'View thread',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() {
|
||||||
|
expanded = false;
|
||||||
|
});
|
||||||
|
Navigator.popUntil(
|
||||||
|
context,
|
||||||
|
(Route<dynamic> route) =>
|
||||||
|
route.settings.name == ItemScreen.routeName ||
|
||||||
|
route.isFirst,
|
||||||
|
);
|
||||||
|
goToItemScreen(
|
||||||
|
args: ItemScreenArgs(
|
||||||
|
item: replyingTo,
|
||||||
|
useCommentCache: true,
|
||||||
|
),
|
||||||
|
forceNewScreen: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
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) {
|
||||||
@ -71,9 +68,6 @@ class TimeMachineDialog extends StatelessWidget {
|
|||||||
in state.ancestors) ...<Widget>[
|
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,
|
||||||
),
|
),
|
||||||
|
@ -231,7 +231,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
authState: authState,
|
authState: authState,
|
||||||
magicWord: Constants.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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -226,10 +226,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
else if (e is Comment)
|
else if (e is Comment)
|
||||||
FadeIn(
|
FadeIn(
|
||||||
child: CommentTile(
|
child: CommentTile(
|
||||||
myUsername: '',
|
|
||||||
actionable: false,
|
actionable: false,
|
||||||
comment: e,
|
comment: e,
|
||||||
onStoryLinkTapped: (_) {},
|
|
||||||
fetchMode: FetchMode.eager,
|
fetchMode: FetchMode.eager,
|
||||||
onTap: () => goToItemScreen(
|
onTap: () => goToItemScreen(
|
||||||
args: ItemScreenArgs(item: e),
|
args: ItemScreenArgs(item: e),
|
||||||
|
@ -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,
|
||||||
@ -28,7 +25,6 @@ class CommentTile extends StatelessWidget {
|
|||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? myUsername;
|
|
||||||
final String? opUsername;
|
final String? opUsername;
|
||||||
final Comment comment;
|
final Comment comment;
|
||||||
final int level;
|
final int level;
|
||||||
@ -39,7 +35,6 @@ class CommentTile extends StatelessWidget {
|
|||||||
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;
|
|
||||||
|
|
||||||
/// Override for search screen.
|
/// Override for search screen.
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
@ -193,11 +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: () {
|
||||||
onTap: onTap,
|
if (onTap == null) {
|
||||||
|
_onTextTapped(context);
|
||||||
|
} else {
|
||||||
|
onTap!.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -250,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;
|
||||||
|
|
||||||
@ -333,83 +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,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Comment comment;
|
|
||||||
final void Function(LinkableElement) onLinkTapped;
|
|
||||||
|
|
||||||
/// Override for search screen.
|
|
||||||
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 (comment is BuildableComment) {
|
|
||||||
return SelectableText.rich(
|
|
||||||
buildTextSpan(
|
|
||||||
(comment as BuildableComment).elements,
|
|
||||||
style: style,
|
|
||||||
linkStyle: linkStyle,
|
|
||||||
onOpen: onLinkTapped,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (onTap == null) {
|
|
||||||
onTextTapped(context);
|
|
||||||
} else {
|
|
||||||
onTap!.call();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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: () {
|
|
||||||
if (onTap == null) {
|
|
||||||
onTextTapped(context);
|
|
||||||
} else {
|
|
||||||
onTap!.call();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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/models.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';
|
||||||
|
@ -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,21 @@ 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 1.2.0+93
|
version: 1.2.2+95
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
Reference in New Issue
Block a user