diff --git a/README.md b/README.md index 3d843de..904d31d 100644 --- a/README.md +++ b/README.md @@ -35,22 +35,20 @@ Features:

- 01 - 02 - 03 - 04 - 05 - 06 - 07 - 08 - 09 - 10 - 11 - 12 + 01 + 06 + 02 + 07 + 03 + 08 + 04 + 09 + 05 + 10 - ipad-01 - ipad-02 - ipad-03 - ipad-04 + ipad-01 + ipad-02 + ipad-03 + ipad-04

diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2608025..23c147f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,7 +23,8 @@ android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:fullBackupContent="@xml/backup_rules" - android:usesCleartextTraffic="true"> + android:usesCleartextTraffic="true" + android:enableOnBackInvokedCallback="true"> { preferenceRepository ?? locator.get(), _logger = logger ?? locator.get(), super(const StoriesState.init()) { + on( + onLoadStories, + transformer: sequential(), + ); on(onInitialize); on(onRefresh); on(onLoadMore); - on(onStoryLoaded); + on( + onStoryLoaded, + transformer: sequential(), + ); on(onStoryRead); on(onStoryUnread); on(onStoriesLoaded); @@ -88,14 +96,15 @@ class StoriesBloc extends Bloc { ), ); for (final StoryType type in StoryType.values) { - await loadStories(type: type, emit: emit); + add(LoadStories(type: type)); } } - Future loadStories({ - required StoryType type, - required Emitter emit, - }) async { + Future onLoadStories( + LoadStories event, + Emitter emit, + ) async { + final StoryType type = event.type; if (state.isOfflineReading) { final List ids = await _offlineRepository.getCachedStoryIds(type: type); @@ -121,13 +130,12 @@ class StoriesBloc extends Bloc { .copyWithStoryIdsUpdated(type: type, to: ids) .copyWithCurrentPageUpdated(type: type, to: 0), ); - _hackerNewsRepository + await _hackerNewsRepository .fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize)) .listen((Story story) { add(StoryLoaded(story: story, type: type)); - }).onDone(() { - add(StoriesLoaded(type: type)); - }); + }).asFuture(); + add(StoriesLoaded(type: type)); } } @@ -153,7 +161,7 @@ class StoriesBloc extends Bloc { ); } else { emit(state.copyWithRefreshed(type: event.type)); - await loadStories(type: event.type, emit: emit); + add(LoadStories(type: event.type)); } } diff --git a/lib/blocs/stories/stories_event.dart b/lib/blocs/stories/stories_event.dart index 0736027..f0a5c6a 100644 --- a/lib/blocs/stories/stories_event.dart +++ b/lib/blocs/stories/stories_event.dart @@ -5,6 +5,15 @@ abstract class StoriesEvent extends Equatable { List get props => []; } +class LoadStories extends StoriesEvent { + LoadStories({required this.type}); + + final StoryType type; + + @override + List get props => [type]; +} + class StoriesInitialize extends StoriesEvent { @override List get props => []; diff --git a/lib/config/locator.dart b/lib/config/locator.dart index b0352ec..811a64b 100644 --- a/lib/config/locator.dart +++ b/lib/config/locator.dart @@ -25,6 +25,7 @@ Future setUpLocator() async { ) ..registerSingleton(SembastRepository()) ..registerSingleton(HackerNewsRepository()) + ..registerSingleton(HackerNewsWebRepository()) ..registerSingleton(PreferenceRepository()) ..registerSingleton(SearchRepository()) ..registerSingleton(AuthRepository()) diff --git a/lib/cubits/comments/comments_cubit.dart b/lib/cubits/comments/comments_cubit.dart index 11d92c0..673aa3b 100644 --- a/lib/cubits/comments/comments_cubit.dart +++ b/lib/cubits/comments/comments_cubit.dart @@ -325,16 +325,16 @@ class CommentsCubit extends Cubit { if (parent == null) { return; } else { + await router.push( + '/${ItemScreen.routeName}', + extra: ItemScreenArgs(item: parent), + ); + emit( state.copyWith( fetchRootStatus: CommentsStatus.loaded, ), ); - - await router.push( - '/${ItemScreen.routeName}', - extra: ItemScreenArgs(item: parent), - ); } } diff --git a/lib/cubits/fav/fav_cubit.dart b/lib/cubits/fav/fav_cubit.dart index 5042364..c977ba7 100644 --- a/lib/cubits/fav/fav_cubit.dart +++ b/lib/cubits/fav/fav_cubit.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:collection'; + import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hacki/blocs/blocs.dart'; @@ -13,12 +16,15 @@ class FavCubit extends Cubit { AuthRepository? authRepository, PreferenceRepository? preferenceRepository, HackerNewsRepository? hackerNewsRepository, + HackerNewsWebRepository? hackerNewsWebRepository, }) : _authBloc = authBloc, _authRepository = authRepository ?? locator.get(), _preferenceRepository = preferenceRepository ?? locator.get(), _hackerNewsRepository = hackerNewsRepository ?? locator.get(), + _hackerNewsWebRepository = + hackerNewsWebRepository ?? locator.get(), super(FavState.init()) { init(); } @@ -27,43 +33,41 @@ class FavCubit extends Cubit { final AuthRepository _authRepository; final PreferenceRepository _preferenceRepository; final HackerNewsRepository _hackerNewsRepository; + final HackerNewsWebRepository _hackerNewsWebRepository; + late final StreamSubscription? _usernameSubscription; static const int _pageSize = 20; - String? _username; Future init() async { - _authBloc.stream.listen((AuthState authState) { - if (authState.username != _username) { - _preferenceRepository - .favList(of: authState.username) - .then((List favIds) { + _usernameSubscription = _authBloc.stream + .map((AuthState event) => event.username) + .distinct() + .listen((String username) { + _preferenceRepository.favList(of: username).then((List favIds) { + emit( + state.copyWith( + favIds: favIds, + favItems: [], + currentPage: 0, + ), + ); + _hackerNewsRepository + .fetchItemsStream( + ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)), + ) + .listen(_onItemLoaded) + .onDone(() { emit( state.copyWith( - favIds: favIds, - favItems: [], - currentPage: 0, + status: Status.success, ), ); - _hackerNewsRepository - .fetchItemsStream( - ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)), - ) - .listen(_onItemLoaded) - .onDone(() { - emit( - state.copyWith( - status: Status.success, - ), - ); - }); }); - - _username = authState.username; - } + }); }); } Future addFav(int id) async { - final String username = _authBloc.state.username; + if (state.favIds.contains(id)) return; await _preferenceRepository.addFav(username: username, id: id); @@ -89,8 +93,6 @@ class FavCubit extends Cubit { } void removeFav(int id) { - final String username = _authBloc.state.username; - _preferenceRepository.removeFav(username: username, id: id); emit( @@ -136,8 +138,6 @@ class FavCubit extends Cubit { } void refresh() { - final String username = _authBloc.state.username; - emit( state.copyWith( status: Status.inProgress, @@ -167,6 +167,23 @@ class FavCubit extends Cubit { emit(FavState.init()); } + Future merge() async { + if (_authBloc.state.isLoggedIn) { + emit(state.copyWith(mergeStatus: Status.inProgress)); + final Iterable ids = await _hackerNewsWebRepository.fetchFavorites( + of: _authBloc.state.username, + ); + final List combinedIds = [...ids, ...state.favIds]; + final LinkedHashSet mergedIds = LinkedHashSet.from(combinedIds); + await _preferenceRepository.overwriteFav( + username: username, + ids: mergedIds, + ); + emit(state.copyWith(mergeStatus: Status.success)); + refresh(); + } + } + void _onItemLoaded(Item item) { emit( state.copyWith( @@ -174,4 +191,14 @@ class FavCubit extends Cubit { ), ); } + + @override + Future close() { + _usernameSubscription?.cancel(); + return super.close(); + } +} + +extension on FavCubit { + String get username => _authBloc.state.username; } diff --git a/lib/cubits/fav/fav_state.dart b/lib/cubits/fav/fav_state.dart index f8e75c2..f637a1e 100644 --- a/lib/cubits/fav/fav_state.dart +++ b/lib/cubits/fav/fav_state.dart @@ -5,6 +5,7 @@ class FavState extends Equatable { required this.favIds, required this.favItems, required this.status, + required this.mergeStatus, required this.currentPage, }); @@ -12,23 +13,27 @@ class FavState extends Equatable { : favIds = [], favItems = [], status = Status.idle, + mergeStatus = Status.idle, currentPage = 0; final List favIds; final List favItems; final Status status; + final Status mergeStatus; final int currentPage; FavState copyWith({ List? favIds, List? favItems, Status? status, + Status? mergeStatus, int? currentPage, }) { return FavState( favIds: favIds ?? this.favIds, favItems: favItems ?? this.favItems, status: status ?? this.status, + mergeStatus: mergeStatus ?? this.mergeStatus, currentPage: currentPage ?? this.currentPage, ); } @@ -36,6 +41,7 @@ class FavState extends Equatable { @override List get props => [ status, + mergeStatus, currentPage, favIds, favItems, diff --git a/lib/main.dart b/lib/main.dart index d0cb64c..e653b73 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -289,13 +289,13 @@ class HackiApp extends StatelessWidget { ); return FeatureDiscovery( child: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear( - state.textScaleFactor == 1 - ? 1 - : state.textScaleFactor, - ), - ), + data: state.textScaleFactor == 1 + ? MediaQuery.of(context) + : MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + state.textScaleFactor, + ), + ), child: MaterialApp.router( key: Key(state.appColor.hashCode.toString()), title: 'Hacki', diff --git a/lib/repositories/hacker_news_web_repository.dart b/lib/repositories/hacker_news_web_repository.dart new file mode 100644 index 0000000..2ea418a --- /dev/null +++ b/lib/repositories/hacker_news_web_repository.dart @@ -0,0 +1,45 @@ +import 'package:collection/collection.dart'; +import 'package:html/dom.dart'; +import 'package:html/parser.dart'; +import 'package:http/http.dart'; + +/// For fetching anything that cannot be fetched through Hacker News API. +class HackerNewsWebRepository { + HackerNewsWebRepository(); + + static const String _favoritesBaseUrl = + 'https://news.ycombinator.com/favorites?id='; + static const String _aThingSelector = + '#hnmain > tbody > tr:nth-child(3) > td > table > tbody > .athing'; + + Future> fetchFavorites({required String of}) async { + final String username = of; + final List allIds = []; + int page = 0; + + Future> fetchIds(int page) async { + final Uri url = Uri.parse('$_favoritesBaseUrl$username&p=$page'); + final Response response = await get(url); + final Document document = parse(response.body); + final List elements = document.querySelectorAll(_aThingSelector); + final Iterable parsedIds = elements + .map( + (Element e) => int.tryParse(e.id), + ) + .whereNotNull(); + return parsedIds; + } + + Iterable ids; + while (true) { + ids = await fetchIds(page); + if (ids.isEmpty) { + break; + } + allIds.addAll(ids); + page++; + } + + return allIds; + } +} diff --git a/lib/repositories/preference_repository.dart b/lib/repositories/preference_repository.dart index f22c8b5..d5cf383 100644 --- a/lib/repositories/preference_repository.dart +++ b/lib/repositories/preference_repository.dart @@ -157,7 +157,6 @@ class PreferenceRepository { ((prefs.getStringList(_getFavKey('')) ?? []) ..addAll(prefs.getStringList(_getFavKey(of)) ?? [])) .map(int.parse) - .toSet() .toList(); return favList; @@ -175,7 +174,7 @@ class PreferenceRepository { await _syncedPrefs.setStringList( key: key, - val: favList.map((int e) => e.toString()).toSet().toList(), + val: favList.map((int e) => e.toString()).toList(), ); } else { final SharedPreferences prefs = await _prefs; @@ -186,7 +185,30 @@ class PreferenceRepository { await prefs.setStringList( key, - favList.map((int e) => e.toString()).toSet().toList(), + favList.map((int e) => e.toString()).toList(), + ); + } + } + + Future overwriteFav({ + required String username, + required Iterable ids, + }) async { + final String key = _getFavKey(username); + final List favList = + ids.map((int e) => e.toString()).toList(growable: false); + + if (Platform.isIOS) { + await _syncedPrefs.setStringList( + key: key, + val: favList, + ); + } else { + final SharedPreferences prefs = await _prefs; + + await prefs.setStringList( + key, + favList, ); } } diff --git a/lib/repositories/repositories.dart b/lib/repositories/repositories.dart index c3d1f19..9995396 100644 --- a/lib/repositories/repositories.dart +++ b/lib/repositories/repositories.dart @@ -1,5 +1,6 @@ export 'auth_repository.dart'; export 'hacker_news_repository.dart'; +export 'hacker_news_web_repository.dart'; export 'offline_repository.dart'; export 'post_repository.dart'; export 'preference_repository.dart'; diff --git a/lib/screens/item/widgets/login_dialog.dart b/lib/screens/item/widgets/login_dialog.dart index 1f29390..c195e3c 100644 --- a/lib/screens/item/widgets/login_dialog.dart +++ b/lib/screens/item/widgets/login_dialog.dart @@ -188,11 +188,11 @@ class _LoginDialogState extends State with ItemActionMixin { : Palette.grey, ), ), - child: const Text( + child: Text( 'Log in', style: TextStyle( fontWeight: FontWeight.bold, - color: Palette.white, + color: Theme.of(context).colorScheme.onPrimary, ), ), ), diff --git a/lib/screens/item/widgets/main_view.dart b/lib/screens/item/widgets/main_view.dart index 8e3308b..6be2bca 100644 --- a/lib/screens/item/widgets/main_view.dart +++ b/lib/screens/item/widgets/main_view.dart @@ -301,7 +301,7 @@ class _ParentItemSection extends StatelessWidget { left: Dimens.pt6, right: Dimens.pt6, bottom: Dimens.pt12, - top: Dimens.pt12, + top: Dimens.pt6, ), child: Text.rich( TextSpan( @@ -390,83 +390,105 @@ class _ParentItemSection extends StatelessWidget { height: Dimens.zero, ), ] else ...[ - Row( - children: [ - if (item is Story) ...[ + SizedBox( + height: 48, + child: Row( + children: [ + if (item is Story) ...[ + const SizedBox( + width: Dimens.pt12, + ), + Text( + '''${item.score} karma, ${item.descendants} cmt${item.descendants > 1 ? 's' : ''}''', + style: Theme.of(context).textTheme.labelLarge, + textScaler: MediaQuery.of(context).clampedTextScaler, + ), + ] else ...[ + const SizedBox( + width: Dimens.pt4, + ), + BlocSelector( + selector: (CommentsState state) => + state.fetchParentStatus, + builder: (BuildContext context, CommentsStatus status) { + return TextButton( + onPressed: + context.read().loadParentThread, + child: status == CommentsStatus.inProgress + ? const SizedBox( + height: Dimens.pt12, + width: Dimens.pt12, + child: CustomCircularProgressIndicator( + strokeWidth: Dimens.pt2, + ), + ) + : Text( + 'View Parent', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + ), + textScaler: + MediaQuery.of(context).clampedTextScaler, + ), + ); + }, + ), + BlocSelector( + selector: (CommentsState state) => state.fetchRootStatus, + builder: (BuildContext context, CommentsStatus status) { + return TextButton( + onPressed: + context.read().loadRootThread, + child: status == CommentsStatus.inProgress + ? const SizedBox( + height: Dimens.pt12, + width: Dimens.pt12, + child: CustomCircularProgressIndicator( + strokeWidth: Dimens.pt2, + ), + ) + : Text( + 'View Root', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + ), + textScaler: + MediaQuery.of(context).clampedTextScaler, + ), + ); + }, + ), + ], + const Spacer(), + if (!state.isOfflineReading) + CustomDropdownMenu( + menuChildren: FetchMode.values, + onSelected: context.read().updateFetchMode, + selected: state.fetchMode, + ), const SizedBox( - width: Dimens.pt12, + width: Dimens.pt6, ), - Text( - '''${item.score} karma, ${item.descendants} comment${item.descendants > 1 ? 's' : ''}''', - style: Theme.of(context).textTheme.labelMedium, - textScaler: TextScaler.noScaling, + CustomDropdownMenu( + menuChildren: CommentsOrder.values, + onSelected: context.read().updateOrder, + selected: state.order, ), - ] else ...[ const SizedBox( width: Dimens.pt4, ), - TextButton( - onPressed: context.read().loadParentThread, - child: state.fetchParentStatus == CommentsStatus.inProgress - ? const SizedBox( - height: Dimens.pt12, - width: Dimens.pt12, - child: CustomCircularProgressIndicator( - strokeWidth: Dimens.pt2, - ), - ) - : Text( - 'View Parent', - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - textScaler: TextScaler.noScaling, - ), - ), - TextButton( - onPressed: context.read().loadRootThread, - child: state.fetchRootStatus == CommentsStatus.inProgress - ? const SizedBox( - height: Dimens.pt12, - width: Dimens.pt12, - child: CustomCircularProgressIndicator( - strokeWidth: Dimens.pt2, - ), - ) - : Text( - 'View Root', - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - textScaler: TextScaler.noScaling, - ), - ), ], - const Spacer(), - if (!state.isOfflineReading) - CustomDropdownMenu( - menuChildren: FetchMode.values, - onSelected: context.read().updateFetchMode, - selected: state.fetchMode, - ), - const SizedBox( - width: Dimens.pt6, - ), - CustomDropdownMenu( - menuChildren: CommentsOrder.values, - onSelected: context.read().updateOrder, - selected: state.order, - ), - const SizedBox( - width: Dimens.pt4, - ), - ], + ), ), const Divider( height: Dimens.zero, diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index 80580bd..b8231fa 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -138,6 +138,8 @@ class _ProfileScreenState extends State ..loadComplete(); } }, + buildWhen: (FavState previous, FavState current) => + previous.favItems.length != current.favItems.length, builder: (BuildContext context, FavState favState) { if (favState.favItems.isEmpty && favState.status != Status.inProgress) { @@ -148,6 +150,31 @@ class _ProfileScreenState extends State ); } + Widget? header() => authState.isLoggedIn + ? BlocSelector( + selector: (FavState state) => state.mergeStatus, + builder: ( + BuildContext context, + Status status, + ) => + TextButton( + onPressed: () { + context.read().merge(); + }, + child: status == Status.inProgress + ? const SizedBox( + height: Dimens.pt12, + width: Dimens.pt12, + child: + CustomCircularProgressIndicator( + strokeWidth: Dimens.pt2, + ), + ) + : const Text('Sync from Hacker News'), + ), + ) + : null; + return BlocBuilder( buildWhen: ( PreferenceState previous, @@ -181,6 +208,7 @@ class _ProfileScreenState extends State onTap: (Item item) => goToItemScreen( args: ItemScreenArgs(item: item), ), + header: header(), itemBuilder: (Widget child, Item item) { return Slidable( dragStartBehavior: DragStartBehavior.start, diff --git a/lib/screens/profile/widgets/settings.dart b/lib/screens/profile/widgets/settings.dart index c7be6a1..fe0ea32 100644 --- a/lib/screens/profile/widgets/settings.dart +++ b/lib/screens/profile/widgets/settings.dart @@ -82,34 +82,10 @@ class _SettingsState extends State with ItemActionMixin { const SizedBox( height: Dimens.pt8, ), - const Flex( - direction: Axis.horizontal, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: Row( - children: [ - SizedBox( - width: Dimens.pt16, - ), - Text('Default fetch mode'), - Spacer(), - ], - ), - ), - Flexible( - child: Row( - children: [ - Text('Default comments order'), - Spacer(), - ], - ), - ), - ], - ), Flex( direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Row( @@ -117,60 +93,67 @@ class _SettingsState extends State with ItemActionMixin { const SizedBox( width: Dimens.pt16, ), - DropdownMenu( - initialSelection: preferenceState.fetchMode, - dropdownMenuEntries: FetchMode.values - .map( - (FetchMode val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (FetchMode? fetchMode) { - if (fetchMode != null) { - HapticFeedbackUtil.selection(); - context.read().update( - FetchModePreference( - val: fetchMode.index, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Default fetch mode'), + DropdownMenu( + initialSelection: preferenceState.fetchMode, + dropdownMenuEntries: FetchMode.values + .map( + (FetchMode val) => + DropdownMenuEntry( + value: val, + label: val.description, ), - ); - } - }, + ) + .toList(), + onSelected: (FetchMode? fetchMode) { + if (fetchMode != null) { + HapticFeedbackUtil.selection(); + context.read().update( + FetchModePreference( + val: fetchMode.index, + ), + ); + } + }, + ), + ], ), - const Spacer(), ], ), ), - Flexible( - child: Row( - children: [ - DropdownMenu( - initialSelection: preferenceState.order, - dropdownMenuEntries: CommentsOrder.values - .map( - (CommentsOrder val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (CommentsOrder? order) { - if (order != null) { - HapticFeedbackUtil.selection(); - context.read().update( - CommentsOrderPreference( - val: order.index, - ), - ); - } - }, - ), - const Spacer(), - ], - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Default comments order'), + DropdownMenu( + initialSelection: preferenceState.order, + dropdownMenuEntries: CommentsOrder.values + .map( + (CommentsOrder val) => + DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (CommentsOrder? order) { + if (order != null) { + HapticFeedbackUtil.selection(); + context.read().update( + CommentsOrderPreference( + val: order.index, + ), + ); + } + }, + ), + ], + ), + const SizedBox( + width: Dimens.pt16, ), ], ), @@ -228,6 +211,7 @@ class _SettingsState extends State with ItemActionMixin { horizontal: Dimens.pt16, ), child: DropdownMenu( + enabled: preferenceState.markReadStoriesEnabled, label: Text(StoryMarkingModePreference().title), initialSelection: preferenceState.storyMarkingMode, onSelected: (StoryMarkingMode? storyMarkingMode) { @@ -249,6 +233,13 @@ class _SettingsState extends State with ItemActionMixin { ), ) .toList(), + inputDecorationTheme: const InputDecorationTheme( + disabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Palette.grey, + ), + ), + ), expandedInsets: EdgeInsets.zero, ), ), diff --git a/lib/screens/widgets/custom_dropdown_menu.dart b/lib/screens/widgets/custom_dropdown_menu.dart index 16378c9..3a1ab9e 100644 --- a/lib/screens/widgets/custom_dropdown_menu.dart +++ b/lib/screens/widgets/custom_dropdown_menu.dart @@ -22,10 +22,8 @@ class CustomDropdownMenu extends StatelessWidget { onPressed: () => onSelected(val), child: Text( val.toString(), - style: const TextStyle( - fontSize: TextDimens.pt13, - ), - textScaler: TextScaler.noScaling, + style: Theme.of(context).textTheme.labelLarge, + textScaler: MediaQuery.of(context).clampedTextScaler, ), ), ) @@ -42,7 +40,8 @@ class CustomDropdownMenu extends StatelessWidget { children: [ Text( selected.toString(), - style: Theme.of(context).textTheme.labelMedium, + style: Theme.of(context).textTheme.labelLarge, + textScaler: MediaQuery.of(context).clampedTextScaler, ), Icon( controller.isOpen diff --git a/lib/screens/widgets/device_gesture_wrapper.dart b/lib/screens/widgets/device_gesture_wrapper.dart index e8caa6d..b9b63d2 100644 --- a/lib/screens/widgets/device_gesture_wrapper.dart +++ b/lib/screens/widgets/device_gesture_wrapper.dart @@ -16,8 +16,9 @@ class DeviceGestureWrapper extends StatelessWidget { @override Widget build(BuildContext context) { return MediaQuery( - data: const MediaQueryData( - gestureSettings: DeviceGestureSettings(touchSlop: 12), + data: MediaQueryData( + gestureSettings: const DeviceGestureSettings(touchSlop: 12), + textScaler: MediaQuery.of(context).textScaler, ), child: child, ); diff --git a/lib/screens/widgets/onboarding_view.dart b/lib/screens/widgets/onboarding_view.dart index 8054854..99c0b26 100644 --- a/lib/screens/widgets/onboarding_view.dart +++ b/lib/screens/widgets/onboarding_view.dart @@ -17,15 +17,14 @@ class _OnboardingViewState extends State { final Throttle throttle = Throttle(delay: _throttleDelay); static const Duration _throttleDelay = AppDurations.ms100; - static const double _screenshotHeight = 550; + static const double _screenshotHeight = 600; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).brightness == Brightness.light - ? Theme.of(context).colorScheme.primary - : Theme.of(context).canvasColor, + backgroundColor: Palette.transparent, + surfaceTintColor: Palette.transparent, elevation: Dimens.zero, leading: IconButton( icon: const Icon( @@ -94,10 +93,10 @@ class _OnboardingViewState extends State { Dimens.pt18, ), ), - child: const Icon( - Icons.arrow_drop_down_circle_outlined, - size: TextDimens.pt24, - color: Palette.white, + child: Icon( + Icons.arrow_drop_down_outlined, + size: TextDimens.pt36, + color: Theme.of(context).colorScheme.onPrimary, ), ), ), @@ -116,15 +115,18 @@ class _PageViewChild extends StatelessWidget { final String path; final String description; - static const double _height = 400; + static const double _height = 500; @override Widget build(BuildContext context) { return Column( children: [ - SizedBox( - height: _height, - child: Image.asset(path), + Material( + elevation: 8, + child: SizedBox( + height: _height, + child: Image.asset(path), + ), ), Padding( padding: const EdgeInsets.symmetric( diff --git a/lib/screens/widgets/stories_list_view.dart b/lib/screens/widgets/stories_list_view.dart index 91acf66..d7094bc 100644 --- a/lib/screens/widgets/stories_list_view.dart +++ b/lib/screens/widgets/stories_list_view.dart @@ -164,7 +164,7 @@ class _StoriesListViewState extends State : null, label: preferenceState.complexStoryTileEnabled ? null - : 'Pin to top', + : 'Pin', ), SlidableAction( onPressed: (_) => onMoreTapped(story, context.rect), diff --git a/lib/screens/widgets/story_tile.dart b/lib/screens/widgets/story_tile.dart index 21e8b04..d52a99a 100644 --- a/lib/screens/widgets/story_tile.dart +++ b/lib/screens/widgets/story_tile.dart @@ -53,6 +53,7 @@ class StoryTile extends StatelessWidget { story.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: hasRead ? Theme.of(context).readGrey : null, + fontWeight: FontWeight.bold, ), textAlign: TextAlign.left, ), @@ -125,13 +126,9 @@ class StoryTile extends StatelessWidget { TextSpan( text: story.title, style: TextStyle( - color: hasRead - ? Palette.grey[500] - : Theme.of(context) - .textTheme - .bodyLarge - ?.color, - fontWeight: hasRead ? null : FontWeight.w500, + color: + hasRead ? Theme.of(context).readGrey : null, + fontWeight: hasRead ? null : FontWeight.bold, fontSize: simpleTileFontSize, ), ), diff --git a/lib/styles/media_query.dart b/lib/styles/media_query.dart new file mode 100644 index 0000000..b1cb55b --- /dev/null +++ b/lib/styles/media_query.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +extension MediaQueryDataExtension on MediaQueryData { + TextScaler get clampedTextScaler => textScaler.clamp(maxScaleFactor: 1.2); +} diff --git a/lib/styles/styles.dart b/lib/styles/styles.dart index 36f29c2..3eff279 100644 --- a/lib/styles/styles.dart +++ b/lib/styles/styles.dart @@ -1,3 +1,4 @@ export 'dimens.dart'; +export 'media_query.dart'; export 'palette.dart'; export 'theme.dart'; diff --git a/pubspec.lock b/pubspec.lock index 3cc2134..624f0f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.2" + bloc_concurrency: + dependency: "direct main" + description: + name: bloc_concurrency + sha256: "44535c9f429cd7e91d548cf89fde1c23a8b4b3637decdb1865bb583091a00d4e" + url: "https://pub.dev" + source: hosted + version: "0.2.2" bloc_test: dependency: "direct dev" description: @@ -1077,6 +1085,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cd5fa67..4a392b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: animations: ^2.0.8 badges: ^3.0.2 bloc: ^8.1.1 + bloc_concurrency: ^0.2.2 cached_network_image: ^3.3.0 clipboard: ^0.1.3 collection: ^1.17.1