diff --git a/android/app/build.gradle b/android/app/build.gradle index 19e9b19a..7201a53b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -81,7 +81,5 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - // Workmanager - // implementation 'androidx.work:work-runtime-ktx:2.7.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } diff --git a/android/build.gradle b/android/build.gradle index fde7724a..dc79e569 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.6.21' + kotlin_version = '1.7.10' } repositories { @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle.properties b/android/gradle.properties index 66e4d9ab..7614ad11 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,4 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true # Without this line there may be crashes on Android 6.0 devices apparently. diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index cc5527d7..cfe88f69 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d1ea88d2..600786bc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,8 +6,15 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter - - path_provider_ios (0.0.1): + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) - url_launcher_ios (0.0.1): - Flutter - workmanager (0.0.1): @@ -18,10 +25,15 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - workmanager (from `.symlinks/plugins/workmanager/ios`) +SPEC REPOS: + trunk: + - FMDB + EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" @@ -31,8 +43,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" workmanager: @@ -43,8 +57,10 @@ SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index dbc68c15..091b2f55 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -207,6 +207,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -260,6 +261,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 03de4938..cb75688a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -71,5 +71,7 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/lib/activity/activity_providers.dart b/lib/activity/activities_providers.dart similarity index 51% rename from lib/activity/activity_providers.dart rename to lib/activity/activities_providers.dart index c4538d5c..d4a9ff85 100644 --- a/lib/activity/activity_providers.dart +++ b/lib/activity/activities_providers.dart @@ -1,86 +1,20 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/activity/activity_models.dart'; +import 'package:otraku/common/pagination.dart'; import 'package:otraku/home/home_provider.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; import 'package:otraku/utils/options.dart'; -/// Toggles an activity like and returns an error if unsuccessful. -Future toggleActivityLike(Activity activity) async { - try { - await Api.get(GqlMutation.toggleLike, { - 'id': activity.id, - 'type': 'ACTIVITY', - }); - return null; - } catch (e) { - return e; - } -} - -/// Toggles an activity subscription and returns an error if unsuccessful. -Future toggleActivitySubscription(Activity activity) async { - try { - await Api.get(GqlMutation.toggleActivitySubscription, { - 'id': activity.id, - 'subscribe': activity.isSubscribed, - }); - return null; - } catch (e) { - return e; - } -} - -/// Pins/Unpins an activity and returns an error if unsuccessful. -Future toggleActivityPin(Activity activity) async { - try { - await Api.get(GqlMutation.toggleActivityPin, { - 'id': activity.id, - 'pinned': activity.isPinned, - }); - return null; - } catch (e) { - return e; - } -} - -/// Toggles a reply like and returns an error if unsuccessful. -Future toggleReplyLike(ActivityReply reply) async { - try { - await Api.get(GqlMutation.toggleLike, { - 'id': reply.id, - 'type': 'ACTIVITY_REPLY', - }); - return null; - } catch (e) { - return e; - } -} - -/// Deletes an activity and returns an error if unsuccessful. -Future deleteActivity(int activityId) async { - try { - await Api.get(GqlMutation.deleteActivity, {'id': activityId}); - return null; - } catch (e) { - return e; - } -} - -/// Deletes an activity reply and returns an error if unsuccessful. -Future deleteActivityReply(int replyId) async { - try { - await Api.get(GqlMutation.deleteActivityReply, {'id': replyId}); - return null; - } catch (e) { - return e; - } -} - -final activityProvider = StateNotifierProvider.autoDispose - .family, int>( - (ref, userId) => ActivityNotifier(userId, Options().id!), +final activitiesProvider = StateNotifierProvider.autoDispose + .family>, int?>( + (ref, userId) => ActivitiesNotifier( + userId: userId, + viewerId: Options().id!, + filter: ref.watch(activityFilterProvider(userId)), + shouldLoad: + userId != null || ref.watch(homeProvider.select((s) => s.didLoadFeed)), + ), ); final activityFilterProvider = StateNotifierProvider.autoDispose @@ -101,134 +35,6 @@ final activityFilterProvider = StateNotifierProvider.autoDispose }, ); -final activitiesProvider = StateNotifierProvider.autoDispose - .family>, int?>( - (ref, userId) => ActivitiesNotifier( - userId: userId, - viewerId: Options().id!, - filter: ref.watch(activityFilterProvider(userId)), - shouldLoad: - userId != null || ref.watch(homeProvider.select((s) => s.didLoadFeed)), - ), -); - -class ActivityNotifier extends StateNotifier> { - ActivityNotifier(this.userId, this.viewerId) - : super(const AsyncValue.loading()) { - fetch(); - } - - final int userId; - final int viewerId; - - Future fetch() async { - state = await AsyncValue.guard(() async { - final replies = state.value?.replies ?? Pagination(); - - final data = await Api.get(GqlQuery.activity, { - 'id': userId, - 'page': replies.next, - if (replies.next == 1) 'withActivity': true, - }); - - final items = []; - for (final r in data['Page']['activityReplies']) { - final item = ActivityReply.maybe(r); - if (item != null) items.add(item); - } - - final activity = - state.value?.activity ?? Activity.maybe(data['Activity'], viewerId); - if (activity == null) throw StateError('Could not parse activity'); - - return ActivityState( - activity, - replies.append( - items, - data['Page']['pageInfo']['hasNextPage'] ?? false, - ), - ); - }); - } - - /// Deserializes [map] and replaces the current activity. - void replaceActivity(Map map, int viewerId) { - if (!state.hasValue) return; - final value = state.value!; - - final activity = Activity.maybe(map, viewerId); - if (activity == null) return; - - state = AsyncData(ActivityState(activity, value.replies)); - } - - /// Deserializes [map] and appends it at the end. - void appendReply(Map map) { - if (!state.hasValue) return; - final value = state.value!; - - final reply = ActivityReply.maybe(map); - if (reply == null) return; - - value.activity.replyCount++; - state = AsyncData(ActivityState( - value.activity, - Pagination.from( - items: [...value.replies.items, reply], - hasNext: value.replies.hasNext, - next: value.replies.next, - ), - )); - } - - /// Replaces an existing reply with another one. - void replaceReply(Map map) { - if (!state.hasValue) return; - final value = state.value!; - - final reply = ActivityReply.maybe(map); - if (reply == null) return; - - for (int i = 0; i < value.replies.items.length; i++) { - if (value.replies.items[i].id == reply.id) { - value.replies.items[i] = reply; - state = AsyncData(ActivityState( - value.activity, - Pagination.from( - items: value.replies.items, - hasNext: value.replies.hasNext, - next: value.replies.next, - ), - )); - return; - } - } - } - - /// Removes an already deleted reply. - void removeReply(int replyId) { - if (!state.hasValue) return; - final value = state.value!; - - for (int i = 0; i < value.replies.items.length; i++) { - if (value.replies.items[i].id == replyId) { - value.replies.items.removeAt(i); - value.activity.replyCount--; - - state = AsyncData(ActivityState( - value.activity, - Pagination.from( - items: value.replies.items, - hasNext: value.replies.hasNext, - next: value.replies.next, - ), - )); - return; - } - } - } -} - class ActivitiesNotifier extends StateNotifier>> { ActivitiesNotifier({ @@ -245,19 +51,27 @@ class ActivitiesNotifier final int viewerId; final ActivityFilter filter; + /// [_lastCreatedAt] is used to track pages, instead of the next page value + /// of the state. This prevents duplicates when more pages are loaded, + /// as new activities are created often. + int? _lastCreatedAt; + Future fetch() async { state = await AsyncValue.guard(() async { final value = state.valueOrNull ?? Pagination(); final data = await Api.get(GqlQuery.activities, { - 'page': value.next, 'typeIn': filter.typeIn.map((t) => t.name).toList(), if (userId != null) ...{ 'userId': userId, } else ...{ 'isFollowing': filter.onFollowing, - 'hasRepliesOrTypeText': (filter.onFollowing ?? true) ? null : true, + if ((filter.onFollowing ?? false)) + 'userIdNot': viewerId + else + 'hasRepliesOrText': true, }, + if (_lastCreatedAt != null) 'createdBefore': _lastCreatedAt! }); final items = []; @@ -266,6 +80,10 @@ class ActivitiesNotifier if (item != null) items.add(item); } + if (data['Page']['activities']?.isNotEmpty ?? false) { + _lastCreatedAt = data['Page']['activities'].last['createdAt']; + } + return value.append( items, data['Page']['pageInfo']['hasNextPage'] ?? false, diff --git a/lib/activity/activities_view.dart b/lib/activity/activities_view.dart index 974d8bd6..ad820a33 100644 --- a/lib/activity/activities_view.dart +++ b/lib/activity/activities_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/activity/activity_providers.dart'; +import 'package:otraku/activity/activities_providers.dart'; import 'package:otraku/activity/activity_card.dart'; import 'package:otraku/composition/composition_model.dart'; import 'package:otraku/composition/composition_view.dart'; @@ -13,8 +13,9 @@ import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/fields/checkbox_field.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/segment_switcher.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; import 'package:otraku/widgets/pagination_view.dart'; @@ -92,34 +93,36 @@ class _ActivitiesViewState extends ConsumerState { @override Widget build(BuildContext context) { - return PageLayout( - topBar: const TopBar(title: 'Activities'), - floatingBar: FloatingBar( - scrollCtrl: _ctrl, - children: [ - ActionButton( - tooltip: widget.id == Options().id ? 'New Post' : 'New Message', - icon: Icons.edit_outlined, - onTap: () => showSheet( - context, - CompositionView( - composition: widget.id == Options().id - ? Composition.status(null, '') - : Composition.message(null, '', widget.id), - onDone: (map) => ref - .read(activitiesProvider(widget.id).notifier) - .insertActivity(map, Options().id!), + return PageScaffold( + child: TabScaffold( + topBar: const TopBar(title: 'Activities'), + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: [ + ActionButton( + tooltip: widget.id == Options().id ? 'New Post' : 'New Message', + icon: Icons.edit_outlined, + onTap: () => showSheet( + context, + CompositionView( + composition: widget.id == Options().id + ? Composition.status(null, '') + : Composition.message(null, '', widget.id), + onDone: (map) => ref + .read(activitiesProvider(widget.id).notifier) + .insertActivity(map, Options().id!), + ), ), ), - ), - ActionButton( - tooltip: 'Filter', - icon: Ionicons.funnel_outline, - onTap: () => showActivityFilterSheet(context, ref, widget.id), - ), - ], + ActionButton( + tooltip: 'Filter', + icon: Ionicons.funnel_outline, + onTap: () => showActivityFilterSheet(context, ref, widget.id), + ), + ], + ), + child: ActivitiesSubView(widget.id, _ctrl), ), - child: ActivitiesSubView(widget.id, _ctrl), ); } } @@ -145,51 +148,48 @@ class ActivitiesSubView extends StatelessWidget { } return Future.value(); }, - onData: (data) => SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - childCount: data.items.length, - (context, i) => ActivityCard( - withHeader: true, + onData: (data) => SliverList( + delegate: SliverChildBuilderDelegate( + childCount: data.items.length, + (context, i) => ActivityCard( + withHeader: true, + activity: data.items[i], + footer: ActivityFooter( activity: data.items[i], - footer: ActivityFooter( - activity: data.items[i], - onDeleted: () => ref - .read(activitiesProvider(id).notifier) - .remove(data.items[i].id), - onChanged: null, - onPinned: id == Options().id - ? () => ref - .read(activitiesProvider(id).notifier) - .togglePin(data.items[i].id) - : null, - onOpenReplies: () => Navigator.pushNamed( - context, - RouteArg.activity, - arguments: RouteArg( - id: data.items[i].id, - callback: (arg) { - final updatedActivity = arg as Activity?; - if (updatedActivity == null) { - ref - .read(activitiesProvider(id).notifier) - .remove(data.items[i].id); - return; - } - + onDeleted: () => ref + .read(activitiesProvider(id).notifier) + .remove(data.items[i].id), + onChanged: null, + onPinned: id == Options().id + ? () => ref + .read(activitiesProvider(id).notifier) + .togglePin(data.items[i].id) + : null, + onOpenReplies: () => Navigator.pushNamed( + context, + RouteArg.activity, + arguments: RouteArg( + id: data.items[i].id, + callback: (arg) { + final updatedActivity = arg as Activity?; + if (updatedActivity == null) { ref .read(activitiesProvider(id).notifier) - .updateActivity(updatedActivity); - }, - ), + .remove(data.items[i].id); + return; + } + + ref + .read(activitiesProvider(id).notifier) + .updateActivity(updatedActivity); + }, ), - onEdited: (map) { - ref - .read(activitiesProvider(id).notifier) - .replaceActivity(map); - }, ), + onEdited: (map) { + ref + .read(activitiesProvider(id).notifier) + .replaceActivity(map); + }, ), ), ), diff --git a/lib/activity/activity_card.dart b/lib/activity/activity_card.dart index 20c6b21f..aa0767e5 100644 --- a/lib/activity/activity_card.dart +++ b/lib/activity/activity_card.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/activity/activity_models.dart'; -import 'package:otraku/activity/activity_providers.dart'; +import 'package:otraku/activity/activity_provider.dart'; import 'package:otraku/composition/composition_model.dart'; import 'package:otraku/composition/composition_view.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -48,7 +48,7 @@ class ActivityCard extends StatelessWidget { child: Text( activity.createdAt, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.labelSmall, ), ), footer, @@ -76,7 +76,7 @@ class ActivityCard extends StatelessWidget { children: [ ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage( + child: CachedImage( activity.agent.imageUrl, height: 50, width: 50, @@ -110,7 +110,7 @@ class ActivityCard extends StatelessWidget { discoverType: DiscoverType.user, child: ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage( + child: CachedImage( activity.reciever!.imageUrl, height: 50, width: 50, @@ -150,7 +150,7 @@ class _ActivityMediaBox extends StatelessWidget { children: [ ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage(activityMedia.imageUrl, width: 70), + child: CachedImage(activityMedia.imageUrl, width: 70), ), Expanded( child: Padding( @@ -166,11 +166,11 @@ class _ActivityMediaBox extends StatelessWidget { children: [ TextSpan( text: text, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), TextSpan( text: activityMedia.title, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, ), ], ), @@ -180,7 +180,7 @@ class _ActivityMediaBox extends StatelessWidget { const SizedBox(height: 5), Text( activityMedia.format!, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ], ], @@ -248,7 +248,7 @@ class _ActivityFooterState extends State { children: [ Text( activity.replyCount.toString(), - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.labelSmall, ), const SizedBox(width: 5), const Icon(Ionicons.chatbox, size: Consts.iconSmall), @@ -270,8 +270,8 @@ class _ActivityFooterState extends State { Text( activity.likeCount.toString(), style: !activity.isLiked - ? Theme.of(context).textTheme.subtitle2 - : Theme.of(context).textTheme.subtitle2!.copyWith( + ? Theme.of(context).textTheme.labelSmall + : Theme.of(context).textTheme.labelSmall!.copyWith( color: Theme.of(context).colorScheme.error), ), const SizedBox(width: 5), diff --git a/lib/activity/activity_models.dart b/lib/activity/activity_models.dart index f18bf36c..99c15e56 100644 --- a/lib/activity/activity_models.dart +++ b/lib/activity/activity_models.dart @@ -194,28 +194,21 @@ class ActivityMedia { } enum ActivityType { - TEXT, - ANIME_LIST, - MANGA_LIST, - MESSAGE; - - String get text { - switch (this) { - case ActivityType.TEXT: - return 'Statuses'; - case ActivityType.ANIME_LIST: - return 'Anime Progress'; - case ActivityType.MANGA_LIST: - return 'Manga Progress'; - case ActivityType.MESSAGE: - return 'Messages'; - } - } + TEXT('Statuses'), + ANIME_LIST('Anime Progress'), + MANGA_LIST('Manga Progress'), + MESSAGE('Messages'); + + const ActivityType(this.text); + + final String text; } class ActivityFilter { const ActivityFilter(this.typeIn, this.onFollowing); final List typeIn; + + /// Not `null` only for the main feed. Switches between following/global. final bool? onFollowing; } diff --git a/lib/activity/activity_provider.dart b/lib/activity/activity_provider.dart new file mode 100644 index 00000000..aada8580 --- /dev/null +++ b/lib/activity/activity_provider.dart @@ -0,0 +1,200 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/activity/activity_models.dart'; +import 'package:otraku/utils/api.dart'; +import 'package:otraku/utils/graphql.dart'; +import 'package:otraku/common/pagination.dart'; +import 'package:otraku/utils/options.dart'; + +/// Toggles an activity like and returns an error if unsuccessful. +Future toggleActivityLike(Activity activity) async { + try { + await Api.get(GqlMutation.toggleLike, { + 'id': activity.id, + 'type': 'ACTIVITY', + }); + return null; + } catch (e) { + return e; + } +} + +/// Toggles an activity subscription and returns an error if unsuccessful. +Future toggleActivitySubscription(Activity activity) async { + try { + await Api.get(GqlMutation.toggleActivitySubscription, { + 'id': activity.id, + 'subscribe': activity.isSubscribed, + }); + return null; + } catch (e) { + return e; + } +} + +/// Pins/Unpins an activity and returns an error if unsuccessful. +Future toggleActivityPin(Activity activity) async { + try { + await Api.get(GqlMutation.toggleActivityPin, { + 'id': activity.id, + 'pinned': activity.isPinned, + }); + return null; + } catch (e) { + return e; + } +} + +/// Toggles a reply like and returns an error if unsuccessful. +Future toggleReplyLike(ActivityReply reply) async { + try { + await Api.get(GqlMutation.toggleLike, { + 'id': reply.id, + 'type': 'ACTIVITY_REPLY', + }); + return null; + } catch (e) { + return e; + } +} + +/// Deletes an activity and returns an error if unsuccessful. +Future deleteActivity(int activityId) async { + try { + await Api.get(GqlMutation.deleteActivity, {'id': activityId}); + return null; + } catch (e) { + return e; + } +} + +/// Deletes an activity reply and returns an error if unsuccessful. +Future deleteActivityReply(int replyId) async { + try { + await Api.get(GqlMutation.deleteActivityReply, {'id': replyId}); + return null; + } catch (e) { + return e; + } +} + +final activityProvider = StateNotifierProvider.autoDispose + .family, int>( + (ref, userId) => ActivityNotifier(userId, Options().id!), +); + +class ActivityNotifier extends StateNotifier> { + ActivityNotifier(this.userId, this.viewerId) + : super(const AsyncValue.loading()) { + fetch(); + } + + final int userId; + final int viewerId; + + Future fetch() async { + state = await AsyncValue.guard(() async { + final replies = state.value?.replies ?? Pagination(); + + final data = await Api.get(GqlQuery.activity, { + 'id': userId, + 'page': replies.next, + if (replies.next == 1) 'withActivity': true, + }); + + final items = []; + for (final r in data['Page']['activityReplies']) { + final item = ActivityReply.maybe(r); + if (item != null) items.add(item); + } + + final activity = + state.value?.activity ?? Activity.maybe(data['Activity'], viewerId); + if (activity == null) throw StateError('Could not parse activity'); + + return ActivityState( + activity, + replies.append( + items, + data['Page']['pageInfo']['hasNextPage'] ?? false, + ), + ); + }); + } + + /// Deserializes [map] and replaces the current activity. + void replaceActivity(Map map, int viewerId) { + if (!state.hasValue) return; + final value = state.value!; + + final activity = Activity.maybe(map, viewerId); + if (activity == null) return; + + state = AsyncData(ActivityState(activity, value.replies)); + } + + /// Deserializes [map] and appends it at the end. + void appendReply(Map map) { + if (!state.hasValue) return; + final value = state.value!; + + final reply = ActivityReply.maybe(map); + if (reply == null) return; + + value.activity.replyCount++; + state = AsyncData(ActivityState( + value.activity, + Pagination.from( + items: [...value.replies.items, reply], + hasNext: value.replies.hasNext, + next: value.replies.next, + ), + )); + } + + /// Replaces an existing reply with another one. + void replaceReply(Map map) { + if (!state.hasValue) return; + final value = state.value!; + + final reply = ActivityReply.maybe(map); + if (reply == null) return; + + for (int i = 0; i < value.replies.items.length; i++) { + if (value.replies.items[i].id == reply.id) { + value.replies.items[i] = reply; + state = AsyncData(ActivityState( + value.activity, + Pagination.from( + items: value.replies.items, + hasNext: value.replies.hasNext, + next: value.replies.next, + ), + )); + return; + } + } + } + + /// Removes an already deleted reply. + void removeReply(int replyId) { + if (!state.hasValue) return; + final value = state.value!; + + for (int i = 0; i < value.replies.items.length; i++) { + if (value.replies.items[i].id == replyId) { + value.replies.items.removeAt(i); + value.activity.replyCount--; + + state = AsyncData(ActivityState( + value.activity, + Pagination.from( + items: value.replies.items, + hasNext: value.replies.hasNext, + next: value.replies.next, + ), + )); + return; + } + } + } +} diff --git a/lib/activity/activity_view.dart b/lib/activity/activity_view.dart index 677b6891..a65c12ca 100644 --- a/lib/activity/activity_view.dart +++ b/lib/activity/activity_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/activity/activity_models.dart'; -import 'package:otraku/activity/activity_providers.dart'; +import 'package:otraku/activity/activity_provider.dart'; import 'package:otraku/activity/activity_card.dart'; import 'package:otraku/activity/reply_card.dart'; import 'package:otraku/composition/composition_model.dart'; @@ -11,10 +11,11 @@ import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/utils/options.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -45,175 +46,173 @@ class _ActivityViewState extends ConsumerState { final activity = ref.watch( activityProvider(widget.id).select((s) => s.valueOrNull?.activity)); - return PageLayout( - topBar: PreferredSize( - preferredSize: const Size.fromHeight(Consts.tapTargetSize), - child: activity == null ? const TopBar() : _TopBar(activity), - ), - floatingBar: FloatingBar( - scrollCtrl: _ctrl, - children: [ - ActionButton( - tooltip: 'New Reply', - icon: Icons.edit_outlined, - onTap: () => showSheet( - context, - CompositionView( - composition: Composition.reply(null, '', widget.id), - onDone: (map) => ref - .read(activityProvider(widget.id).notifier) - .appendReply(map), - ), - ), - ), - ], - ), - child: Consumer( - child: SliverRefreshControl( - onRefresh: () => ref.invalidate(activityProvider(widget.id)), + return PageScaffold( + child: TabScaffold( + topBar: TopBar( + trailing: [if (activity != null) _TopBarContent(activity)], ), - builder: (context, ref, refreshControl) { - ref.listen( - activityProvider(widget.id), - (_, s) => s.whenOrNull( - error: (error, _) => showPopUp( + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: [ + ActionButton( + tooltip: 'New Reply', + icon: Icons.edit_outlined, + onTap: () => showSheet( context, - ConfirmationDialog( - title: 'Failed to load activity', - content: error.toString(), + CompositionView( + composition: Composition.reply(null, '', widget.id), + onDone: (map) => ref + .read(activityProvider(widget.id).notifier) + .appendReply(map), ), ), ), - ); + ], + ), + child: Consumer( + child: SliverRefreshControl( + onRefresh: () => ref.invalidate(activityProvider(widget.id)), + ), + builder: (context, ref, refreshControl) { + ref.listen( + activityProvider(widget.id), + (_, s) => s.whenOrNull( + error: (error, _) => showPopUp( + context, + ConfirmationDialog( + title: 'Failed to load activity', + content: error.toString(), + ), + ), + ), + ); - return ref.watch(activityProvider(widget.id)).unwrapPrevious().when( - loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load activity')), - data: (data) { - return Padding( - padding: Consts.padding, - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl!, - SliverToBoxAdapter( - child: ActivityCard( - withHeader: false, - activity: data.activity, - footer: ActivityFooter( + return ref.watch(activityProvider(widget.id)).unwrapPrevious().when( + loading: () => const Center(child: Loader()), + error: (_, __) => + const Center(child: Text('Failed to load activity')), + data: (data) { + return Padding( + padding: Consts.padding, + child: CustomScrollView( + physics: Consts.physics, + controller: _ctrl, + slivers: [ + refreshControl!, + SliverToBoxAdapter( + child: ActivityCard( + withHeader: false, activity: data.activity, - onChanged: () => - widget.onChanged?.call(data.activity), - onDeleted: () { - widget.onChanged?.call(null); - Navigator.pop(context); - }, - onPinned: () => setState(() {}), - onOpenReplies: null, - onEdited: (map) { - ref - .read(activityProvider(widget.id).notifier) - .replaceActivity(map, Options().id!); - }, + footer: ActivityFooter( + activity: data.activity, + onChanged: () => + widget.onChanged?.call(data.activity), + onDeleted: () { + widget.onChanged?.call(null); + Navigator.pop(context); + }, + onPinned: () => setState(() {}), + onOpenReplies: null, + onEdited: (map) { + ref + .read( + activityProvider(widget.id).notifier) + .replaceActivity(map, Options().id!); + }, + ), ), ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: data.replies.items.length, - (context, i) => - ReplyCard(widget.id, data.replies.items[i]), + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: data.replies.items.length, + (context, i) => + ReplyCard(widget.id, data.replies.items[i]), + ), ), - ), - SliverFooter(loading: data.replies.hasNext), - ], - ), - ); - }, - ); - }, + SliverFooter(loading: data.replies.hasNext), + ], + ), + ); + }, + ); + }, + ), ), ); } } -class _TopBar extends StatelessWidget { - const _TopBar(this.activity); +class _TopBarContent extends StatelessWidget { + const _TopBarContent(this.activity); final Activity activity; @override Widget build(BuildContext context) { - return TopBar( - items: [ - Expanded( - child: Row( - children: [ - Flexible( - child: LinkTile( - id: activity.agent.id, - info: activity.agent.imageUrl, - discoverType: DiscoverType.user, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Hero( - tag: activity.agent.id, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: FadeImage( - activity.agent.imageUrl, - height: 40, - width: 40, - ), - ), - ), - const SizedBox(width: 10), - Flexible( - child: Text( - activity.agent.name, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + return Expanded( + child: Row( + children: [ + Flexible( + child: LinkTile( + id: activity.agent.id, + info: activity.agent.imageUrl, + discoverType: DiscoverType.user, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Hero( + tag: activity.agent.id, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: CachedImage( + activity.agent.imageUrl, + height: 40, + width: 40, ), - ], - ), - ), - ), - if (activity.reciever != null) ...[ - if (activity.isPrivate) - const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Ionicons.eye_off_outline), + ), ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Icon(Icons.arrow_right_alt), - ), - LinkTile( - id: activity.reciever!.id, - info: activity.reciever!.imageUrl, - discoverType: DiscoverType.user, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: FadeImage( - activity.reciever!.imageUrl, - height: 40, - width: 40, + const SizedBox(width: 10), + Flexible( + child: Text( + activity.agent.name, + overflow: TextOverflow.ellipsis, + maxLines: 1, ), ), - ), - ] else if (activity.isPinned) - const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Icons.push_pin_outlined), - ), - ], + ], + ), + ), ), - ), - ], + if (activity.reciever != null) ...[ + if (activity.isPrivate) + const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Ionicons.eye_off_outline), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Icon(Icons.arrow_right_alt), + ), + LinkTile( + id: activity.reciever!.id, + info: activity.reciever!.imageUrl, + discoverType: DiscoverType.user, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: CachedImage( + activity.reciever!.imageUrl, + height: 40, + width: 40, + ), + ), + ), + ] else if (activity.isPinned) + const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.push_pin_outlined), + ), + ], + ), ); } } diff --git a/lib/activity/reply_card.dart b/lib/activity/reply_card.dart index fc4dd2e4..13fed623 100644 --- a/lib/activity/reply_card.dart +++ b/lib/activity/reply_card.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/activity/activity_models.dart'; -import 'package:otraku/activity/activity_providers.dart'; +import 'package:otraku/activity/activity_provider.dart'; import 'package:otraku/composition/composition_model.dart'; import 'package:otraku/composition/composition_view.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -34,7 +34,7 @@ class ReplyCard extends StatelessWidget { children: [ ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage( + child: CachedImage( reply.user.imageUrl, height: 50, width: 50, @@ -61,7 +61,7 @@ class ReplyCard extends StatelessWidget { children: [ Text( reply.createdAt, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.labelSmall, ), const Spacer(), if (reply.user.id == Options().id) ...[ @@ -173,10 +173,10 @@ class _ReplyLikeButtonState extends State<_ReplyLikeButton> { Text( widget.reply.likeCount.toString(), style: !widget.reply.isLiked - ? Theme.of(context).textTheme.subtitle2 + ? Theme.of(context).textTheme.labelSmall : Theme.of(context) .textTheme - .subtitle2! + .labelSmall! .copyWith(color: Theme.of(context).colorScheme.error), ), const SizedBox(width: 5), diff --git a/lib/auth/auth_view.dart b/lib/auth/auth_view.dart index a4cb97ae..bca59cc0 100644 --- a/lib/auth/auth_view.dart +++ b/lib/auth/auth_view.dart @@ -7,7 +7,7 @@ import 'package:otraku/utils/consts.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/route_arg.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/toast.dart'; @@ -124,7 +124,7 @@ class AuthViewState extends State { children: [ Text( 'Otraku for AniList', - style: Theme.of(context).textTheme.headline1, + style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 20), Container( @@ -141,13 +141,13 @@ class AuthViewState extends State { children: [ Text( 'Primary Account', - style: Theme.of(context).textTheme.headline2, + style: Theme.of(context).textTheme.titleMedium, ), if (available0) ...[ const SizedBox(height: 5), Text( Options().idOf(0)?.toString() ?? '', - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ], ], @@ -192,13 +192,13 @@ class AuthViewState extends State { children: [ Text( 'Secondary Account', - style: Theme.of(context).textTheme.headline2, + style: Theme.of(context).textTheme.titleMedium, ), if (available1) ...[ const SizedBox(height: 5), Text( Options().idOf(1)?.toString() ?? '', - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ], ], @@ -233,7 +233,7 @@ class AuthViewState extends State { const EdgeInsets.symmetric(horizontal: 10, vertical: 20), child: Text( 'Before connecting another account, you should log out from the first one in the browser.', - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ), ], diff --git a/lib/character/character_info_tab.dart b/lib/character/character_info_tab.dart index 11d35948..d55e26ee 100644 --- a/lib/character/character_info_tab.dart +++ b/lib/character/character_info_tab.dart @@ -3,21 +3,23 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/character/character_models.dart'; import 'package:otraku/character/character_providers.dart'; import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/toast.dart'; class CharacterInfoTab extends StatelessWidget { - const CharacterInfoTab(this.id, this.imageUrl, this.scrollCtrl); + const CharacterInfoTab(this.id, this.imageUrl, this.scrollCtrl, this.topBar); final int id; final String? imageUrl; final ScrollController scrollCtrl; + final TopBar topBar; @override Widget build(BuildContext context) { @@ -34,6 +36,7 @@ class CharacterInfoTab extends StatelessWidget { imageUrl: imageUrl, scrollCtrl: scrollCtrl, refreshControl: refreshControl, + topBar: topBar, loading: true, ), error: (_, __) => _TabContent( @@ -42,6 +45,7 @@ class CharacterInfoTab extends StatelessWidget { imageUrl: imageUrl, scrollCtrl: scrollCtrl, refreshControl: refreshControl, + topBar: topBar, loading: false, ), data: (data) => _TabContent( @@ -50,6 +54,7 @@ class CharacterInfoTab extends StatelessWidget { imageUrl: imageUrl, scrollCtrl: scrollCtrl, refreshControl: refreshControl, + topBar: topBar, loading: false, ), ); @@ -65,6 +70,7 @@ class _TabContent extends StatelessWidget { required this.imageUrl, required this.scrollCtrl, required this.refreshControl, + required this.topBar, required this.loading, }); @@ -73,6 +79,7 @@ class _TabContent extends StatelessWidget { final String? imageUrl; final ScrollController scrollCtrl; final Widget refreshControl; + final TopBar topBar; final bool loading; @override @@ -99,7 +106,7 @@ class _TabContent extends StatelessWidget { height: imageHeight, color: Theme.of(context).colorScheme.surfaceVariant, child: GestureDetector( - child: FadeImage(imageUrl), + child: CachedImage(imageUrl), onTap: () => showPopUp(context, ImageDialog(imageUrl)), ), ), @@ -116,7 +123,7 @@ class _TabContent extends StatelessWidget { onTap: () => Toast.copy(context, data!.name), child: Text( data!.name, - style: Theme.of(context).textTheme.headline1, + style: Theme.of(context).textTheme.titleLarge, ), ), if (data!.altNames.isNotEmpty) @@ -126,7 +133,7 @@ class _TabContent extends StatelessWidget { behavior: HitTestBehavior.opaque, child: Text( 'Spoiler names', - style: Theme.of(context).textTheme.bodyText1, + style: Theme.of(context).textTheme.bodyLarge, ), onTap: () => showPopUp( context, @@ -145,7 +152,8 @@ class _TabContent extends StatelessWidget { const space = SliverToBoxAdapter(child: SizedBox(height: 10)); - return PageLayout( + return TabScaffold( + topBar: topBar, floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [if (data != null) _FavoriteButton(data!)], @@ -259,7 +267,7 @@ class _InfoTile extends StatelessWidget { Text( title, maxLines: 1, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), Text(subtitle, maxLines: 1), ], diff --git a/lib/character/character_media_tab.dart b/lib/character/character_media_tab.dart index 69e5ab83..70fff658 100644 --- a/lib/character/character_media_tab.dart +++ b/lib/character/character_media_tab.dart @@ -9,20 +9,23 @@ import 'package:otraku/common/relation.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/widgets/grids/relation_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; class CharacterAnimeTab extends StatelessWidget { - const CharacterAnimeTab(this.id, this.scrollCtrl); + const CharacterAnimeTab(this.id, this.scrollCtrl, this.topBar); final int id; final ScrollController scrollCtrl; + final TopBar topBar; @override Widget build(BuildContext context) { - return PageLayout( + return TabScaffold( + topBar: topBar, floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [_FilterButton(id), _LanguageButton(id)], @@ -99,14 +102,16 @@ class CharacterAnimeTab extends StatelessWidget { } class CharacterMangaTab extends StatelessWidget { - const CharacterMangaTab(this.id, this.scrollCtrl); + const CharacterMangaTab(this.id, this.scrollCtrl, this.topBar); final int id; final ScrollController scrollCtrl; + final TopBar topBar; @override Widget build(BuildContext context) { - return PageLayout( + return TabScaffold( + topBar: topBar, floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [_FilterButton(id)], @@ -208,8 +213,8 @@ class _LanguageButton extends StatelessWidget { Text( languages.elementAt(i), style: languages.elementAt(i) != language - ? Theme.of(context).textTheme.headline1 - : Theme.of(context).textTheme.headline1?.copyWith( + ? Theme.of(context).textTheme.titleLarge + : Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), diff --git a/lib/character/character_view.dart b/lib/character/character_view.dart index 03314845..8f6c9e19 100644 --- a/lib/character/character_view.dart +++ b/lib/character/character_view.dart @@ -6,8 +6,9 @@ import 'package:otraku/character/character_info_tab.dart'; import 'package:otraku/character/character_media_tab.dart'; import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; class CharacterView extends ConsumerStatefulWidget { @@ -54,9 +55,9 @@ class _CharacterViewState extends ConsumerState { ref.watch(characterMediaProvider(widget.id).select((_) => null)); final name = ref.watch(characterProvider(widget.id)).valueOrNull?.name; + final topBar = TopBar(title: name); - return PageLayout( - topBar: TopBar(title: name), + return PageScaffold( bottomBar: BottomBarIconTabs( current: _tab, onChanged: (i) => setState(() => _tab = i), @@ -71,9 +72,9 @@ class _CharacterViewState extends ConsumerState { current: _tab, onChanged: (i) => setState(() => _tab = i), children: [ - CharacterInfoTab(widget.id, widget.imageUrl, _ctrl), - CharacterAnimeTab(widget.id, _ctrl), - CharacterMangaTab(widget.id, _ctrl), + CharacterInfoTab(widget.id, widget.imageUrl, _ctrl, topBar), + CharacterAnimeTab(widget.id, _ctrl, topBar), + CharacterMangaTab(widget.id, _ctrl, topBar), ], ), ); diff --git a/lib/collection/collection_grid.dart b/lib/collection/collection_grid.dart index 5b60286a..e2687ad7 100644 --- a/lib/collection/collection_grid.dart +++ b/lib/collection/collection_grid.dart @@ -3,18 +3,14 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/collection/collection_models.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/edit/edit_providers.dart'; +import 'package:otraku/edit/edit_view.dart'; import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/convert.dart'; import 'package:otraku/utils/consts.dart'; -import 'package:otraku/edit/edit_view.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/link_tile.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; -import 'package:otraku/widgets/text_rail.dart'; - -const _TILE_HEIGHT = 140.0; class CollectionGrid extends StatelessWidget { const CollectionGrid({ @@ -33,269 +29,94 @@ class CollectionGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverGrid( - delegate: SliverChildBuilderDelegate( - (_, i) => _Tile(items[i], scoreFormat, onProgressUpdate), - childCount: items.length, - ), - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 350, - height: _TILE_HEIGHT, - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndExtraHeight( + minWidth: 100, + extraHeight: 70, + rawHWRatio: Consts.coverHtoWRatio, ), - ); - } -} - -class _Tile extends StatelessWidget { - const _Tile(this.entry, this.scoreFormat, this.onProgressUpdate); - - final Entry entry; - final ScoreFormat scoreFormat; - final void Function(Entry, List)? onProgressUpdate; - - @override - Widget build(BuildContext context) { - return Card( - child: LinkTile( - key: ValueKey(entry.mediaId), - id: entry.mediaId, - discoverType: DiscoverType.anime, - info: entry.imageUrl, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Hero( - tag: entry.mediaId, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: Container( - width: _TILE_HEIGHT / Consts.coverHtoWRatio, - color: Theme.of(context).colorScheme.surfaceVariant, - child: FadeImage(entry.imageUrl), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) => Card( + child: LinkTile( + id: items[i].mediaId, + discoverType: DiscoverType.anime, + info: items[i].imageUrl, + child: Column( + children: [ + Expanded( + child: Hero( + tag: items[i].mediaId, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: CachedImage(items[i].imageUrl), + ), + ), + ), ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 10, left: 10, right: 10), - child: _TileContent(entry, scoreFormat, onProgressUpdate), - ), + Padding( + padding: const EdgeInsets.only(left: 5, right: 5, top: 5), + child: SizedBox( + height: 35, + child: Text( + items[i].titles[0], + overflow: TextOverflow.fade, + maxLines: 2, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + _IncrementButton(items[i], onProgressUpdate), + ], ), - ], + ), ), ), ); } } -/// The content is a [StatefulWidget], as it -/// needs to update when the progress increments. -class _TileContent extends StatefulWidget { - const _TileContent(this.item, this.scoreFormat, this.onProgressUpdate); +class _IncrementButton extends StatefulWidget { + const _IncrementButton(this.item, this.onProgressUpdate); final Entry item; - final ScoreFormat scoreFormat; final void Function(Entry, List)? onProgressUpdate; @override - State<_TileContent> createState() => __TileContentState(); + State<_IncrementButton> createState() => _IncrementButtonState(); } -class __TileContentState extends State<_TileContent> { +class _IncrementButtonState extends State<_IncrementButton> { @override Widget build(BuildContext context) { final item = widget.item; - double progressPercent = 0; - if (item.progressMax != null) { - progressPercent = item.progress / item.progressMax!; - } else if (item.nextEpisode != null) { - progressPercent = item.progress / (item.nextEpisode! - 1); - } else if (item.progress > 0) { - progressPercent = 1; - } - - final textRailItems = {}; - if (widget.item.format != null) { - textRailItems[Convert.clarifyEnum(widget.item.format)!] = false; - } - if (widget.item.airingAt != null) { - textRailItems['Ep ${widget.item.nextEpisode} in ' - '${Convert.timeUntilTimestamp(widget.item.airingAt)}'] = false; - } - if (widget.item.nextEpisode != null && - widget.item.nextEpisode! - 1 > widget.item.progress) { - textRailItems['${widget.item.nextEpisode! - 1 - widget.item.progress}' - ' ep behind'] = true; - } - - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - child: Text( - widget.item.titles[0], - overflow: TextOverflow.fade, - ), - ), - const SizedBox(height: 5), - TextRail(textRailItems), - ], - ), - ), - Container( - height: 5, - margin: const EdgeInsets.symmetric(vertical: 3), - decoration: BoxDecoration( - borderRadius: Consts.borderRadiusMin, - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.onSurfaceVariant, - Theme.of(context).colorScheme.onSurfaceVariant, - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background, - ], - stops: [0.0, progressPercent, progressPercent, 1.0], + if (item.progress == item.progressMax) { + return Tooltip( + message: 'Progress', + child: SizedBox( + height: 30, + child: Center( + child: Text( + item.progress.toString(), + style: Theme.of(context).textTheme.labelSmall, ), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Tooltip(message: 'Score', child: _buildScore(context)), - if (widget.item.repeat > 0) - Tooltip( - message: 'Repeats', - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Ionicons.repeat, size: Consts.iconSmall), - const SizedBox(width: 3), - Text( - widget.item.repeat.toString(), - style: Theme.of(context).textTheme.subtitle2, - ), - ], - ), - ) - else - const SizedBox(), - if (widget.item.notes != null) - SizedBox( - height: 40, - child: Tooltip( - message: 'Comment', - child: InkResponse( - radius: 10, - child: const Icon(Ionicons.chatbox, size: Consts.iconSmall), - onTap: () => showPopUp( - context, - TextDialog( - title: 'Comment', - text: widget.item.notes!, - ), - ), - ), - ), - ) - else - const SizedBox(), - _buildProgressButton(), - ], - ), - ], - ); - } - - Widget _buildScore(BuildContext context) { - if (widget.item.score == 0) return const SizedBox(); - - switch (widget.scoreFormat) { - case ScoreFormat.POINT_3: - if (widget.item.score == 3) { - return const Icon( - Icons.sentiment_very_satisfied, - size: Consts.iconSmall, - ); - } - - if (widget.item.score == 2) { - return const Icon(Icons.sentiment_neutral, size: Consts.iconSmall); - } - - return const Icon( - Icons.sentiment_very_dissatisfied, - size: Consts.iconSmall, - ); - case ScoreFormat.POINT_5: - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.star_rounded, size: Consts.iconSmall), - const SizedBox(width: 3), - Text( - widget.item.score.toStringAsFixed(0), - style: Theme.of(context).textTheme.subtitle2, - ), - ], - ); - case ScoreFormat.POINT_10_DECIMAL: - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.star_half_rounded, size: Consts.iconSmall), - const SizedBox(width: 3), - Text( - widget.item.score.toStringAsFixed( - widget.item.score.truncate() == widget.item.score ? 0 : 1, - ), - style: Theme.of(context).textTheme.subtitle2, - ), - ], - ); - default: - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.star_half_rounded, size: Consts.iconSmall), - const SizedBox(width: 3), - Text( - widget.item.score.toStringAsFixed(0), - style: Theme.of(context).textTheme.subtitle2, - ), - ], - ); + ); } - } - - Widget _buildProgressButton() { - final item = widget.item; - final text = Text( - item.progress == item.progressMax - ? item.progress.toString() - : '${item.progress}/${item.progressMax ?? "?"}', - style: Theme.of(context).textTheme.subtitle2, - ); - if (widget.onProgressUpdate == null || item.progress == item.progressMax) { - return Tooltip(message: 'Progress', child: text); - } + final warning = + item.nextEpisode != null && item.progress + 1 < item.nextEpisode!; return TextButton( style: TextButton.styleFrom( - minimumSize: const Size(0, 40), - padding: const EdgeInsets.only(left: 5), + minimumSize: const Size(0, 30), + padding: const EdgeInsets.symmetric(horizontal: 5), tapTargetSize: MaterialTapTargetSize.shrinkWrap, - foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, + foregroundColor: warning ? Theme.of(context).colorScheme.error : null, ), onPressed: () async { if (item.progressMax != null && @@ -325,8 +146,12 @@ class __TileContentState extends State<_TileContent> { child: Tooltip( message: 'Increment Progress', child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - text, + Text( + '${item.progress}/${item.progressMax ?? "?"}', + style: const TextStyle(fontSize: Consts.fontSmall), + ), const SizedBox(width: 3), const Icon(Ionicons.add_outline, size: Consts.iconSmall), ], diff --git a/lib/collection/collection_list.dart b/lib/collection/collection_list.dart new file mode 100644 index 00000000..22f45e87 --- /dev/null +++ b/lib/collection/collection_list.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:ionicons/ionicons.dart'; +import 'package:otraku/collection/collection_models.dart'; +import 'package:otraku/discover/discover_models.dart'; +import 'package:otraku/edit/edit_providers.dart'; +import 'package:otraku/media/media_constants.dart'; +import 'package:otraku/utils/convert.dart'; +import 'package:otraku/utils/consts.dart'; +import 'package:otraku/edit/edit_view.dart'; +import 'package:otraku/widgets/cached_image.dart'; +import 'package:otraku/widgets/link_tile.dart'; +import 'package:otraku/widgets/overlays/dialogs.dart'; +import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/widgets/text_rail.dart'; + +const _TILE_HEIGHT = 140.0; + +class CollectionList extends StatelessWidget { + const CollectionList({ + required this.items, + required this.scoreFormat, + required this.onProgressUpdate, + }); + + final List items; + final ScoreFormat scoreFormat; + + /// Called when a tile's progress gets incremented. + /// If `null` the increment button won't appear, so this + /// should only be `null` when viewing other users' collections. + final void Function(Entry, List)? onProgressUpdate; + + @override + Widget build(BuildContext context) { + return SliverFixedExtentList( + delegate: SliverChildBuilderDelegate( + (_, i) => _Tile(items[i], scoreFormat, onProgressUpdate), + childCount: items.length, + ), + // The added pixels are for the bottom margin. + itemExtent: _TILE_HEIGHT + 10, + ); + } +} + +class _Tile extends StatelessWidget { + const _Tile(this.entry, this.scoreFormat, this.onProgressUpdate); + + final Entry entry; + final ScoreFormat scoreFormat; + final void Function(Entry, List)? onProgressUpdate; + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 10), + child: LinkTile( + key: ValueKey(entry.mediaId), + id: entry.mediaId, + discoverType: DiscoverType.anime, + info: entry.imageUrl, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: entry.mediaId, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: Container( + width: _TILE_HEIGHT / Consts.coverHtoWRatio, + color: Theme.of(context).colorScheme.surfaceVariant, + child: CachedImage(entry.imageUrl), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10, left: 10, right: 10), + child: _TileContent(entry, scoreFormat, onProgressUpdate), + ), + ), + ], + ), + ), + ); + } +} + +/// The content is a [StatefulWidget], as it +/// needs to update when the progress increments. +class _TileContent extends StatefulWidget { + const _TileContent(this.item, this.scoreFormat, this.onProgressUpdate); + + final Entry item; + final ScoreFormat scoreFormat; + final void Function(Entry, List)? onProgressUpdate; + + @override + State<_TileContent> createState() => __TileContentState(); +} + +class __TileContentState extends State<_TileContent> { + @override + Widget build(BuildContext context) { + final item = widget.item; + + double progressPercent = 0; + if (item.progressMax != null) { + progressPercent = item.progress / item.progressMax!; + } else if (item.nextEpisode != null) { + progressPercent = item.progress / (item.nextEpisode! - 1); + } else if (item.progress > 0) { + progressPercent = 1; + } + + final textRailItems = {}; + if (widget.item.format != null) { + textRailItems[Convert.clarifyEnum(widget.item.format)!] = false; + } + if (widget.item.airingAt != null) { + textRailItems['Ep ${widget.item.nextEpisode} in ' + '${Convert.timeUntilTimestamp(widget.item.airingAt)}'] = false; + } + if (widget.item.nextEpisode != null && + widget.item.nextEpisode! - 1 > widget.item.progress) { + textRailItems['${widget.item.nextEpisode! - 1 - widget.item.progress}' + ' ep behind'] = true; + } + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + child: Text( + widget.item.titles[0], + overflow: TextOverflow.fade, + ), + ), + const SizedBox(height: 5), + TextRail(textRailItems), + ], + ), + ), + Container( + height: 5, + margin: const EdgeInsets.symmetric(vertical: 3), + decoration: BoxDecoration( + borderRadius: Consts.borderRadiusMin, + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.onSurfaceVariant, + Theme.of(context).colorScheme.onSurfaceVariant, + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background, + ], + stops: [0.0, progressPercent, progressPercent, 1.0], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Tooltip(message: 'Score', child: _buildScore(context)), + if (widget.item.repeat > 0) + Tooltip( + message: 'Repeats', + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Ionicons.repeat, size: Consts.iconSmall), + const SizedBox(width: 3), + Text( + widget.item.repeat.toString(), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + ) + else + const SizedBox(), + if (widget.item.notes != null) + SizedBox( + height: 40, + child: Tooltip( + message: 'Comment', + child: InkResponse( + radius: 10, + child: const Icon(Ionicons.chatbox, size: Consts.iconSmall), + onTap: () => showPopUp( + context, + TextDialog( + title: 'Comment', + text: widget.item.notes!, + ), + ), + ), + ), + ) + else + const SizedBox(), + _buildProgressButton(), + ], + ), + ], + ); + } + + Widget _buildScore(BuildContext context) { + if (widget.item.score == 0) return const SizedBox(); + + switch (widget.scoreFormat) { + case ScoreFormat.POINT_3: + if (widget.item.score == 3) { + return const Icon( + Icons.sentiment_very_satisfied, + size: Consts.iconSmall, + ); + } + + if (widget.item.score == 2) { + return const Icon(Icons.sentiment_neutral, size: Consts.iconSmall); + } + + return const Icon( + Icons.sentiment_very_dissatisfied, + size: Consts.iconSmall, + ); + case ScoreFormat.POINT_5: + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_rounded, size: Consts.iconSmall), + const SizedBox(width: 3), + Text( + widget.item.score.toStringAsFixed(0), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + case ScoreFormat.POINT_10_DECIMAL: + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_half_rounded, size: Consts.iconSmall), + const SizedBox(width: 3), + Text( + widget.item.score.toStringAsFixed( + widget.item.score.truncate() == widget.item.score ? 0 : 1, + ), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + default: + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_half_rounded, size: Consts.iconSmall), + const SizedBox(width: 3), + Text( + widget.item.score.toStringAsFixed(0), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + } + } + + Widget _buildProgressButton() { + final item = widget.item; + final text = Text( + item.progress == item.progressMax + ? item.progress.toString() + : '${item.progress}/${item.progressMax ?? "?"}', + style: Theme.of(context).textTheme.labelSmall, + ); + + if (widget.onProgressUpdate == null || item.progress == item.progressMax) { + return Tooltip(message: 'Progress', child: text); + } + + return TextButton( + style: TextButton.styleFrom( + minimumSize: const Size(0, 40), + padding: const EdgeInsets.only(left: 5), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onPressed: () async { + if (item.progressMax != null && + item.progress >= item.progressMax! - 1) { + showSheet(context, EditView(EditTag(item.mediaId, true))); + return; + } + + setState(() => item.progress++); + final result = await updateProgress(item.mediaId, item.progress); + + if (result is! List) { + if (mounted) { + showPopUp( + context, + ConfirmationDialog( + title: 'Could not update progress', + content: result.toString(), + ), + ); + } + return; + } + + widget.onProgressUpdate?.call(item, result); + }, + child: Tooltip( + message: 'Increment Progress', + child: Row( + children: [ + text, + const SizedBox(width: 3), + const Icon(Ionicons.add_outline, size: Consts.iconSmall), + ], + ), + ), + ); + } +} diff --git a/lib/collection/collection_models.dart b/lib/collection/collection_models.dart index 81cded31..3367a07b 100644 --- a/lib/collection/collection_models.dart +++ b/lib/collection/collection_models.dart @@ -280,6 +280,40 @@ int Function(Entry, Entry) entryComparator(EntrySort s) { if (comparison != 0) return comparison; return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); }; + case EntrySort.AVG_SCORE: + return (a, b) { + if (a.avgScore == null) { + if (b.avgScore == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return 1; + } + + if (b.avgScore == null) return -1; + + final comparison = a.avgScore!.compareTo(b.avgScore!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }; + case EntrySort.AVG_SCORE_DESC: + return (a, b) { + if (b.avgScore == null) { + if (a.avgScore == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return -1; + } + + if (a.avgScore == null) return 1; + + final comparison = b.avgScore!.compareTo(a.avgScore!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }; } } @@ -303,6 +337,7 @@ class Entry { required this.repeat, required this.score, required this.notes, + required this.avgScore, required this.releaseStart, required this.watchStart, required this.watchEnd, @@ -344,6 +379,7 @@ class Entry { repeat: map['repeat'] ?? 0, score: map['score'].toDouble() ?? 0.0, notes: map['notes'], + avgScore: map['media']['averageScore'], releaseStart: Convert.mapToMillis(map['media']['startDate']), watchStart: Convert.mapToMillis(map['startedAt']), watchEnd: Convert.mapToMillis(map['completedAt']), @@ -368,6 +404,7 @@ class Entry { int repeat; double score; String? notes; + int? avgScore; int? releaseStart; int? watchStart; int? watchEnd; diff --git a/lib/collection/collection_preview_view.dart b/lib/collection/collection_preview_view.dart index cbcbeb04..7a575726 100644 --- a/lib/collection/collection_preview_view.dart +++ b/lib/collection/collection_preview_view.dart @@ -4,14 +4,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/collection/collection_grid.dart'; +import 'package:otraku/collection/collection_list.dart'; import 'package:otraku/collection/collection_models.dart'; import 'package:otraku/collection/collection_preview_provider.dart'; import 'package:otraku/home/home_provider.dart'; import 'package:otraku/utils/consts.dart'; +import 'package:otraku/utils/options.dart'; import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/widgets/layouts/constrained_view.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -61,19 +64,25 @@ class _CollectionPreviewViewState extends State { ); } else { notEmpty = true; - content = CollectionGrid( - items: entries, - scoreFormat: notifier.scoreFormat, - onProgressUpdate: (_, __) {}, - ); + content = Options().collectionPreviewItemView == 0 + ? CollectionList( + items: entries, + scoreFormat: notifier.scoreFormat, + onProgressUpdate: (_, __) {}, + ) + : CollectionGrid( + items: entries, + scoreFormat: notifier.scoreFormat, + onProgressUpdate: (_, __) {}, + ); } } - return PageLayout( + return TabScaffold( topBar: TopBar( title: 'Current', canPop: false, - items: [ + trailing: [ if (notEmpty) TopBarIcon( tooltip: 'Random', @@ -98,8 +107,8 @@ class _CollectionPreviewViewState extends State { floatingBar: FloatingBar( scrollCtrl: widget.scrollCtrl, children: [ - ExpandedActionButton( - title: 'Expand', + ActionButton( + tooltip: 'Load Entire Connection', icon: Ionicons.enter_outline, onTap: () => ref.read(homeProvider).expandCollection(widget.tag.ofAnime), diff --git a/lib/collection/collection_providers.dart b/lib/collection/collection_providers.dart index bec68835..544e79a3 100644 --- a/lib/collection/collection_providers.dart +++ b/lib/collection/collection_providers.dart @@ -26,6 +26,13 @@ final entriesProvider = Provider.autoDispose.family( final entries = []; final list = collection.lists[collection.index]; + final releaseStartFrom = filter.startYearFrom != null + ? DateTime(filter.startYearFrom!).millisecondsSinceEpoch + : 0; + final releaseStartTo = filter.startYearTo != null + ? DateTime(filter.startYearTo! + 1).millisecondsSinceEpoch + : DateTime.now().add(const Duration(days: 900)).millisecondsSinceEpoch; + for (final entry in list.entries) { if (search.isNotEmpty) { bool contains = false; @@ -51,6 +58,11 @@ final entriesProvider = Provider.autoDispose.family( continue; } + if (entry.releaseStart != null) { + if (releaseStartFrom > entry.releaseStart!) continue; + if (releaseStartTo < entry.releaseStart!) continue; + } + if (filter.genreIn.isNotEmpty) { bool isIn = true; for (final genre in filter.genreIn) { diff --git a/lib/collection/collection_view.dart b/lib/collection/collection_view.dart index 48a977ad..aae135e5 100644 --- a/lib/collection/collection_view.dart +++ b/lib/collection/collection_view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; +import 'package:otraku/collection/collection_grid.dart'; import 'package:otraku/collection/collection_models.dart'; import 'package:otraku/collection/collection_providers.dart'; import 'package:otraku/utils/consts.dart'; @@ -12,9 +13,10 @@ import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/layouts/constrained_view.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/collection/collection_grid.dart'; +import 'package:otraku/collection/collection_list.dart'; import 'package:otraku/filter/filter_search_field.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -42,16 +44,18 @@ class _CollectionViewState extends State { Widget build(BuildContext context) { final tag = CollectionTag(widget.userId, widget.ofAnime); - return Consumer( - child: CollectionSubView(scrollCtrl: _ctrl, tag: tag), - builder: (context, ref, child) => WillPopScope( - child: child!, - onWillPop: () { - final notifier = ref.read(searchProvider(tag).notifier); - if (notifier.state == null) return Future.value(true); - notifier.state = null; - return Future.value(false); - }, + return PageScaffold( + child: Consumer( + child: CollectionSubView(scrollCtrl: _ctrl, tag: tag), + builder: (context, ref, child) => WillPopScope( + child: child!, + onWillPop: () { + final notifier = ref.read(searchProvider(tag).notifier); + if (notifier.state == null) return Future.value(true); + notifier.state = null; + return Future.value(false); + }, + ), ), ); } @@ -69,18 +73,18 @@ class CollectionSubView extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - return PageLayout( - topBar: PreferredSize( - preferredSize: const Size.fromHeight(Consts.tapTargetSize), - child: _TopBar(tag, tag.userId != Options().id), - ), - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [_ActionButton(tag)], - ), - child: ConstrainedView( + return TabScaffold( + topBar: TopBar( + canPop: tag.userId != Options().id, + trailing: [_TopBarContent(tag)], + ), + floatingBar: FloatingBar( + scrollCtrl: scrollCtrl, + children: [_ActionButton(tag)], + ), + child: Consumer( + builder: (context, ref, _) { + return ConstrainedView( child: CustomScrollView( physics: Consts.physics, controller: scrollCtrl, @@ -92,72 +96,69 @@ class CollectionSubView extends StatelessWidget { const SliverFooter(), ], ), - ), - ); - }, + ); + }, + ), ); } } -class _TopBar extends StatelessWidget { - const _TopBar(this.tag, this.canPop); +class _TopBarContent extends StatelessWidget { + const _TopBarContent(this.tag); final CollectionTag tag; - final bool canPop; @override Widget build(BuildContext context) { return Consumer( builder: (context, ref, _) { final notifier = ref.watch(collectionProvider(tag)); - if (notifier.lists.isEmpty) return TopBar(canPop: canPop); + if (notifier.lists.isEmpty) return const SizedBox(); /// If [entriesProvider] returns an empty list, /// the random entry button shouldn't appear. final noResults = ref.watch(entriesProvider(tag).select((s) => s.isEmpty)); - return TopBar( - canPop: canPop, - items: [ - SearchFilterField( - title: notifier.lists[notifier.index].name, - tag: tag, - ), - if (noResults) - const SizedBox(width: 45) - else - TopBarIcon( - tooltip: 'Random', - icon: Ionicons.shuffle_outline, - onTap: () { - final entries = ref.read(entriesProvider(tag)); - final e = entries[Random().nextInt(entries.length)]; - - Navigator.pushNamed( - context, - RouteArg.media, - arguments: RouteArg(id: e.mediaId, info: e.imageUrl), - ); - }, + return Expanded( + child: Row( + children: [ + SearchFilterField( + title: notifier.lists[notifier.index].name, + tag: tag, ), - TopBarIcon( - tooltip: 'Filter', - icon: Ionicons.funnel_outline, - onTap: () { - final notifier = - ref.read(collectionFilterProvider(tag).notifier); - - showSheet( + if (noResults) + const SizedBox(width: 45) + else + TopBarIcon( + tooltip: 'Random', + icon: Ionicons.shuffle_outline, + onTap: () { + final entries = ref.read(entriesProvider(tag)); + final e = entries[Random().nextInt(entries.length)]; + + Navigator.pushNamed( + context, + RouteArg.media, + arguments: RouteArg(id: e.mediaId, info: e.imageUrl), + ); + }, + ), + TopBarIcon( + tooltip: 'Filter', + icon: Ionicons.funnel_outline, + onTap: () => showSheet( context, CollectionFilterView( - filter: notifier.state, - onChanged: (filter) => notifier.state = filter, + filter: ref.read(collectionFilterProvider(tag)), + onChanged: (filter) => ref + .read(collectionFilterProvider(tag).notifier) + .update((_) => filter), ), - ); - }, - ), - ], + ), + ), + ], + ), ); }, ); @@ -200,15 +201,15 @@ class _ActionButton extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: i != notifier.index - ? theme.textTheme.headline1 - : theme.textTheme.headline1?.copyWith( + ? theme.textTheme.titleLarge + : theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.primary, ), ), ), Text( ' ${notifier.lists[i].entries.length}', - style: theme.textTheme.headline3, + style: theme.textTheme.titleSmall, ), ], ), @@ -294,11 +295,17 @@ class _ContentState extends State<_Content> { }; } - return CollectionGrid( - items: entries, - scoreFormat: notifier.scoreFormat, - onProgressUpdate: update, - ); + return Options().collectionItemView == 0 + ? CollectionList( + items: entries, + scoreFormat: notifier.scoreFormat, + onProgressUpdate: update, + ) + : CollectionGrid( + items: entries, + scoreFormat: notifier.scoreFormat, + onProgressUpdate: update, + ); }, ); } diff --git a/lib/composition/composition_view.dart b/lib/composition/composition_view.dart index ac6c4e23..74001bba 100644 --- a/lib/composition/composition_view.dart +++ b/lib/composition/composition_view.dart @@ -4,8 +4,8 @@ import 'package:otraku/composition/composition_model.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; import 'package:otraku/widgets/layouts/segment_switcher.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -119,7 +119,7 @@ class _CompositionView extends StatelessWidget { autofocus: true, focusNode: focus, controller: textCtrl, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, maxLines: null, decoration: InputDecoration( fillColor: Theme.of(context).colorScheme.background, diff --git a/lib/discover/discover_media_grid.dart b/lib/discover/discover_media_grid.dart index c92b0237..bb2236f4 100644 --- a/lib/discover/discover_media_grid.dart +++ b/lib/discover/discover_media_grid.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/link_tile.dart'; import 'package:otraku/widgets/text_rail.dart'; @@ -17,17 +17,14 @@ class DiscoverMediaGrid extends StatelessWidget { return const SliverFillRemaining(child: Center(child: Text('No Media'))); } - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 290, - height: 110, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (context, index) => _Tile(items[index]), - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 290, + height: 110, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, index) => _Tile(items[index]), ), ); } @@ -49,7 +46,7 @@ class _Tile extends StatelessWidget { if (item.listStatus != null) textRailItems[item.listStatus!] = true; if (item.isAdult) textRailItems['Adult'] = true; - final detailTextStyle = Theme.of(context).textTheme.subtitle2; + final detailTextStyle = Theme.of(context).textTheme.labelSmall; return Card( child: LinkTile( @@ -65,7 +62,7 @@ class _Tile extends StatelessWidget { child: Container( width: 120 / Consts.coverHtoWRatio, color: Theme.of(context).colorScheme.surfaceVariant, - child: FadeImage(item.imageUrl), + child: CachedImage(item.imageUrl), ), ), ), @@ -90,7 +87,7 @@ class _Tile extends StatelessWidget { const SizedBox(height: 5), TextRail( textRailItems, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ], ), diff --git a/lib/discover/discover_providers.dart b/lib/discover/discover_providers.dart index 419e98e1..55551630 100644 --- a/lib/discover/discover_providers.dart +++ b/lib/discover/discover_providers.dart @@ -128,8 +128,11 @@ class DiscoverMediaNotifier final data = await Api.get(GqlQuery.medias, { 'page': value.next, 'type': filter.ofAnime ? 'ANIME' : 'MANGA', - if (search != null && search!.isNotEmpty) 'search': search, - ...filter.toMap(), + if (search != null && search!.isNotEmpty) ...{ + 'search': search, + ...filter.toMap()..['sort'] = 'SEARCH_MATCH', + } else + ...filter.toMap(), }); final items = []; diff --git a/lib/discover/discover_view.dart b/lib/discover/discover_view.dart index ba7c12f8..365b905a 100644 --- a/lib/discover/discover_view.dart +++ b/lib/discover/discover_view.dart @@ -4,7 +4,6 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/common/tile_item.dart'; import 'package:otraku/discover/discover_media_grid.dart'; import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_providers.dart'; import 'package:otraku/filter/filter_providers.dart'; import 'package:otraku/filter/filter_view.dart'; @@ -20,8 +19,9 @@ import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/grids/tile_item_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/filter/filter_search_field.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; import 'package:otraku/widgets/pagination_view.dart'; @@ -60,11 +60,8 @@ class DiscoverView extends ConsumerWidget { return Future.value(); }; - return PageLayout( - topBar: const PreferredSize( - preferredSize: Size.fromHeight(Consts.tapTargetSize), - child: _TopBar(), - ), + return TabScaffold( + topBar: const TopBar(canPop: false, trailing: [_TopBarContent()]), floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: const [_ActionButton()], @@ -74,8 +71,8 @@ class DiscoverView extends ConsumerWidget { } } -class _TopBar extends StatelessWidget { - const _TopBar(); +class _TopBarContent extends StatelessWidget { + const _TopBarContent(); @override Widget build(BuildContext context) { @@ -83,62 +80,64 @@ class _TopBar extends StatelessWidget { builder: (context, ref, _) { final type = ref.watch(discoverFilterProvider.select((s) => s.type)); - return TopBar( - canPop: false, - items: [ - SearchFilterField( - title: Convert.clarifyEnum(type.name)!, - enabled: type != DiscoverType.review, - ), - if (type == DiscoverType.anime || type == DiscoverType.manga) - TopBarIcon( - tooltip: 'Filter', - icon: Ionicons.funnel_outline, - onTap: () => showSheet( - context, - DiscoverFilterView( - filter: ref.read(discoverFilterProvider).filter, - onChanged: (filter) => - ref.read(discoverFilterProvider).filter = filter, - ), - ), - ) - else if (type == DiscoverType.character || - type == DiscoverType.staff) - _BirthdayFilter(ref) - else if (type == DiscoverType.review) - TopBarIcon( - tooltip: 'Sort', - icon: Ionicons.funnel_outline, - onTap: () { - final notifier = ref.read(reviewSortProvider(null).notifier); - final theme = Theme.of(context); - - showSheet( + return Expanded( + child: Row( + children: [ + SearchFilterField( + title: Convert.clarifyEnum(type.name)!, + enabled: type != DiscoverType.review, + ), + if (type == DiscoverType.anime || type == DiscoverType.manga) + TopBarIcon( + tooltip: 'Filter', + icon: Ionicons.funnel_outline, + onTap: () => showSheet( context, - DynamicGradientDragSheet( - onTap: (i) => - notifier.state = ReviewSort.values.elementAt(i), - children: [ - for (int i = 0; i < ReviewSort.values.length; i++) - Text( - ReviewSort.values.elementAt(i).text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: i != notifier.state.index - ? theme.textTheme.headline1 - : theme.textTheme.headline1?.copyWith( - color: theme.colorScheme.primary, - ), - ), - ], + DiscoverFilterView( + filter: ref.read(discoverFilterProvider).filter, + onChanged: (filter) => + ref.read(discoverFilterProvider).filter = filter, ), - ); - }, - ) - else - const SizedBox(width: 10), - ], + ), + ) + else if (type == DiscoverType.character || + type == DiscoverType.staff) + _BirthdayFilter(ref) + else if (type == DiscoverType.review) + TopBarIcon( + tooltip: 'Sort', + icon: Ionicons.funnel_outline, + onTap: () { + final notifier = + ref.read(reviewSortProvider(null).notifier); + final theme = Theme.of(context); + + showSheet( + context, + DynamicGradientDragSheet( + onTap: (i) => + notifier.state = ReviewSort.values.elementAt(i), + children: [ + for (int i = 0; i < ReviewSort.values.length; i++) + Text( + ReviewSort.values.elementAt(i).text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: i != notifier.state.index + ? theme.textTheme.titleLarge + : theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + ), + ); + }, + ) + else + const SizedBox(width: 10), + ], + ), ); }, ); @@ -182,8 +181,8 @@ class _ActionButton extends StatelessWidget { Text( Convert.clarifyEnum(DiscoverType.values[i].name)!, style: i != type.index - ? theme.textTheme.headline1 - : theme.textTheme.headline1?.copyWith( + ? theme.textTheme.titleLarge + : theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.primary, ), ), @@ -256,9 +255,9 @@ class _Grid extends StatelessWidget { scrollCtrl: scrollCtrl, onRefresh: onRefresh, dataType: 'anime', - onData: (data) => Options().compactDiscoverGrid - ? TileItemGrid(data.items) - : DiscoverMediaGrid(data.items), + onData: (data) => Options().discoverItemView == 0 + ? DiscoverMediaGrid(data.items) + : TileItemGrid(data.items), ); case DiscoverType.manga: return PaginationView( @@ -266,9 +265,9 @@ class _Grid extends StatelessWidget { scrollCtrl: scrollCtrl, onRefresh: onRefresh, dataType: 'manga', - onData: (data) => Options().compactDiscoverGrid - ? TileItemGrid(data.items) - : DiscoverMediaGrid(data.items), + onData: (data) => Options().discoverItemView == 0 + ? DiscoverMediaGrid(data.items) + : TileItemGrid(data.items), ); case DiscoverType.character: return PaginationView( diff --git a/lib/edit/edit_view.dart b/lib/edit/edit_view.dart index 1b1b8daa..41477262 100644 --- a/lib/edit/edit_view.dart +++ b/lib/edit/edit_view.dart @@ -422,7 +422,7 @@ class _Label extends StatelessWidget { @override Widget build(BuildContext context) { return SliverToBoxAdapter( - child: Text(label, style: Theme.of(context).textTheme.subtitle1), + child: Text(label, style: Theme.of(context).textTheme.labelMedium), ); } } diff --git a/lib/favorites/favorites_view.dart b/lib/favorites/favorites_view.dart index 48446f27..564e2502 100644 --- a/lib/favorites/favorites_view.dart +++ b/lib/favorites/favorites_view.dart @@ -8,8 +8,9 @@ import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/widgets/grids/tile_item_grid.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; import 'package:otraku/widgets/layouts/constrained_view.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -38,20 +39,7 @@ class _FavoritesViewState extends ConsumerState { onRefresh: () => ref.invalidate(favoritesProvider(widget.id)), ); - return PageLayout( - topBar: TopBar( - title: _tab.text, - items: [ - if (count > 0) - Padding( - padding: const EdgeInsets.only(right: 10), - child: Text( - count.toString(), - style: Theme.of(context).textTheme.headline3, - ), - ), - ], - ), + return PageScaffold( bottomBar: BottomBarIconTabs( current: _tab.index, onChanged: (page) { @@ -67,17 +55,32 @@ class _FavoritesViewState extends ConsumerState { 'Studios': Ionicons.business_outline, }, ), - child: DirectPageView( - current: _tab.index, - onChanged: (page) => - setState(() => _tab = FavoriteType.values.elementAt(page)), - children: [ - _AnimeTab(widget.id, _ctrl, refreshControl), - _MangaTab(widget.id, _ctrl, refreshControl), - _CharactersTab(widget.id, _ctrl, refreshControl), - _StaffTab(widget.id, _ctrl, refreshControl), - _StudiosTab(widget.id, _ctrl, refreshControl), - ], + child: TabScaffold( + topBar: TopBar( + title: _tab.text, + trailing: [ + if (count > 0) + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + count.toString(), + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ], + ), + child: DirectPageView( + current: _tab.index, + onChanged: (page) => + setState(() => _tab = FavoriteType.values.elementAt(page)), + children: [ + _AnimeTab(widget.id, _ctrl, refreshControl), + _MangaTab(widget.id, _ctrl, refreshControl), + _CharactersTab(widget.id, _ctrl, refreshControl), + _StaffTab(widget.id, _ctrl, refreshControl), + _StudiosTab(widget.id, _ctrl, refreshControl), + ], + ), ), ); } diff --git a/lib/feed/feed_view.dart b/lib/feed/feed_view.dart index fccd20c7..9931dc05 100644 --- a/lib/feed/feed_view.dart +++ b/lib/feed/feed_view.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; +import 'package:otraku/activity/activities_providers.dart'; import 'package:otraku/activity/activities_view.dart'; -import 'package:otraku/activity/activity_providers.dart'; import 'package:otraku/composition/composition_model.dart'; import 'package:otraku/composition/composition_view.dart'; import 'package:otraku/settings/settings_provider.dart'; import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; class FeedView extends StatelessWidget { @@ -32,61 +33,27 @@ class FeedView extends StatelessWidget { Navigator.pushNamed(context, RouteArg.notifications); }; - if (count < 1) { - return TopBarIcon( - tooltip: 'Notifications', - icon: Ionicons.notifications_outline, - onTap: openNotifications, + Widget result = TopBarIcon( + tooltip: 'Notifications', + icon: Ionicons.notifications_outline, + onTap: openNotifications, + ); + + if (count > 0) { + result = Badge.count( + count: count, + alignment: AlignmentDirectional.bottomStart, + child: result, ); } - return Padding( - padding: const EdgeInsets.only(right: 10), - child: Tooltip( - message: 'Notifications', - child: GestureDetector( - onTap: openNotifications, - child: Stack( - children: [ - Positioned( - right: 0, - child: Icon( - Ionicons.notifications_outline, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - Container( - constraints: const BoxConstraints( - minWidth: 20, - minHeight: 20, - maxHeight: 20, - ), - margin: const EdgeInsets.only(right: 15, bottom: 5), - padding: const EdgeInsets.symmetric(horizontal: 5), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.error, - borderRadius: BorderRadius.circular(20), - ), - child: Center( - child: Text( - count.toString(), - style: Theme.of(context).textTheme.subtitle2!.copyWith( - color: Theme.of(context).colorScheme.background, - ), - ), - ), - ), - ], - ), - ), - ), - ); + return result; }, ); return Consumer( builder: (context, ref, _) { - return PageLayout( + return TabScaffold( floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [ @@ -108,7 +75,7 @@ class FeedView extends StatelessWidget { topBar: TopBar( canPop: false, title: 'Feed', - items: [ + trailing: [ TopBarIcon( tooltip: 'Filter', icon: Ionicons.funnel_outline, diff --git a/lib/filter/chip_selector.dart b/lib/filter/chip_selector.dart index 2e1fcd07..e3594260 100644 --- a/lib/filter/chip_selector.dart +++ b/lib/filter/chip_selector.dart @@ -40,10 +40,6 @@ class _ChipSelectorState extends State { itemBuilder: (context, index) => Padding( padding: const EdgeInsets.only(right: 10), child: FilterChip( - backgroundColor: Theme.of(context).colorScheme.surface, - labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), label: Text(widget.options[index]), selected: index == _current, onSelected: (selected) { @@ -96,10 +92,6 @@ class _ChipEnumMultiSelectorState extends State { itemBuilder: (context, index) => Padding( padding: const EdgeInsets.only(right: 10), child: FilterChip( - backgroundColor: Theme.of(context).colorScheme.surface, - labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), label: Text(_options[index]), selected: widget.selected.contains(_values[index]), onSelected: (selected) { @@ -135,7 +127,7 @@ class ChipSelectorLayout extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - child: Text(title, style: Theme.of(context).textTheme.subtitle1), + child: Text(title, style: Theme.of(context).textTheme.labelMedium), ), SizedBox( height: 40, diff --git a/lib/filter/filter_models.dart b/lib/filter/filter_models.dart index 476521f0..aca0d0d7 100644 --- a/lib/filter/filter_models.dart +++ b/lib/filter/filter_models.dart @@ -27,6 +27,8 @@ class CollectionFilter extends ApplicableMediaFilter { final tagIdNotIn = []; late EntrySort sort = _ofAnime ? Options().defaultAnimeSort : Options().defaultMangaSort; + int? startYearFrom; + int? startYearTo; OriginCountry? country; @override @@ -40,6 +42,8 @@ class CollectionFilter extends ApplicableMediaFilter { ..tagIdIn.addAll(tagIdIn) ..tagIdNotIn.addAll(tagIdNotIn) ..sort = sort + ..startYearFrom = startYearFrom + ..startYearTo = startYearTo ..country = country; @override diff --git a/lib/filter/filter_providers.dart b/lib/filter/filter_providers.dart index 803a851d..69206f61 100644 --- a/lib/filter/filter_providers.dart +++ b/lib/filter/filter_providers.dart @@ -46,7 +46,7 @@ class DiscoverFilterNotifier extends ChangeNotifier { } set birthday(bool val) { - if (_birthday = val) return; + if (_birthday == val) return; _birthday = val; notifyListeners(); } diff --git a/lib/filter/filter_search_field.dart b/lib/filter/filter_search_field.dart index 52054af3..1bd0f4af 100644 --- a/lib/filter/filter_search_field.dart +++ b/lib/filter/filter_search_field.dart @@ -6,7 +6,7 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/collection/collection_models.dart'; import 'package:otraku/filter/filter_providers.dart'; import 'package:otraku/widgets/fields/search_field.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; /// After [_delay] time has passed, since the last [run] call, call [callback]. /// E.g. do a search query after the user stops typing. @@ -62,7 +62,7 @@ class _SearchFilterFieldState extends State { widget.title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.headline1, + style: Theme.of(context).textTheme.titleLarge, ), ); } @@ -88,7 +88,7 @@ class _SearchFilterFieldState extends State { widget.title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.headline1, + style: Theme.of(context).textTheme.titleLarge, ), ), TopBarIcon( diff --git a/lib/filter/filter_view.dart b/lib/filter/filter_view.dart index 84554cf1..e6fcef04 100644 --- a/lib/filter/filter_view.dart +++ b/lib/filter/filter_view.dart @@ -107,6 +107,15 @@ class CollectionFilterView extends StatelessWidget { ), ), ), + YearRangePicker( + title: 'Start Year', + from: filter.startYearFrom, + to: filter.startYearTo, + onChanged: (from, to) { + filter.startYearFrom = from; + filter.startYearTo = to; + }, + ), const Divider(indent: 15, endIndent: 15), ChipSelector( title: 'Country', @@ -167,15 +176,6 @@ class DiscoverFilterView extends StatelessWidget { : null, ), const Divider(indent: 15, endIndent: 15), - YearRangePicker( - title: 'Start Year', - from: filter.startYearFrom, - to: filter.startYearTo, - onChanged: (from, to) { - filter.startYearFrom = from; - filter.startYearTo = to; - }, - ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Consumer( @@ -191,6 +191,15 @@ class DiscoverFilterView extends StatelessWidget { ), ), ), + YearRangePicker( + title: 'Start Year', + from: filter.startYearFrom, + to: filter.startYearTo, + onChanged: (from, to) { + filter.startYearFrom = from; + filter.startYearTo = to; + }, + ), const Divider(indent: 15, endIndent: 15), ChipSelector( title: 'Country', @@ -407,7 +416,7 @@ class TagSheetBodyState extends ConsumerState { filter: Consts.filter, child: Container( height: 95, - color: Theme.of(context).bottomAppBarColor, + color: Theme.of(context).bottomAppBarTheme.color, padding: const EdgeInsets.only(top: 10, bottom: 5), child: Column( children: [ diff --git a/lib/filter/year_range_picker.dart b/lib/filter/year_range_picker.dart index 76b8828f..43385807 100644 --- a/lib/filter/year_range_picker.dart +++ b/lib/filter/year_range_picker.dart @@ -54,14 +54,15 @@ class _YearRangePickerState extends State { children: [ Row( children: [ - Text(widget.title, style: Theme.of(context).textTheme.subtitle1), + Text(widget.title, + style: Theme.of(context).textTheme.labelMedium), const Spacer(), SizedBox( width: 50, child: Text( _from.truncate().toString(), textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyText1, + style: Theme.of(context).textTheme.bodyLarge, ), ), const Text(' - '), @@ -70,7 +71,7 @@ class _YearRangePickerState extends State { child: Text( _to.truncate().toString(), textAlign: TextAlign.right, - style: Theme.of(context).textTheme.bodyText1, + style: Theme.of(context).textTheme.bodyLarge, ), ), ], diff --git a/lib/home/home_provider.dart b/lib/home/home_provider.dart index bdb7d76c..f1f79922 100644 --- a/lib/home/home_provider.dart +++ b/lib/home/home_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/activity/activity_providers.dart'; +import 'package:otraku/activity/activities_providers.dart'; import 'package:otraku/discover/discover_providers.dart'; import 'package:otraku/utils/options.dart'; diff --git a/lib/home/home_view.dart b/lib/home/home_view.dart index ca851ead..3aea2979 100644 --- a/lib/home/home_view.dart +++ b/lib/home/home_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/activity/activity_providers.dart'; +import 'package:otraku/activity/activities_providers.dart'; import 'package:otraku/collection/collection_models.dart'; import 'package:otraku/collection/collection_preview_provider.dart'; import 'package:otraku/collection/collection_preview_view.dart'; @@ -21,7 +21,7 @@ import 'package:otraku/feed/feed_view.dart'; import 'package:otraku/user/user_view.dart'; import 'package:otraku/utils/background_handler.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -137,7 +137,7 @@ class _HomeViewState extends ConsumerState { return WillPopScope( onWillPop: () => _onWillPop(context), - child: PageLayout( + child: PageScaffold( bottomBar: BottomBarIconTabs( current: notifier.homeTab, onChanged: (i) => ref.read(homeProvider).homeTab = i, diff --git a/lib/main.dart b/lib/main.dart index 58ffdb90..8dff602a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -95,7 +95,7 @@ class AppState extends State { } SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - statusBarColor: scheme.background, + statusBarColor: Colors.transparent, statusBarBrightness: scheme.brightness, statusBarIconBrightness: overlayBrightness, systemNavigationBarColor: scheme.background, diff --git a/lib/media/media_constants.dart b/lib/media/media_constants.dart index 2f27eec5..3deaaa93 100644 --- a/lib/media/media_constants.dart +++ b/lib/media/media_constants.dart @@ -104,6 +104,8 @@ enum EntrySort { RELEASED_ON_DESC('Last Release'), PROGRESS('Least Progress'), PROGRESS_DESC('Most Progress'), + AVG_SCORE('Lowest Rated'), + AVG_SCORE_DESC('Highest Rated'), REPEATED('Least Repeated'), REPEATED_DESC('Most Repeated'); diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index 3e660730..feb33e1f 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/discover/discover_models.dart'; +import 'package:otraku/media/media_models.dart'; import 'package:otraku/media/media_providers.dart'; +import 'package:otraku/utils/consts.dart'; import 'package:otraku/utils/convert.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; -import 'package:otraku/widgets/custom_sliver_header.dart'; +import 'package:otraku/widgets/cached_image.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; +import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; import 'package:otraku/widgets/overlays/toast.dart'; import 'package:otraku/widgets/text_rail.dart'; @@ -19,98 +23,300 @@ class MediaHeader extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( - builder: (context, ref, _) => ref.watch(mediaProvider(id)).when( - loading: _placeholder, - error: (_, __) => _placeholder(), - data: (data) { - final info = data.info; - final textRailItems = {}; - - if (info.isAdult) textRailItems['Adult'] = true; - if (info.format != null) { - textRailItems[Convert.clarifyEnum(info.format)!] = false; - } - if (data.edit.status != null) { - textRailItems[Convert.adaptListStatus( - data.edit.status!, - info.type == DiscoverType.anime, - )] = false; - } - if (info.airingAt != null) { - textRailItems['Ep ${info.nextEpisode} in ' - '${Convert.timeUntilTimestamp(info.airingAt)}'] = true; - } - if (data.edit.status != null) { - final progress = data.edit.progress; - if (info.nextEpisode != null && - info.nextEpisode! - 1 > progress) { - textRailItems['${info.nextEpisode! - 1 - progress}' - ' ep behind'] = true; - } - } - - return CustomSliverHeader( - title: info.preferredTitle, - image: info.cover, - extraLargeImage: info.extraLargeCover, - banner: info.banner, - squareImage: false, - implyLeading: true, - heroId: id, - maxWidth: null, - actions: [ - if (info.siteUrl != null) + builder: (context, ref, _) { + final data = ref.watch(mediaProvider(id).select((s) => s.valueOrNull)); + final textRailItems = {}; + + if (data != null) { + final info = data.info; + + if (info.isAdult) textRailItems['Adult'] = true; + + if (info.format != null) { + textRailItems[Convert.clarifyEnum(info.format)!] = false; + } + + if (data.edit.status != null) { + textRailItems[Convert.adaptListStatus( + data.edit.status!, + info.type == DiscoverType.anime, + )] = false; + } + + if (info.airingAt != null) { + textRailItems['Ep ${info.nextEpisode} in ' + '${Convert.timeUntilTimestamp(info.airingAt)}'] = true; + } + + if (data.edit.status != null) { + final progress = data.edit.progress; + if (info.nextEpisode != null && info.nextEpisode! - 1 > progress) { + textRailItems['${info.nextEpisode! - 1 - progress}' + ' ep behind'] = true; + } + } + } + + return SliverPersistentHeader( + pinned: true, + delegate: _Delegate( + id: id, + info: data?.info, + coverUrl: coverUrl, + textRailItems: textRailItems, + imageWidth: MediaQuery.of(context).size.width < 430.0 + ? MediaQuery.of(context).size.width * 0.30 + : 100.0, + ), + ); + }, + ); + } +} + +class _Delegate implements SliverPersistentHeaderDelegate { + _Delegate({ + required this.id, + required this.info, + required this.imageWidth, + required this.coverUrl, + required this.textRailItems, + }); + + final int id; + final MediaInfo? info; + final double imageWidth; + final String? coverUrl; + final Map textRailItems; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + final height = maxExtent; + final extent = height - shrinkOffset; + final opacity = shrinkOffset < (_bannerHeight - minExtent) + ? shrinkOffset / (_bannerHeight - minExtent) + : 1.0; + + final cover = info?.cover ?? coverUrl; + final theme = Theme.of(context); + + return DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 5, + color: theme.colorScheme.background, + ), + ], + ), + child: FlexibleSpaceBar.createSettings( + minExtent: minExtent, + maxExtent: height, + currentExtent: extent > minExtent ? extent : minExtent, + child: Stack( + fit: StackFit.expand, + children: [ + FlexibleSpaceBar( + collapseMode: CollapseMode.pin, + stretchModes: const [StretchMode.zoomBackground], + background: Column( + children: [ + Expanded( + child: info?.banner != null + ? GestureDetector( + child: CachedImage(info!.banner!), + onTap: () => showPopUp( + context, + ImageDialog(info!.banner!), + ), + ) + : const SizedBox(), + ), + SizedBox(height: height - _bannerHeight), + ], + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: height - _bannerHeight, + alignment: Alignment.topCenter, + color: theme.colorScheme.background, + child: Container( + height: 0, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: 15, + spreadRadius: 25, + color: theme.colorScheme.background, + ), + ], + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 10, + right: 10, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Hero( + tag: id, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: Container( + height: imageHeight, + width: imageWidth, + color: theme.colorScheme.surfaceVariant, + child: cover != null + ? GestureDetector( + onTap: () => showPopUp( + context, + ImageDialog( + info?.extraLargeCover ?? cover, + ), + ), + child: CachedImage(cover), + ) + : null, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (info?.preferredTitle != null) + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => + Toast.copy(context, info!.preferredTitle!), + child: Text( + info!.preferredTitle!, + maxLines: 8, + overflow: TextOverflow.fade, + style: theme.textTheme.titleLarge!.copyWith( + shadows: [ + Shadow( + blurRadius: 10, + color: theme.colorScheme.background, + ), + ], + ), + ), + ), + if (textRailItems.isNotEmpty) + TextRail( + textRailItems, + style: theme.textTheme.labelMedium, + ), + ], + ), + ), + ], + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: Opacity( + opacity: opacity, + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.background, + boxShadow: [ + BoxShadow( + blurRadius: 10, + spreadRadius: 10, + color: theme.colorScheme.background, + ), + ], + ), + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: Row( + children: [ + TopBarShadowIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: Navigator.of(context).pop, + ), + Expanded( + child: info?.preferredTitle == null + ? const SizedBox() + : Opacity( + opacity: opacity, + child: Text( + info!.preferredTitle!, + style: theme.textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (info?.siteUrl != null) TopBarShadowIcon( tooltip: 'More', icon: Ionicons.ellipsis_horizontal, onTap: () => showSheet( context, - FixedGradientDragSheet.link(context, info.siteUrl!), + FixedGradientDragSheet.link(context, info!.siteUrl!), ), ), ], - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => Toast.copy(context, info.preferredTitle!), - child: Text( - info.preferredTitle!, - maxLines: 8, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.headline1!.copyWith( - shadows: [ - Shadow( - blurRadius: 10, - color: Theme.of(context).colorScheme.background, - ), - ], - ), - ), - ), - TextRail( - textRailItems, - style: Theme.of(context).textTheme.subtitle1, - ), - ], - ), - ); - }, - ), + ), + ), + ], + ), + ), ); } - Widget _placeholder() => CustomSliverHeader( - heroId: id, - image: coverUrl, - squareImage: false, - implyLeading: true, - actions: const [], - title: null, - banner: null, - maxWidth: null, - child: null, - ); + static const _bannerHeight = 200.0; + + double get imageHeight => imageWidth * Consts.coverHtoWRatio; + + @override + double get maxExtent => _bannerHeight + imageHeight / 2; + + @override + double get minExtent => Consts.tapTargetSize; + + @override + OverScrollHeaderStretchConfiguration? get stretchConfiguration => + OverScrollHeaderStretchConfiguration(stretchTriggerOffset: 100); + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => + true; + + @override + PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => + null; + + @override + FloatingHeaderSnapConfiguration? get snapConfiguration => null; + + @override + TickerProvider? get vsync => null; } diff --git a/lib/media/media_info_view.dart b/lib/media/media_info_view.dart index 9c659fe5..0dbff466 100644 --- a/lib/media/media_info_view.dart +++ b/lib/media/media_info_view.dart @@ -10,10 +10,11 @@ import 'package:otraku/home/home_provider.dart'; import 'package:otraku/home/home_view.dart'; import 'package:otraku/media/media_models.dart'; import 'package:otraku/media/media_providers.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/link_tile.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -74,7 +75,7 @@ class MediaInfoView extends StatelessWidget { .innerController; return Consumer( - builder: (context, ref, _) => PageLayout( + builder: (context, ref, _) => TabScaffold( floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [_EditButton(media), _FavoriteButton(info)], @@ -131,7 +132,7 @@ class MediaInfoView extends StatelessWidget { Text( infoTitles[i], maxLines: 1, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), Text(infoData[i].toString(), maxLines: 1), ], @@ -283,7 +284,7 @@ class _ScrollCards extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 10), child: Text( title, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ), const Spacer(), @@ -372,7 +373,7 @@ class _TagScrollCardsState extends State<_TagScrollCards> { final spoilerTextStyle = Theme.of(context) .textTheme - .bodyText1 + .bodyLarge ?.copyWith(color: Theme.of(context).colorScheme.error); return _ScrollCards( @@ -414,7 +415,7 @@ class _TagScrollCardsState extends State<_TagScrollCards> { const SizedBox(width: 5), Text( '${tags[i].rank}%', - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ], ), @@ -439,7 +440,8 @@ class _Title extends StatelessWidget { children: [ SizedBox( width: 90, - child: Text(label, style: Theme.of(context).textTheme.subtitle1), + child: + Text(label, style: Theme.of(context).textTheme.labelMedium), ), Flexible( child: GestureDetector( @@ -449,7 +451,7 @@ class _Title extends StatelessWidget { title, maxLines: null, textAlign: TextAlign.right, - style: Theme.of(context).textTheme.bodyText2?.copyWith( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), diff --git a/lib/media/media_other_view.dart b/lib/media/media_other_view.dart index 9523c2f3..7bf192cb 100644 --- a/lib/media/media_other_view.dart +++ b/lib/media/media_other_view.dart @@ -4,10 +4,10 @@ import 'package:otraku/utils/consts.dart'; import 'package:otraku/media/media_models.dart'; import 'package:otraku/media/media_providers.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/text_rail.dart'; @@ -26,7 +26,7 @@ class MediaOtherView extends StatelessWidget { .findAncestorStateOfType()! .innerController; - return PageLayout( + return TabScaffold( floatingBar: FloatingBar( scrollCtrl: scrollCtrl, centered: true, @@ -125,7 +125,7 @@ class _RelatedGrid extends StatelessWidget { borderRadius: Consts.borderRadiusMin, child: Container( color: Theme.of(context).colorScheme.surfaceVariant, - child: FadeImage( + child: CachedImage( items[i].imageUrl, width: 100 / Consts.coverHtoWRatio, ), @@ -147,7 +147,7 @@ class _RelatedGrid extends StatelessWidget { const SizedBox(height: 5), TextRail( details, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ], ), @@ -202,7 +202,7 @@ class _RecommendationsGrid extends StatelessWidget { borderRadius: Consts.borderRadiusMin, child: Container( color: Theme.of(context).colorScheme.surfaceVariant, - child: FadeImage(items[i].imageUrl!), + child: CachedImage(items[i].imageUrl!), ), ), ), @@ -215,7 +215,7 @@ class _RecommendationsGrid extends StatelessWidget { items[i].title, overflow: TextOverflow.fade, maxLines: 2, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, ), ), ), diff --git a/lib/media/media_people_view.dart b/lib/media/media_people_view.dart index 6fc460c6..5ec55a61 100644 --- a/lib/media/media_people_view.dart +++ b/lib/media/media_people_view.dart @@ -6,8 +6,8 @@ import 'package:otraku/common/relation.dart'; import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/widgets/grids/relation_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -24,7 +24,7 @@ class MediaPeopleView extends StatelessWidget { .findAncestorStateOfType()! .innerController; - return PageLayout( + return TabScaffold( floatingBar: FloatingBar( scrollCtrl: scrollCtrl, centered: true, @@ -35,7 +35,8 @@ class MediaPeopleView extends StatelessWidget { onChanged: (i) => toggleTab(i == 1), ), if (tabToggled) - const SizedBox(width: actionButtonSize, height: actionButtonSize) + const SizedBox( + width: floatingBarItemHeight, height: floatingBarItemHeight) else _LanguageButton(id, scrollCtrl), ], @@ -141,8 +142,8 @@ class _LanguageButton extends StatelessWidget { Text( notifier.languages[i], style: i != notifier.languageIndex - ? Theme.of(context).textTheme.headline1 - : Theme.of(context).textTheme.headline1?.copyWith( + ? Theme.of(context).textTheme.titleLarge + : Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), diff --git a/lib/media/media_social_view.dart b/lib/media/media_social_view.dart index e9fecc33..968f52f0 100644 --- a/lib/media/media_social_view.dart +++ b/lib/media/media_social_view.dart @@ -6,11 +6,11 @@ import 'package:otraku/media/media_providers.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/statistics/charts.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; @@ -29,7 +29,7 @@ class MediaSocialView extends StatelessWidget { .innerController; final stats = media.stats; - return PageLayout( + return TabScaffold( floatingBar: FloatingBar( scrollCtrl: scrollCtrl, centered: true, @@ -126,7 +126,7 @@ class _ReviewGrid extends StatelessWidget { tag: items[i].userId, child: ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage( + child: CachedImage( items[i].avatar, height: 50, width: 50, @@ -135,6 +135,13 @@ class _ReviewGrid extends StatelessWidget { ), const SizedBox(width: 10), Text(items[i].username), + const Spacer(), + const Icon(Icons.thumb_up_outlined, size: Consts.iconSmall), + const SizedBox(width: 10), + Text( + items[i].rating, + style: Theme.of(context).textTheme.labelMedium, + ), ], ), ), @@ -151,7 +158,7 @@ class _ReviewGrid extends StatelessWidget { padding: Consts.padding, child: Text( items[i].summary, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, overflow: TextOverflow.fade, ), ), diff --git a/lib/media/media_view.dart b/lib/media/media_view.dart index 5ba4fc4d..530a79df 100644 --- a/lib/media/media_view.dart +++ b/lib/media/media_view.dart @@ -9,7 +9,7 @@ import 'package:otraku/media/media_other_view.dart'; import 'package:otraku/media/media_people_view.dart'; import 'package:otraku/media/media_social_view.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/media/media_header.dart'; @@ -37,7 +37,7 @@ class _MediaViewState extends State { @override Widget build(BuildContext context) { - return PageLayout( + return PageScaffold( bottomBar: BottomBarIconTabs( current: _tab, onChanged: (i) => setState(() => _tab = i), @@ -80,10 +80,10 @@ class _MediaViewState extends State { loading: () => const Center(child: Loader()), error: (_, __) => const Center(child: Text('Failed to load media')), - data: (data) => _MediaView( + data: (media) => _MediaView( widget.id, _tab, - data, + media, (i) => setState(() => _tab = i), ), ); @@ -157,7 +157,7 @@ class __MediaSubViewState extends ConsumerState<_MediaView> { : ref.read(mediaContentProvider(widget.id)).fetchCharacters(); return; case 3: - if (_socialTabToggled) { + if (!_socialTabToggled) { ref.read(mediaContentProvider(widget.id)).fetchReviews(); } return; diff --git a/lib/notifications/notification_model.dart b/lib/notifications/notification_model.dart index d9404550..c24af93d 100644 --- a/lib/notifications/notification_model.dart +++ b/lib/notifications/notification_model.dart @@ -3,36 +3,32 @@ import 'package:otraku/utils/convert.dart'; import 'package:otraku/utils/options.dart'; enum NotificationFilterType { - all, - airing, - activity, - forum, - follows, - media; + all('All'), + replies('Replies'), + activity('Activity'), + forum('Forum'), + airing('Airing'), + follows('Follows'), + media('Media'); - String get text { - switch (this) { - case NotificationFilterType.all: - return 'All'; - case NotificationFilterType.airing: - return 'Airing'; - case NotificationFilterType.activity: - return 'Activity'; - case NotificationFilterType.forum: - return 'Forum'; - case NotificationFilterType.follows: - return 'Follows'; - case NotificationFilterType.media: - return 'Media'; - } - } + const NotificationFilterType(this.text); + + final String text; List? get vars { switch (this) { case NotificationFilterType.all: return null; - case NotificationFilterType.airing: - return const ['AIRING']; + case NotificationFilterType.replies: + return const [ + 'ACTIVITY_MESSAGE', + 'ACTIVITY_REPLY', + 'ACTIVITY_REPLY_SUBSCRIBED', + 'ACTIVITY_MENTION', + 'THREAD_COMMENT_REPLY', + 'THREAD_COMMENT_MENTION', + 'THREAD_SUBSCRIBED', + ]; case NotificationFilterType.activity: return const [ 'ACTIVITY_MESSAGE', @@ -50,6 +46,8 @@ enum NotificationFilterType { 'THREAD_LIKE', 'THREAD_COMMENT_LIKE', ]; + case NotificationFilterType.airing: + return const ['AIRING']; case NotificationFilterType.follows: return const ['FOLLOWING']; case NotificationFilterType.media: diff --git a/lib/notifications/notifications_view.dart b/lib/notifications/notifications_view.dart index 13baee62..e864c7ee 100644 --- a/lib/notifications/notifications_view.dart +++ b/lib/notifications/notifications_view.dart @@ -11,11 +11,12 @@ import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/edit/edit_view.dart'; import 'package:otraku/widgets/layouts/constrained_view.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -48,99 +49,99 @@ class _NotificationsViewState extends ConsumerState { @override Widget build(BuildContext context) { - return PageLayout( - topBar: TopBar( - items: [ - Expanded( - child: Consumer( - builder: (context, ref, _) => Text( - '${ref.watch(notificationFilterProvider).text} Notifications', - style: Theme.of(context).textTheme.headline1, - overflow: TextOverflow.ellipsis, - maxLines: 1, + return PageScaffold( + child: TabScaffold( + topBar: TopBar( + trailing: [ + Expanded( + child: Consumer( + builder: (context, ref, _) => Text( + '${ref.watch(notificationFilterProvider).text} Notifications', + style: Theme.of(context).textTheme.titleLarge, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ), ), - ), - ], - ), - floatingBar: FloatingBar( - scrollCtrl: _ctrl, - children: [ - ActionButton( - tooltip: 'Filter', - icon: Ionicons.funnel_outline, - onTap: () { - showSheet( - context, - Consumer( - builder: (context, ref, _) { - final notifier = ref.read( - notificationFilterProvider.notifier, - ); + ], + ), + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: [ + ActionButton( + tooltip: 'Filter', + icon: Ionicons.funnel_outline, + onTap: () { + showSheet( + context, + Consumer( + builder: (context, ref, _) { + final theme = Theme.of(context); + final notifier = ref.read( + notificationFilterProvider.notifier, + ); - final tiles = []; - for (int i = 0; - i < NotificationFilterType.values.length; - i++) { - tiles.add(Text( - NotificationFilterType.values.elementAt(i).text, - style: i != notifier.state.index - ? Theme.of(context).textTheme.headline1 - : Theme.of(context).textTheme.headline1?.copyWith( - color: Theme.of(context).colorScheme.primary, + final tiles = []; + for (int i = 0; + i < NotificationFilterType.values.length; + i++) { + tiles.add(Text( + NotificationFilterType.values.elementAt(i).text, + style: i != notifier.state.index + ? theme.textTheme.titleLarge + : theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.primary, ), - )); - } + )); + } - return DynamicGradientDragSheet( - children: tiles, - onTap: (i) => notifier.state = - NotificationFilterType.values.elementAt(i), - ); - }, - ), - ); - }, - ), - ], - ), - child: Consumer( - child: SliverRefreshControl( - onRefresh: () => ref.invalidate(notificationsProvider), + return DynamicGradientDragSheet( + children: tiles, + onTap: (i) => notifier.state = + NotificationFilterType.values.elementAt(i), + ); + }, + ), + ); + }, + ), + ], ), - builder: (context, ref, refreshControl) { - ref.listen( - notificationsProvider, - (_, s) => s.notifications.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load notifications', - content: error.toString(), + child: Consumer( + child: SliverRefreshControl( + onRefresh: () => ref.invalidate(notificationsProvider), + ), + builder: (context, ref, refreshControl) { + ref.listen( + notificationsProvider, + (_, s) => s.notifications.whenOrNull( + error: (error, _) => showPopUp( + context, + ConfirmationDialog( + title: 'Failed to load notifications', + content: error.toString(), + ), ), ), - ), - ); + ); - final notifier = ref.watch(notificationsProvider); - return notifier.notifications.when( - loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load notifications')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No notifications')); - } + final notifier = ref.watch(notificationsProvider); + return notifier.notifications.when( + loading: () => const Center(child: Loader()), + error: (_, __) => + const Center(child: Text('Failed to load notifications')), + data: (data) { + if (data.items.isEmpty) { + return const Center(child: Text('No notifications')); + } - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl!, - SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverList( + return ConstrainedView( + child: CustomScrollView( + physics: Consts.physics, + controller: _ctrl, + slivers: [ + refreshControl!, + SliverList( delegate: SliverChildBuilderDelegate( (context, i) => _NotificationItem( data.items[i], @@ -149,14 +150,14 @@ class _NotificationsViewState extends ConsumerState { childCount: data.items.length, ), ), - ), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }, - ); - }, + SliverFooter(loading: data.hasNext), + ], + ), + ); + }, + ); + }, + ), ), ); } @@ -197,7 +198,7 @@ class _NotificationItem extends StatelessWidget { borderRadius: const BorderRadius.horizontal( left: Consts.radiusMin, ), - child: FadeImage(item.imageUrl!, width: 70), + child: CachedImage(item.imageUrl!, width: 70), ), ), Flexible( @@ -294,15 +295,17 @@ class _NotificationItem extends StatelessWidget { text: item.texts[i], style: (i % 2 == 0) == item.markTextOnEvenIndex - ? Theme.of(context).textTheme.bodyText1 - : Theme.of(context).textTheme.bodyText2, + ? Theme.of(context).textTheme.bodyLarge + : Theme.of(context) + .textTheme + .bodyMedium, ), ], ), ), Text( item.timestamp, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.labelSmall, ), ], ), @@ -344,8 +347,8 @@ class _NotificationDialog extends StatelessWidget { TextSpan( text: item.texts[i], style: (i % 2 == 0) == item.markTextOnEvenIndex - ? Theme.of(context).textTheme.bodyText1 - : Theme.of(context).textTheme.bodyText2, + ? Theme.of(context).textTheme.bodyLarge + : Theme.of(context).textTheme.bodyMedium, ), ], ), @@ -363,7 +366,7 @@ class _NotificationDialog extends StatelessWidget { if (item.imageUrl != null) ...[ ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage( + child: CachedImage( item.imageUrl!, width: imageWidth, height: imageWidth * Consts.coverHtoWRatio, diff --git a/lib/review/review_grid.dart b/lib/review/review_grid.dart index 561e3576..f778d747 100644 --- a/lib/review/review_grid.dart +++ b/lib/review/review_grid.dart @@ -3,7 +3,7 @@ import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/review/review_models.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; class ReviewGrid extends StatelessWidget { @@ -13,17 +13,14 @@ class ReviewGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 270, - height: 200, - ), - delegate: SliverChildBuilderDelegate( - (_, i) => _Tile(items[i]), - childCount: items.length, - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 270, + height: 200, + ), + delegate: SliverChildBuilderDelegate( + (_, i) => _Tile(items[i]), + childCount: items.length, ), ); } @@ -52,7 +49,7 @@ class _Tile extends StatelessWidget { const BorderRadius.vertical(top: Consts.radiusMin), child: Hero( tag: item.id, - child: FadeImage(item.bannerUrl!), + child: CachedImage(item.bannerUrl!), ), ), ), @@ -69,7 +66,7 @@ class _Tile extends StatelessWidget { alignment: Alignment.bottomLeft, child: Text( 'Review of ${item.mediaTitle} by ${item.userName}', - style: Theme.of(context).textTheme.headline2, + style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.fade, ), ), @@ -82,7 +79,7 @@ class _Tile extends StatelessWidget { Expanded( child: Text( item.summary, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, overflow: TextOverflow.fade, ), ), @@ -94,13 +91,14 @@ class _Tile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( - Icons.thumbs_up_down_outlined, + Icons.thumb_up_outlined, size: Consts.iconSmall, ), const SizedBox(height: 5), Text( item.rating, - style: Theme.of(context).textTheme.subtitle1, + style: + Theme.of(context).textTheme.labelMedium, ), ], ), diff --git a/lib/review/review_models.dart b/lib/review/review_models.dart index 0cbd52ad..f940fd6f 100644 --- a/lib/review/review_models.dart +++ b/lib/review/review_models.dart @@ -104,7 +104,11 @@ class Review { score: score, rating: map['rating'] ?? rating, totalRating: map['ratingAmount'] ?? totalRating, - viewerRating: map['userRating'], + viewerRating: map['userRating'] == 'UP_VOTE' + ? true + : map['userRating'] == 'DOWN_VOTE' + ? false + : null, ); } diff --git a/lib/review/review_providers.dart b/lib/review/review_providers.dart index f3374fcc..3fc4bb7c 100644 --- a/lib/review/review_providers.dart +++ b/lib/review/review_providers.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/review/review_models.dart'; import 'package:otraku/utils/api.dart'; @@ -14,8 +13,8 @@ final reviewSortProvider = StateProvider.autoDispose.family( (ref, _) => ReviewSort.CREATED_AT_DESC, ); -final reviewsProvider = - ChangeNotifierProvider.autoDispose.family( +final reviewsProvider = StateNotifierProvider.autoDispose + .family>, int>( (ref, userId) => ReviewsNotifier(userId, ref.watch(reviewSortProvider(userId))), ); @@ -48,29 +47,31 @@ class ReviewNotifier extends StateNotifier> { ? 'UP_VOTE' : 'DOWN_VOTE', }); - if (data['RateReview'] == null) throw StateError('Review data is empty.'); + + if (data['RateReview'] == null) { + throw StateError('Failed to rate review.'); + } + return value.copyWith(data['RateReview']); }); } } -class ReviewsNotifier extends ChangeNotifier { - ReviewsNotifier(this.userId, this.sort) { +class ReviewsNotifier + extends StateNotifier>> { + ReviewsNotifier(this.userId, this.sort) : super(const AsyncValue.loading()) { fetch(); } final int userId; final ReviewSort sort; - int _count = 0; - var _reviews = const AsyncValue>.loading(); - - int get reviewCount => _count; - AsyncValue> get reviews => _reviews; + int _reviewCount = 0; + int get reviewCount => _reviewCount; Future fetch() async { - _reviews = await AsyncValue.guard(() async { - final value = _reviews.valueOrNull ?? Pagination(); + state = await AsyncValue.guard(() async { + final value = state.valueOrNull ?? Pagination(); final data = await Api.get(GqlQuery.reviews, { 'userId': userId, @@ -78,18 +79,17 @@ class ReviewsNotifier extends ChangeNotifier { 'sort': sort.name, }); - _count = data['Page']['pageInfo']?['total'] ?? 0; - final items = []; for (final r in data['Page']['reviews']) { items.add(ReviewItem(r)); } + _reviewCount = data['Page']['pageInfo']?['total'] ?? 0; + return value.append( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); }); - notifyListeners(); } } diff --git a/lib/review/review_view.dart b/lib/review/review_view.dart index f4e1708c..32eea52d 100644 --- a/lib/review/review_view.dart +++ b/lib/review/review_view.dart @@ -5,10 +5,10 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/review/review_providers.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/html_content.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -56,7 +56,7 @@ class ReviewView extends StatelessWidget { ), child: Text( data.mediaTitle, - style: Theme.of(context).textTheme.headline2, + style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center, ), ), @@ -71,11 +71,11 @@ class ReviewView extends StatelessWidget { child: RichText( textAlign: TextAlign.center, text: TextSpan( - style: Theme.of(context).textTheme.headline2, + style: Theme.of(context).textTheme.titleMedium, children: [ TextSpan( text: 'review by ', - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), TextSpan(text: data.userName), ], @@ -86,7 +86,7 @@ class ReviewView extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10), child: Text( data.summary, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, textAlign: TextAlign.center, ), ), @@ -103,7 +103,7 @@ class ReviewView extends StatelessWidget { '${data.score}/100', style: Theme.of(context) .textTheme - .bodyText2 + .bodyMedium ?.copyWith( color: Theme.of(context).colorScheme.onPrimary, @@ -118,7 +118,7 @@ class ReviewView extends StatelessWidget { padding: const EdgeInsets.only(bottom: 10, top: 20), child: Text( data.createdAt, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, textAlign: TextAlign.center, ), ), @@ -171,7 +171,8 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate { children: [ Expanded( child: GestureDetector( - child: Hero(tag: id, child: FadeImage(bannerUrl!)), + child: + Hero(tag: id, child: CachedImage(bannerUrl!)), onTap: () => showPopUp(context, ImageDialog(bannerUrl!)), ), @@ -225,7 +226,7 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate { opacity: opacity, child: Text( title!, - style: Theme.of(context).textTheme.headline2, + style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.ellipsis, ), ), @@ -327,7 +328,7 @@ class _RateButtonsState extends State<_RateButtons> { ), Text( '${value.rating}/${value.totalRating} users liked this review', - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, textAlign: TextAlign.center, ), ], diff --git a/lib/review/reviews_view.dart b/lib/review/reviews_view.dart index d36e5248..ada1ab4c 100644 --- a/lib/review/reviews_view.dart +++ b/lib/review/reviews_view.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/consts.dart'; import 'package:otraku/review/review_models.dart'; import 'package:otraku/review/review_providers.dart'; import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/review/review_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/widgets/pagination_view.dart'; class ReviewsView extends ConsumerStatefulWidget { const ReviewsView(this.id); @@ -23,7 +22,7 @@ class ReviewsView extends ConsumerStatefulWidget { class _ReviewsViewState extends ConsumerState { late final _ctrl = PaginationController( - loadMore: () => ref.read(reviewsProvider(widget.id)).fetch(), + loadMore: () => ref.read(reviewsProvider(widget.id).notifier).fetch(), ); @override @@ -34,100 +33,75 @@ class _ReviewsViewState extends ConsumerState { @override Widget build(BuildContext context) { - final count = ref.watch( - reviewsProvider(widget.id).select((s) => s.reviewCount), - ); + // The [reviewCount] is not part of the state of [ReviewsNotifier] and + // changes cannot be tracket through selecting it. it would be good to + // make it part of the state later. + ref.watch(reviewsProvider(widget.id)); + final count = ref.watch(reviewsProvider(widget.id).notifier).reviewCount; - return PageLayout( - topBar: TopBar( - title: 'Reviews', - items: [ - if (count > 0) - Padding( - padding: const EdgeInsets.only(right: 10), - child: Text( - count.toString(), - style: Theme.of(context).textTheme.headline3, - ), - ), - ], - ), - floatingBar: FloatingBar( - scrollCtrl: _ctrl, - children: [ - ActionButton( - tooltip: 'Sort', - icon: Ionicons.funnel_outline, - onTap: () { - final notifier = ref.read(reviewSortProvider(widget.id).notifier); - - showSheet( - context, - DynamicGradientDragSheet( - onTap: (i) => notifier.state = ReviewSort.values.elementAt(i), - children: [ - for (int i = 0; i < ReviewSort.values.length; i++) - Text( - ReviewSort.values.elementAt(i).text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: i != notifier.state.index - ? Theme.of(context).textTheme.headline1 - : Theme.of(context).textTheme.headline1?.copyWith( - color: Theme.of(context).colorScheme.primary), - ), - ], - ), - ); - }, - ), - ], - ), - child: Consumer( - child: SliverRefreshControl( - onRefresh: () => ref.invalidate(reviewsProvider(widget.id)), - ), - builder: (context, ref, refreshControl) { - ref.listen( - reviewsProvider(widget.id), - (_, s) => s.reviews.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load reviews', - content: error.toString(), + return PageScaffold( + child: TabScaffold( + topBar: TopBar( + title: 'Reviews', + trailing: [ + if (count > 0) + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + count.toString(), + style: Theme.of(context).textTheme.titleSmall, ), ), - ), - ); - - return ref.watch(reviewsProvider(widget.id)).reviews.when( - loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load reviews')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No Reviews')); - } + ], + ), + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: [ + ActionButton( + tooltip: 'Sort', + icon: Ionicons.funnel_outline, + onTap: () { + final theme = Theme.of(context); + final notifier = + ref.read(reviewSortProvider(widget.id).notifier); - return Center( - child: ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: Consts.layoutBig), - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl!, - ReviewGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ), - ); - }, - ); - }, + showSheet( + context, + DynamicGradientDragSheet( + onTap: (i) => + notifier.state = ReviewSort.values.elementAt(i), + children: [ + for (int i = 0; i < ReviewSort.values.length; i++) + Text( + ReviewSort.values.elementAt(i).text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: i != notifier.state.index + ? theme.textTheme.titleLarge + : theme.textTheme.titleLarge + ?.copyWith(color: theme.colorScheme.primary), + ), + ], + ), + ); + }, + ), + ], + ), + child: Consumer( + builder: (context, ref, refreshControl) { + return PaginationView( + provider: reviewsProvider(widget.id), + scrollCtrl: _ctrl, + dataType: 'reviews', + onRefresh: () { + ref.invalidate(reviewsProvider(widget.id)); + return Future.value(); + }, + onData: (data) => ReviewGrid(data.items), + ); + }, + ), ), ); } diff --git a/lib/settings/settings_about_tab.dart b/lib/settings/settings_about_tab.dart index 20211880..9beff8c2 100644 --- a/lib/settings/settings_about_tab.dart +++ b/lib/settings/settings_about_tab.dart @@ -4,7 +4,8 @@ import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/utils/options.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/cached_image.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/overlays/toast.dart'; class SettingsAboutTab extends StatelessWidget { @@ -14,7 +15,7 @@ class SettingsAboutTab extends StatelessWidget { @override Widget build(BuildContext context) { - final pageLayout = PageLayout.of(context); + final offsets = scaffoldOffsets(context); return Align( alignment: Alignment.center, @@ -25,8 +26,8 @@ class SettingsAboutTab extends StatelessWidget { padding: EdgeInsets.only( left: 10, right: 10, - top: pageLayout.topOffset + 10, - bottom: pageLayout.bottomOffset + 10, + top: offsets.top + 10, + bottom: offsets.bottom + 10, ), children: [ ClipRRect( @@ -43,7 +44,7 @@ class SettingsAboutTab extends StatelessWidget { child: Text( 'Otraku - v.$versionCode', textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline2, + style: Theme.of(context).textTheme.titleMedium, ), ), const Text( @@ -77,19 +78,28 @@ class SettingsAboutTab extends StatelessWidget { 'https://sites.google.com/view/otraku/privacy-policy', ), ), + ElevatedButton.icon( + icon: const Icon(Ionicons.log_out_outline), + label: const Text('Accounts'), + onPressed: Api.logOut, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), + ), ElevatedButton.icon( icon: const Icon(Ionicons.trash_bin_outline), - label: const Text('Reset Options'), - onPressed: Options.resetOptions, + label: const Text('Clear Image Cache'), + onPressed: clearImageCache, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), ), ElevatedButton.icon( - icon: const Icon(Ionicons.log_out_outline), - label: const Text('Accounts'), - onPressed: Api.logOut, + icon: const Icon(Ionicons.refresh_outline), + label: const Text('Reset Options'), + onPressed: Options.resetOptions, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, @@ -109,7 +119,7 @@ class SettingsAboutTab extends StatelessWidget { return Text( 'Performed a notification check around ${Convert.millisToStr((time / 1000).truncate())}.', textAlign: TextAlign.center, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ); } } diff --git a/lib/settings/settings_app_tab.dart b/lib/settings/settings_app_tab.dart index dbe9394c..7cd24fd8 100644 --- a/lib/settings/settings_app_tab.dart +++ b/lib/settings/settings_app_tab.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:otraku/discover/discover_models.dart'; +import 'package:otraku/filter/chip_selector.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/media/media_constants.dart'; import 'package:otraku/utils/convert.dart'; @@ -8,7 +9,7 @@ import 'package:otraku/home/home_view.dart'; import 'package:otraku/widgets/fields/checkbox_field.dart'; import 'package:otraku/widgets/fields/drop_down_field.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/segment_switcher.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/settings/theme_preview.dart'; @@ -25,12 +26,13 @@ class SettingsAppTab extends StatelessWidget { controller: scrollCtrl, slivers: [ SliverToBoxAdapter( - child: SizedBox(height: PageLayout.of(context).topOffset), + child: SizedBox(height: scaffoldOffsets(context).top), ), SliverPadding( padding: const EdgeInsets.only(left: 10, top: 10), sliver: SliverToBoxAdapter( - child: Text('Theme', style: Theme.of(context).textTheme.subtitle1), + child: + Text('Theme', style: Theme.of(context).textTheme.labelMedium), ), ), SliverPadding( @@ -115,6 +117,76 @@ class SettingsAppTab extends StatelessWidget { ]), ), ), + const SliverToBoxAdapter(child: SizedBox(height: 5)), + _SheetExpandButton( + title: 'Grid Views', + initialSheetHeight: 250, + sheetContentBuilder: (context, scrollCtrl) => ListView( + controller: scrollCtrl, + padding: const EdgeInsets.symmetric(vertical: 10), + children: [ + ChipSelector( + title: 'Discover View', + options: const ['Detailed List', 'Simple Grid'], + selected: Options().discoverItemView, + onChanged: (val) => Options().discoverItemView = val!, + mustHaveSelected: true, + ), + ChipSelector( + title: 'Collection View', + options: const ['Detailed List', 'Simple Grid'], + selected: Options().collectionItemView, + onChanged: (val) => Options().collectionItemView = val!, + mustHaveSelected: true, + ), + ChipSelector( + title: 'Collection Preview View', + options: const ['Detailed List', 'Simple Grid'], + selected: Options().collectionPreviewItemView, + onChanged: (val) => Options().collectionPreviewItemView = val!, + mustHaveSelected: true, + ), + ], + ), + ), + _SheetExpandButton( + title: 'Collection Previews', + initialSheetHeight: Consts.tapTargetSize * 3 + 150, + sheetContentBuilder: (context, scrollCtrl) => ListView( + controller: scrollCtrl, + padding: Consts.padding, + children: [ + CheckBoxField( + title: 'Anime Collection Preview', + initial: Options().animeCollectionPreview, + onChanged: (v) => Options().animeCollectionPreview = v, + ), + CheckBoxField( + title: 'Manga Collection Preview', + initial: Options().mangaCollectionPreview, + onChanged: (v) => Options().mangaCollectionPreview = v, + ), + const SizedBox(height: 5), + Text( + 'Collection previews only load your current and repeated ' + 'media, which results in faster loading times. Disabling ' + 'a preview means the whole collection will be loaded at once.', + style: Theme.of(context).textTheme.labelMedium, + ), + CheckBoxField( + title: 'Exclusive Airing Sort for Anime Preview', + initial: Options().airingSortForPreview, + onChanged: (v) => Options().airingSortForPreview = v, + ), + const SizedBox(height: 5), + Text( + 'Anime collection preview will sort anime by ' + 'airing time, instead of the default sort.', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 10), sliver: SliverGrid( @@ -140,66 +212,44 @@ class SettingsAppTab extends StatelessWidget { initial: Options().confirmExit, onChanged: (val) => Options().confirmExit = val, ), - CheckBoxField( - title: 'Compact Discover Grid', - initial: Options().compactDiscoverGrid, - onChanged: (val) => Options().compactDiscoverGrid = val, - ), ]), ), ), - SliverToBoxAdapter( - child: ListTile( - title: const Text('Collection Previews'), - trailing: const Icon(Icons.chevron_right_outlined), - textColor: Theme.of(context).colorScheme.onBackground, - iconColor: Theme.of(context).colorScheme.onBackground, - visualDensity: VisualDensity.compact, - contentPadding: const EdgeInsets.symmetric(horizontal: 10), - onTap: () => showSheet( - context, - OpaqueSheet( - initialHeight: Consts.tapTargetSize * 3 + 150, - builder: (context, scrollCtrl) => ListView( - controller: scrollCtrl, - padding: Consts.padding, - children: [ - CheckBoxField( - title: 'Anime Collection Preview', - initial: Options().animeCollectionPreview, - onChanged: (v) => Options().animeCollectionPreview = v, - ), - CheckBoxField( - title: 'Manga Collection Preview', - initial: Options().mangaCollectionPreview, - onChanged: (v) => Options().mangaCollectionPreview = v, - ), - const SizedBox(height: 5), - Text( - 'Collection previews only load your current and repeated ' - 'media, which results in faster loading times. Disabling ' - 'a preview means the whole collection will be loaded at once.', - style: Theme.of(context).textTheme.subtitle1, - ), - CheckBoxField( - title: 'Exclusive Airing Sort for Anime Preview', - initial: Options().airingSortForPreview, - onChanged: (v) => Options().airingSortForPreview = v, - ), - const SizedBox(height: 5), - Text( - 'Anime collection preview will sort anime by ' - 'airing time, instead of the default sort.', - style: Theme.of(context).textTheme.subtitle1, - ), - ], - ), - ), - ), - ), - ), const SliverFooter(), ], ); } } + +class _SheetExpandButton extends StatelessWidget { + const _SheetExpandButton({ + required this.title, + required this.initialSheetHeight, + required this.sheetContentBuilder, + }); + + final String title; + final double initialSheetHeight; + final Widget Function(BuildContext, ScrollController) sheetContentBuilder; + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: ListTile( + title: Text(title), + trailing: const Icon(Icons.chevron_right_outlined), + textColor: Theme.of(context).colorScheme.onBackground, + iconColor: Theme.of(context).colorScheme.onBackground, + contentPadding: const EdgeInsets.symmetric(horizontal: 10), + visualDensity: VisualDensity.compact, + onTap: () => showSheet( + context, + OpaqueSheet( + builder: sheetContentBuilder, + initialHeight: initialSheetHeight, + ), + ), + ), + ); + } +} diff --git a/lib/settings/settings_content_tab.dart b/lib/settings/settings_content_tab.dart index 3f60a0e1..3d966157 100644 --- a/lib/settings/settings_content_tab.dart +++ b/lib/settings/settings_content_tab.dart @@ -7,7 +7,7 @@ import 'package:otraku/widgets/fields/checkbox_field.dart'; import 'package:otraku/widgets/fields/drop_down_field.dart'; import 'package:otraku/widgets/grids/chip_grids.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; class SettingsContentTab extends StatelessWidget { @@ -40,7 +40,7 @@ class SettingsContentTab extends StatelessWidget { controller: scrollCtrl, slivers: [ SliverPadding( - padding: EdgeInsets.only(top: PageLayout.of(context).topOffset), + padding: EdgeInsets.only(top: scaffoldOffsets(context).top), sliver: SliverToBoxAdapter( child: CheckBoxField( title: 'Restrict Messages to Following', diff --git a/lib/settings/settings_notifications_tab.dart b/lib/settings/settings_notifications_tab.dart index 89180fac..332884de 100644 --- a/lib/settings/settings_notifications_tab.dart +++ b/lib/settings/settings_notifications_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:otraku/settings/settings_provider.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/widgets/fields/checkbox_field.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; class SettingsNotificationsTab extends StatelessWidget { @@ -24,7 +24,7 @@ class SettingsNotificationsTab extends StatelessWidget { controller: scrollCtrl, slivers: [ SliverToBoxAdapter( - child: SizedBox(height: PageLayout.of(context).topOffset), + child: SizedBox(height: scaffoldOffsets(context).top), ), SliverList( delegate: SliverChildBuilderDelegate( diff --git a/lib/settings/settings_view.dart b/lib/settings/settings_view.dart index b250a91f..443a1c16 100644 --- a/lib/settings/settings_view.dart +++ b/lib/settings/settings_view.dart @@ -11,8 +11,9 @@ import 'package:otraku/settings/settings_content_tab.dart'; import 'package:otraku/settings/settings_notifications_tab.dart'; import 'package:otraku/settings/settings_about_tab.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; class SettingsView extends ConsumerStatefulWidget { @@ -87,8 +88,7 @@ class _SettingsViewState extends ConsumerState { } return Future.value(true); }, - child: PageLayout( - topBar: TopBar(title: pageNames[_tabIndex]), + child: PageScaffold( bottomBar: BottomBarIconTabs( current: _tabIndex, onSame: (_) => _ctrl.scrollToTop(), @@ -100,10 +100,13 @@ class _SettingsViewState extends ConsumerState { 'About': Ionicons.information_outline, }, ), - child: DirectPageView( - current: _tabIndex, - onChanged: (i) => setState(() => _tabIndex = i), - children: tabs, + child: TabScaffold( + topBar: TopBar(title: pageNames[_tabIndex]), + child: DirectPageView( + current: _tabIndex, + onChanged: (i) => setState(() => _tabIndex = i), + children: tabs, + ), ), ), ); diff --git a/lib/settings/theme_preview.dart b/lib/settings/theme_preview.dart index eda1f65c..354d8f9c 100644 --- a/lib/settings/theme_preview.dart +++ b/lib/settings/theme_preview.dart @@ -66,8 +66,8 @@ class _ThemePreviewState extends State { class _ThemeCard extends StatelessWidget { const _ThemeCard({ required this.name, - required this.scheme, required this.active, + required this.scheme, required this.onTap, }); @@ -82,7 +82,7 @@ class _ThemeCard extends StatelessWidget { final borderColor = active ? scheme.primary : scheme.surfaceVariant; return GestureDetector( - onTap: () => onTap(), + onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: Column( diff --git a/lib/staff/staff_info_tab.dart b/lib/staff/staff_info_tab.dart index 4498d411..f6568a51 100644 --- a/lib/staff/staff_info_tab.dart +++ b/lib/staff/staff_info_tab.dart @@ -3,21 +3,23 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/staff/staff_models.dart'; import 'package:otraku/staff/staff_providers.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/toast.dart'; class StaffInfoTab extends StatelessWidget { - const StaffInfoTab(this.id, this.imageUrl, this.scrollCtrl); + const StaffInfoTab(this.id, this.imageUrl, this.scrollCtrl, this.topBar); final int id; final String? imageUrl; final ScrollController scrollCtrl; + final TopBar topBar; @override Widget build(BuildContext context) { @@ -34,6 +36,7 @@ class StaffInfoTab extends StatelessWidget { imageUrl: imageUrl, scrollCtrl: scrollCtrl, refreshControl: refreshControl, + topBar: topBar, loading: true, ), error: (_, __) => _TabContent( @@ -42,6 +45,7 @@ class StaffInfoTab extends StatelessWidget { imageUrl: imageUrl, scrollCtrl: scrollCtrl, refreshControl: refreshControl, + topBar: topBar, loading: false, ), data: (data) => _TabContent( @@ -50,6 +54,7 @@ class StaffInfoTab extends StatelessWidget { imageUrl: imageUrl, scrollCtrl: scrollCtrl, refreshControl: refreshControl, + topBar: topBar, loading: false, ), ); @@ -65,6 +70,7 @@ class _TabContent extends StatelessWidget { required this.imageUrl, required this.scrollCtrl, required this.refreshControl, + required this.topBar, required this.loading, }); @@ -73,6 +79,7 @@ class _TabContent extends StatelessWidget { final String? imageUrl; final ScrollController scrollCtrl; final Widget refreshControl; + final TopBar topBar; final bool loading; @override @@ -99,7 +106,7 @@ class _TabContent extends StatelessWidget { height: imageHeight, color: Theme.of(context).colorScheme.surfaceVariant, child: GestureDetector( - child: FadeImage(imageUrl), + child: CachedImage(imageUrl), onTap: () => showPopUp(context, ImageDialog(imageUrl)), ), ), @@ -116,7 +123,7 @@ class _TabContent extends StatelessWidget { onTap: () => Toast.copy(context, data!.name), child: Text( data!.name, - style: Theme.of(context).textTheme.headline1, + style: Theme.of(context).textTheme.titleLarge, ), ), if (data!.altNames.isNotEmpty) @@ -130,7 +137,8 @@ class _TabContent extends StatelessWidget { const space = SliverToBoxAdapter(child: SizedBox(height: 10)); - return PageLayout( + return TabScaffold( + topBar: topBar, floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [if (data != null) _FavoriteButton(data!)], @@ -248,7 +256,7 @@ class _InfoTile extends StatelessWidget { Text( title, maxLines: 1, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), Text(subtitle, maxLines: 1), ], diff --git a/lib/staff/staff_relations_tab.dart b/lib/staff/staff_relations_tab.dart index 3932e4c9..ca0ba932 100644 --- a/lib/staff/staff_relations_tab.dart +++ b/lib/staff/staff_relations_tab.dart @@ -8,20 +8,23 @@ import 'package:otraku/staff/staff_providers.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/widgets/grids/relation_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; class StaffCharactersTab extends StatelessWidget { - const StaffCharactersTab(this.id, this.scrollCtrl); + const StaffCharactersTab(this.id, this.scrollCtrl, this.topBar); final int id; final ScrollController scrollCtrl; + final TopBar topBar; @override Widget build(BuildContext context) { - return PageLayout( + return TabScaffold( + topBar: topBar, floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [_FilterButton(id, false)], @@ -92,14 +95,16 @@ class StaffCharactersTab extends StatelessWidget { } class StaffRolesTab extends StatelessWidget { - const StaffRolesTab(this.id, this.scrollCtrl); + const StaffRolesTab(this.id, this.scrollCtrl, this.topBar); final int id; final ScrollController scrollCtrl; + final TopBar topBar; @override Widget build(BuildContext context) { - return PageLayout( + return TabScaffold( + topBar: topBar, floatingBar: FloatingBar( scrollCtrl: scrollCtrl, children: [_FilterButton(id, true)], diff --git a/lib/staff/staff_view.dart b/lib/staff/staff_view.dart index 68db4ff4..de6340d0 100644 --- a/lib/staff/staff_view.dart +++ b/lib/staff/staff_view.dart @@ -6,8 +6,9 @@ import 'package:otraku/staff/staff_relations_tab.dart'; import 'package:otraku/staff/staff_providers.dart'; import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; class StaffView extends ConsumerStatefulWidget { @@ -54,9 +55,9 @@ class _StaffViewState extends ConsumerState { ref.watch(staffRelationProvider(widget.id).select((_) => null)); final name = ref.watch(staffProvider(widget.id)).valueOrNull?.name; + final topBar = TopBar(title: name); - return PageLayout( - topBar: TopBar(title: name), + return PageScaffold( bottomBar: BottomBarIconTabs( current: _tab, onChanged: (i) => setState(() => _tab = i), @@ -71,9 +72,9 @@ class _StaffViewState extends ConsumerState { current: _tab, onChanged: (i) => setState(() => _tab = i), children: [ - StaffInfoTab(widget.id, widget.imageUrl, _ctrl), - StaffCharactersTab(widget.id, _ctrl), - StaffRolesTab(widget.id, _ctrl), + StaffInfoTab(widget.id, widget.imageUrl, _ctrl, topBar), + StaffCharactersTab(widget.id, _ctrl, topBar), + StaffRolesTab(widget.id, _ctrl, topBar), ], ), ); diff --git a/lib/statistics/charts.dart b/lib/statistics/charts.dart index c8f57e29..e07ff14a 100644 --- a/lib/statistics/charts.dart +++ b/lib/statistics/charts.dart @@ -33,7 +33,7 @@ class BarChart extends StatelessWidget { children: [ Padding( padding: Consts.padding, - child: Text(title, style: Theme.of(context).textTheme.headline3), + child: Text(title, style: Theme.of(context).textTheme.titleSmall), ), if (action != null) action!, SizedBox( @@ -48,7 +48,7 @@ class BarChart extends StatelessWidget { children: [ Text( values[i].toString(), - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -67,7 +67,7 @@ class BarChart extends StatelessWidget { ), ), ), - Text(names[i], style: Theme.of(context).textTheme.subtitle1), + Text(names[i], style: Theme.of(context).textTheme.labelMedium), ], ), ), @@ -92,7 +92,7 @@ class PieChart extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: Theme.of(context).textTheme.headline3), + Text(title, style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 5), Container( height: 225, @@ -153,7 +153,7 @@ class PieChart extends StatelessWidget { const SizedBox(width: 5), Text( values[i].toString(), - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ], ), diff --git a/lib/statistics/statistics_view.dart b/lib/statistics/statistics_view.dart index f13a2e40..e682d3dc 100644 --- a/lib/statistics/statistics_view.dart +++ b/lib/statistics/statistics_view.dart @@ -8,8 +8,9 @@ import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/statistics/charts.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/layouts/segment_switcher.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -89,7 +90,7 @@ class _StatisticsViewState extends State { }, ); - return PageLayout( + return PageScaffold( bottomBar: BottomBarIconTabs( current: _onAnime ? 0 : 1, onChanged: (page) => @@ -100,10 +101,12 @@ class _StatisticsViewState extends State { 'Manga': Ionicons.bookmark_outline, }, ), - topBar: TopBar( - title: _onAnime ? 'Anime Statistics' : 'Manga Statistics', + child: TabScaffold( + topBar: TopBar( + title: _onAnime ? 'Anime Statistics' : 'Manga Statistics', + ), + child: content, ), - child: content, ); } } @@ -129,13 +132,13 @@ class _StatisticsView extends StatelessWidget { @override Widget build(BuildContext context) { - final pageLayout = PageLayout.of(context); + final offsets = scaffoldOffsets(context); return ListView( controller: scrollCtrl, padding: EdgeInsets.only( - top: pageLayout.topOffset + 10, - bottom: pageLayout.bottomOffset, + top: offsets.top + 10, + bottom: offsets.bottom, ), children: [ _Details(statistics, ofAnime), @@ -235,7 +238,8 @@ class _Details extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(titles[i], style: Theme.of(context).textTheme.subtitle1), + Text(titles[i], + style: Theme.of(context).textTheme.labelMedium), Text(subtitles[i].toString()), ], ), diff --git a/lib/studio/studio_grid.dart b/lib/studio/studio_grid.dart index e38b3b55..bb255347 100644 --- a/lib/studio/studio_grid.dart +++ b/lib/studio/studio_grid.dart @@ -11,27 +11,24 @@ class StudioGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 230, - height: 50, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (_, i) => LinkTile( - id: items[i].id, - info: items[i].name, - discoverType: DiscoverType.studio, - child: Hero( - tag: items[i].id, - child: Text( - items[i].name, - maxLines: 2, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.headline1, - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 230, + height: 50, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (_, i) => LinkTile( + id: items[i].id, + info: items[i].name, + discoverType: DiscoverType.studio, + child: Hero( + tag: items[i].id, + child: Text( + items[i].name, + maxLines: 2, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.titleLarge, ), ), ), diff --git a/lib/studio/studio_providers.dart b/lib/studio/studio_providers.dart index d6a82300..bf59a399 100644 --- a/lib/studio/studio_providers.dart +++ b/lib/studio/studio_providers.dart @@ -88,9 +88,10 @@ class StudioNotifier extends StateNotifier> { for (final m in data['nodes']) { var category = m[key]?['year']?.toString(); - category ??= 'Unfinished'; + category ??= + m['status'] == 'CANCELLED' ? 'Cancelled' : 'To Be Announced'; - if (s.categories.isEmpty || !s.categories.containsKey(category)) { + if (!s.categories.containsKey(category)) { s.categories[category] = index; } diff --git a/lib/studio/studio_view.dart b/lib/studio/studio_view.dart index 8b635c34..71a72b1b 100644 --- a/lib/studio/studio_view.dart +++ b/lib/studio/studio_view.dart @@ -12,7 +12,8 @@ import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/widgets/grids/tile_item_grid.dart'; import 'package:otraku/widgets/layouts/constrained_view.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; @@ -49,123 +50,131 @@ class _StudioViewState extends ConsumerState { studioProvider(widget.id).select((s) => s.valueOrNull?.studio), ); - return PageLayout( - topBar: const TopBar(), - floatingBar: FloatingBar( - scrollCtrl: _ctrl, - children: [ - if (studio != null) ...[ - _FavoriteButton(studio), - _FilterButton(widget.id), + return PageScaffold( + child: TabScaffold( + topBar: const TopBar(), + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: [ + if (studio != null) ...[ + _FavoriteButton(studio), + _FilterButton(widget.id), + ], ], - ], - ), - child: ConstrainedView( - child: Consumer( - builder: (context, ref, _) { - ref.listen( - studioProvider(widget.id), - (_, s) { - if (s.hasError) { - showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load studio', - content: s.error.toString(), - ), - ); - } - }, - ); - - final name = studio?.name ?? widget.name; - final titleWidget = name != null - ? SliverToBoxAdapter( - child: GestureDetector( - onTap: () => Toast.copy(context, name), - child: Hero( - tag: widget.id, - child: Text( - name, - style: Theme.of(context).textTheme.headline1, - ), + ), + child: ConstrainedView( + child: Consumer( + builder: (context, ref, _) { + ref.listen( + studioProvider(widget.id), + (_, s) { + if (s.hasError) { + showPopUp( + context, + ConfirmationDialog( + title: 'Failed to load studio', + content: s.error.toString(), ), - ), - ) - : null; + ); + } + }, + ); - return ref.watch(studioProvider(widget.id)).unwrapPrevious().when( - loading: () => CustomScrollView( - physics: Consts.physics, - slivers: [ - refreshControl, - if (titleWidget != null) titleWidget, - const SliverFillRemaining( - child: Center(child: Loader()), - ), - ], - ), - error: (_, __) => CustomScrollView( - physics: Consts.physics, - slivers: [ - refreshControl, - if (titleWidget != null) titleWidget, - const SliverFillRemaining( - child: Center(child: Text('Failed to load studio')), - ), - ], - ), - data: (data) { - final items = [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 10, bottom: 20), + final name = studio?.name ?? widget.name; + final titleWidget = name != null + ? SliverToBoxAdapter( + child: GestureDetector( + onTap: () => Toast.copy(context, name), + child: Hero( + tag: widget.id, child: Text( - '${data.studio.favorites.toString()} favourites', - style: Theme.of(context).textTheme.subtitle1, + name, + style: Theme.of(context).textTheme.titleLarge, ), ), - ) - ]; - final sort = - ref.watch(studioFilterProvider(widget.id)).sort; + ), + ) + : null; - if (sort == MediaSort.START_DATE || - sort == MediaSort.START_DATE_DESC) { - for (int i = 0; i < data.categories.length; i++) { - items.add(SliverToBoxAdapter( - child: Text( - data.categories.keys.elementAt(i), - style: Theme.of(context).textTheme.headline2, + return ref.watch(studioProvider(widget.id)).unwrapPrevious().when( + loading: () => CustomScrollView( + physics: Consts.physics, + slivers: [ + refreshControl, + if (titleWidget != null) titleWidget, + const SliverFillRemaining( + child: Center(child: Loader()), + ), + ], + ), + error: (_, __) => CustomScrollView( + physics: Consts.physics, + slivers: [ + refreshControl, + if (titleWidget != null) titleWidget, + const SliverFillRemaining( + child: Center(child: Text('Failed to load studio')), + ), + ], + ), + data: (data) { + final items = [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 20), + child: Text( + '${data.studio.favorites.toString()} favourites', + style: Theme.of(context).textTheme.labelMedium, + ), ), - )); + ) + ]; + final sort = + ref.watch(studioFilterProvider(widget.id)).sort; - final beg = data.categories.values.elementAt(i); - final end = i < data.categories.length - 1 - ? data.categories.values.elementAt(i + 1) - : data.media.items.length; + if (sort == MediaSort.START_DATE || + sort == MediaSort.START_DATE_DESC) { + for (int i = 0; i < data.categories.length; i++) { + items.add(SliverToBoxAdapter( + child: Text( + data.categories.keys.elementAt(i), + style: Theme.of(context).textTheme.titleMedium, + ), + )); - items.add( - TileItemGrid(data.media.items.sublist(beg, end)), - ); + final beg = data.categories.values.elementAt(i); + final end = i < data.categories.length - 1 + ? data.categories.values.elementAt(i + 1) + : data.media.items.length; + + items.add(const SliverToBoxAdapter( + child: SizedBox(height: 10), + )); + items.add( + TileItemGrid(data.media.items.sublist(beg, end)), + ); + items.add(const SliverToBoxAdapter( + child: SizedBox(height: 10), + )); + } + } else { + items.add(TileItemGrid(data.media.items)); } - } else { - items.add(TileItemGrid(data.media.items)); - } - return CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl, - titleWidget!, - ...items, - SliverFooter(loading: data.media.hasNext), - ], - ); - }, - ); - }, + return CustomScrollView( + physics: Consts.physics, + controller: _ctrl, + slivers: [ + refreshControl, + titleWidget!, + ...items, + SliverFooter(loading: data.media.hasNext), + ], + ); + }, + ); + }, + ), ), ), ); diff --git a/lib/user/friends_view.dart b/lib/user/friends_view.dart index dcdf181d..46f036fd 100644 --- a/lib/user/friends_view.dart +++ b/lib/user/friends_view.dart @@ -6,8 +6,9 @@ import 'package:otraku/user/friends_provider.dart'; import 'package:otraku/user/user_grid.dart'; import 'package:otraku/utils/pagination_controller.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -43,20 +44,7 @@ class _FriendsViewState extends ConsumerState { onRefresh: () => ref.invalidate(friendsProvider(widget.id)), ); - return PageLayout( - topBar: TopBar( - title: _onFollowing ? 'Following' : 'Followers', - items: [ - if (count > 0) - Padding( - padding: const EdgeInsets.only(right: 10), - child: Text( - count.toString(), - style: Theme.of(context).textTheme.headline3, - ), - ), - ], - ), + return PageScaffold( bottomBar: BottomBarIconTabs( current: _onFollowing ? 0 : 1, onChanged: (page) { @@ -68,25 +56,40 @@ class _FriendsViewState extends ConsumerState { 'Followers': Ionicons.person_circle, }, ), - child: DirectPageView( - current: _onFollowing ? 0 : 1, - onChanged: (page) { - setState(() => _onFollowing = page == 0 ? true : false); - }, - children: [ - _FriendTab( - id: widget.id, - onFollowing: true, - refreshControl: refreshControl, - paginationController: _ctrl, - ), - _FriendTab( - id: widget.id, - onFollowing: false, - refreshControl: refreshControl, - paginationController: _ctrl, - ), - ], + child: TabScaffold( + topBar: TopBar( + title: _onFollowing ? 'Following' : 'Followers', + trailing: [ + if (count > 0) + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + count.toString(), + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ], + ), + child: DirectPageView( + current: _onFollowing ? 0 : 1, + onChanged: (page) { + setState(() => _onFollowing = page == 0 ? true : false); + }, + children: [ + _FriendTab( + id: widget.id, + onFollowing: true, + refreshControl: refreshControl, + paginationController: _ctrl, + ), + _FriendTab( + id: widget.id, + onFollowing: false, + refreshControl: refreshControl, + paginationController: _ctrl, + ), + ], + ), ), ); } diff --git a/lib/user/user_grid.dart b/lib/user/user_grid.dart index 6a5f5f21..86aa28ed 100644 --- a/lib/user/user_grid.dart +++ b/lib/user/user_grid.dart @@ -3,7 +3,7 @@ import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; class UserGrid extends StatelessWidget { @@ -13,17 +13,14 @@ class UserGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndExtraHeight( - minWidth: 100, - extraHeight: 40, - ), - delegate: SliverChildBuilderDelegate( - (_, i) => _Tile(items[i]), - childCount: items.length, - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndExtraHeight( + minWidth: 100, + extraHeight: 40, + ), + delegate: SliverChildBuilderDelegate( + (_, i) => _Tile(items[i]), + childCount: items.length, ), ); } @@ -47,7 +44,7 @@ class _Tile extends StatelessWidget { tag: item.id, child: ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage(item.imageUrl, fit: BoxFit.contain), + child: CachedImage(item.imageUrl, fit: BoxFit.contain), ), ), ), @@ -58,7 +55,7 @@ class _Tile extends StatelessWidget { item.name, maxLines: 2, overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, ), ), ], diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index 8632c920..d62a529b 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/user/user_providers.dart'; import 'package:otraku/utils/route_arg.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; -import 'package:otraku/widgets/custom_sliver_header.dart'; +import 'package:otraku/widgets/cached_image.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; import 'package:otraku/widgets/overlays/sheets.dart'; import 'package:otraku/widgets/overlays/toast.dart'; @@ -14,14 +15,14 @@ import 'package:otraku/widgets/text_rail.dart'; class UserHeader extends StatelessWidget { const UserHeader({ required this.id, - required this.user, required this.isMe, + required this.user, required this.imageUrl, }); final int id; - final User? user; final bool isMe; + final User? user; final String? imageUrl; @override @@ -32,76 +33,293 @@ class UserHeader extends StatelessWidget { if (user!.donatorTier > 0) textRailItems[user!.donatorBadge] = true; } - return CustomSliverHeader( - title: user?.name, - image: user?.imageUrl ?? imageUrl, - banner: user?.bannerUrl, - squareImage: true, - implyLeading: !isMe, - heroId: id, - actions: [ - if (!isMe && user != null) _FollowButton(user!), - if (user?.siteUrl != null) - TopBarShadowIcon( - tooltip: 'More', - icon: Ionicons.ellipsis_horizontal, - onTap: () => showSheet( - context, - FixedGradientDragSheet.link(context, user!.siteUrl!), - ), - ), - if (isMe) - TopBarShadowIcon( - tooltip: 'Settings', - icon: Ionicons.cog_outline, - onTap: () => Navigator.pushNamed(context, RouteArg.settings), + return SliverPersistentHeader( + pinned: true, + delegate: _Delegate( + id: id, + isMe: isMe, + user: user, + imageUrl: imageUrl, + textRailItems: textRailItems, + imageWidth: MediaQuery.of(context).size.width < 430.0 + ? MediaQuery.of(context).size.width * 0.30 + : 100.0, + ), + ); + } +} + +class _Delegate implements SliverPersistentHeaderDelegate { + _Delegate({ + required this.id, + required this.isMe, + required this.user, + required this.imageUrl, + required this.imageWidth, + required this.textRailItems, + }); + + final int id; + final bool isMe; + final User? user; + final String? imageUrl; + final double imageWidth; + final Map textRailItems; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + final sidePadding = + MediaQuery.of(context).size.width > Consts.layoutBig + 20 + ? (MediaQuery.of(context).size.width - Consts.layoutBig) / 2 + : 10.0; + + final height = maxExtent; + final extent = maxExtent - shrinkOffset; + final opacity = shrinkOffset < (_bannerHeight - minExtent) + ? shrinkOffset / (_bannerHeight - minExtent) + : 1.0; + + final image = user?.imageUrl ?? imageUrl; + final theme = Theme.of(context); + + return DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 5, + color: theme.colorScheme.background, ), - ], - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: user != null - ? [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => Toast.copy(context, user!.name), - child: Text( - user!.name, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.headline1!.copyWith( - shadows: [ - Shadow( - color: Theme.of(context).colorScheme.background, - blurRadius: 10, - ), + ], + ), + child: FlexibleSpaceBar.createSettings( + minExtent: minExtent, + maxExtent: maxExtent, + currentExtent: extent > minExtent ? extent : minExtent, + child: Stack( + fit: StackFit.expand, + children: [ + FlexibleSpaceBar( + collapseMode: CollapseMode.pin, + stretchModes: const [StretchMode.zoomBackground], + background: Column( + children: [ + Expanded( + child: user?.bannerUrl != null + ? GestureDetector( + child: CachedImage(user!.bannerUrl!), + onTap: () => showPopUp( + context, + ImageDialog(user!.bannerUrl!), + ), + ) + : const SizedBox(), + ), + SizedBox(height: height - _bannerHeight), + ], + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: height - _bannerHeight, + alignment: Alignment.topCenter, + color: theme.colorScheme.background, + child: Container( + height: 0, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: 15, + spreadRadius: 25, + color: theme.colorScheme.background, + ), + ], + ), + ), + ), + ), + Positioned( + bottom: 0, + left: sidePadding, + right: sidePadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Hero( + tag: id, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: SizedBox( + height: imageWidth, + width: imageWidth, + child: image != null + ? GestureDetector( + onTap: () => showPopUp( + context, + ImageDialog(image), + ), + child: CachedImage(image, fit: BoxFit.contain), + ) + : null, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (user != null) + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => Toast.copy(context, user!.name), + child: Text( + user!.name, + overflow: TextOverflow.fade, + style: theme.textTheme.titleLarge!.copyWith( + shadows: [ + Shadow( + color: theme.colorScheme.background, + blurRadius: 10, + ), + ], + ), + ), + ), + if (textRailItems.isNotEmpty) + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (user?.modRoles.isNotEmpty ?? false) { + showPopUp( + context, + TextDialog( + title: 'Roles', + text: user!.modRoles.join(', '), + ), + ); + } + }, + child: TextRail( + textRailItems, + style: theme.textTheme.labelMedium, + ), + ), ], ), ), + ], + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: Opacity( + opacity: opacity, + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.background, + boxShadow: [ + BoxShadow( + blurRadius: 10, + spreadRadius: 10, + color: theme.colorScheme.background, + ), + ], + ), ), - if (textRailItems.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (user!.modRoles.isNotEmpty) { - showPopUp( - context, - TextDialog( - title: 'Roles', - text: user!.modRoles.join(', '), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: Row( + children: [ + isMe + ? const SizedBox(width: 10) + : TopBarShadowIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: Navigator.of(context).pop, + ), + Expanded( + child: user?.name == null + ? const SizedBox() + : Opacity( + opacity: opacity, + child: Text( + user!.name, + style: theme.textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), ), - ); - } - }, - child: TextRail( - textRailItems, - style: Theme.of(context).textTheme.subtitle1, - ), ), - ] - : [], + if (!isMe && user != null) _FollowButton(user!), + if (user?.siteUrl != null) + TopBarShadowIcon( + tooltip: 'More', + icon: Ionicons.ellipsis_horizontal, + onTap: () => showSheet( + context, + FixedGradientDragSheet.link(context, user!.siteUrl!), + ), + ), + if (isMe) + TopBarShadowIcon( + tooltip: 'Settings', + icon: Ionicons.cog_outline, + onTap: () => Navigator.pushNamed( + context, + RouteArg.settings, + ), + ), + ], + ), + ), + ], + ), ), ); } + + static const _bannerHeight = 200.0; + + @override + double get maxExtent => _bannerHeight + imageWidth / 2; + + @override + double get minExtent => Consts.tapTargetSize; + + @override + OverScrollHeaderStretchConfiguration? get stretchConfiguration => + OverScrollHeaderStretchConfiguration(stretchTriggerOffset: 100); + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => + true; + + @override + PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => + null; + + @override + FloatingHeaderSnapConfiguration? get snapConfiguration => null; + + @override + TickerProvider? get vsync => null; } class _FollowButton extends StatefulWidget { diff --git a/lib/user/user_view.dart b/lib/user/user_view.dart index c3deda0b..fbd79d3c 100644 --- a/lib/user/user_view.dart +++ b/lib/user/user_view.dart @@ -11,7 +11,7 @@ import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/home/home_view.dart'; import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -23,7 +23,7 @@ class UserView extends StatelessWidget { @override Widget build(BuildContext context) => - PageLayout(child: UserSubView(id, avatarUrl)); + PageScaffold(child: UserSubView(id, avatarUrl)); } class UserSubView extends StatelessWidget { @@ -45,165 +45,169 @@ class UserSubView extends StatelessWidget { top: 10, ); - return Consumer( - builder: (context, ref, _) { - ref.listen>( - userProvider(id), - (_, s) => s.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load user', - content: error.toString(), + return TabScaffold( + child: Consumer( + builder: (context, ref, _) { + ref.listen>( + userProvider(id), + (_, s) => s.whenOrNull( + error: (error, _) => showPopUp( + context, + ConfirmationDialog( + title: 'Failed to load user', + content: error.toString(), + ), ), ), - ), - ); - - final items = []; - ref.watch(userProvider(id)).when( - error: (_, __) { - items.add(UserHeader( - id: id, - user: null, - isMe: id == Options().id, - imageUrl: avatarUrl, - )); - items.add( - const SliverFillRemaining( - child: Center(child: Text('Failed to load user')), - ), - ); - }, - loading: () { - items.add(UserHeader( - id: id, - user: null, - isMe: id == Options().id, - imageUrl: avatarUrl, - )); - items.add( - const SliverFillRemaining(child: Center(child: Loader())), - ); - }, - data: (data) { - items.add(UserHeader( - id: id, - user: data, - isMe: id == Options().id, - imageUrl: avatarUrl, - )); + ); - items.add(SliverPadding( - padding: padding, - sliver: SliverGrid( - gridDelegate: - const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 160, - height: 40, + final items = []; + ref.watch(userProvider(id)).when( + error: (_, __) { + items.add(UserHeader( + id: id, + user: null, + isMe: id == Options().id, + imageUrl: avatarUrl, + )); + items.add( + const SliverFillRemaining( + child: Center(child: Text('Failed to load user')), ), - delegate: SliverChildListDelegate.fixed( - [ - _Button( - Ionicons.film, - 'Anime', - () => id == Options().id - ? ref.read(homeProvider).homeTab = HomeView.ANIME_LIST - : Navigator.pushNamed( - context, - RouteArg.collection, - arguments: RouteArg(id: id, variant: true), - ), - ), - _Button( - Ionicons.bookmark, - 'Manga', - () => id == Options().id - ? ref.read(homeProvider).homeTab = HomeView.MANGA_LIST - : Navigator.pushNamed( - context, - RouteArg.collection, - arguments: RouteArg(id: id, variant: false), - ), - ), - _Button( - Ionicons.people_circle, - 'Following', - () => Navigator.pushNamed( - context, - RouteArg.friends, - arguments: RouteArg(id: id, variant: true), + ); + }, + loading: () { + items.add(UserHeader( + id: id, + user: null, + isMe: id == Options().id, + imageUrl: avatarUrl, + )); + items.add( + const SliverFillRemaining(child: Center(child: Loader())), + ); + }, + data: (data) { + items.add(UserHeader( + id: id, + user: data, + isMe: id == Options().id, + imageUrl: avatarUrl, + )); + + items.add(SliverPadding( + padding: padding, + sliver: SliverGrid( + gridDelegate: + const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 160, + height: 40, + ), + delegate: SliverChildListDelegate.fixed( + [ + _Button( + Ionicons.film, + 'Anime', + () => id == Options().id + ? ref.read(homeProvider).homeTab = + HomeView.ANIME_LIST + : Navigator.pushNamed( + context, + RouteArg.collection, + arguments: RouteArg(id: id, variant: true), + ), ), - ), - _Button( - Ionicons.person_circle, - 'Followers', - () => Navigator.pushNamed( - context, - RouteArg.friends, - arguments: RouteArg(id: id, variant: false), + _Button( + Ionicons.bookmark, + 'Manga', + () => id == Options().id + ? ref.read(homeProvider).homeTab = + HomeView.MANGA_LIST + : Navigator.pushNamed( + context, + RouteArg.collection, + arguments: RouteArg(id: id, variant: false), + ), ), - ), - _Button( - Ionicons.chatbox, - 'Activities', - () => Navigator.pushNamed( - context, - RouteArg.activities, - arguments: RouteArg(id: id), + _Button( + Ionicons.people_circle, + 'Following', + () => Navigator.pushNamed( + context, + RouteArg.friends, + arguments: RouteArg(id: id, variant: true), + ), ), - ), - _Button( - Icons.favorite, - 'Favourites', - () => Navigator.pushNamed( - context, - RouteArg.favourites, - arguments: RouteArg(id: id), + _Button( + Ionicons.person_circle, + 'Followers', + () => Navigator.pushNamed( + context, + RouteArg.friends, + arguments: RouteArg(id: id, variant: false), + ), ), - ), - _Button( - Ionicons.stats_chart, - 'Statistics', - () => Navigator.pushNamed( - context, - RouteArg.statistics, - arguments: RouteArg(id: id), + _Button( + Ionicons.chatbox, + 'Activities', + () => Navigator.pushNamed( + context, + RouteArg.activities, + arguments: RouteArg(id: id), + ), ), - ), - _Button( - Icons.rate_review, - 'Reviews', - () => Navigator.pushNamed( - context, - RouteArg.reviews, - arguments: RouteArg(id: id), + _Button( + Icons.favorite, + 'Favourites', + () => Navigator.pushNamed( + context, + RouteArg.favourites, + arguments: RouteArg(id: id), + ), ), - ), - ], - ), - ), - )); - - if (data.description.isNotEmpty) { - items.add(SliverToBoxAdapter( - child: Card( - margin: padding, - child: Padding( - padding: Consts.padding, - child: HtmlContent(data.description), + _Button( + Ionicons.stats_chart, + 'Statistics', + () => Navigator.pushNamed( + context, + RouteArg.statistics, + arguments: RouteArg(id: id), + ), + ), + _Button( + Icons.rate_review, + 'Reviews', + () => Navigator.pushNamed( + context, + RouteArg.reviews, + arguments: RouteArg(id: id), + ), + ), + ], ), ), )); - } - }, - ); - items.add(const SliverFooter()); - return SafeArea( - child: CustomScrollView(controller: scrollCtrl, slivers: items), - ); - }, + if (data.description.isNotEmpty) { + items.add(SliverToBoxAdapter( + child: Card( + margin: padding, + child: Padding( + padding: Consts.padding, + child: HtmlContent(data.description), + ), + ), + )); + } + }, + ); + items.add(const SliverFooter()); + + return SafeArea( + child: CustomScrollView(controller: scrollCtrl, slivers: items), + ); + }, + ), ); } } @@ -227,7 +231,7 @@ class _Button extends StatelessWidget { Expanded(child: Icon(icon)), Expanded( flex: 2, - child: Text(title, style: Theme.of(context).textTheme.headline2), + child: Text(title, style: Theme.of(context).textTheme.titleMedium), ), ], ), diff --git a/lib/utils/api.dart b/lib/utils/api.dart index 2d3dec1c..e425d6ab 100644 --- a/lib/utils/api.dart +++ b/lib/utils/api.dart @@ -108,7 +108,7 @@ abstract class Api { 'Content-type': 'application/json', 'Authorization': 'Bearer $_accessToken', }, - ).timeout(const Duration(seconds: 10)); + ).timeout(const Duration(seconds: 20)); final Map body = json.decode(response.body); diff --git a/lib/utils/graphql.dart b/lib/utils/graphql.dart index bc187e52..0ad71a4c 100644 --- a/lib/utils/graphql.dart +++ b/lib/utils/graphql.dart @@ -485,10 +485,13 @@ abstract class GqlQuery { '${_GqlFragment.textActivity}${_GqlFragment.listActivity}${_GqlFragment.messageActivity}'; static const activities = r''' - query Activities($userId: Int, $page: Int = 1, $isFollowing: Boolean, $hasRepliesOrTypeText: Boolean, $typeIn: [ActivityType]) { + query Activities($userId: Int, $userIdNot: Int, $page: Int = 1, $isFollowing: Boolean, + $hasRepliesOrText: Boolean, $typeIn: [ActivityType], $createdBefore: Int) { Page(page: $page) { pageInfo {hasNextPage} - activities(userId: $userId, isFollowing: $isFollowing, hasRepliesOrTypeText: $hasRepliesOrTypeText, type_in: $typeIn, sort: [PINNED, ID_DESC]) { + activities(userId: $userId, userId_not: $userIdNot, isFollowing: $isFollowing, + hasRepliesOrTypeText: $hasRepliesOrText, type_in: $typeIn, sort: [PINNED, ID_DESC], + createdAt_lesser: $createdBefore) { ... on TextActivity {...textActivity} ... on ListActivity {...listActivity} ... on MessageActivity {...messageActivity} @@ -502,7 +505,7 @@ abstract class GqlQuery { query Settings($withData: Boolean = true) { Viewer { unreadNotificationCount - @include(if: $withData) ...userSettings + ...userSettings @include(if: $withData) } } ''' @@ -806,6 +809,7 @@ abstract class _GqlFragment { status episodes chapters + averageScore genres tags {id} nextAiringEpisode {episode airingAt} diff --git a/lib/utils/options.dart b/lib/utils/options.dart index e33f5a12..c049d77a 100644 --- a/lib/utils/options.dart +++ b/lib/utils/options.dart @@ -7,7 +7,7 @@ import 'package:otraku/utils/theming.dart'; import 'package:path_provider/path_provider.dart'; /// Current app version. -const versionCode = '1.2.2+1'; +const versionCode = '1.2.3'; /// General options keys. enum _OptionKey { @@ -26,8 +26,10 @@ enum _OptionKey { confirmExit, leftHanded, analogueClock, - compactDiscoverGrid, feedOnFollowing, + discoverItemView, + collectionItemView, + collectionPreviewItemView, feedActivityFilters, lastNotificationId, lastVersionCode, @@ -75,7 +77,9 @@ class Options extends ChangeNotifier { this._leftHanded, this._analogueClock, this._feedOnFollowing, - this._compactDiscoverGrid, + this._discoverItemView, + this._collectionItemView, + this._collectionPreviewItemView, this._feedActivityFilters, this._lastNotificationId, this._lastVersionCode, @@ -117,6 +121,24 @@ class Options extends ChangeNotifier { imageQualityIndex = 1; } + int discoverItemView = + _optionBox.get(_OptionKey.discoverItemView.name) ?? 0; + if (discoverItemView < 0 || discoverItemView > 1) { + discoverItemView = 0; + } + + int collectionItemView = + _optionBox.get(_OptionKey.collectionItemView.name) ?? 0; + if (collectionItemView < 0 || collectionItemView > 1) { + collectionItemView = 0; + } + + int collectionPreviewItemView = + _optionBox.get(_OptionKey.collectionPreviewItemView.name) ?? 0; + if (collectionPreviewItemView < 0 || collectionPreviewItemView > 1) { + collectionPreviewItemView = 0; + } + return Options._( ThemeMode.values[themeMode], _optionBox.get(_OptionKey.themeIndex.name), @@ -134,7 +156,9 @@ class Options extends ChangeNotifier { _optionBox.get(_OptionKey.leftHanded.name) ?? false, _optionBox.get(_OptionKey.analogueClock.name) ?? false, _optionBox.get(_OptionKey.feedOnFollowing.name) ?? false, - _optionBox.get(_OptionKey.compactDiscoverGrid.name) ?? false, + discoverItemView, + collectionItemView, + collectionPreviewItemView, _optionBox.get(_OptionKey.feedActivityFilters.name) ?? [0, 1, 2], _optionBox.get(_OptionKey.lastNotificationId.name) ?? -1, _optionBox.get(_OptionKey.lastVersionCode.name) ?? '', @@ -202,8 +226,10 @@ class Options extends ChangeNotifier { bool _confirmExit; bool _leftHanded; bool _analogueClock; - bool _compactDiscoverGrid; bool _feedOnFollowing; + int _discoverItemView; + int _collectionItemView; + int _collectionPreviewItemView; List _feedActivityFilters; int _lastNotificationId; String _lastVersionCode; @@ -226,8 +252,10 @@ class Options extends ChangeNotifier { bool get confirmExit => _confirmExit; bool get leftHanded => _leftHanded; bool get analogueClock => _analogueClock; - bool get compactDiscoverGrid => _compactDiscoverGrid; bool get feedOnFollowing => _feedOnFollowing; + int get discoverItemView => _discoverItemView; + int get collectionItemView => _collectionItemView; + int get collectionPreviewItemView => _collectionPreviewItemView; List get feedActivityFilters => _feedActivityFilters; int get lastNotificationId => _lastNotificationId; String get lastVersionCode => _lastVersionCode; @@ -364,16 +392,26 @@ class Options extends ChangeNotifier { _optionBox.put(_OptionKey.analogueClock.name, v); } - set compactDiscoverGrid(bool v) { - _compactDiscoverGrid = v; - _optionBox.put(_OptionKey.compactDiscoverGrid.name, v); - } - set feedOnFollowing(bool v) { _feedOnFollowing = v; _optionBox.put(_OptionKey.feedOnFollowing.name, v); } + set discoverItemView(int v) { + _discoverItemView = v; + _optionBox.put(_OptionKey.discoverItemView.name, v); + } + + set collectionItemView(int v) { + _collectionItemView = v; + _optionBox.put(_OptionKey.collectionItemView.name, v); + } + + set collectionPreviewItemView(int v) { + _collectionPreviewItemView = v; + _optionBox.put(_OptionKey.collectionPreviewItemView.name, v); + } + set feedActivityFilters(List v) { _feedActivityFilters = v; _optionBox.put(_OptionKey.feedActivityFilters.name, v); diff --git a/lib/utils/route_arg.dart b/lib/utils/route_arg.dart index ae4325bc..b37e8e3a 100644 --- a/lib/utils/route_arg.dart +++ b/lib/utils/route_arg.dart @@ -129,7 +129,7 @@ class RouteArg { static final _unknown = MaterialPageRoute( builder: (context) => Scaffold( body: Center( - child: Text('404', style: Theme.of(context).textTheme.headline1), + child: Text('404', style: Theme.of(context).textTheme.titleLarge), ), ), ); diff --git a/lib/utils/theming.dart b/lib/utils/theming.dart index 53335b3b..7fd77cbd 100644 --- a/lib/utils/theming.dart +++ b/lib/utils/theming.dart @@ -27,8 +27,6 @@ ThemeData themeDataFrom(ColorScheme scheme) => ThemeData( scaffoldBackgroundColor: scheme.background, disabledColor: scheme.surface, unselectedWidgetColor: scheme.surface, - toggleableActiveColor: scheme.primary, - bottomAppBarColor: scheme.background.withAlpha(190), splashColor: scheme.onBackground.withAlpha(20), highlightColor: Colors.transparent, pageTransitionsTheme: const PageTransitionsTheme( @@ -42,6 +40,9 @@ ThemeData themeDataFrom(ColorScheme scheme) => ThemeData( color: scheme.onSurfaceVariant, size: Consts.iconBig, ), + bottomAppBarTheme: BottomAppBarTheme( + color: scheme.background.withAlpha(190), + ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: scheme.primary, @@ -49,53 +50,49 @@ ThemeData themeDataFrom(ColorScheme scheme) => ThemeData( textStyle: const TextStyle(fontWeight: FontWeight.w500), ), ), + chipTheme: ChipThemeData( + labelStyle: TextStyle( + color: scheme.onSecondaryContainer, + fontWeight: FontWeight.normal, + ), + ), typography: Typography.material2014(), textTheme: TextTheme( - headline1: TextStyle( + titleLarge: TextStyle( fontSize: Consts.fontBig, color: scheme.onBackground, fontWeight: FontWeight.w500, ), - headline2: TextStyle( + titleMedium: TextStyle( fontSize: Consts.fontMedium, color: scheme.onBackground, fontWeight: FontWeight.w500, ), - headline3: TextStyle( + titleSmall: TextStyle( fontSize: Consts.fontMedium, color: scheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), - headline4: TextStyle( - fontSize: Consts.fontMedium, - color: scheme.onSurfaceVariant, - fontWeight: FontWeight.normal, - ), - bodyText1: TextStyle( + bodyLarge: TextStyle( fontSize: Consts.fontMedium, color: scheme.primary, fontWeight: FontWeight.normal, ), - bodyText2: TextStyle( + bodyMedium: TextStyle( fontSize: Consts.fontMedium, color: scheme.onBackground, fontWeight: FontWeight.normal, ), - subtitle1: TextStyle( + labelMedium: TextStyle( fontSize: Consts.fontMedium, color: scheme.onSurfaceVariant, fontWeight: FontWeight.normal, ), - subtitle2: TextStyle( + labelSmall: TextStyle( fontSize: Consts.fontSmall, color: scheme.onSurfaceVariant, fontWeight: FontWeight.normal, ), - button: TextStyle( - fontSize: Consts.fontMedium, - color: scheme.background, - fontWeight: FontWeight.normal, - ), ), textSelectionTheme: TextSelectionThemeData( cursorColor: scheme.primary, diff --git a/lib/widgets/cached_image.dart b/lib/widgets/cached_image.dart new file mode 100644 index 00000000..635e960c --- /dev/null +++ b/lib/widgets/cached_image.dart @@ -0,0 +1,49 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:otraku/widgets/overlays/toast.dart'; + +/// A custom cache manager is needed to define exact image cap and stale period. +final _cacheManager = CacheManager( + Config( + 'imageCache', + maxNrOfCacheObjects: 1000, + stalePeriod: const Duration(days: 10), + ), +); + +/// Erases image cache. +void clearImageCache() => _cacheManager.emptyCache(); + +/// A [CachedNetworkImage] wrapper that simplifies the interface +/// and uses the custom cache manager, without exposing it. +class CachedImage extends StatelessWidget { + const CachedImage( + this.imageUrl, { + this.fit = BoxFit.cover, + this.width = double.infinity, + this.height = double.infinity, + }); + + final String imageUrl; + final BoxFit fit; + final double width; + final double height; + + @override + Widget build(BuildContext context) { + return CachedNetworkImage( + imageUrl: imageUrl, + fit: fit, + width: width, + height: height, + cacheManager: _cacheManager, + fadeInDuration: const Duration(milliseconds: 300), + fadeOutDuration: const Duration(milliseconds: 300), + errorWidget: (context, url, error) => IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: () => Toast.show(context, 'Failed loading: $imageUrl'), + ), + ); + } +} diff --git a/lib/widgets/custom_sliver_header.dart b/lib/widgets/custom_sliver_header.dart deleted file mode 100644 index 1c3a6e19..00000000 --- a/lib/widgets/custom_sliver_header.dart +++ /dev/null @@ -1,293 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/fade_image.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; - -class CustomSliverHeader extends StatelessWidget { - const CustomSliverHeader({ - required this.title, - required this.image, - required this.banner, - required this.squareImage, - required this.implyLeading, - required this.actions, - required this.child, - required this.heroId, - this.extraLargeImage, - this.maxWidth = Consts.layoutBig, - }); - - final String? title; - final String? image; - final String? banner; - final String? extraLargeImage; - final bool squareImage; - final bool implyLeading; - final List actions; - final Widget? child; - final int heroId; - - /// If not null the row with the [image] and the [child] will be restrained. - final double? maxWidth; - - @override - Widget build(BuildContext context) { - double sidePadding = 10; - if (maxWidth != null && - MediaQuery.of(context).size.width > maxWidth! + 20) { - sidePadding = (MediaQuery.of(context).size.width - maxWidth!) / 2; - } - - final imageWidth = MediaQuery.of(context).size.width < 430.0 - ? MediaQuery.of(context).size.width * 0.30 - : 100.0; - final imageHeight = imageWidth * (squareImage ? 1 : Consts.coverHtoWRatio); - const bannerHeight = 200.0; - final height = bannerHeight + imageHeight / 2; - - return SliverPersistentHeader( - pinned: true, - delegate: _Delegate( - title: title ?? '', - image: image, - extraLargeImage: extraLargeImage, - banner: banner, - height: height, - bannerHeight: bannerHeight, - imageHeight: imageHeight, - imageWidth: imageWidth, - sidePadding: sidePadding, - implyLeading: implyLeading, - actions: actions, - child: child, - heroId: heroId, - ), - ); - } -} - -class _Delegate implements SliverPersistentHeaderDelegate { - _Delegate({ - required this.title, - required this.image, - required this.extraLargeImage, - required this.banner, - required this.height, - required this.bannerHeight, - required this.imageHeight, - required this.imageWidth, - required this.sidePadding, - required this.implyLeading, - required this.actions, - required this.child, - required this.heroId, - }); - - final String title; - final String? image; - final String? extraLargeImage; - final String? banner; - final double height; - final double bannerHeight; - final double imageHeight; - final double imageWidth; - final double sidePadding; - final bool implyLeading; - final List actions; - final Widget? child; - final int heroId; - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - final extent = maxExtent - shrinkOffset; - final complexImage = imageHeight != imageWidth; - final opacity = shrinkOffset < (bannerHeight - minExtent) - ? shrinkOffset / (bannerHeight - minExtent) - : 1.0; - - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 5, - color: Theme.of(context).colorScheme.background, - ), - ], - ), - child: FlexibleSpaceBar.createSettings( - minExtent: minExtent, - maxExtent: maxExtent, - currentExtent: extent > minExtent ? extent : minExtent, - child: Stack( - fit: StackFit.expand, - children: [ - FlexibleSpaceBar( - collapseMode: CollapseMode.pin, - stretchModes: const [StretchMode.zoomBackground], - background: Column( - children: [ - Expanded( - child: banner != null - ? GestureDetector( - child: FadeImage(banner!), - onTap: () => - showPopUp(context, ImageDialog(banner!)), - ) - : const SizedBox(), - ), - SizedBox(height: height - bannerHeight), - ], - ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - height: height - bannerHeight, - alignment: Alignment.topCenter, - color: Theme.of(context).colorScheme.background, - child: Container( - height: 0, - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - blurRadius: 15, - spreadRadius: 25, - color: Theme.of(context).colorScheme.background, - ), - ], - ), - ), - ), - ), - Positioned( - bottom: 0, - left: sidePadding, - right: sidePadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Hero( - tag: heroId, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: Container( - height: imageHeight, - width: imageWidth, - color: complexImage - ? Theme.of(context).colorScheme.surfaceVariant - : null, - child: image != null - ? GestureDetector( - onTap: () => showPopUp( - context, - ImageDialog(extraLargeImage ?? image!), - ), - child: FadeImage( - image!, - fit: complexImage - ? BoxFit.cover - : BoxFit.contain, - alignment: complexImage - ? Alignment.center - : Alignment.bottomCenter, - ), - ) - : null, - ), - ), - ), - const SizedBox(width: 10), - if (child != null) Expanded(child: child!), - ], - ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, - child: Opacity( - opacity: opacity, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - boxShadow: [ - BoxShadow( - blurRadius: 10, - spreadRadius: 10, - color: Theme.of(context).colorScheme.background, - ), - ], - ), - ), - ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, - child: Row( - children: [ - implyLeading - ? TopBarShadowIcon( - tooltip: 'Close', - icon: Ionicons.chevron_back_outline, - onTap: Navigator.of(context).pop, - ) - : const SizedBox(width: 10), - Expanded( - child: Opacity( - opacity: opacity, - child: Text( - title, - style: Theme.of(context).textTheme.headline2, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ...actions, - ], - ), - ), - ], - ), - ), - ); - } - - @override - double get maxExtent => height; - - @override - double get minExtent => Consts.tapTargetSize; - - @override - OverScrollHeaderStretchConfiguration? get stretchConfiguration => - OverScrollHeaderStretchConfiguration(stretchTriggerOffset: 100); - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => - true; - - @override - PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => - null; - - @override - FloatingHeaderSnapConfiguration? get snapConfiguration => null; - - @override - TickerProvider? get vsync => null; -} diff --git a/lib/widgets/fade_image.dart b/lib/widgets/fade_image.dart deleted file mode 100644 index 4fd1bb63..00000000 --- a/lib/widgets/fade_image.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; - -class FadeImage extends StatelessWidget { - const FadeImage( - this.imageUrl, { - this.fit = BoxFit.cover, - this.width = double.infinity, - this.height = double.infinity, - this.alignment = Alignment.center, - }); - - final String imageUrl; - final BoxFit fit; - final double? width; - final double? height; - final Alignment alignment; - - @override - Widget build(BuildContext context) => FadeInImage.memoryNetwork( - fit: fit, - image: imageUrl, - width: width, - height: height, - alignment: alignment, - fadeInDuration: const Duration(milliseconds: 300), - fadeOutDuration: const Duration(milliseconds: 300), - placeholder: _transparentImage, - imageErrorBuilder: (_, err, stackTrace) => - const Center(child: Icon(Icons.close_outlined)), - ); - - static final Uint8List _transparentImage = Uint8List.fromList([ - 0x89, - 0x50, - 0x4E, - 0x47, - 0x0D, - 0x0A, - 0x1A, - 0x0A, - 0x00, - 0x00, - 0x00, - 0x0D, - 0x49, - 0x48, - 0x44, - 0x52, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - 0x01, - 0x08, - 0x06, - 0x00, - 0x00, - 0x00, - 0x1F, - 0x15, - 0xC4, - 0x89, - 0x00, - 0x00, - 0x00, - 0x0A, - 0x49, - 0x44, - 0x41, - 0x54, - 0x78, - 0x9C, - 0x63, - 0x00, - 0x01, - 0x00, - 0x00, - 0x05, - 0x00, - 0x01, - 0x0D, - 0x0A, - 0x2D, - 0xB4, - 0x00, - 0x00, - 0x00, - 0x00, - 0x49, - 0x45, - 0x4E, - 0x44, - 0xAE, - ]); -} diff --git a/lib/widgets/fields/checkbox_field.dart b/lib/widgets/fields/checkbox_field.dart index 39f80f0a..d42c1aed 100644 --- a/lib/widgets/fields/checkbox_field.dart +++ b/lib/widgets/fields/checkbox_field.dart @@ -68,7 +68,7 @@ class CheckBoxFieldState extends State { widget.title, maxLines: 2, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, ), ), ], @@ -148,7 +148,7 @@ class CheckBoxTriFieldState extends State { Expanded( child: Text( widget.title, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, ), ), ], diff --git a/lib/widgets/fields/drop_down_field.dart b/lib/widgets/fields/drop_down_field.dart index e11b43aa..5e7badc0 100644 --- a/lib/widgets/fields/drop_down_field.dart +++ b/lib/widgets/fields/drop_down_field.dart @@ -45,8 +45,8 @@ class DropDownFieldState extends State> { child: Text( key, style: widget.items[key] != _value - ? Theme.of(context).textTheme.bodyText2 - : Theme.of(context).textTheme.bodyText1, + ? Theme.of(context).textTheme.bodyMedium + : Theme.of(context).textTheme.bodyLarge, ), )); } @@ -66,7 +66,7 @@ class DropDownFieldState extends State> { }, hint: Text( widget.hint, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), dropdownColor: Theme.of(context).colorScheme.surfaceVariant, borderRadius: Consts.borderRadiusMax, diff --git a/lib/widgets/fields/growable_text_field.dart b/lib/widgets/fields/growable_text_field.dart index c67c7064..ebbed887 100644 --- a/lib/widgets/fields/growable_text_field.dart +++ b/lib/widgets/fields/growable_text_field.dart @@ -23,7 +23,7 @@ class GrowableTextFieldState extends State { child: TextField( minLines: 1, maxLines: 10, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, decoration: const InputDecoration(contentPadding: Consts.padding), controller: _ctrl, onChanged: (text) => widget.onChanged(text), diff --git a/lib/widgets/fields/labeled_field.dart b/lib/widgets/fields/labeled_field.dart index 98c80aa5..22efe07c 100644 --- a/lib/widgets/fields/labeled_field.dart +++ b/lib/widgets/fields/labeled_field.dart @@ -13,7 +13,7 @@ class LabeledField extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(label, style: Theme.of(context).textTheme.subtitle1), + Text(label, style: Theme.of(context).textTheme.labelMedium), child, ], ); diff --git a/lib/widgets/fields/number_field.dart b/lib/widgets/fields/number_field.dart index a95c3f27..9fdaab95 100644 --- a/lib/widgets/fields/number_field.dart +++ b/lib/widgets/fields/number_field.dart @@ -63,7 +63,7 @@ class NumberFieldState extends State { FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')), ], textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.all(0), diff --git a/lib/widgets/fields/search_field.dart b/lib/widgets/fields/search_field.dart index bdd28629..04c18926 100644 --- a/lib/widgets/fields/search_field.dart +++ b/lib/widgets/fields/search_field.dart @@ -47,7 +47,7 @@ class _SearchFieldState extends State { return TextField( controller: _ctrl, autofocus: widget.onHide != null, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( isDense: false, hintText: widget.hint, diff --git a/lib/widgets/grids/chip_grids.dart b/lib/widgets/grids/chip_grids.dart index 8093fca4..aeb0e82f 100644 --- a/lib/widgets/grids/chip_grids.dart +++ b/lib/widgets/grids/chip_grids.dart @@ -25,8 +25,11 @@ class ChipOptionField extends StatelessWidget { child: Chip( label: Text(name), labelStyle: selected - ? Theme.of(context).textTheme.button - : Theme.of(context).textTheme.bodyText2, + ? Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).colorScheme.background) + : Theme.of(context).textTheme.bodyMedium, backgroundColor: selected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSecondary, @@ -65,8 +68,13 @@ class __InputChipState extends State<_InputChip> { return InputChip( label: Text(widget.text), labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, + color: _positive + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onErrorContainer, ), + deleteIconColor: _positive + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onErrorContainer, backgroundColor: _positive ? Theme.of(context).colorScheme.primaryContainer : Theme.of(context).colorScheme.errorContainer, @@ -101,14 +109,15 @@ class _ChipGrid extends StatelessWidget { children: [ Row( children: [ - Text(title, style: Theme.of(context).textTheme.subtitle1), + Text(title, style: Theme.of(context).textTheme.labelMedium), const Spacer(), if (onClear != null && children.isNotEmpty) SizedBox( height: 35, child: IconButton( + key: const ValueKey('Clear'), icon: const Icon(Ionicons.close_outline), - tooltip: 'Close', + tooltip: 'Clear', onPressed: onClear!, color: Theme.of(context).colorScheme.onBackground, padding: const EdgeInsets.symmetric(horizontal: 10), @@ -133,7 +142,7 @@ class _ChipGrid extends StatelessWidget { child: Center( child: Text( 'No $placeholder', - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.labelMedium, ), ), ), @@ -322,6 +331,8 @@ class ChipTagGridState extends State { widget.exclusiveGenres.clear(); widget.inclusiveTags.clear(); widget.exclusiveTags.clear(); + widget.tagIdIn?.clear(); + widget.tagIdNotIn?.clear(); }), ); } diff --git a/lib/widgets/grids/relation_grid.dart b/lib/widgets/grids/relation_grid.dart index 6e798f0e..c5be7f57 100644 --- a/lib/widgets/grids/relation_grid.dart +++ b/lib/widgets/grids/relation_grid.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/common/relation.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; class RelationGrid extends StatelessWidget { @@ -72,7 +72,7 @@ class _RelationTile extends StatelessWidget { item.subtitle!, maxLines: 2, overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.labelSmall, ), ], ), @@ -100,7 +100,7 @@ class _RelationTile extends StatelessWidget { connection!.subtitle!, maxLines: 2, overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.labelSmall, ), ], ), @@ -122,7 +122,7 @@ class _RelationTile extends StatelessWidget { item.subtitle!, maxLines: 4, overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.labelSmall, ), ], ), @@ -138,7 +138,7 @@ class _RelationTile extends StatelessWidget { info: item.imageUrl, child: ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage(item.imageUrl, width: 80), + child: CachedImage(item.imageUrl, width: 80), ), ), Expanded( @@ -152,7 +152,7 @@ class _RelationTile extends StatelessWidget { info: connection!.imageUrl, child: ClipRRect( borderRadius: Consts.borderRadiusMin, - child: FadeImage(connection!.imageUrl, width: 80), + child: CachedImage(connection!.imageUrl, width: 80), ), ), ], diff --git a/lib/widgets/grids/tile_item_grid.dart b/lib/widgets/grids/tile_item_grid.dart index f24e8e34..573d0467 100644 --- a/lib/widgets/grids/tile_item_grid.dart +++ b/lib/widgets/grids/tile_item_grid.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:otraku/common/tile_item.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/fade_image.dart'; +import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; class TileItemGrid extends StatelessWidget { @@ -16,46 +16,43 @@ class TileItemGrid extends StatelessWidget { return const SliverFillRemaining(child: Center(child: Text('No Media'))); } - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndExtraHeight( - minWidth: 100, - extraHeight: 40, - rawHWRatio: Consts.coverHtoWRatio, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (_, i) => LinkTile( - id: items[i].id, - info: items[i].imageUrl, - discoverType: items[i].type, - child: Column( - children: [ - Expanded( - child: Hero( - tag: items[i].id, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: Container( - color: Theme.of(context).colorScheme.surfaceVariant, - child: FadeImage(items[i].imageUrl), - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndExtraHeight( + minWidth: 100, + extraHeight: 40, + rawHWRatio: Consts.coverHtoWRatio, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (_, i) => LinkTile( + id: items[i].id, + info: items[i].imageUrl, + discoverType: items[i].type, + child: Column( + children: [ + Expanded( + child: Hero( + tag: items[i].id, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: CachedImage(items[i].imageUrl), ), ), ), - const SizedBox(height: 5), - SizedBox( - height: 35, - child: Text( - items[i].title, - maxLines: 2, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.bodyText2, - ), + ), + const SizedBox(height: 5), + SizedBox( + height: 35, + child: Text( + items[i].title, + maxLines: 2, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.bodyMedium, ), - ], - ), + ), + ], ), ), ), diff --git a/lib/widgets/html_content.dart b/lib/widgets/html_content.dart index 86c3bbd5..d3c1c51c 100644 --- a/lib/widgets/html_content.dart +++ b/lib/widgets/html_content.dart @@ -14,12 +14,11 @@ class HtmlContent extends StatelessWidget { Widget build(BuildContext context) { return HtmlWidget( text, - textStyle: Theme.of(context).textTheme.bodyText2, + textStyle: Theme.of(context).textTheme.bodyMedium, onTapUrl: (url) => Toast.launch(context, url), onLoadingBuilder: (_, __, ___) => const Center(child: Loader()), onErrorBuilder: (_, element, err) => IconButton( - icon: const Icon(Icons.close), - color: Theme.of(context).colorScheme.error, + icon: const Icon(Icons.close_outlined), onPressed: () => showPopUp( context, ConfirmationDialog( diff --git a/lib/widgets/layouts/bottom_bar.dart b/lib/widgets/layouts/bottom_bar.dart index ffcd1c4a..06717647 100644 --- a/lib/widgets/layouts/bottom_bar.dart +++ b/lib/widgets/layouts/bottom_bar.dart @@ -11,15 +11,15 @@ class BottomBar extends StatelessWidget { @override Widget build(BuildContext context) { - final paddingBottom = MediaQuery.of(context).viewPadding.bottom; + final bottomPadding = MediaQuery.of(context).viewPadding.bottom; return ClipRect( child: BackdropFilter( filter: Consts.filter, child: Container( - height: paddingBottom + Consts.tapTargetSize, - padding: EdgeInsets.only(bottom: paddingBottom), - color: Theme.of(context).bottomAppBarColor, + height: Consts.tapTargetSize + bottomPadding, + padding: EdgeInsets.only(bottom: bottomPadding), + color: Theme.of(context).bottomAppBarTheme.color, child: Material(color: Colors.transparent, child: child), ), ), @@ -77,8 +77,8 @@ class BottomBarIconTabs extends StatelessWidget { Text( items.keys.elementAt(i), style: i != current - ? Theme.of(context).textTheme.subtitle1 - : Theme.of(context).textTheme.bodyText1, + ? Theme.of(context).textTheme.labelMedium + : Theme.of(context).textTheme.bodyLarge, ), ], ], diff --git a/lib/widgets/layouts/floating_bar.dart b/lib/widgets/layouts/floating_bar.dart index 25c733be..e01e522f 100644 --- a/lib/widgets/layouts/floating_bar.dart +++ b/lib/widgets/layouts/floating_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/drag_detector.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; /// Hides the [child] on scroll-down and reveals it on scroll-up. class FloatingBar extends StatefulWidget { @@ -78,9 +78,9 @@ class FloatingBarState extends State return Padding( padding: EdgeInsets.only( - left: 15, - right: 15, - bottom: PageLayout.of(context).bottomOffset + 20, + left: 16, + right: 16, + bottom: scaffoldOffsets(context).bottom + 16, ), child: SlideTransition( position: _slideAnimation, @@ -105,6 +105,8 @@ class FloatingBarState extends State } } +const floatingBarItemHeight = 56.0; + class ActionTabSwitcher extends StatefulWidget { const ActionTabSwitcher({ required this.items, @@ -149,8 +151,8 @@ class _ActionTabSwitcherState extends State { widget.items[i], overflow: TextOverflow.ellipsis, style: i != _index - ? Theme.of(context).textTheme.headline2 - : Theme.of(context).textTheme.headline2?.copyWith( + ? Theme.of(context).textTheme.titleMedium + : Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onPrimary, ), ), @@ -176,7 +178,10 @@ class _ActionTabSwitcherState extends State { boxShadow: [ BoxShadow( blurRadius: 5, - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context) + .colorScheme + .surfaceVariant + .withAlpha(50), ), ], ), @@ -193,10 +198,6 @@ class _ActionTabSwitcherState extends State { decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, borderRadius: Consts.borderRadiusMax, - border: Border.all( - width: 5, - color: Theme.of(context).colorScheme.background, - ), ), ), ), @@ -210,8 +211,6 @@ class _ActionTabSwitcherState extends State { } } -const actionButtonSize = 56.0; - /// A [FloatingActionButton] implementation. class ActionButton extends StatelessWidget { const ActionButton({ @@ -234,29 +233,29 @@ class ActionButton extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - width: actionButtonSize, - height: actionButtonSize, + width: floatingBarItemHeight, + height: floatingBarItemHeight, child: Tooltip( message: tooltip, child: DecoratedBox( decoration: BoxDecoration( - shape: BoxShape.circle, + borderRadius: Consts.borderRadiusMax, boxShadow: [ BoxShadow( blurRadius: 5, - color: Theme.of(context) - .colorScheme - .primaryContainer - .withAlpha(100), + color: + Theme.of(context).colorScheme.surfaceVariant.withAlpha(50), ), ], ), child: Material( color: Theme.of(context).colorScheme.primary, - shape: const CircleBorder(), + shape: const RoundedRectangleBorder( + borderRadius: Consts.borderRadiusMax, + ), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(30), + borderRadius: Consts.borderRadiusMax, splashColor: Theme.of(context).colorScheme.onPrimary.withAlpha(50), child: onSwipe == null @@ -270,66 +269,6 @@ class ActionButton extends StatelessWidget { } } -/// A [FloatingActionButton.extended] implementation. -class ExpandedActionButton extends StatelessWidget { - const ExpandedActionButton({ - required this.title, - required this.icon, - required this.onTap, - }); - - final String title; - final IconData icon; - final void Function() onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: actionButtonSize, - child: DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - blurRadius: 5, - color: - Theme.of(context).colorScheme.primaryContainer.withAlpha(100), - ), - ], - ), - child: Material( - color: Theme.of(context).colorScheme.primary, - shape: const RoundedRectangleBorder( - borderRadius: Consts.borderRadiusMax, - ), - child: InkWell( - onTap: onTap, - borderRadius: Consts.borderRadiusMax, - splashColor: Theme.of(context).colorScheme.onPrimary.withAlpha(50), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - title, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(width: 5), - Icon(icon, color: Theme.of(context).colorScheme.onPrimary), - ], - ), - ), - ), - ), - ), - ); - } -} - // Detects swiping and animates the icon switching. class _DraggableIcon extends StatefulWidget { const _DraggableIcon({ diff --git a/lib/widgets/layouts/scaffolds.dart b/lib/widgets/layouts/scaffolds.dart new file mode 100644 index 00000000..624e0881 --- /dev/null +++ b/lib/widgets/layouts/scaffolds.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:otraku/utils/consts.dart'; +import 'package:otraku/widgets/layouts/floating_bar.dart'; +import 'package:otraku/widgets/layouts/top_bar.dart'; + +/// Simple wrapper around [Scaffold], only supporting a bottom bar. +/// For top bars and floating bars, use [TabScaffold]. +class PageScaffold extends StatefulWidget { + const PageScaffold({ + required this.child, + this.bottomBar, + }); + + final Widget child; + final Widget? bottomBar; + + @override + State createState() => _PageScaffoldState(); +} + +class _PageScaffoldState extends State { + double _topOffset = 0; + double _bottomOffset = 0; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final padding = MediaQuery.of(context).padding; + _topOffset = padding.top; + _bottomOffset = padding.bottom; + if (widget.bottomBar != null) { + _bottomOffset += Consts.tapTargetSize; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBody: true, + bottomNavigationBar: widget.bottomBar, + resizeToAvoidBottomInset: false, + body: widget.child, + ); + } +} + +/// Must have a [PageScaffold] (or at least a [Scaffold]) ancestor. +/// Simulates floating buttons and top app bars without using [Scaffold]. +/// The point is to allow for different top/floating bars on each tab +/// in a page with multiple tabs. +class TabScaffold extends StatelessWidget { + const TabScaffold({ + required this.child, + this.topBar, + this.floatingBar, + }); + + final Widget child; + final TopBar? topBar; + final FloatingBar? floatingBar; + + @override + Widget build(BuildContext context) { + assert( + context.findAncestorWidgetOfExactType() != null, + 'TabScaffold must have a PageScaffold ancestor', + ); + + return Stack( + fit: StackFit.expand, + children: [ + child, + if (floatingBar != null) + Positioned(left: 0, right: 0, bottom: 0, child: floatingBar!), + if (topBar != null) + Positioned(left: 0, right: 0, top: 0, child: topBar!), + ], + ); + } +} + +/// Calculates top and bottom offsets, using the device view padding and: +/// - includes the top offset of a potential [TabScaffold] with a top bar. +/// - includes the bottom offset of a potential [PageScaffold] +/// with a bottom bar. +VerticalOffsets scaffoldOffsets(BuildContext context) { + var top = 0.0; + var bottom = 0.0; + + final inner = context.findAncestorWidgetOfExactType(); + if (inner?.topBar != null) top += inner!.topBar!.preferredSize.height; + + final outer = context.findAncestorStateOfType<_PageScaffoldState>(); + if (outer != null) { + top += outer._topOffset; + bottom += outer._bottomOffset; + } + + return VerticalOffsets(top, bottom); +} + +class VerticalOffsets { + const VerticalOffsets(this.top, this.bottom); + + final double top; + final double bottom; +} diff --git a/lib/widgets/layouts/segment_switcher.dart b/lib/widgets/layouts/segment_switcher.dart index d877beaf..febc7139 100644 --- a/lib/widgets/layouts/segment_switcher.dart +++ b/lib/widgets/layouts/segment_switcher.dart @@ -43,8 +43,8 @@ class _SegmentSwitcherState extends State { widget.items[i], overflow: TextOverflow.ellipsis, style: _index != i - ? Theme.of(context).textTheme.headline2 - : Theme.of(context).textTheme.headline2?.copyWith( + ? Theme.of(context).textTheme.titleMedium + : Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onPrimary, ), ), diff --git a/lib/widgets/layouts/page_layout.dart b/lib/widgets/layouts/top_bar.dart similarity index 51% rename from lib/widgets/layouts/page_layout.dart rename to lib/widgets/layouts/top_bar.dart index fe37b3ad..0b02164c 100644 --- a/lib/widgets/layouts/page_layout.dart +++ b/lib/widgets/layouts/top_bar.dart @@ -1,100 +1,18 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/layouts/floating_bar.dart'; - -class PageLayout extends StatefulWidget { - const PageLayout({ - required this.child, - this.topBar, - this.floatingBar, - this.bottomBar, - }); - - final Widget child; - final PreferredSizeWidget? topBar; - final FloatingBar? floatingBar; - final Widget? bottomBar; - - static PageLayoutState of(BuildContext context) { - final PageLayoutState? result = - context.findAncestorStateOfType(); - if (result != null) return result; - throw FlutterError.fromParts([ - ErrorSummary( - 'PageLayout.of() called with a context that does not contain a PageLayout.', - ), - context.describeElement('The context used was'), - ]); - } - - @override - State createState() => PageLayoutState(); -} - -class PageLayoutState extends State { - double _topOffset = 0; - double _bottomOffset = 0; - bool _didCalculateOffsets = false; - - /// The offset from the top that this widget's children should avoid. - /// It takes into consideration [viewPadding.top] of [MediaQueryData], - /// the space taken by [widget.topBar] and the [topOffset] of the - /// ancestral [PageLayoutState]. - double get topOffset => _topOffset; - - /// The offset from the bottom that this widget's children should avoid. - /// It takes into consideration [viewPadding.bottom] of [MediaQueryData], - /// the space taken by [widget.bottomBar] and the [bottomOffset] of the - /// ancestral [PageLayoutState]. - double get bottomOffset => _bottomOffset; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (_didCalculateOffsets) return; - _didCalculateOffsets = true; - - if (widget.topBar != null) { - _topOffset += widget.topBar!.preferredSize.height; - } - - if (widget.bottomBar != null) _bottomOffset += Consts.tapTargetSize; - - final pageLayout = context.findAncestorStateOfType(); - if (pageLayout != null) { - _topOffset += pageLayout._topOffset; - _bottomOffset += pageLayout._bottomOffset; - } else { - _topOffset += MediaQuery.of(context).viewPadding.top; - _bottomOffset += MediaQuery.of(context).viewPadding.bottom; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: widget.child, - appBar: widget.topBar, - floatingActionButton: widget.floatingBar, - bottomNavigationBar: widget.bottomBar, - extendBody: true, - extendBodyBehindAppBar: true, - floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, - resizeToAvoidBottomInset: false, - ); - } -} /// A top app bar implementation that uses a blurred, translucent background. -/// [items] are the widgets that will appear on the top of it. If [canPop] -/// is true, a button that can pop the page will be placed before [items]. +/// It has (in order): +/// - A button to pop the page (if [canPop] is `true`). +/// - The formatted [title] (if not `null`). +/// - The [trailing] widgets (if the list is not empty). class TopBar extends StatelessWidget implements PreferredSizeWidget { - const TopBar({this.items = const [], this.canPop = true, this.title}); + const TopBar({this.trailing = const [], this.canPop = true, this.title}); final bool canPop; final String? title; - final List items; + final List trailing; @override Size get preferredSize => const Size.fromHeight(Consts.tapTargetSize); @@ -106,7 +24,7 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { filter: Consts.filter, child: DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).bottomAppBarColor, + color: Theme.of(context).bottomAppBarTheme.color, ), child: Padding( padding: EdgeInsets.only( @@ -131,10 +49,10 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { Expanded( child: Text( title!, - style: Theme.of(context).textTheme.headline1, + style: Theme.of(context).textTheme.titleLarge, ), ), - ...items, + ...trailing, ], ), ), diff --git a/lib/widgets/loaders.dart/loaders.dart b/lib/widgets/loaders.dart/loaders.dart index 70b29394..11710234 100644 --- a/lib/widgets/loaders.dart/loaders.dart +++ b/lib/widgets/loaders.dart/loaders.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/layouts/page_layout.dart'; +import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/shimmer.dart'; class Loader extends StatelessWidget { @@ -28,7 +28,7 @@ class SliverRefreshControl extends StatelessWidget { @override Widget build(BuildContext context) { return SliverPadding( - padding: EdgeInsets.only(top: PageLayout.of(context).topOffset), + padding: EdgeInsets.only(top: scaffoldOffsets(context).top + 10), sliver: CupertinoSliverRefreshControl( refreshIndicatorExtent: 15, refreshTriggerPullDistance: 160, @@ -80,7 +80,7 @@ class SliverFooter extends StatelessWidget { child: Padding( padding: EdgeInsets.only( top: 10, - bottom: PageLayout.of(context).bottomOffset + 10, + bottom: scaffoldOffsets(context).bottom + 10, ), child: loading ? const Loader() : null, ), diff --git a/lib/widgets/overlays/dialogs.dart b/lib/widgets/overlays/dialogs.dart index 65cb15d6..d8c72412 100644 --- a/lib/widgets/overlays/dialogs.dart +++ b/lib/widgets/overlays/dialogs.dart @@ -62,7 +62,7 @@ class InputDialog extends StatelessWidget { maxLines: 5, autofocus: true, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline1, + style: Theme.of(context).textTheme.titleLarge, decoration: const InputDecoration( filled: false, contentPadding: EdgeInsets.symmetric(horizontal: 10), @@ -273,7 +273,8 @@ class _DialogColumn extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Text(title, style: Theme.of(context).textTheme.headline2), + child: + Text(title, style: Theme.of(context).textTheme.titleMedium), ), const Divider(height: 2, thickness: 2), Flexible( diff --git a/lib/widgets/overlays/sheets.dart b/lib/widgets/overlays/sheets.dart index 7a0ae8e0..d3bcc055 100644 --- a/lib/widgets/overlays/sheets.dart +++ b/lib/widgets/overlays/sheets.dart @@ -9,8 +9,9 @@ Future showSheet(BuildContext context, Widget sheet) => context: context, builder: (context) => sheet, isScrollControlled: true, - backgroundColor: Colors.transparent, barrierColor: Theme.of(context).colorScheme.background.withAlpha(100), + backgroundColor: Colors.transparent, + elevation: 0, ); /// An implementation of [DraggableScrollableSheet] with opaque background. @@ -258,7 +259,7 @@ class FixedGradientSheetTile extends StatelessWidget { children: [ Icon(icon, color: Theme.of(context).colorScheme.onBackground), const SizedBox(width: 10), - Text(text, style: Theme.of(context).textTheme.headline1), + Text(text, style: Theme.of(context).textTheme.titleLarge), ], ), ); diff --git a/lib/widgets/overlays/toast.dart b/lib/widgets/overlays/toast.dart index 1beb9d89..8b0d5209 100644 --- a/lib/widgets/overlays/toast.dart +++ b/lib/widgets/overlays/toast.dart @@ -34,12 +34,12 @@ class Toast { ), ], ), - child: Text(text, style: Theme.of(context).textTheme.bodyText1), + child: Text(text, style: Theme.of(context).textTheme.bodyLarge), ), ), ); - Overlay.of(context)!.insert(_entry!); + Overlay.of(context).insert(_entry!); Future.delayed(const Duration(seconds: 2)).then((_) { if (!_busy) { diff --git a/lib/widgets/text_rail.dart b/lib/widgets/text_rail.dart index b6156f47..3f0c121c 100644 --- a/lib/widgets/text_rail.dart +++ b/lib/widgets/text_rail.dart @@ -14,7 +14,7 @@ class TextRail extends StatelessWidget { const spacing = TextSpan(text: ' • '); - final style = this.style ?? Theme.of(context).textTheme.subtitle2; + final style = this.style ?? Theme.of(context).textTheme.labelSmall; final highlightStyle = style?.copyWith( color: Theme.of(context).colorScheme.primary, ); diff --git a/pubspec.lock b/pubspec.lock index de8ce8a1..97ccf2a8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,147 +5,168 @@ packages: dependency: "direct main" description: name: app_links - url: "https://pub.dartlang.org" + sha256: d572dcdff49c4cfcfa6f315e2683e518ec6eb54e084d01e51d9631a4dcc1b5e8 + url: "https://pub.dev" source: hosted - version: "3.3.0" - app_links_macos: - dependency: transitive - description: - name: app_links_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - app_links_windows: - dependency: transitive - description: - name: app_links_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" + version: "3.4.2" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.3.6" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" + source: hosted + version: "3.2.3" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" + source: hosted + version: "1.0.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" + source: hosted + version: "1.17.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" source: hosted version: "0.7.8" dynamic_color: dependency: "direct main" description: name: dynamic_color - url: "https://pub.dartlang.org" + sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b + url: "https://pub.dev" source: hosted - version: "1.5.4" + version: "1.6.2" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" flutter: @@ -153,90 +174,118 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" + url: "https://pub.dev" + source: hosted + version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + sha256: "02dcaf49d405f652b7160e882bacfc02cb497041bb2eab2a49b1c393cf9aac12" + url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.12.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - url: "https://pub.dartlang.org" + sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + url: "https://pub.dev" source: hosted - version: "12.0.4" + version: "13.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - url: "https://pub.dartlang.org" + sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "3.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - url: "https://pub.dartlang.org" + sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + url: "https://pub.dev" source: hosted version: "6.0.0" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - url: "https://pub.dartlang.org" + sha256: "3ea325c2de7ef2589023413c489875467730b2c1d822759efedd3e0e28a906c9" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - url: "https://pub.dartlang.org" + sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "8.0.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - url: "https://pub.dartlang.org" + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - url: "https://pub.dartlang.org" + sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - url: "https://pub.dartlang.org" + sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + url: "https://pub.dev" source: hosted version: "1.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - url: "https://pub.dartlang.org" + sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + url: "https://pub.dev" source: hosted version: "1.1.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - url: "https://pub.dartlang.org" + sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -251,191 +300,242 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html_core - url: "https://pub.dartlang.org" + sha256: "2ee1f47662f5ba34fe535915b034fae0450bc0a15ae6935ca4abcc7fa987e948" + url: "https://pub.dev" source: hosted - version: "0.9.0+2" + version: "0.10.0" fwfh_text_style: dependency: transitive description: name: fwfh_text_style - url: "https://pub.dartlang.org" + sha256: "37806ee0222f79b6e8d4c698c322c897eae6a817258156f40aeece4e588fac60" + url: "https://pub.dev" source: hosted version: "2.22.08+1" hive: dependency: "direct main" description: name: hive - url: "https://pub.dartlang.org" + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" source: hosted version: "2.2.3" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.2" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227" + url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.15" ionicons: dependency: "direct main" description: name: ionicons - url: "https://pub.dartlang.org" + sha256: "5496bc65a16115ecf05b15b78f494ee4a8869504357668f0a11d689e970523cf" + url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.8.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" + source: hosted + version: "1.0.2" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.13" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + url: "https://pub.dev" source: hosted - version: "2.0.20" - path_provider_ios: + version: "2.0.24" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "026b97a6c29da75181a37aae2eba9227f5fe13cb2838c6b975ce209328b8ab4e" + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.10" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + url: "https://pub.dev" + source: hosted + version: "3.6.2" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" riverpod: dependency: transitive description: name: riverpod - url: "https://pub.dartlang.org" + sha256: "7da5a0febdb7fd0e4340f7b7023cb8f17cf7acd405b1e67a0e98bf574085bfa5" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" sky_engine: dependency: transitive description: flutter @@ -445,163 +545,218 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "851d5040552cf911f4cabda08d003eca76b27da3ed0002978272e27c8fbf8ecc" + url: "https://pub.dev" + source: hosted + version: "2.2.5" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "2.4.2+2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" state_notifier: dependency: transitive description: name: state_notifier - url: "https://pub.dartlang.org" + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" source: hosted version: "0.7.2+1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + url: "https://pub.dev" + source: hosted + version: "3.0.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964" + url: "https://pub.dev" source: hosted - version: "0.9.0" + version: "0.9.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" source: hosted - version: "6.1.7" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1" + url: "https://pub.dev" source: hosted - version: "6.0.18" + version: "6.0.25" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "7ab1e5b646623d6a2537aa59d5d039f90eebef75a7c25e105f6f75de1f7750c3" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.3" workmanager: dependency: "direct main" description: name: workmanager - url: "https://pub.dartlang.org" + sha256: e0be7e35d644643f164ee45d2ce14414f0e0fdde19456aa66065f35a0b1d2ea1 + url: "https://pub.dev" source: hosted version: "0.5.1" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: dart: ">=2.18.0 <3.0.0" - flutter: ">=3.3.0" + flutter: ">=3.7.0-0" diff --git a/pubspec.yaml b/pubspec.yaml index bbdd1435..260d10bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: An unofficial AniList app. publish_to: 'none' -version: 1.2.2+54 +version: 1.2.3+55 environment: sdk: '>=2.17.0 <3.0.0' @@ -13,37 +13,43 @@ dependencies: sdk: flutter # State management. - flutter_riverpod: ^2.1.1 + flutter_riverpod: ^2.3.1 # Data fetching. http: ^0.13.5 - # Cache and settings storage. + # Settings storage. hive: ^2.2.3 # Access to device storage. Used for [hive] setup. - path_provider: ^2.0.11 + path_provider: ^2.0.13 # Secure storage for the access tokens. - flutter_secure_storage: ^7.0.1 + flutter_secure_storage: ^8.0.0 + + # Used for configuring [cached_network_image], which already imports it. + flutter_cache_manager: ^3.3.0 + + # Image caching. + cached_network_image: ^3.2.3 # Opening links in the browser. - url_launcher: ^6.1.7 + url_launcher: ^6.1.10 # Flutter deep linking didn't handle url fragments before. When [go_router] is implemented, this can be removed. - app_links: ^3.3.0 + app_links: ^3.4.2 # Access to platform theme and easy theme interpolation. - dynamic_color: ^1.5.4 + dynamic_color: ^1.6.2 # Background tasks for notification fetching. workmanager: ^0.5.1 # Sending device notifications. - flutter_local_notifications: ^12.0.4 + flutter_local_notifications: ^13.0.0 # Translating html into flutter widgets. - flutter_widget_from_html_core: ^0.9.0+2 + flutter_widget_from_html_core: ^0.10.0 # An addition to the material icons. ionicons: ^0.2.1 @@ -52,7 +58,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_launcher_icons: ^0.11.0 + flutter_launcher_icons: ^0.12.0 flutter_lints: ^2.0.1 flutter_icons: