Compare commits

...

2 Commits

Author SHA1 Message Date
e77c0e3e73 update bottom sheet. (#190) 2023-03-31 23:15:53 -07:00
cb6f41ec49 add keyword filter. (#189) 2023-03-31 13:59:12 -07:00
24 changed files with 307 additions and 68 deletions

View File

@ -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,
), ),
); );

View File

@ -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));

View File

@ -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';

View 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));
}
}

View 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];
}

View File

@ -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'),
),
],
),
), ),
), ),
); );

View File

@ -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>(

View File

@ -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

View File

@ -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)

View File

@ -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,
); );
} }

View File

@ -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,
]; ];
} }

View File

@ -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()

View File

@ -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' : ''}''';

View File

@ -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(),

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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),
], ),
],
),
), ),
), ),
); );

View File

@ -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) =>

View File

@ -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;

View File

@ -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,

View File

@ -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(

View File

@ -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"

View File

@ -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