From 4cdc2153f1d439b5fdb16c9a092037ea91ce2d55 Mon Sep 17 00:00:00 2001 From: lotusprey <35681808+lotusprey@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:35:27 +0200 Subject: [PATCH] v.1.7.0 (#188) * easy sorting picker for preview collections * tweaks * doujin filter * home nav bar profile icon now also opens settings * update versioning --------- Co-authored-by: lotus --- .../metadata/android/en-US/changelogs/83.txt | 3 ++ .../collection_entries_provider.dart | 4 +- .../collection/collection_filter_model.dart | 19 +++++++-- .../collection_filter_provider.dart | 28 +++---------- .../collection/collection_filter_view.dart | 22 +++++----- lib/feature/collection/collection_list.dart | 6 +-- .../collection/collection_provider.dart | 15 ++++--- .../discover/discover_filter_model.dart | 12 ++++-- .../discover/discover_filter_view.dart | 6 +++ lib/feature/home/home_view.dart | 29 +++++++------ lib/feature/settings/settings_app_view.dart | 14 +------ lib/feature/user/user_view.dart | 41 +++++++++++-------- lib/feature/viewer/persistence_model.dart | 14 ++----- lib/util/graphql.dart | 5 ++- pubspec.lock | 4 +- pubspec.yaml | 4 +- 16 files changed, 117 insertions(+), 109 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/83.txt diff --git a/fastlane/metadata/android/en-US/changelogs/83.txt b/fastlane/metadata/android/en-US/changelogs/83.txt new file mode 100644 index 00000000..ceb69825 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/83.txt @@ -0,0 +1,3 @@ +- In the collection filter sheets for both your anime and manga collection, you can explicitly set the preview collection sorting, separately from the one for the full collection. The exclusive airing sorting for anime collection preview toggle is removed from settings. +- Added a doujin filter in the discover filter sheet. +- While on the profile tab of the home screen, tapping the profile icon will scroll to top like before. But now it will also open settings, if you're already at the top. \ No newline at end of file diff --git a/lib/feature/collection/collection_entries_provider.dart b/lib/feature/collection/collection_entries_provider.dart index 5bf1ab56..31b81af5 100644 --- a/lib/feature/collection/collection_entries_provider.dart +++ b/lib/feature/collection/collection_entries_provider.dart @@ -13,7 +13,9 @@ final collectionEntriesProvider = final mediaFilter = filter.mediaFilter; final search = filter.search.toLowerCase(); - ref.watch(collectionProvider(tag).notifier).ensureSorted(mediaFilter.sort); + ref + .watch(collectionProvider(tag).notifier) + .ensureSorted(mediaFilter.sort, mediaFilter.previewSort); final entries = ref .watch(collectionProvider(tag)) diff --git a/lib/feature/collection/collection_filter_model.dart b/lib/feature/collection/collection_filter_model.dart index 49f2dd60..cff7df8e 100644 --- a/lib/feature/collection/collection_filter_model.dart +++ b/lib/feature/collection/collection_filter_model.dart @@ -20,12 +20,19 @@ class CollectionFilter { } class CollectionMediaFilter { - CollectionMediaFilter(this.sort); + CollectionMediaFilter() + : sort = EntrySort.title, + previewSort = EntrySort.title; - factory CollectionMediaFilter.fromPersistenceMap(Map map) { + factory CollectionMediaFilter.fromPersistenceMap( + Map map, + ) { final sort = EntrySort.values.getOrFirst(map['sort']); + final previewSort = EntrySort.values.getOrFirst(map['previewSort']); - final filter = CollectionMediaFilter(sort) + final filter = CollectionMediaFilter() + ..sort = sort + ..previewSort = previewSort ..startYearFrom = map['startYearFrom'] ..startYearTo = map['startYearTo'] ..country = OriginCountry.values.getOrNull(map['country']) @@ -61,6 +68,7 @@ class CollectionMediaFilter { final tagIn = []; final tagNotIn = []; EntrySort sort; + EntrySort previewSort; int? startYearFrom; int? startYearTo; OriginCountry? country; @@ -80,7 +88,9 @@ class CollectionMediaFilter { isPrivate != null || hasNotes != null; - CollectionMediaFilter copy() => CollectionMediaFilter(sort) + CollectionMediaFilter copy() => CollectionMediaFilter() + ..sort = sort + ..previewSort = previewSort ..statuses.addAll(statuses) ..formats.addAll(formats) ..genreIn.addAll(genreIn) @@ -101,6 +111,7 @@ class CollectionMediaFilter { 'tagIn': tagIn, 'tagNotIn': tagNotIn, 'sort': sort.index, + 'previewSort': previewSort.index, 'startYearFrom': startYearFrom, 'startYearTo': startYearTo, 'country': country?.index, diff --git a/lib/feature/collection/collection_filter_provider.dart b/lib/feature/collection/collection_filter_provider.dart index da09dc48..3bc45164 100644 --- a/lib/feature/collection/collection_filter_provider.dart +++ b/lib/feature/collection/collection_filter_provider.dart @@ -2,8 +2,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/feature/collection/collection_filter_model.dart'; import 'package:otraku/feature/viewer/persistence_provider.dart'; import 'package:otraku/feature/collection/collection_models.dart'; -import 'package:otraku/feature/home/home_provider.dart'; -import 'package:otraku/feature/media/media_models.dart'; final collectionFilterProvider = NotifierProvider.autoDispose .family( @@ -14,27 +12,13 @@ class CollectionFilterNotifier extends AutoDisposeFamilyNotifier { @override CollectionFilter build(arg) { - final mediaFilter = arg.ofAnime - ? ref.watch( - persistenceProvider.select((s) => s.animeCollectionMediaFilter), - ) - : ref.watch( - persistenceProvider.select((s) => s.mangaCollectionMediaFilter), - ); + final mediaFilter = ref.watch(persistenceProvider.select( + (s) => arg.ofAnime + ? s.animeCollectionMediaFilter + : s.mangaCollectionMediaFilter, + )); - final options = ref.watch(persistenceProvider.select((s) => s.options)); - final isInAnimePreviewProvider = homeProvider.select( - (s) => !s.didExpandAnimeCollection, - ); - - if (arg.userId == ref.watch(viewerIdProvider) && - arg.ofAnime && - options.airingSortForAnimePreview && - ref.watch(isInAnimePreviewProvider)) { - mediaFilter.sort = EntrySort.airing; - } - - return CollectionFilter(mediaFilter); + return CollectionFilter(mediaFilter.copy()); } CollectionFilter update( diff --git a/lib/feature/collection/collection_filter_view.dart b/lib/feature/collection/collection_filter_view.dart index 928eb726..61b7dcdb 100644 --- a/lib/feature/collection/collection_filter_view.dart +++ b/lib/feature/collection/collection_filter_view.dart @@ -87,6 +87,17 @@ class _FilterCollectionViewState extends ConsumerState { ), ); + Widget? previewSortPicker; + if (ofViewer && + (widget.tag.ofAnime && options.animeCollectionPreview || + !widget.tag.ofAnime && options.mangaCollectionPreview)) { + previewSortPicker = EntrySortChipSelector( + title: 'Preview Sorting', + value: _filter.previewSort, + onChanged: (v) => _filter.previewSort = v, + ); + } + return SheetWithButtonRow( buttons: BottomBar( options.leftHanded @@ -104,16 +115,7 @@ class _FilterCollectionViewState extends ConsumerState { value: _filter.sort, onChanged: (v) => _filter.sort = v, ), - if (ofViewer && - _filter.sort == EntrySort.airing && - options.airingSortForAnimePreview) - Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: Text( - 'Note: Airing sort is set to replace your default one for the anime preview. You can turn it off in the settings.', - style: TextTheme.of(context).labelMedium, - ), - ), + if (previewSortPicker != null) previewSortPicker, ChipMultiSelector( title: 'Statuses', items: ReleaseStatus.values.map((v) => (v.label, v)).toList(), diff --git a/lib/feature/collection/collection_list.dart b/lib/feature/collection/collection_list.dart index 9fc7cd1b..7447fb70 100644 --- a/lib/feature/collection/collection_list.dart +++ b/lib/feature/collection/collection_list.dart @@ -74,11 +74,7 @@ class _Tile extends StatelessWidget { ), Expanded( child: Padding( - padding: const EdgeInsets.only( - top: Theming.offset, - left: Theming.offset, - right: Theming.offset, - ), + padding: Theming.paddingAll, child: _TileContent(entry, scoreFormat, onProgressUpdated), ), ), diff --git a/lib/feature/collection/collection_provider.dart b/lib/feature/collection/collection_provider.dart index d5f731ee..b4f3aebc 100644 --- a/lib/feature/collection/collection_provider.dart +++ b/lib/feature/collection/collection_provider.dart @@ -57,12 +57,17 @@ class CollectionNotifier return collection; } - void ensureSorted(EntrySort sort) { - if (_sort == sort) return; - _sort = sort; - + void ensureSorted(EntrySort sort, EntrySort previewSort) { _updateState((collection) { - collection.sort(sort); + final selectedSort = switch (collection) { + FullCollection _ => sort, + PreviewCollection _ => previewSort, + }; + + if (_sort == selectedSort) return; + _sort = selectedSort; + + collection.sort(selectedSort); return null; }); } diff --git a/lib/feature/discover/discover_filter_model.dart b/lib/feature/discover/discover_filter_model.dart index d3a8edf3..edcd5882 100644 --- a/lib/feature/discover/discover_filter_model.dart +++ b/lib/feature/discover/discover_filter_model.dart @@ -52,7 +52,8 @@ class DiscoverMediaFilter { ..startYearTo = map['startYearTo'] ..country = OriginCountry.values.getOrNull(map['country']) ..inLists = map['inLists'] - ..isAdult = map['isAdult']; + ..isAdult = map['isAdult'] + ..isLicensed = map['isLicensed']; for (final e in map['statuses'] ?? const []) { final status = ReleaseStatus.values.getOrNull(e); @@ -105,6 +106,7 @@ class DiscoverMediaFilter { OriginCountry? country; bool? inLists; bool? isAdult; + bool? isLicensed; bool get isActive => statuses.isNotEmpty || @@ -120,7 +122,8 @@ class DiscoverMediaFilter { startYearTo != null || country != null || inLists != null || - isAdult != null; + isAdult != null || + isLicensed != null; DiscoverMediaFilter copy() => DiscoverMediaFilter(sort) ..statuses.addAll(statuses) @@ -136,7 +139,8 @@ class DiscoverMediaFilter { ..startYearTo = startYearTo ..country = country ..inLists = inLists - ..isAdult = isAdult; + ..isAdult = isAdult + ..isLicensed = isLicensed; static DiscoverMediaFilter fromCollection({ required CollectionMediaFilter filter, @@ -174,6 +178,7 @@ class DiscoverMediaFilter { if (country != null) 'countryOfOrigin': country!.code, if (inLists != null) 'onList': inLists, if (isAdult != null) 'isAdult': isAdult, + if (isLicensed != null) 'isLicensed': isLicensed, }; Map toPersistenceMap() => { @@ -192,5 +197,6 @@ class DiscoverMediaFilter { 'country': country?.index, 'inLists': inLists, 'isAdult': isAdult, + 'isLicensed': isLicensed, }; } diff --git a/lib/feature/discover/discover_filter_view.dart b/lib/feature/discover/discover_filter_view.dart index 02102632..4bfd7e9e 100644 --- a/lib/feature/discover/discover_filter_view.dart +++ b/lib/feature/discover/discover_filter_view.dart @@ -174,6 +174,12 @@ class _DiscoverFilterViewState extends ConsumerState { value: _filter.isAdult, onChanged: (v) => _filter.isAdult = v, ), + ChipSelector( + title: 'Licensing', + items: const [('Licensed', true), ('Doujin', false)], + value: _filter.isLicensed, + onChanged: (v) => _filter.isLicensed = v, + ), SizedBox( height: MediaQuery.paddingOf(context).bottom + BottomBar.height + diff --git a/lib/feature/home/home_view.dart b/lib/feature/home/home_view.dart index c777e5a6..65392c3d 100644 --- a/lib/feature/home/home_view.dart +++ b/lib/feature/home/home_view.dart @@ -174,32 +174,37 @@ class _HomeViewState extends ConsumerState final tab = HomeTab.values[i]; switch (tab) { + case HomeTab.feed: + _feedScrollCtrl.scrollToTop(); case HomeTab.anime: if (_animeScrollCtrl.position.pixels > 0) { _animeScrollCtrl.scrollToTop(); - } else { - _toggleSearchFocus(); + return; } - return; + + _toggleSearchFocus(); case HomeTab.manga: if (_mangaScrollCtrl.position.pixels > 0) { _mangaScrollCtrl.scrollToTop(); - } else { - _toggleSearchFocus(); + return; } - return; + + _toggleSearchFocus(); case HomeTab.discover: if (_discoverScrollCtrl.position.pixels > 0) { _discoverScrollCtrl.scrollToTop(); - } else { - _toggleSearchFocus(); + return; } + + _toggleSearchFocus(); return; - case HomeTab.feed: - _feedScrollCtrl.scrollToTop(); case HomeTab.profile: - primaryScrollCtrl.scrollToTop(); - return; + if (primaryScrollCtrl.positions.last.pixels > 0) { + primaryScrollCtrl.scrollToTop(); + return; + } + + context.push(Routes.settings); } }, ); diff --git a/lib/feature/settings/settings_app_view.dart b/lib/feature/settings/settings_app_view.dart index 2432f1e0..bfb9992c 100644 --- a/lib/feature/settings/settings_app_view.dart +++ b/lib/feature/settings/settings_app_view.dart @@ -33,7 +33,7 @@ class SettingsAppSubview extends ConsumerWidget { controller: scrollCtrl, padding: EdgeInsets.only( top: listPadding.top + Theming.offset, - bottom: listPadding.bottom + Theming.offset, + bottom: listPadding.bottom + Theming.offset + 60, ), children: [ ExpansionTile( @@ -50,6 +50,7 @@ class SettingsAppSubview extends ConsumerWidget { ThemePreview(ref: ref, options: options), StatefulSwitchListTile( title: const Text('High Contrast'), + subtitle: const Text('Pure white/black backgrounds'), value: options.highContrast, onChanged: (v) => update(options.copyWith(highContrast: v)), ), @@ -80,17 +81,6 @@ class SettingsAppSubview extends ConsumerWidget { options.copyWith(mangaCollectionPreview: v), ), ), - StatefulSwitchListTile( - title: const Text('Force Airing Sort for Anime Preview'), - subtitle: const Text( - 'Sort by soonest airing, instead of the default', - ), - value: options.airingSortForAnimePreview, - onChanged: (v) => update( - options.copyWith(airingSortForAnimePreview: v), - ), - ), - const SizedBox(height: 5), ], ), ExpansionTile( diff --git a/lib/feature/user/user_view.dart b/lib/feature/user/user_view.dart index 75b6d33b..b862eed8 100644 --- a/lib/feature/user/user_view.dart +++ b/lib/feature/user/user_view.dart @@ -87,23 +87,28 @@ class UserHomeView extends StatelessWidget { } class _UserView extends StatelessWidget { - const _UserView(this.tag, this.avatarUrl, [this.homeScrollCtrl]); + const _UserView(this.tag, this.avatarUrl, [this.scrollCtrl]); final UserTag tag; final String? avatarUrl; - final ScrollController? homeScrollCtrl; + final ScrollController? scrollCtrl; @override Widget build(BuildContext context) { return Consumer( builder: (context, ref, _) { - final viewerId = ref.watch(viewerIdProvider); + final viewer = ref.watch( + persistenceProvider.select((s) => s.accountGroup.account), + ); + + final isViewer = viewer != null && + (tag.id != null ? tag.id == viewer.id : tag.name == viewer.name); ref.listen>( userProvider(tag), (_, s) => s.whenOrNull( data: (data) { - if (homeScrollCtrl == null) return; + if (!isViewer) return; ref .read(persistenceProvider.notifier) @@ -121,7 +126,7 @@ class _UserView extends StatelessWidget { final header = UserHeader( id: tag.id, user: user.valueOrNull, - isViewer: homeScrollCtrl != null, + isViewer: isViewer, imageUrl: avatarUrl ?? user.valueOrNull?.imageUrl, toggleFollow: () { final userId = user.valueOrNull?.id; @@ -160,12 +165,12 @@ class _UserView extends StatelessWidget { ], ), data: (data) => CustomScrollView( - controller: homeScrollCtrl, + controller: scrollCtrl, physics: Theming.bouncyPhysics, slivers: [ header, refreshControl, - _ButtonRow(data.id, viewerId), + _ButtonRow(data.id, isViewer), if (data.description.isNotEmpty) ...[ const SliverToBoxAdapter( child: SizedBox(height: Theming.offset), @@ -187,50 +192,50 @@ class _UserView extends StatelessWidget { } class _ButtonRow extends StatelessWidget { - const _ButtonRow(this.id, this.viewerId); + const _ButtonRow(this.userId, this.isViewer); - final int id; - final int? viewerId; + final int userId; + final bool isViewer; @override Widget build(BuildContext context) { final buttons = [ - if (id != viewerId) ...[ + if (!isViewer) ...[ _Button( label: 'Anime', icon: Ionicons.film, - onTap: () => context.push(Routes.animeCollection(id)), + onTap: () => context.push(Routes.animeCollection(userId)), ), _Button( label: 'Manga', icon: Ionicons.book, - onTap: () => context.push(Routes.mangaCollection(id)), + onTap: () => context.push(Routes.mangaCollection(userId)), ), ], _Button( label: 'Activities', icon: Ionicons.chatbox, - onTap: () => context.push(Routes.activities(id)), + onTap: () => context.push(Routes.activities(userId)), ), _Button( label: 'Social', icon: Ionicons.people_circle, - onTap: () => context.push(Routes.social(id)), + onTap: () => context.push(Routes.social(userId)), ), _Button( label: 'Favourites', icon: Icons.favorite, - onTap: () => context.push(Routes.favorites(id)), + onTap: () => context.push(Routes.favorites(userId)), ), _Button( label: 'Statistics', icon: Ionicons.stats_chart, - onTap: () => context.push(Routes.statistics(id)), + onTap: () => context.push(Routes.statistics(userId)), ), _Button( label: 'Reviews', icon: Icons.rate_review, - onTap: () => context.push(Routes.reviews(id)), + onTap: () => context.push(Routes.reviews(userId)), ), ]; diff --git a/lib/feature/viewer/persistence_model.dart b/lib/feature/viewer/persistence_model.dart index 5ec7d908..a32cf1c8 100644 --- a/lib/feature/viewer/persistence_model.dart +++ b/lib/feature/viewer/persistence_model.dart @@ -10,7 +10,7 @@ import 'package:otraku/feature/home/home_model.dart'; import 'package:otraku/feature/media/media_models.dart'; import 'package:otraku/util/theming.dart'; -const appVersion = '1.6.4'; +const appVersion = '1.7.0'; class Persistence { const Persistence({ @@ -30,8 +30,8 @@ class Persistence { accountGroup: AccountGroup.empty(), options: Options.empty(), appMeta: AppMeta.empty(), - animeCollectionMediaFilter: CollectionMediaFilter(EntrySort.title), - mangaCollectionMediaFilter: CollectionMediaFilter(EntrySort.title), + animeCollectionMediaFilter: CollectionMediaFilter(), + mangaCollectionMediaFilter: CollectionMediaFilter(), discoverMediaFilter: DiscoverMediaFilter(MediaSort.titleRomaji), homeActivitiesFilter: HomeActivitiesFilter.empty(), calendarFilter: CalendarFilter.empty(), @@ -191,7 +191,6 @@ class Options { required this.imageQuality, required this.animeCollectionPreview, required this.mangaCollectionPreview, - required this.airingSortForAnimePreview, required this.confirmExit, required this.leftHanded, required this.analogueClock, @@ -209,7 +208,6 @@ class Options { imageQuality: ImageQuality.high, animeCollectionPreview: true, mangaCollectionPreview: true, - airingSortForAnimePreview: true, confirmExit: false, leftHanded: false, analogueClock: false, @@ -228,7 +226,6 @@ class Options { ImageQuality.high, animeCollectionPreview: map['animeCollectionPreview'] ?? true, mangaCollectionPreview: map['mangaCollectionPreview'] ?? true, - airingSortForAnimePreview: map['airingSortForAnimePreview'] ?? true, confirmExit: map['confirmExit'] ?? false, leftHanded: map['leftHanded'] ?? false, analogueClock: map['analogueClock'] ?? false, @@ -251,7 +248,6 @@ class Options { final ImageQuality imageQuality; final bool animeCollectionPreview; final bool mangaCollectionPreview; - final bool airingSortForAnimePreview; final bool confirmExit; final bool leftHanded; final bool analogueClock; @@ -268,7 +264,6 @@ class Options { ImageQuality? imageQuality, bool? animeCollectionPreview, bool? mangaCollectionPreview, - bool? airingSortForAnimePreview, bool? confirmExit, bool? leftHanded, bool? analogueClock, @@ -287,8 +282,6 @@ class Options { animeCollectionPreview ?? this.animeCollectionPreview, mangaCollectionPreview: mangaCollectionPreview ?? this.mangaCollectionPreview, - airingSortForAnimePreview: - airingSortForAnimePreview ?? this.airingSortForAnimePreview, confirmExit: confirmExit ?? this.confirmExit, leftHanded: leftHanded ?? this.leftHanded, analogueClock: analogueClock ?? this.analogueClock, @@ -307,7 +300,6 @@ class Options { 'imageQuality': imageQuality.index, 'animeCollectionPreview': animeCollectionPreview, 'mangaCollectionPreview': mangaCollectionPreview, - 'airingSortForAnimePreview': airingSortForAnimePreview, 'confirmExit': confirmExit, 'leftHanded': leftHanded, 'analogueClock': analogueClock, diff --git a/lib/util/graphql.dart b/lib/util/graphql.dart index a939e8ee..a3f089d0 100644 --- a/lib/util/graphql.dart +++ b/lib/util/graphql.dart @@ -212,13 +212,14 @@ abstract class GqlQuery { $format_in: [MediaFormat], $genre_in: [String], $genre_not_in: [String], $tag_in: [String], $tag_not_in: [String], $onList: Boolean, $startFrom: FuzzyDateInt, $startTo: FuzzyDateInt, $countryOfOrigin: CountryCode, $season: MediaSeason, - $sources: [MediaSource], $isAdult: Boolean, $sort: [MediaSort]) { + $sources: [MediaSource], $isAdult: Boolean, $isLicensed: Boolean, $sort: [MediaSort]) { Page(page: $page) { pageInfo {hasNextPage} media(type: $type, search: $search, status_in: $status_in, format_in: $format_in, genre_in: $genre_in, genre_not_in: $genre_not_in, tag_in: $tag_in, tag_not_in: $tag_not_in, onList: $onList, startDate_greater: $startFrom, startDate_lesser: $startTo, isAdult: $isAdult, - countryOfOrigin: $countryOfOrigin, season: $season, source_in: $sources, sort: $sort) { + isLicensed: $isLicensed, countryOfOrigin: $countryOfOrigin, season: $season, + source_in: $sources, sort: $sort) { id type title {userPreferred} diff --git a/pubspec.lock b/pubspec.lock index dfa038e1..71435f3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -356,10 +356,10 @@ packages: dependency: "direct main" description: name: markdown - sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.3.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c586e8fe..19357ca1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: An unofficial AniList app. publish_to: 'none' -version: 1.6.4+82 +version: 1.7.0+83 environment: sdk: '>=3.0.0 <4.0.0' @@ -31,7 +31,7 @@ dependencies: flutter_secure_storage: ^9.2.4 # Markdown to HTML parsing. - markdown: ^7.2.2 + markdown: ^7.3.0 # Used for configuring [cached_network_image]. flutter_cache_manager: ^3.4.1