mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
e77c0e3e73 | |||
cb6f41ec49 |
@ -17,11 +17,13 @@ part 'stories_state.dart';
|
|||||||
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||||
StoriesBloc({
|
StoriesBloc({
|
||||||
required PreferenceCubit preferenceCubit,
|
required PreferenceCubit preferenceCubit,
|
||||||
|
required FilterCubit filterCubit,
|
||||||
OfflineRepository? offlineRepository,
|
OfflineRepository? offlineRepository,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
}) : _preferenceCubit = preferenceCubit,
|
}) : _preferenceCubit = preferenceCubit,
|
||||||
|
_filterCubit = filterCubit,
|
||||||
_offlineRepository =
|
_offlineRepository =
|
||||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
@ -45,6 +47,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final PreferenceCubit _preferenceCubit;
|
final PreferenceCubit _preferenceCubit;
|
||||||
|
final FilterCubit _filterCubit;
|
||||||
final OfflineRepository _offlineRepository;
|
final OfflineRepository _offlineRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
@ -224,10 +227,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
Emitter<StoriesState> emit,
|
Emitter<StoriesState> emit,
|
||||||
) async {
|
) async {
|
||||||
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
|
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
|
||||||
|
final bool hidden = _filterCubit.state.keywords.any(
|
||||||
|
(String keyword) =>
|
||||||
|
event.story.title.toLowerCase().contains(keyword) ||
|
||||||
|
event.story.text.toLowerCase().contains(keyword),
|
||||||
|
);
|
||||||
emit(
|
emit(
|
||||||
state.copyWithStoryAdded(
|
state.copyWithStoryAdded(
|
||||||
type: event.type,
|
type: event.type,
|
||||||
story: event.story,
|
story: event.story.copyWith(hidden: hidden),
|
||||||
hasRead: hasRead,
|
hasRead: hasRead,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/cubits/cubits.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/repositories/repositories.dart';
|
||||||
@ -20,6 +21,7 @@ part 'comments_state.dart';
|
|||||||
|
|
||||||
class CommentsCubit extends Cubit<CommentsState> {
|
class CommentsCubit extends Cubit<CommentsState> {
|
||||||
CommentsCubit({
|
CommentsCubit({
|
||||||
|
required FilterCubit filterCubit,
|
||||||
required CollapseCache collapseCache,
|
required CollapseCache collapseCache,
|
||||||
CommentCache? commentCache,
|
CommentCache? commentCache,
|
||||||
OfflineRepository? offlineRepository,
|
OfflineRepository? offlineRepository,
|
||||||
@ -30,7 +32,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
required Item item,
|
required Item item,
|
||||||
required FetchMode defaultFetchMode,
|
required FetchMode defaultFetchMode,
|
||||||
required CommentsOrder defaultCommentsOrder,
|
required CommentsOrder defaultCommentsOrder,
|
||||||
}) : _collapseCache = collapseCache,
|
}) : _filterCubit = filterCubit,
|
||||||
|
_collapseCache = collapseCache,
|
||||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||||
_offlineRepository =
|
_offlineRepository =
|
||||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||||
@ -48,6 +51,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final FilterCubit _filterCubit;
|
||||||
final CollapseCache _collapseCache;
|
final CollapseCache _collapseCache;
|
||||||
final CommentCache _commentCache;
|
final CommentCache _commentCache;
|
||||||
final OfflineRepository _offlineRepository;
|
final OfflineRepository _offlineRepository;
|
||||||
@ -348,9 +352,12 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
_commentCache.cacheComment(comment);
|
_commentCache.cacheComment(comment);
|
||||||
_sembastRepository.cacheComment(comment);
|
_sembastRepository.cacheComment(comment);
|
||||||
|
|
||||||
|
final bool hidden = _filterCubit.state.keywords.any(
|
||||||
|
(String keyword) => comment.text.toLowerCase().contains(keyword),
|
||||||
|
);
|
||||||
final List<Comment> updatedComments = <Comment>[
|
final List<Comment> updatedComments = <Comment>[
|
||||||
...state.comments,
|
...state.comments,
|
||||||
comment
|
comment.copyWith(hidden: hidden),
|
||||||
];
|
];
|
||||||
|
|
||||||
emit(state.copyWith(comments: updatedComments));
|
emit(state.copyWith(comments: updatedComments));
|
||||||
|
@ -3,6 +3,7 @@ export 'collapse/collapse_cubit.dart';
|
|||||||
export 'comments/comments_cubit.dart';
|
export 'comments/comments_cubit.dart';
|
||||||
export 'edit/edit_cubit.dart';
|
export 'edit/edit_cubit.dart';
|
||||||
export 'fav/fav_cubit.dart';
|
export 'fav/fav_cubit.dart';
|
||||||
|
export 'filter/filter_cubit.dart';
|
||||||
export 'history/history_cubit.dart';
|
export 'history/history_cubit.dart';
|
||||||
export 'notification/notification_cubit.dart';
|
export 'notification/notification_cubit.dart';
|
||||||
export 'pin/pin_cubit.dart';
|
export 'pin/pin_cubit.dart';
|
||||||
|
40
lib/cubits/filter/filter_cubit.dart
Normal file
40
lib/cubits/filter/filter_cubit.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
|
||||||
|
part 'filter_state.dart';
|
||||||
|
|
||||||
|
class FilterCubit extends Cubit<FilterState> {
|
||||||
|
FilterCubit({PreferenceRepository? preferenceRepository})
|
||||||
|
: _preferenceRepository =
|
||||||
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
|
super(FilterState.init()) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
final PreferenceRepository _preferenceRepository;
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
_preferenceRepository.filterKeywords.then(
|
||||||
|
(List<String> keywords) => emit(
|
||||||
|
state.copyWith(
|
||||||
|
keywords: keywords.toSet(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addKeyword(String keyword) {
|
||||||
|
final Set<String> updated = Set<String>.from(state.keywords)..add(keyword);
|
||||||
|
emit(state.copyWith(keywords: updated));
|
||||||
|
_preferenceRepository.updateFilterKeywords(updated.toList(growable: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeKeyword(String keyword) {
|
||||||
|
final Set<String> updated = Set<String>.from(state.keywords)
|
||||||
|
..remove(keyword);
|
||||||
|
emit(state.copyWith(keywords: updated));
|
||||||
|
_preferenceRepository.updateFilterKeywords(updated.toList(growable: false));
|
||||||
|
}
|
||||||
|
}
|
20
lib/cubits/filter/filter_state.dart
Normal file
20
lib/cubits/filter/filter_state.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
part of 'filter_cubit.dart';
|
||||||
|
|
||||||
|
class FilterState extends Equatable {
|
||||||
|
const FilterState({
|
||||||
|
required this.keywords,
|
||||||
|
});
|
||||||
|
|
||||||
|
FilterState.init() : keywords = <String>{};
|
||||||
|
|
||||||
|
final Set<String> keywords;
|
||||||
|
|
||||||
|
FilterState copyWith({Set<String>? keywords}) {
|
||||||
|
return FilterState(
|
||||||
|
keywords: keywords ?? this.keywords,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[keywords];
|
||||||
|
}
|
@ -108,24 +108,26 @@ extension StateExtension on State {
|
|||||||
linkToShare = await showModalBottomSheet<String>(
|
linkToShare = await showModalBottomSheet<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return Container(
|
return SafeArea(
|
||||||
height: 140,
|
child: ColoredBox(
|
||||||
color: Theme.of(context).canvasColor,
|
color: Theme.of(context).canvasColor,
|
||||||
child: Material(
|
child: Material(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
ListTile(
|
children: <Widget>[
|
||||||
onTap: () => Navigator.pop(context, item.url),
|
ListTile(
|
||||||
title: const Text('Link to article'),
|
onTap: () => Navigator.pop(context, item.url),
|
||||||
),
|
title: const Text('Link to article'),
|
||||||
ListTile(
|
|
||||||
onTap: () => Navigator.pop(
|
|
||||||
context,
|
|
||||||
'https://news.ycombinator.com/item?id=${item.id}',
|
|
||||||
),
|
),
|
||||||
title: const Text('Link to HN'),
|
ListTile(
|
||||||
),
|
onTap: () => Navigator.pop(
|
||||||
],
|
context,
|
||||||
|
'https://news.ycombinator.com/item?id=${item.id}',
|
||||||
|
),
|
||||||
|
title: const Text('Link to HN'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -183,9 +183,14 @@ class HackiApp extends StatelessWidget {
|
|||||||
lazy: false,
|
lazy: false,
|
||||||
create: (BuildContext context) => PreferenceCubit(),
|
create: (BuildContext context) => PreferenceCubit(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<FilterCubit>(
|
||||||
|
lazy: false,
|
||||||
|
create: (BuildContext context) => FilterCubit(),
|
||||||
|
),
|
||||||
BlocProvider<StoriesBloc>(
|
BlocProvider<StoriesBloc>(
|
||||||
create: (BuildContext context) => StoriesBloc(
|
create: (BuildContext context) => StoriesBloc(
|
||||||
preferenceCubit: context.read<PreferenceCubit>(),
|
preferenceCubit: context.read<PreferenceCubit>(),
|
||||||
|
filterCubit: context.read<FilterCubit>(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider<AuthBloc>(
|
BlocProvider<AuthBloc>(
|
||||||
|
@ -15,6 +15,7 @@ class BuildableComment extends Comment with Buildable {
|
|||||||
required super.kids,
|
required super.kids,
|
||||||
required super.dead,
|
required super.dead,
|
||||||
required super.deleted,
|
required super.deleted,
|
||||||
|
required super.hidden,
|
||||||
required super.level,
|
required super.level,
|
||||||
required this.elements,
|
required this.elements,
|
||||||
});
|
});
|
||||||
@ -31,6 +32,7 @@ class BuildableComment extends Comment with Buildable {
|
|||||||
dead: comment.dead,
|
dead: comment.dead,
|
||||||
deleted: comment.deleted,
|
deleted: comment.deleted,
|
||||||
level: comment.level,
|
level: comment.level,
|
||||||
|
hidden: comment.hidden,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -17,6 +17,7 @@ class BuildableStory extends Story with Buildable {
|
|||||||
required super.type,
|
required super.type,
|
||||||
required super.url,
|
required super.url,
|
||||||
required super.parts,
|
required super.parts,
|
||||||
|
required super.hidden,
|
||||||
required this.elements,
|
required this.elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ class BuildableStory extends Story with Buildable {
|
|||||||
type: story.type,
|
type: story.type,
|
||||||
url: story.url,
|
url: story.url,
|
||||||
parts: story.parts,
|
parts: story.parts,
|
||||||
|
hidden: story.hidden,
|
||||||
);
|
);
|
||||||
|
|
||||||
BuildableStory.fromTitleOnlyStory(Story story)
|
BuildableStory.fromTitleOnlyStory(Story story)
|
||||||
|
@ -11,6 +11,7 @@ class Comment extends Item {
|
|||||||
required super.kids,
|
required super.kids,
|
||||||
required super.dead,
|
required super.dead,
|
||||||
required super.deleted,
|
required super.deleted,
|
||||||
|
required super.hidden,
|
||||||
required this.level,
|
required this.level,
|
||||||
}) : super(
|
}) : super(
|
||||||
descendants: 0,
|
descendants: 0,
|
||||||
@ -26,7 +27,10 @@ class Comment extends Item {
|
|||||||
|
|
||||||
String get metadata => '''by $by $timeAgo''';
|
String get metadata => '''by $by $timeAgo''';
|
||||||
|
|
||||||
Comment copyWith({int? level}) {
|
Comment copyWith({
|
||||||
|
int? level,
|
||||||
|
bool? hidden,
|
||||||
|
}) {
|
||||||
return Comment(
|
return Comment(
|
||||||
id: id,
|
id: id,
|
||||||
time: time,
|
time: time,
|
||||||
@ -37,6 +41,7 @@ class Comment extends Item {
|
|||||||
kids: kids,
|
kids: kids,
|
||||||
dead: dead,
|
dead: dead,
|
||||||
deleted: deleted,
|
deleted: deleted,
|
||||||
|
hidden: hidden ?? this.hidden,
|
||||||
level: level ?? this.level,
|
level: level ?? this.level,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ class Item extends Equatable {
|
|||||||
required this.type,
|
required this.type,
|
||||||
required this.parts,
|
required this.parts,
|
||||||
required this.descendants,
|
required this.descendants,
|
||||||
|
required this.hidden,
|
||||||
});
|
});
|
||||||
|
|
||||||
Item.empty()
|
Item.empty()
|
||||||
@ -39,9 +40,10 @@ class Item extends Equatable {
|
|||||||
title = '',
|
title = '',
|
||||||
url = '',
|
url = '',
|
||||||
kids = <int>[],
|
kids = <int>[],
|
||||||
dead = false,
|
|
||||||
parts = <int>[],
|
parts = <int>[],
|
||||||
|
dead = false,
|
||||||
deleted = false,
|
deleted = false,
|
||||||
|
hidden = false,
|
||||||
parent = 0,
|
parent = 0,
|
||||||
text = '',
|
text = '',
|
||||||
type = '';
|
type = '';
|
||||||
@ -60,7 +62,8 @@ class Item extends Equatable {
|
|||||||
deleted = json['deleted'] as bool? ?? false,
|
deleted = json['deleted'] as bool? ?? false,
|
||||||
parent = json['parent'] as int? ?? 0,
|
parent = json['parent'] as int? ?? 0,
|
||||||
parts = (json['parts'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
parts = (json['parts'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
||||||
type = json['type'] as String? ?? '';
|
type = json['type'] as String? ?? '',
|
||||||
|
hidden = json['hidden'] as bool? ?? false;
|
||||||
|
|
||||||
final int id;
|
final int id;
|
||||||
final int time;
|
final int time;
|
||||||
@ -73,6 +76,11 @@ class Item extends Equatable {
|
|||||||
final bool deleted;
|
final bool deleted;
|
||||||
final bool dead;
|
final bool dead;
|
||||||
|
|
||||||
|
/// Whether or not the item should be hidden.
|
||||||
|
/// true if any of filter keywords set by user presents in [text]
|
||||||
|
/// or [title].
|
||||||
|
final bool hidden;
|
||||||
|
|
||||||
final String by;
|
final String by;
|
||||||
final String text;
|
final String text;
|
||||||
final String url;
|
final String url;
|
||||||
@ -128,5 +136,6 @@ class Item extends Equatable {
|
|||||||
type,
|
type,
|
||||||
parts,
|
parts,
|
||||||
descendants,
|
descendants,
|
||||||
|
hidden,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ class PollOption extends Item {
|
|||||||
descendants: 0,
|
descendants: 0,
|
||||||
dead: false,
|
dead: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
hidden: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
PollOption.empty()
|
PollOption.empty()
|
||||||
|
@ -14,6 +14,7 @@ class Story extends Item {
|
|||||||
required super.text,
|
required super.text,
|
||||||
required super.kids,
|
required super.kids,
|
||||||
required super.parts,
|
required super.parts,
|
||||||
|
required super.hidden,
|
||||||
}) : super(
|
}) : super(
|
||||||
dead: false,
|
dead: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
@ -38,10 +39,28 @@ class Story extends Item {
|
|||||||
parent: 0,
|
parent: 0,
|
||||||
text: '',
|
text: '',
|
||||||
type: '',
|
type: '',
|
||||||
|
hidden: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Story.fromJson(super.json) : super.fromJson();
|
Story.fromJson(super.json) : super.fromJson();
|
||||||
|
|
||||||
|
Story copyWith({bool? hidden}) {
|
||||||
|
return Story(
|
||||||
|
descendants: descendants,
|
||||||
|
id: id,
|
||||||
|
score: score,
|
||||||
|
time: time,
|
||||||
|
by: by,
|
||||||
|
title: title,
|
||||||
|
type: type,
|
||||||
|
url: url,
|
||||||
|
text: text,
|
||||||
|
kids: kids,
|
||||||
|
parts: parts,
|
||||||
|
hidden: hidden ?? this.hidden,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String get metadata =>
|
String get metadata =>
|
||||||
'''$score point${score > 1 ? 's' : ''} by $by $timeAgo | $descendants comment${descendants > 1 ? 's' : ''}''';
|
'''$score point${score > 1 ? 's' : ''} by $by $timeAgo | $descendants comment${descendants > 1 ? 's' : ''}''';
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
|||||||
static final List<Preference<dynamic>> allPreferences =
|
static final List<Preference<dynamic>> allPreferences =
|
||||||
UnmodifiableListView<Preference<dynamic>>(
|
UnmodifiableListView<Preference<dynamic>>(
|
||||||
<Preference<dynamic>>[
|
<Preference<dynamic>>[
|
||||||
// Order of these first four preferences does not matter.
|
// Order of these preferences does not matter.
|
||||||
FetchModePreference(),
|
FetchModePreference(),
|
||||||
CommentsOrderPreference(),
|
CommentsOrderPreference(),
|
||||||
FontPreference(),
|
FontPreference(),
|
||||||
|
@ -22,6 +22,7 @@ class PreferenceRepository {
|
|||||||
static const String _usernameKey = 'username';
|
static const String _usernameKey = 'username';
|
||||||
static const String _passwordKey = 'password';
|
static const String _passwordKey = 'password';
|
||||||
static const String _blocklistKey = 'blocklist';
|
static const String _blocklistKey = 'blocklist';
|
||||||
|
static const String _filterKeywordsKey = 'filterKeywords';
|
||||||
static const String _pinnedStoriesIdsKey = 'pinnedStoriesIds';
|
static const String _pinnedStoriesIdsKey = 'pinnedStoriesIds';
|
||||||
static const String _unreadCommentsIdsKey = 'unreadCommentsIds';
|
static const String _unreadCommentsIdsKey = 'unreadCommentsIds';
|
||||||
static const String _lastReadStoryIdKey = 'lastReadStoryId';
|
static const String _lastReadStoryIdKey = 'lastReadStoryId';
|
||||||
@ -274,6 +275,20 @@ class PreferenceRepository {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region filter
|
||||||
|
|
||||||
|
Future<List<String>> get filterKeywords async => _prefs.then(
|
||||||
|
(SharedPreferences prefs) =>
|
||||||
|
prefs.getStringList(_filterKeywordsKey) ?? <String>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> updateFilterKeywords(List<String> keywords) async {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
await prefs.setStringList(_filterKeywordsKey, keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region pins
|
//#region pins
|
||||||
|
|
||||||
Future<List<int>> get pinnedStoriesIds async {
|
Future<List<int>> get pinnedStoriesIds async {
|
||||||
|
@ -58,6 +58,7 @@ class SearchRepository {
|
|||||||
parent: parentId,
|
parent: parentId,
|
||||||
dead: false,
|
dead: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
hidden: false,
|
||||||
level: 0,
|
level: 0,
|
||||||
);
|
);
|
||||||
yield comment;
|
yield comment;
|
||||||
@ -80,6 +81,7 @@ class SearchRepository {
|
|||||||
// response doesn't contain kids and parts.
|
// response doesn't contain kids and parts.
|
||||||
kids: const <int>[],
|
kids: const <int>[],
|
||||||
parts: const <int>[],
|
parts: const <int>[],
|
||||||
|
hidden: false,
|
||||||
);
|
);
|
||||||
yield story;
|
yield story;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
providers: <BlocProvider<dynamic>>[
|
providers: <BlocProvider<dynamic>>[
|
||||||
BlocProvider<CommentsCubit>(
|
BlocProvider<CommentsCubit>(
|
||||||
create: (BuildContext context) => CommentsCubit(
|
create: (BuildContext context) => CommentsCubit(
|
||||||
|
filterCubit: context.read<FilterCubit>(),
|
||||||
isOfflineReading:
|
isOfflineReading:
|
||||||
context.read<StoriesBloc>().state.isOfflineReading,
|
context.read<StoriesBloc>().state.isOfflineReading,
|
||||||
item: args.item,
|
item: args.item,
|
||||||
@ -106,6 +107,7 @@ class ItemScreen extends StatefulWidget {
|
|||||||
providers: <BlocProvider<dynamic>>[
|
providers: <BlocProvider<dynamic>>[
|
||||||
BlocProvider<CommentsCubit>(
|
BlocProvider<CommentsCubit>(
|
||||||
create: (BuildContext context) => CommentsCubit(
|
create: (BuildContext context) => CommentsCubit(
|
||||||
|
filterCubit: context.read<FilterCubit>(),
|
||||||
isOfflineReading:
|
isOfflineReading:
|
||||||
context.read<StoriesBloc>().state.isOfflineReading,
|
context.read<StoriesBloc>().state.isOfflineReading,
|
||||||
item: args.item,
|
item: args.item,
|
||||||
@ -434,45 +436,45 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onRightMoreTapped(Comment comment) {
|
void onRightMoreTapped(Comment comment) {
|
||||||
const double bottomSheetHeight = 140;
|
|
||||||
|
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
showModalBottomSheet<void>(
|
showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return Container(
|
return SafeArea(
|
||||||
height: bottomSheetHeight,
|
child: ColoredBox(
|
||||||
color: Theme.of(context).canvasColor,
|
color: Theme.of(context).canvasColor,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Palette.transparent,
|
color: Palette.transparent,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
ListTile(
|
children: <Widget>[
|
||||||
leading: const Icon(Icons.av_timer),
|
ListTile(
|
||||||
title: const Text('View ancestors'),
|
leading: const Icon(Icons.av_timer),
|
||||||
onTap: () {
|
title: const Text('View ancestors'),
|
||||||
Navigator.pop(context);
|
onTap: () {
|
||||||
onTimeMachineActivated(comment);
|
Navigator.pop(context);
|
||||||
},
|
onTimeMachineActivated(comment);
|
||||||
enabled:
|
},
|
||||||
comment.level > 0 && !(comment.dead || comment.deleted),
|
enabled:
|
||||||
),
|
comment.level > 0 && !(comment.dead || comment.deleted),
|
||||||
ListTile(
|
),
|
||||||
leading: const Icon(Icons.list),
|
ListTile(
|
||||||
title: const Text('View in separate thread'),
|
leading: const Icon(Icons.list),
|
||||||
onTap: () {
|
title: const Text('View in separate thread'),
|
||||||
Navigator.pop(context);
|
onTap: () {
|
||||||
goToItemScreen(
|
Navigator.pop(context);
|
||||||
args: ItemScreenArgs(
|
goToItemScreen(
|
||||||
item: comment,
|
args: ItemScreenArgs(
|
||||||
useCommentCache: true,
|
item: comment,
|
||||||
),
|
useCommentCache: true,
|
||||||
forceNewScreen: true,
|
),
|
||||||
);
|
forceNewScreen: true,
|
||||||
},
|
);
|
||||||
enabled: !(comment.dead || comment.deleted),
|
},
|
||||||
),
|
enabled: !(comment.dead || comment.deleted),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -23,9 +23,6 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
final bool isBlocked;
|
final bool isBlocked;
|
||||||
final VoidCallback onLoginTapped;
|
final VoidCallback onLoginTapped;
|
||||||
|
|
||||||
static const double _storySheetHeight = 485;
|
|
||||||
static const double _commentSheetHeight = 470;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<VoteCubit>(
|
return BlocProvider<VoteCubit>(
|
||||||
@ -69,12 +66,12 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
builder: (BuildContext context, VoteState voteState) {
|
builder: (BuildContext context, VoteState voteState) {
|
||||||
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 ColoredBox(
|
||||||
height: item is Comment ? _commentSheetHeight : _storySheetHeight,
|
|
||||||
color: Theme.of(context).canvasColor,
|
color: Theme.of(context).canvasColor,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Palette.transparent,
|
color: Palette.transparent,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
BlocProvider<UserCubit>(
|
BlocProvider<UserCubit>(
|
||||||
create: (BuildContext context) =>
|
create: (BuildContext context) =>
|
||||||
|
@ -232,6 +232,12 @@ class _SettingsState extends State<Settings> {
|
|||||||
onTap: showThemeSettingDialog,
|
onTap: showThemeSettingDialog,
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Filter Keywords',
|
||||||
|
),
|
||||||
|
onTap: onFilterKeywordsTapped,
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Export Favorites',
|
'Export Favorites',
|
||||||
@ -640,6 +646,100 @@ class _SettingsState extends State<Settings> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onFilterKeywordsTapped() {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text(
|
||||||
|
'Filter Keywords',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: BlocBuilder<FilterCubit, FilterState>(
|
||||||
|
builder: (BuildContext context, FilterState state) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
if (state.keywords.isEmpty)
|
||||||
|
const CenteredText(
|
||||||
|
text:
|
||||||
|
'''story or comment that contains keywords here will be hidden.''',
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
spacing: Dimens.pt4,
|
||||||
|
children: <Widget>[
|
||||||
|
for (final String keyword in state.keywords)
|
||||||
|
ActionChip(
|
||||||
|
avatar: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: TextDimens.pt14,
|
||||||
|
),
|
||||||
|
label: Text(keyword),
|
||||||
|
onPressed: () => context
|
||||||
|
.read<FilterCubit>()
|
||||||
|
.removeKeyword(keyword),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: onAddKeywordTapped,
|
||||||
|
child: const Text(
|
||||||
|
'Add keyword',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text(
|
||||||
|
'Okay',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddKeywordTapped() {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final String keyword = controller.text.trim();
|
||||||
|
if (keyword.isEmpty) return;
|
||||||
|
context.read<FilterCubit>().addKeyword(keyword.toLowerCase());
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Confirm',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> onExportFavoritesTapped() async {
|
Future<void> onExportFavoritesTapped() async {
|
||||||
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
|
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (comment.hidden) return const SizedBox.shrink();
|
||||||
return BlocProvider<CollapseCubit>(
|
return BlocProvider<CollapseCubit>(
|
||||||
key: ValueKey<String>('${comment.id}-BlocProvider'),
|
key: ValueKey<String>('${comment.id}-BlocProvider'),
|
||||||
lazy: false,
|
lazy: false,
|
||||||
|
@ -31,6 +31,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (story.hidden) return const SizedBox.shrink();
|
||||||
if (showWebPreview) {
|
if (showWebPreview) {
|
||||||
final double height = context.storyTileHeight;
|
final double height = context.storyTileHeight;
|
||||||
return Semantics(
|
return Semantics(
|
||||||
|
@ -1375,4 +1375,4 @@ packages:
|
|||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.0 <3.0.0"
|
dart: ">=2.19.0 <3.0.0"
|
||||||
flutter: ">=3.7.8"
|
flutter: ">=3.7.9"
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 1.3.4+103
|
version: 1.4.0+104
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
flutter: "3.7.8"
|
flutter: "3.7.9"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.2.0
|
adaptive_theme: ^3.2.0
|
||||||
|
Submodule submodules/flutter updated: 90c64ed42b...62bd79521d
Reference in New Issue
Block a user