From cd1ad6d15e2f14b8dc2b580aa16f9b10dc83ee76 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Fri, 17 Mar 2023 21:43:45 +0200 Subject: [PATCH 01/55] Typo. --- lib/collection/collection_preview_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/collection/collection_preview_view.dart b/lib/collection/collection_preview_view.dart index 7a575726..e3937e3c 100644 --- a/lib/collection/collection_preview_view.dart +++ b/lib/collection/collection_preview_view.dart @@ -108,7 +108,7 @@ class _CollectionPreviewViewState extends State { scrollCtrl: widget.scrollCtrl, children: [ ActionButton( - tooltip: 'Load Entire Connection', + tooltip: 'Load Entire Collection', icon: Ionicons.enter_outline, onTap: () => ref.read(homeProvider).expandCollection(widget.tag.ofAnime), From 418101da95d3ef2f4d588b9704317fd859e5d95b Mon Sep 17 00:00:00 2001 From: lotusgate Date: Fri, 17 Mar 2023 23:35:07 +0200 Subject: [PATCH 02/55] Design fixes. --- lib/media/media_header.dart | 22 +++- lib/review/review_header.dart | 206 +++++++++++++++++++++++++++++++ lib/review/review_view.dart | 161 +----------------------- lib/user/user_header.dart | 24 +++- lib/widgets/layouts/top_bar.dart | 28 ----- 5 files changed, 253 insertions(+), 188 deletions(-) create mode 100644 lib/review/review_header.dart diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index feb33e1f..b094c673 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -229,6 +229,24 @@ class _Delegate implements SliverPersistentHeaderDelegate { ], ), ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background.withAlpha(0), + ], + ), + ), + ), + ), Positioned( top: 0, left: 0, @@ -257,7 +275,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { height: minExtent, child: Row( children: [ - TopBarShadowIcon( + TopBarIcon( tooltip: 'Close', icon: Ionicons.chevron_back_outline, onTap: Navigator.of(context).pop, @@ -275,7 +293,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { ), ), if (info?.siteUrl != null) - TopBarShadowIcon( + TopBarIcon( tooltip: 'More', icon: Ionicons.ellipsis_horizontal, onTap: () => showSheet( diff --git a/lib/review/review_header.dart b/lib/review/review_header.dart new file mode 100644 index 00000000..349e8e8d --- /dev/null +++ b/lib/review/review_header.dart @@ -0,0 +1,206 @@ +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/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'; + +class ReviewHeader extends StatelessWidget { + const ReviewHeader({ + required this.id, + required this.bannerUrl, + required this.mediaTitle, + required this.siteUrl, + }); + + final int id; + final String? bannerUrl; + final String? mediaTitle; + final String? siteUrl; + + @override + Widget build(BuildContext context) { + return SliverPersistentHeader( + pinned: true, + delegate: _HeaderDelegate(id, bannerUrl, mediaTitle, siteUrl), + ); + } +} + +class _HeaderDelegate extends SliverPersistentHeaderDelegate { + _HeaderDelegate(this.id, this.bannerUrl, this.title, this.siteUrl); + + final int id; + final String? bannerUrl; + final String? title; + final String? siteUrl; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + final extent = maxExtent - shrinkOffset; + final opacity = shrinkOffset < (maxExtent - minExtent) + ? shrinkOffset / (maxExtent - minExtent) + : 1.0; + + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + ), + 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: [ + if (bannerUrl != null) + Expanded( + child: GestureDetector( + child: Hero(tag: id, child: CachedImage(bannerUrl!)), + onTap: () => + showPopUp(context, ImageDialog(bannerUrl!)), + ), + ), + + /// An annoying workaround for a bug in the + /// anti-aliasing of the overlaying [DecoratedBox]. + Container( + color: Theme.of(context).colorScheme.background, + height: 1, + ), + ], + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + height: maxExtent * 0.4, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background.withAlpha(0), + ], + ), + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background.withAlpha(0), + ], + ), + ), + ), + ), + 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: [ + TopBarIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: Navigator.of(context).pop, + ), + if (title != null) + Expanded( + child: Opacity( + opacity: opacity, + child: Text( + title!, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (siteUrl != null) + TopBarIcon( + tooltip: 'More', + icon: Ionicons.ellipsis_horizontal, + onTap: () => showSheet( + context, + FixedGradientDragSheet.link(context, siteUrl!), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + @override + double get maxExtent => 150; + + @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/review/review_view.dart b/lib/review/review_view.dart index 32eea52d..5571e63f 100644 --- a/lib/review/review_view.dart +++ b/lib/review/review_view.dart @@ -1,16 +1,11 @@ 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/review/review_header.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/cached_image.dart'; import 'package:otraku/widgets/html_content.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; class ReviewView extends StatelessWidget { const ReviewView(this.id, this.bannerUrl); @@ -28,14 +23,11 @@ class ReviewView extends StatelessWidget { return CustomScrollView( slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: _HeaderDelegate( - id, - bannerUrl, - data?.mediaTitle, - data?.siteUrl, - ), + ReviewHeader( + id: id, + bannerUrl: bannerUrl, + mediaTitle: data?.mediaTitle, + siteUrl: data?.siteUrl, ), if (data != null) SliverPadding( @@ -133,147 +125,6 @@ class ReviewView extends StatelessWidget { } } -class _HeaderDelegate extends SliverPersistentHeaderDelegate { - _HeaderDelegate(this.id, this.bannerUrl, this.title, this.siteUrl); - - final int id; - final String? bannerUrl; - final String? title; - final String? siteUrl; - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - final extent = maxExtent - shrinkOffset; - final opacity = shrinkOffset < (maxExtent - minExtent) - ? shrinkOffset / (maxExtent - minExtent) - : 1.0; - - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - ), - child: FlexibleSpaceBar.createSettings( - minExtent: minExtent, - maxExtent: maxExtent, - currentExtent: extent > minExtent ? extent : minExtent, - child: Stack( - fit: StackFit.expand, - children: [ - FlexibleSpaceBar( - collapseMode: CollapseMode.pin, - background: bannerUrl != null - ? Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: GestureDetector( - child: - Hero(tag: id, child: CachedImage(bannerUrl!)), - onTap: () => - showPopUp(context, ImageDialog(bannerUrl!)), - ), - ), - - /// An annoying workaround for a bug in the - /// anti-aliasing of the overlaying [DecoratedBox]. - Container( - color: Theme.of(context).colorScheme.background, - height: 1, - ), - ], - ) - : null, - ), - DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(0), - ], - ), - ), - ), - Opacity( - opacity: opacity, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).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, - ), - if (title != null) - Expanded( - child: Opacity( - opacity: opacity, - child: Text( - title!, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.ellipsis, - ), - ), - ), - if (siteUrl != null) - TopBarShadowIcon( - tooltip: 'More', - icon: Ionicons.ellipsis_horizontal, - onTap: () => showSheet( - context, - FixedGradientDragSheet.link(context, siteUrl!), - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - @override - double get maxExtent => 150; - - @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 _RateButtons extends StatefulWidget { const _RateButtons(this.id); diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index d62a529b..228fc928 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -220,6 +220,24 @@ class _Delegate implements SliverPersistentHeaderDelegate { ], ), ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background.withAlpha(0), + ], + ), + ), + ), + ), Positioned( top: 0, left: 0, @@ -250,7 +268,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { children: [ isMe ? const SizedBox(width: 10) - : TopBarShadowIcon( + : TopBarIcon( tooltip: 'Close', icon: Ionicons.chevron_back_outline, onTap: Navigator.of(context).pop, @@ -269,7 +287,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { ), if (!isMe && user != null) _FollowButton(user!), if (user?.siteUrl != null) - TopBarShadowIcon( + TopBarIcon( tooltip: 'More', icon: Ionicons.ellipsis_horizontal, onTap: () => showSheet( @@ -278,7 +296,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { ), ), if (isMe) - TopBarShadowIcon( + TopBarIcon( tooltip: 'Settings', icon: Ionicons.cog_outline, onTap: () => Navigator.pushNamed( diff --git a/lib/widgets/layouts/top_bar.dart b/lib/widgets/layouts/top_bar.dart index 0b02164c..df969b84 100644 --- a/lib/widgets/layouts/top_bar.dart +++ b/lib/widgets/layouts/top_bar.dart @@ -96,31 +96,3 @@ class TopBarIcon extends StatelessWidget { ); } } - -/// A [TopBarIcon] casting a shadow. Used when the background is a banner. -class TopBarShadowIcon extends StatelessWidget { - const TopBarShadowIcon({ - required this.icon, - required this.tooltip, - required this.onTap, - }); - - final IconData icon; - final String tooltip; - final void Function() onTap; - - @override - Widget build(BuildContext context) => DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.background, - blurRadius: 10, - spreadRadius: -10, - ), - ], - ), - child: TopBarIcon(icon: icon, tooltip: tooltip, onTap: onTap), - ); -} From 6f18c9204e1af9be84239e4bf47aacd8307951e8 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 18 Mar 2023 13:36:32 +0200 Subject: [PATCH 03/55] Updated icon on the about tab. --- assets/icons/about.png | Bin 0 -> 8074 bytes assets/icons/{about_icon.png => about_alt.png} | Bin lib/settings/settings_about_tab.dart | 14 +++++--------- pubspec.lock | 8 ++++---- pubspec.yaml | 4 ++-- 5 files changed, 11 insertions(+), 15 deletions(-) create mode 100644 assets/icons/about.png rename assets/icons/{about_icon.png => about_alt.png} (100%) diff --git a/assets/icons/about.png b/assets/icons/about.png new file mode 100644 index 0000000000000000000000000000000000000000..0fe98736fb671ca0166d14ae6b066991d5f84898 GIT binary patch literal 8074 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 F{txOSOhy0z literal 0 HcmV?d00001 diff --git a/assets/icons/about_icon.png b/assets/icons/about_alt.png similarity index 100% rename from assets/icons/about_icon.png rename to assets/icons/about_alt.png diff --git a/lib/settings/settings_about_tab.dart b/lib/settings/settings_about_tab.dart index 9beff8c2..7fe4c1e2 100644 --- a/lib/settings/settings_about_tab.dart +++ b/lib/settings/settings_about_tab.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; 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/cached_image.dart'; @@ -30,14 +29,11 @@ class SettingsAboutTab extends StatelessWidget { bottom: offsets.bottom + 10, ), children: [ - ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: Image.asset( - 'assets/icons/about_icon.png', - fit: BoxFit.contain, - width: 200, - height: 200, - ), + Image.asset( + 'assets/icons/about.png', + color: Theme.of(context).colorScheme.primary, + width: 180, + height: 180, ), Padding( padding: const EdgeInsets.symmetric(vertical: 5), diff --git a/pubspec.lock b/pubspec.lock index 97ccf2a8..d77902f0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -234,10 +234,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "3ea325c2de7ef2589023413c489875467730b2c1d822759efedd3e0e28a906c9" + sha256: b3c3a8a9714b7f88dd2a41e1efbc47f76d620b06ab427c62ae7bc82298cd7dbb url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" flutter_secure_storage: dependency: "direct main" description: @@ -524,10 +524,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "7da5a0febdb7fd0e4340f7b7023cb8f17cf7acd405b1e67a0e98bf574085bfa5" + sha256: b0fbf7927333c5c318f7e2c22c8b4fd2542ba294de0373e80ecdb34e0dcd8dc4 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 260d10bc..fd0d45c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter # State management. - flutter_riverpod: ^2.3.1 + flutter_riverpod: ^2.3.2 # Data fetching. http: ^0.13.5 @@ -72,7 +72,7 @@ flutter: uses-material-design: true assets: - - assets/icons/about_icon.png + - assets/icons/about.png fonts: - family: Rubik From b1615610ec498d214532929744c871df1e3a7ab0 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 18 Mar 2023 18:53:48 +0200 Subject: [PATCH 04/55] Cosmetic changes. --- lib/feed/feed_view.dart | 2 +- lib/main.dart | 180 ++++++++++++++++++++-------------------- 2 files changed, 92 insertions(+), 90 deletions(-) diff --git a/lib/feed/feed_view.dart b/lib/feed/feed_view.dart index 9931dc05..c31bdea0 100644 --- a/lib/feed/feed_view.dart +++ b/lib/feed/feed_view.dart @@ -42,7 +42,7 @@ class FeedView extends StatelessWidget { if (count > 0) { result = Badge.count( count: count, - alignment: AlignmentDirectional.bottomStart, + alignment: AlignmentDirectional.bottomEnd, child: result, ); } diff --git a/lib/main.dart b/lib/main.dart index 8dff602a..39e0962d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,95 +36,97 @@ class AppState extends State { } @override - Widget build(BuildContext context) => Consumer( - builder: (context, ref, _) => DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - ColorScheme lightScheme; - ColorScheme darkScheme; - var theme = Options().theme; - - /// The system schemes must be cached, so - /// they can later be used in the settings. - final notifier = ref.watch(homeProvider.notifier); - final hasDynamic = lightDynamic != null && darkDynamic != null; - - final darkBackground = - Options().pureBlackDarkTheme ? Colors.black : null; - - if (hasDynamic) { - lightDynamic = lightDynamic.harmonized(); - darkDynamic = darkDynamic.harmonized().copyWith( - background: darkBackground, - ); - notifier.setSystemSchemes(lightDynamic, darkDynamic); - } else { - notifier.setSystemSchemes(null, null); - } - - if (theme == null && hasDynamic) { - lightScheme = lightDynamic!; - darkScheme = darkDynamic!; - } else { - theme ??= 0; - if (theme >= colorSeeds.length) { - theme = colorSeeds.length - 1; - } - - final seed = colorSeeds.values.elementAt(theme); - lightScheme = seed.scheme(Brightness.light); - darkScheme = seed - .scheme(Brightness.dark) - .copyWith(background: darkBackground); - } - - final mode = Options().themeMode; - final platform = - SchedulerBinding.instance.window.platformBrightness; - final isDark = mode == ThemeMode.system - ? platform == Brightness.dark - : mode == ThemeMode.dark; - - final ColorScheme scheme; - final Brightness overlayBrightness; - if (isDark) { - scheme = darkScheme; - overlayBrightness = Brightness.light; - } else { - scheme = lightScheme; - overlayBrightness = Brightness.dark; + Widget build(BuildContext context) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + + return Consumer( + builder: (context, ref, _) => DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + ColorScheme lightScheme; + ColorScheme darkScheme; + var theme = Options().theme; + + /// The system schemes must be cached, so + /// they can later be used in the settings. + final notifier = ref.watch(homeProvider.notifier); + final hasDynamic = lightDynamic != null && darkDynamic != null; + + final darkBackground = + Options().pureBlackDarkTheme ? Colors.black : null; + + if (hasDynamic) { + lightDynamic = lightDynamic.harmonized(); + darkDynamic = darkDynamic.harmonized().copyWith( + background: darkBackground, + ); + notifier.setSystemSchemes(lightDynamic, darkDynamic); + } else { + notifier.setSystemSchemes(null, null); + } + + if (theme == null && hasDynamic) { + lightScheme = lightDynamic!; + darkScheme = darkDynamic!; + } else { + theme ??= 0; + if (theme >= colorSeeds.length) { + theme = colorSeeds.length - 1; } - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: scheme.brightness, - statusBarIconBrightness: overlayBrightness, - systemNavigationBarColor: scheme.background, - systemNavigationBarIconBrightness: overlayBrightness, - )); - final data = themeDataFrom(scheme); - - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Otraku', - theme: data, - darkTheme: data, - navigatorKey: RouteArg.navKey, - onGenerateRoute: RouteArg.generateRoute, - builder: (context, child) { - /// Override the [textScaleFactor], because some devices apply - /// too high of a factor and it breaks the app visually. - /// [child] can't be null, because [onGenerateRoute] is provided. - final mediaQuery = MediaQuery.of(context); - final scale = - mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); - - return MediaQuery( - data: mediaQuery.copyWith(textScaleFactor: scale), - child: child!, - ); - }, - ); - }, - ), - ); + final seed = colorSeeds.values.elementAt(theme); + lightScheme = seed.scheme(Brightness.light); + darkScheme = seed + .scheme(Brightness.dark) + .copyWith(background: darkBackground); + } + + final mode = Options().themeMode; + final platform = SchedulerBinding.instance.window.platformBrightness; + final isDark = mode == ThemeMode.system + ? platform == Brightness.dark + : mode == ThemeMode.dark; + + final ColorScheme scheme; + final Brightness overlayBrightness; + if (isDark) { + scheme = darkScheme; + overlayBrightness = Brightness.light; + } else { + scheme = lightScheme; + overlayBrightness = Brightness.dark; + } + + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: scheme.brightness, + statusBarIconBrightness: overlayBrightness, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: overlayBrightness, + )); + final data = themeDataFrom(scheme); + + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Otraku', + theme: data, + darkTheme: data, + navigatorKey: RouteArg.navKey, + onGenerateRoute: RouteArg.generateRoute, + builder: (context, child) { + /// Override the [textScaleFactor], because some devices apply + /// too high of a factor and it breaks the app visually. + /// [child] can't be null, because [onGenerateRoute] is provided. + final mediaQuery = MediaQuery.of(context); + final scale = mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); + + return MediaQuery( + data: mediaQuery.copyWith(textScaleFactor: scale), + child: child!, + ); + }, + ); + }, + ), + ); + } } From 9817547e2db0c6c061261679b3d1c00d88073191 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 1 Apr 2023 00:07:15 +0300 Subject: [PATCH 05/55] Simplified friends access --- lib/user/friends_view.dart | 27 ++++++++++++--------------- lib/utils/route_arg.dart | 6 ++---- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/user/friends_view.dart b/lib/user/friends_view.dart index 46f036fd..eff88068 100644 --- a/lib/user/friends_view.dart +++ b/lib/user/friends_view.dart @@ -6,6 +6,7 @@ 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/constrained_view.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'; @@ -13,17 +14,16 @@ import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; class FriendsView extends ConsumerStatefulWidget { - const FriendsView(this.id, this.onFollowing); + const FriendsView(this.id); final int id; - final bool onFollowing; @override ConsumerState createState() => _FriendsViewState(); } class _FriendsViewState extends ConsumerState { - late bool _onFollowing = widget.onFollowing; + late bool _onFollowing = true; late final _ctrl = PaginationController( loadMore: () => ref.read(friendsProvider(widget.id).notifier).fetch(), ); @@ -138,18 +138,15 @@ class _FriendTab extends StatelessWidget { return const Center(child: Text('No Users')); } - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: CustomScrollView( - physics: Consts.physics, - controller: paginationController, - slivers: [ - refreshControl, - UserGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), + return ConstrainedView( + child: CustomScrollView( + physics: Consts.physics, + controller: paginationController, + slivers: [ + refreshControl, + UserGrid(data.items), + SliverFooter(loading: data.hasNext), + ], ), ); }); diff --git a/lib/utils/route_arg.dart b/lib/utils/route_arg.dart index b37e8e3a..ceaa133e 100644 --- a/lib/utils/route_arg.dart +++ b/lib/utils/route_arg.dart @@ -85,10 +85,8 @@ class RouteArg { if (arg?.id == null) return _unknown; return MaterialPageRoute(builder: (_) => FavoritesView(arg!.id!)); case friends: - if (arg?.id == null || arg?.variant == null) return _unknown; - return MaterialPageRoute( - builder: (_) => FriendsView(arg!.id!, arg.variant!), - ); + if (arg?.id == null) return _unknown; + return MaterialPageRoute(builder: (_) => FriendsView(arg!.id!)); case statistics: if (arg?.id == null) return _unknown; return MaterialPageRoute(builder: (_) => StatisticsView(arg!.id!)); From c98711ce8c824cc4fd76798091e1044857e849e3 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 1 Apr 2023 00:07:38 +0300 Subject: [PATCH 06/55] Improved user page redirects --- lib/user/user_header.dart | 16 +- lib/user/user_view.dart | 345 ++++++++++++++++++-------------------- 2 files changed, 172 insertions(+), 189 deletions(-) diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index 228fc928..2bcd85ce 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -15,13 +15,13 @@ import 'package:otraku/widgets/text_rail.dart'; class UserHeader extends StatelessWidget { const UserHeader({ required this.id, - required this.isMe, + required this.isViewer, required this.user, required this.imageUrl, }); final int id; - final bool isMe; + final bool isViewer; final User? user; final String? imageUrl; @@ -37,7 +37,7 @@ class UserHeader extends StatelessWidget { pinned: true, delegate: _Delegate( id: id, - isMe: isMe, + isViewer: isViewer, user: user, imageUrl: imageUrl, textRailItems: textRailItems, @@ -52,7 +52,7 @@ class UserHeader extends StatelessWidget { class _Delegate implements SliverPersistentHeaderDelegate { _Delegate({ required this.id, - required this.isMe, + required this.isViewer, required this.user, required this.imageUrl, required this.imageWidth, @@ -60,7 +60,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { }); final int id; - final bool isMe; + final bool isViewer; final User? user; final String? imageUrl; final double imageWidth; @@ -266,7 +266,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { height: minExtent, child: Row( children: [ - isMe + isViewer ? const SizedBox(width: 10) : TopBarIcon( tooltip: 'Close', @@ -285,7 +285,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { ), ), ), - if (!isMe && user != null) _FollowButton(user!), + if (!isViewer && user != null) _FollowButton(user!), if (user?.siteUrl != null) TopBarIcon( tooltip: 'More', @@ -295,7 +295,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { FixedGradientDragSheet.link(context, user!.siteUrl!), ), ), - if (isMe) + if (isViewer) TopBarIcon( tooltip: 'Settings', icon: Ionicons.cog_outline, diff --git a/lib/user/user_view.dart b/lib/user/user_view.dart index fbd79d3c..c235513c 100644 --- a/lib/user/user_view.dart +++ b/lib/user/user_view.dart @@ -1,15 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/home/home_provider.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/user/user_providers.dart'; import 'package:otraku/user/user_header.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/utils/route_arg.dart'; -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/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; @@ -39,201 +36,187 @@ class UserSubView extends StatelessWidget { ? (MediaQuery.of(context).size.width - Consts.layoutBig) / 2 : 10.0; - final padding = EdgeInsets.only( - left: sidePadding, - right: sidePadding, - top: 10, - ); - - 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(), + return SafeArea( + child: 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, - ), - 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), - ), - ), - _Button( - Ionicons.person_circle, - 'Followers', - () => Navigator.pushNamed( - context, - RouteArg.friends, - arguments: RouteArg(id: id, variant: false), - ), - ), - _Button( - Ionicons.chatbox, - 'Activities', - () => Navigator.pushNamed( - context, - RouteArg.activities, - arguments: RouteArg(id: id), - ), - ), - _Button( - Icons.favorite, - 'Favourites', - () => Navigator.pushNamed( - context, - RouteArg.favourites, - arguments: RouteArg(id: id), - ), - ), - _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), + ); + + final user = ref.watch(userProvider(id)); + + final header = UserHeader( + id: id, + isViewer: id == Options().id, + user: user.valueOrNull, + imageUrl: avatarUrl ?? user.valueOrNull?.imageUrl, + ); + + return user.when( + error: (_, __) => CustomScrollView( + controller: scrollCtrl, + slivers: [ + header, + const SliverFillRemaining( + child: Center(child: Text('Failed to load user')), + ) + ], + ), + loading: () => CustomScrollView( + controller: scrollCtrl, + slivers: [ + header, + const SliverFillRemaining(child: Center(child: Loader())) + ], + ), + data: (data) => CustomScrollView( + controller: scrollCtrl, + slivers: [ + header, + _ButtonRow(id), + if (data.description.isNotEmpty) + SliverToBoxAdapter( + child: Card( + margin: EdgeInsets.symmetric(horizontal: sidePadding), + child: Padding( + padding: Consts.padding, + child: HtmlContent(data.description), ), ), - ], - ), - ), - )); - - 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), - ); - }, + const SliverFooter(), + ], + ), + ); + }, + ), + ), + ); + } +} + +class _ButtonRow extends StatelessWidget { + const _ButtonRow(this.id); + + final int id; + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Container( + height: 70, + margin: const EdgeInsets.symmetric(vertical: 15), + alignment: Alignment.center, + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 10), + scrollDirection: Axis.horizontal, + children: [ + if (id != Options().id) ...[ + _Button( + label: 'Anime', + icon: Ionicons.film, + onTap: () => Navigator.pushNamed( + context, + RouteArg.collection, + arguments: RouteArg(id: id, variant: true), + ), + ), + _Button( + label: 'Manga', + icon: Ionicons.bookmark, + onTap: () => Navigator.pushNamed( + context, + RouteArg.collection, + arguments: RouteArg(id: id, variant: false), + ), + ), + ], + _Button( + label: 'Activities', + icon: Ionicons.chatbox, + onTap: () => Navigator.pushNamed( + context, + RouteArg.activities, + arguments: RouteArg(id: id), + ), + ), + _Button( + label: 'Social', + icon: Ionicons.people_circle, + onTap: () => Navigator.pushNamed( + context, + RouteArg.friends, + arguments: RouteArg(id: id), + ), + ), + _Button( + label: 'Favourites', + icon: Icons.favorite, + onTap: () => Navigator.pushNamed( + context, + RouteArg.favourites, + arguments: RouteArg(id: id), + ), + ), + _Button( + label: 'Statistics', + icon: Ionicons.stats_chart, + onTap: () => Navigator.pushNamed( + context, + RouteArg.statistics, + arguments: RouteArg(id: id), + ), + ), + _Button( + label: 'Reviews', + icon: Icons.rate_review, + onTap: () => Navigator.pushNamed( + context, + RouteArg.reviews, + arguments: RouteArg(id: id), + ), + ), + ], + ), ), ); } } class _Button extends StatelessWidget { - const _Button(this.icon, this.title, this.onTap); + const _Button({required this.label, required this.icon, required this.onTap}); + final String label; final IconData icon; - final String title; final void Function() onTap; @override Widget build(BuildContext context) { - return TextButton( - onPressed: onTap, - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.onBackground, - ), - child: Row( - children: [ - Expanded(child: Icon(icon)), - Expanded( - flex: 2, - child: Text(title, style: Theme.of(context).textTheme.titleMedium), - ), - ], + return InkWell( + onTap: onTap, + borderRadius: Consts.borderRadiusMax, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: Theme.of(context).colorScheme.onBackground), + const SizedBox(height: 5), + Text(label, style: Theme.of(context).textTheme.bodyMedium), + ], + ), ), ); } From 921f320a1411852b91638e737257f856efed0436 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 1 Apr 2023 00:17:39 +0300 Subject: [PATCH 07/55] Fixed image popup overlay colour --- lib/widgets/overlays/dialogs.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/widgets/overlays/dialogs.dart b/lib/widgets/overlays/dialogs.dart index d8c72412..8ba4e743 100644 --- a/lib/widgets/overlays/dialogs.dart +++ b/lib/widgets/overlays/dialogs.dart @@ -4,6 +4,7 @@ import 'package:otraku/widgets/html_content.dart'; Future showPopUp(BuildContext context, Widget child) => showDialog( context: context, builder: (context) => PopUpAnimation(child), + barrierColor: Theme.of(context).colorScheme.background.withAlpha(100), ); class PopUpAnimation extends StatefulWidget { @@ -200,6 +201,7 @@ class _ImageDialogState extends State Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.transparent, + surfaceTintColor: Colors.transparent, insetPadding: const EdgeInsets.only(), child: GestureDetector( onDoubleTapDown: (details) => _lastOffset = details.localPosition, From 5ef6406946ddfe4b17173be157d798d67ed501a5 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 1 Apr 2023 00:20:52 +0300 Subject: [PATCH 08/55] Text in text dialogs can be selected --- lib/widgets/overlays/dialogs.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/overlays/dialogs.dart b/lib/widgets/overlays/dialogs.dart index 8ba4e743..5cc5d39c 100644 --- a/lib/widgets/overlays/dialogs.dart +++ b/lib/widgets/overlays/dialogs.dart @@ -239,7 +239,7 @@ class TextDialog extends StatelessWidget { @override Widget build(BuildContext context) => - _DialogColumn(title: title, expand: false, child: Text(text)); + _DialogColumn(title: title, expand: false, child: SelectableText(text)); } class HtmlDialog extends StatelessWidget { From 3ef7d5f742dcdf842af8690d7d68841356210717 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 1 Apr 2023 00:31:26 +0300 Subject: [PATCH 09/55] Renamed some media sort variants --- lib/media/media_constants.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/media/media_constants.dart b/lib/media/media_constants.dart index 3deaaa93..5bd842ce 100644 --- a/lib/media/media_constants.dart +++ b/lib/media/media_constants.dart @@ -71,11 +71,11 @@ enum MediaSort { POPULARITY_DESC('Popularity'), SCORE_DESC('Score'), SCORE('Worst Score'), - ID_DESC('Newest'), - ID('Oldest'), FAVOURITES_DESC('Favourites'), START_DATE_DESC('Released Latest'), START_DATE('Released Earliest'), + ID_DESC('Last Added'), + ID('First Added'), TITLE_ROMAJI('Title Romaji'), TITLE_ENGLISH('Title English'), TITLE_NATIVE('Title Native'); From a00947412e995d127c618f383c94f57151736d37 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Mon, 3 Apr 2023 17:22:25 +0300 Subject: [PATCH 10/55] Made pagination more flexible --- lib/activity/activities_providers.dart | 21 ++++---- lib/activity/activity_models.dart | 4 +- lib/activity/activity_provider.dart | 12 ++--- lib/character/character_providers.dart | 18 +++---- lib/common/paged.dart | 44 ++++++++++++++++ lib/common/pagination.dart | 34 ------------ lib/discover/discover_providers.dart | 54 ++++++++++---------- lib/favorites/favorites_provider.dart | 42 +++++++-------- lib/media/media_providers.dart | 34 ++++++------ lib/notifications/notification_provider.dart | 10 ++-- lib/review/review_providers.dart | 16 +++--- lib/review/reviews_view.dart | 30 +++++------ lib/staff/staff_providers.dart | 18 +++---- lib/studio/studio_models.dart | 6 +-- lib/studio/studio_providers.dart | 6 +-- lib/user/friends_provider.dart | 14 ++--- lib/widgets/pagination_view.dart | 44 ++++++++-------- pubspec.lock | 4 +- pubspec.yaml | 2 +- 19 files changed, 205 insertions(+), 208 deletions(-) create mode 100644 lib/common/paged.dart delete mode 100644 lib/common/pagination.dart diff --git a/lib/activity/activities_providers.dart b/lib/activity/activities_providers.dart index d4a9ff85..1ee6ca9d 100644 --- a/lib/activity/activities_providers.dart +++ b/lib/activity/activities_providers.dart @@ -1,13 +1,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/activity/activity_models.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; import 'package:otraku/home/home_provider.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; import 'package:otraku/utils/options.dart'; final activitiesProvider = StateNotifierProvider.autoDispose - .family>, int?>( + .family>, int?>( (ref, userId) => ActivitiesNotifier( userId: userId, viewerId: Options().id!, @@ -35,8 +35,7 @@ final activityFilterProvider = StateNotifierProvider.autoDispose }, ); -class ActivitiesNotifier - extends StateNotifier>> { +class ActivitiesNotifier extends StateNotifier>> { ActivitiesNotifier({ required this.userId, required this.viewerId, @@ -58,7 +57,7 @@ class ActivitiesNotifier Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.activities, { 'typeIn': filter.typeIn.map((t) => t.name).toList(), @@ -84,7 +83,7 @@ class ActivitiesNotifier _lastCreatedAt = data['Page']['activities'].last['createdAt']; } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); @@ -99,7 +98,7 @@ class ActivitiesNotifier final activity = Activity.maybe(map, viewerId); if (activity == null) return; - state = AsyncData(Pagination.from( + state = AsyncData(Paged( items: [activity, ...value.items], hasNext: value.hasNext, next: value.next, @@ -117,7 +116,7 @@ class ActivitiesNotifier for (int i = 0; i < value.items.length; i++) { if (value.items[i].id == activity.id) { value.items[i] = activity; - state = AsyncData(Pagination.from( + state = AsyncData(Paged( items: value.items, hasNext: value.hasNext, next: value.next, @@ -135,7 +134,7 @@ class ActivitiesNotifier for (int i = 0; i < value.items.length; i++) { if (value.items[i].id == activity.id) { value.items[i] = activity; - state = AsyncData(Pagination.from( + state = AsyncData(Paged( items: value.items, hasNext: value.hasNext, next: value.next, @@ -153,7 +152,7 @@ class ActivitiesNotifier for (int i = 0; i < value.items.length; i++) { if (value.items[i].id == activityId) { value.items.removeAt(i); - state = AsyncData(Pagination.from( + state = AsyncData(Paged( items: value.items, hasNext: value.hasNext, next: value.next, @@ -176,7 +175,7 @@ class ActivitiesNotifier value.items[0].isPinned = false; } - state = AsyncData(Pagination.from( + state = AsyncData(Paged( items: value.items, hasNext: value.hasNext, next: value.next, diff --git a/lib/activity/activity_models.dart b/lib/activity/activity_models.dart index 99c15e56..ec181a21 100644 --- a/lib/activity/activity_models.dart +++ b/lib/activity/activity_models.dart @@ -1,12 +1,12 @@ import 'package:otraku/utils/convert.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; import 'package:otraku/utils/options.dart'; class ActivityState { ActivityState(this.activity, this.replies); final Activity activity; - final Pagination replies; + final Paged replies; } class ActivityReply { diff --git a/lib/activity/activity_provider.dart b/lib/activity/activity_provider.dart index aada8580..0937e342 100644 --- a/lib/activity/activity_provider.dart +++ b/lib/activity/activity_provider.dart @@ -2,7 +2,7 @@ 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/common/paged.dart'; import 'package:otraku/utils/options.dart'; /// Toggles an activity like and returns an error if unsuccessful. @@ -93,7 +93,7 @@ class ActivityNotifier extends StateNotifier> { Future fetch() async { state = await AsyncValue.guard(() async { - final replies = state.value?.replies ?? Pagination(); + final replies = state.value?.replies ?? const Paged(); final data = await Api.get(GqlQuery.activity, { 'id': userId, @@ -113,7 +113,7 @@ class ActivityNotifier extends StateNotifier> { return ActivityState( activity, - replies.append( + replies.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ), @@ -143,7 +143,7 @@ class ActivityNotifier extends StateNotifier> { value.activity.replyCount++; state = AsyncData(ActivityState( value.activity, - Pagination.from( + Paged( items: [...value.replies.items, reply], hasNext: value.replies.hasNext, next: value.replies.next, @@ -164,7 +164,7 @@ class ActivityNotifier extends StateNotifier> { value.replies.items[i] = reply; state = AsyncData(ActivityState( value.activity, - Pagination.from( + Paged( items: value.replies.items, hasNext: value.replies.hasNext, next: value.replies.next, @@ -187,7 +187,7 @@ class ActivityNotifier extends StateNotifier> { state = AsyncData(ActivityState( value.activity, - Pagination.from( + Paged( items: value.replies.items, hasNext: value.replies.hasNext, next: value.replies.next, diff --git a/lib/character/character_providers.dart b/lib/character/character_providers.dart index 211ec2f0..e784ab4d 100644 --- a/lib/character/character_providers.dart +++ b/lib/character/character_providers.dart @@ -6,7 +6,7 @@ import 'package:otraku/common/relation.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; import 'package:otraku/utils/options.dart'; /// Favorite/Unfavorite character. Returns `true` if successful. @@ -44,8 +44,8 @@ class CharacterMediaNotifier extends ChangeNotifier { final int id; final CharacterFilter filter; - var _anime = const AsyncValue>.loading(); - var _manga = const AsyncValue>.loading(); + var _anime = const AsyncValue>.loading(); + var _manga = const AsyncValue>.loading(); /// For each language, a list of voice actors /// is mapped to the corresponding media's id. @@ -54,8 +54,8 @@ class CharacterMediaNotifier extends ChangeNotifier { /// The currently selected language. var _language = ''; - AsyncValue> get anime => _anime; - AsyncValue> get manga => _manga; + AsyncValue> get anime => _anime; + AsyncValue> get manga => _manga; Iterable get languages => _languages.keys; String get language => _language; set language(String l) { @@ -115,8 +115,8 @@ class CharacterMediaNotifier extends ChangeNotifier { return; } - _anime = AsyncValue.data(Pagination()); - _manga = AsyncValue.data(Pagination()); + _anime = const AsyncValue.data(Paged()); + _manga = const AsyncValue.data(Paged()); _initAnime(data.value!['anime']); _initManga(data.value!['manga']); @@ -194,7 +194,7 @@ class CharacterMediaNotifier extends ChangeNotifier { } } - value = value.append(items, data['pageInfo']['hasNextPage']); + value = value.withNext(items, data['pageInfo']['hasNextPage']); _anime = AsyncValue.data(value); } @@ -213,7 +213,7 @@ class CharacterMediaNotifier extends ChangeNotifier { )); } - value = value.append(items, data['pageInfo']['hasNextPage']); + value = value.withNext(items, data['pageInfo']['hasNextPage']); _manga = AsyncValue.data(value); } } diff --git a/lib/common/paged.dart b/lib/common/paged.dart new file mode 100644 index 00000000..19b969dc --- /dev/null +++ b/lib/common/paged.dart @@ -0,0 +1,44 @@ +/// Used for pagination. +class Paged { + const Paged({ + this.items = const [], + this.hasNext = true, + this.next = 1, + }); + + final List items; + + /// If it's possible to load a new page, or the list is complete. + final bool hasNext; + + /// The index of the next page to be loaded. + final int next; + + /// Recreate with another page loaded. + Paged withNext(List items, bool hasNext) => Paged( + items: [...this.items, ...items], + hasNext: hasNext, + next: next + 1, + ); +} + +/// [Paged] that additionally keeps track of the total amount of items. +class PagedWithTotal extends Paged { + const PagedWithTotal({ + super.items, + super.hasNext, + super.next, + this.total = 0, + }); + + final int total; + + @override + PagedWithTotal withNext(List items, bool hasNext, [int? total]) => + PagedWithTotal( + items: [...this.items, ...items], + hasNext: hasNext, + next: next + 1, + total: total ?? this.total, + ); +} diff --git a/lib/common/pagination.dart b/lib/common/pagination.dart deleted file mode 100644 index 1d54e9d3..00000000 --- a/lib/common/pagination.dart +++ /dev/null @@ -1,34 +0,0 @@ -class Pagination { - Pagination._({ - required this.items, - required this.hasNext, - required this.next, - }); - - factory Pagination() => Pagination._(items: [], hasNext: true, next: 1); - - factory Pagination.from({ - required List items, - required bool hasNext, - int next = 2, - }) => - Pagination._(items: items, hasNext: hasNext, next: next); - - final List items; - - /// If it's possible to load a new page, or the list is complete. - final bool hasNext; - - /// The index of the next page to be loaded. - final int next; - - /// Recreate [this] as if a new page has been loaded - append items at the - /// back and use a new [hasNext]. Note that instead of creating a new list, - /// [newItems] are appended to the old [items]. This is because [this] is - /// expected to get discarded. - Pagination append(List newItems, bool newHasNext) => Pagination._( - items: items..addAll(newItems), - hasNext: newHasNext, - next: next + 1, - ); -} diff --git a/lib/discover/discover_providers.dart b/lib/discover/discover_providers.dart index 55551630..7423b7c9 100644 --- a/lib/discover/discover_providers.dart +++ b/lib/discover/discover_providers.dart @@ -12,7 +12,7 @@ import 'package:otraku/studio/studio_models.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; /// Fetches another page on the discover tab, depending on the selected type. void discoverLoadMore(WidgetRef ref) { @@ -44,7 +44,7 @@ void discoverLoadMore(WidgetRef ref) { final _searchSelector = (String? s) => s == null || s.isEmpty ? null : s; final discoverAnimeProvider = StateNotifierProvider.autoDispose< - DiscoverMediaNotifier, AsyncValue>>( + DiscoverMediaNotifier, AsyncValue>>( (ref) { final discoverFilter = ref.watch(discoverFilterProvider); return DiscoverMediaNotifier( @@ -57,7 +57,7 @@ final discoverAnimeProvider = StateNotifierProvider.autoDispose< ); final discoverMangaProvider = StateNotifierProvider.autoDispose< - DiscoverMediaNotifier, AsyncValue>>( + DiscoverMediaNotifier, AsyncValue>>( (ref) { final discoverFilter = ref.watch(discoverFilterProvider); return DiscoverMediaNotifier( @@ -70,7 +70,7 @@ final discoverMangaProvider = StateNotifierProvider.autoDispose< ); final discoverCharacterProvider = StateNotifierProvider.autoDispose< - DiscoverCharacterNotifier, AsyncValue>>( + DiscoverCharacterNotifier, AsyncValue>>( (ref) => DiscoverCharacterNotifier( ref.watch(searchProvider(null).select(_searchSelector)), ref.watch(discoverFilterProvider.select((s) => s.birthday)), @@ -79,7 +79,7 @@ final discoverCharacterProvider = StateNotifierProvider.autoDispose< ); final discoverStaffProvider = StateNotifierProvider.autoDispose< - DiscoverStaffNotifier, AsyncValue>>( + DiscoverStaffNotifier, AsyncValue>>( (ref) => DiscoverStaffNotifier( ref.watch(searchProvider(null).select(_searchSelector)), ref.watch(discoverFilterProvider.select((s) => s.birthday)), @@ -88,7 +88,7 @@ final discoverStaffProvider = StateNotifierProvider.autoDispose< ); final discoverStudioProvider = StateNotifierProvider.autoDispose< - DiscoverStudioNotifier, AsyncValue>>( + DiscoverStudioNotifier, AsyncValue>>( (ref) => DiscoverStudioNotifier( ref.watch(searchProvider(null).select(_searchSelector)), ref.watch(homeProvider.select((s) => s.didLoadDiscover)), @@ -96,7 +96,7 @@ final discoverStudioProvider = StateNotifierProvider.autoDispose< ); final discoverUserProvider = StateNotifierProvider.autoDispose< - DiscoverUserNotifier, AsyncValue>>( + DiscoverUserNotifier, AsyncValue>>( (ref) => DiscoverUserNotifier( ref.watch(searchProvider(null).select(_searchSelector)), ref.watch(homeProvider.select((s) => s.didLoadDiscover)), @@ -104,7 +104,7 @@ final discoverUserProvider = StateNotifierProvider.autoDispose< ); final discoverReviewProvider = StateNotifierProvider.autoDispose< - DiscoverReviewNotifier, AsyncValue>>( + DiscoverReviewNotifier, AsyncValue>>( (ref) => DiscoverReviewNotifier( ref.watch(reviewSortProvider(null)), ref.watch(homeProvider.select((s) => s.didLoadDiscover)), @@ -112,7 +112,7 @@ final discoverReviewProvider = StateNotifierProvider.autoDispose< ); class DiscoverMediaNotifier - extends StateNotifier>> { + extends StateNotifier>> { DiscoverMediaNotifier(this.filter, this.search, bool shouldLoad) : super(const AsyncValue.loading()) { if (shouldLoad) fetch(); @@ -123,7 +123,7 @@ class DiscoverMediaNotifier Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.medias, { 'page': value.next, @@ -140,7 +140,7 @@ class DiscoverMediaNotifier items.add(DiscoverMediaItem(m)); } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); @@ -149,7 +149,7 @@ class DiscoverMediaNotifier } class DiscoverCharacterNotifier - extends StateNotifier>> { + extends StateNotifier>> { DiscoverCharacterNotifier(this.search, this.isBirthday, bool shouldLoad) : super(const AsyncValue.loading()) { if (shouldLoad) fetch(); @@ -160,7 +160,7 @@ class DiscoverCharacterNotifier Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.characters, { 'page': value.next, @@ -173,7 +173,7 @@ class DiscoverCharacterNotifier items.add(characterItem(c)); } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); @@ -181,8 +181,7 @@ class DiscoverCharacterNotifier } } -class DiscoverStaffNotifier - extends StateNotifier>> { +class DiscoverStaffNotifier extends StateNotifier>> { DiscoverStaffNotifier(this.search, this.isBirthday, bool shouldLoad) : super(const AsyncValue.loading()) { if (shouldLoad) fetch(); @@ -193,7 +192,7 @@ class DiscoverStaffNotifier Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.staffs, { 'page': value.next, @@ -206,7 +205,7 @@ class DiscoverStaffNotifier items.add(staffItem(s)); } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); @@ -215,7 +214,7 @@ class DiscoverStaffNotifier } class DiscoverStudioNotifier - extends StateNotifier>> { + extends StateNotifier>> { DiscoverStudioNotifier(this.search, bool shouldLoad) : super(const AsyncValue.loading()) { if (shouldLoad) fetch(); @@ -225,7 +224,7 @@ class DiscoverStudioNotifier Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.studios, { 'page': value.next, @@ -237,7 +236,7 @@ class DiscoverStudioNotifier items.add(StudioItem(s)); } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); @@ -245,8 +244,7 @@ class DiscoverStudioNotifier } } -class DiscoverUserNotifier - extends StateNotifier>> { +class DiscoverUserNotifier extends StateNotifier>> { DiscoverUserNotifier(this.search, bool shouldLoad) : super(const AsyncValue.loading()) { if (shouldLoad) fetch(); @@ -256,7 +254,7 @@ class DiscoverUserNotifier Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.users, { 'page': value.next, @@ -268,7 +266,7 @@ class DiscoverUserNotifier items.add(UserItem(u)); } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); @@ -277,7 +275,7 @@ class DiscoverUserNotifier } class DiscoverReviewNotifier - extends StateNotifier>> { + extends StateNotifier>> { DiscoverReviewNotifier(this.sort, bool shouldLoad) : super(const AsyncValue.loading()) { if (shouldLoad) fetch(); @@ -287,7 +285,7 @@ class DiscoverReviewNotifier Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.reviews, { 'page': value.next, @@ -299,7 +297,7 @@ class DiscoverReviewNotifier items.add(ReviewItem(r)); } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); diff --git a/lib/favorites/favorites_provider.dart b/lib/favorites/favorites_provider.dart index 280aa2a3..f6ec628d 100644 --- a/lib/favorites/favorites_provider.dart +++ b/lib/favorites/favorites_provider.dart @@ -8,7 +8,7 @@ import 'package:otraku/staff/staff_models.dart'; import 'package:otraku/studio/studio_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; final favoritesProvider = ChangeNotifierProvider.autoDispose.family( @@ -28,11 +28,11 @@ class FavoritesNotifier extends ChangeNotifier { int _characterCount = 0; int _staffCount = 0; int _studioCount = 0; - var _anime = const AsyncValue>.loading(); - var _manga = const AsyncValue>.loading(); - var _characters = const AsyncValue>.loading(); - var _staff = const AsyncValue>.loading(); - var _studios = const AsyncValue>.loading(); + var _anime = const AsyncValue>.loading(); + var _manga = const AsyncValue>.loading(); + var _characters = const AsyncValue>.loading(); + var _staff = const AsyncValue>.loading(); + var _studios = const AsyncValue>.loading(); int getCount(FavoriteType type) { _type = type; @@ -50,11 +50,11 @@ class FavoritesNotifier extends ChangeNotifier { } } - AsyncValue> get anime => _anime; - AsyncValue> get manga => _manga; - AsyncValue> get characters => _characters; - AsyncValue> get staff => _staff; - AsyncValue> get studios => _studios; + AsyncValue> get anime => _anime; + AsyncValue> get manga => _manga; + AsyncValue> get characters => _characters; + AsyncValue> get staff => _staff; + AsyncValue> get studios => _studios; Future fetch() async { final type = _type; @@ -97,7 +97,7 @@ class FavoritesNotifier extends ChangeNotifier { _anime = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['anime']; - final value = _anime.valueOrNull ?? Pagination(); + final value = _anime.valueOrNull ?? const Paged(); if (_animeCount == 0) _animeCount = map['pageInfo']['total'] ?? 0; @@ -106,7 +106,7 @@ class FavoritesNotifier extends ChangeNotifier { items.add(mediaItem(a)); } - return Future.value(value.append( + return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, )); @@ -117,7 +117,7 @@ class FavoritesNotifier extends ChangeNotifier { _manga = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['manga']; - final value = _manga.valueOrNull ?? Pagination(); + final value = _manga.valueOrNull ?? const Paged(); if (_mangaCount == 0) _mangaCount = map['pageInfo']['total'] ?? 0; @@ -126,7 +126,7 @@ class FavoritesNotifier extends ChangeNotifier { items.add(mediaItem(m)); } - return Future.value(value.append( + return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, )); @@ -137,7 +137,7 @@ class FavoritesNotifier extends ChangeNotifier { _characters = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['characters']; - final value = _characters.valueOrNull ?? Pagination(); + final value = _characters.valueOrNull ?? const Paged(); if (_characterCount == 0) { _characterCount = map['pageInfo']['total'] ?? 0; @@ -148,7 +148,7 @@ class FavoritesNotifier extends ChangeNotifier { items.add(characterItem(c)); } - return Future.value(value.append( + return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, )); @@ -159,7 +159,7 @@ class FavoritesNotifier extends ChangeNotifier { _staff = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['staff']; - final value = _staff.valueOrNull ?? Pagination(); + final value = _staff.valueOrNull ?? const Paged(); if (_staffCount == 0) _staffCount = map['pageInfo']['total'] ?? 0; @@ -168,7 +168,7 @@ class FavoritesNotifier extends ChangeNotifier { items.add(staffItem(s)); } - return Future.value(value.append( + return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, )); @@ -179,7 +179,7 @@ class FavoritesNotifier extends ChangeNotifier { _studios = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['studios']; - final value = _studios.valueOrNull ?? Pagination(); + final value = _studios.valueOrNull ?? const Paged(); if (_studioCount == 0) _studioCount = map['pageInfo']['total'] ?? 0; @@ -188,7 +188,7 @@ class FavoritesNotifier extends ChangeNotifier { items.add(StudioItem(s)); } - return Future.value(value.append( + return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, )); diff --git a/lib/media/media_providers.dart b/lib/media/media_providers.dart index c391eed6..a4dcf9c6 100644 --- a/lib/media/media_providers.dart +++ b/lib/media/media_providers.dart @@ -8,7 +8,7 @@ import 'package:otraku/settings/settings_provider.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; Future toggleFavoriteMedia(int id, bool isAnime) async { try { @@ -69,10 +69,10 @@ class MediaContentNotifier extends ChangeNotifier { final int mediaId; - var _recommended = const AsyncValue>.loading(); - var _characters = const AsyncValue>.loading(); - var _staff = const AsyncValue>.loading(); - var _reviews = const AsyncValue>.loading(); + var _recommended = const AsyncValue>.loading(); + var _characters = const AsyncValue>.loading(); + var _staff = const AsyncValue>.loading(); + var _reviews = const AsyncValue>.loading(); int _languageIndex = 0; final languages = []; @@ -85,10 +85,10 @@ class MediaContentNotifier extends ChangeNotifier { notifyListeners(); } - AsyncValue> get recommended => _recommended; - AsyncValue> get characters => _characters; - AsyncValue> get staff => _staff; - AsyncValue> get reviews => _reviews; + AsyncValue> get recommended => _recommended; + AsyncValue> get characters => _characters; + AsyncValue> get staff => _staff; + AsyncValue> get reviews => _reviews; void selectCharactersAndVoiceActors( List characterList, @@ -138,10 +138,10 @@ class MediaContentNotifier extends ChangeNotifier { return; } - _recommended = AsyncValue.data(Pagination()); - _characters = AsyncValue.data(Pagination()); - _staff = AsyncValue.data(Pagination()); - _reviews = AsyncValue.data(Pagination()); + _recommended = const AsyncValue.data(Paged()); + _characters = const AsyncValue.data(Paged()); + _staff = const AsyncValue.data(Paged()); + _reviews = const AsyncValue.data(Paged()); _initRecommended(data.value!['recommendations']); _initCharacters(data.value!['characters']); @@ -247,7 +247,7 @@ class MediaContentNotifier extends ChangeNotifier { if (r['mediaRecommendation'] != null) items.add(Recommendation(r)); } - value = value.append( + value = value.withNext( items, map['pageInfo']['hasNextPage'], ); @@ -296,7 +296,7 @@ class MediaContentNotifier extends ChangeNotifier { } } - value = value.append( + value = value.withNext( items, map['pageInfo']['hasNextPage'], ); @@ -318,7 +318,7 @@ class MediaContentNotifier extends ChangeNotifier { )); } - value = value.append( + value = value.withNext( items, map['pageInfo']['hasNextPage'], ); @@ -335,7 +335,7 @@ class MediaContentNotifier extends ChangeNotifier { if (item != null) items.add(item); } - value = value.append( + value = value.withNext( items, map['pageInfo']['hasNextPage'], ); diff --git a/lib/notifications/notification_provider.dart b/lib/notifications/notification_provider.dart index b9a42f40..850edee5 100644 --- a/lib/notifications/notification_provider.dart +++ b/lib/notifications/notification_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/notifications/notification_model.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; @@ -21,14 +21,14 @@ class NotificationsNotifier extends ChangeNotifier { final NotificationFilterType filter; int _unreadCount = 0; - var _notifications = const AsyncValue>.loading(); + var _notifications = const AsyncValue>.loading(); int get unreadCount => _unreadCount; - AsyncValue> get notifications => _notifications; + AsyncValue> get notifications => _notifications; Future fetch() async { _notifications = await AsyncValue.guard(() async { - final value = _notifications.valueOrNull ?? Pagination(); + final value = _notifications.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.notifications, { 'page': value.next, @@ -50,7 +50,7 @@ class NotificationsNotifier extends ChangeNotifier { if (item != null) items.add(item); } - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, ); diff --git a/lib/review/review_providers.dart b/lib/review/review_providers.dart index 3fc4bb7c..5753dcbe 100644 --- a/lib/review/review_providers.dart +++ b/lib/review/review_providers.dart @@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/review/review_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; final reviewProvider = StateNotifierProvider.autoDispose .family, int>( @@ -14,7 +14,7 @@ final reviewSortProvider = StateProvider.autoDispose.family( ); final reviewsProvider = StateNotifierProvider.autoDispose - .family>, int>( + .family>, int>( (ref, userId) => ReviewsNotifier(userId, ref.watch(reviewSortProvider(userId))), ); @@ -58,7 +58,7 @@ class ReviewNotifier extends StateNotifier> { } class ReviewsNotifier - extends StateNotifier>> { + extends StateNotifier>> { ReviewsNotifier(this.userId, this.sort) : super(const AsyncValue.loading()) { fetch(); } @@ -66,12 +66,9 @@ class ReviewsNotifier final int userId; final ReviewSort sort; - int _reviewCount = 0; - int get reviewCount => _reviewCount; - Future fetch() async { state = await AsyncValue.guard(() async { - final value = state.valueOrNull ?? Pagination(); + final value = state.valueOrNull ?? const PagedWithTotal(); final data = await Api.get(GqlQuery.reviews, { 'userId': userId, @@ -84,11 +81,10 @@ class ReviewsNotifier items.add(ReviewItem(r)); } - _reviewCount = data['Page']['pageInfo']?['total'] ?? 0; - - return value.append( + return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, + data['Page']['pageInfo']['total'] ?? value.total, ); }); } diff --git a/lib/review/reviews_view.dart b/lib/review/reviews_view.dart index ada1ab4c..8fb57837 100644 --- a/lib/review/reviews_view.dart +++ b/lib/review/reviews_view.dart @@ -33,11 +33,9 @@ class _ReviewsViewState extends ConsumerState { @override Widget build(BuildContext context) { - // 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; + final count = ref.watch( + reviewsProvider(widget.id).select((s) => s.valueOrNull?.total ?? 0), + ); return PageScaffold( child: TabScaffold( @@ -89,18 +87,16 @@ class _ReviewsViewState extends ConsumerState { ], ), 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), - ); - }, + builder: (context, ref, refreshControl) => 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/staff/staff_providers.dart b/lib/staff/staff_providers.dart index 36bfbaf7..c6ee183f 100644 --- a/lib/staff/staff_providers.dart +++ b/lib/staff/staff_providers.dart @@ -6,7 +6,7 @@ import 'package:otraku/staff/staff_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; import 'package:otraku/utils/options.dart'; /// Favorite/Unfavorite staff. Returns `true` if successful. @@ -45,12 +45,12 @@ class StaffRelationNotifier extends ChangeNotifier { final int id; final StaffFilter filter; final _characterMedia = []; - var _characters = const AsyncValue>.loading(); - var _roles = const AsyncValue>.loading(); + var _characters = const AsyncValue>.loading(); + var _roles = const AsyncValue>.loading(); List get characterMedia => _characterMedia; - AsyncValue> get characters => _characters; - AsyncValue> get roles => _roles; + AsyncValue> get characters => _characters; + AsyncValue> get roles => _roles; Future _fetch() async { final data = await AsyncValue.guard>(() async { @@ -71,8 +71,8 @@ class StaffRelationNotifier extends ChangeNotifier { return; } - _characters = AsyncValue.data(Pagination()); - _roles = AsyncValue.data(Pagination()); + _characters = const AsyncValue.data(Paged()); + _roles = const AsyncValue.data(Paged()); _initCharacters(data.value!['characterMedia']); _initRoles(data.value!['staffMedia']); @@ -140,7 +140,7 @@ class StaffRelationNotifier extends ChangeNotifier { } } - value = value.append(items, data['pageInfo']['hasNextPage']); + value = value.withNext(items, data['pageInfo']['hasNextPage']); _characters = AsyncValue.data(value); } @@ -161,7 +161,7 @@ class StaffRelationNotifier extends ChangeNotifier { )); } - value = value.append(items, data['pageInfo']['hasNextPage']); + value = value.withNext(items, data['pageInfo']['hasNextPage']); _roles = AsyncValue.data(value); } } diff --git a/lib/studio/studio_models.dart b/lib/studio/studio_models.dart index b5b0d3d2..21d87e56 100644 --- a/lib/studio/studio_models.dart +++ b/lib/studio/studio_models.dart @@ -1,6 +1,6 @@ import 'package:otraku/common/tile_item.dart'; import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; class StudioItem { StudioItem._({required this.id, required this.name}); @@ -37,7 +37,7 @@ class StudioState { StudioState(this.studio, this.media, this.categories); final Studio studio; - final Pagination media; + final Paged media; /// If the items in [media] are sorted by date, [categories] will represent /// each time category (e.g. "2022") and the index of the first item in @@ -46,7 +46,7 @@ class StudioState { /// If the items in [media] aren't sorted by date, [categories] must be empty. final Map categories; - StudioState copyWith(Pagination media) => + StudioState copyWith(Paged media) => StudioState(studio, media, categories); } diff --git a/lib/studio/studio_providers.dart b/lib/studio/studio_providers.dart index bf59a399..9f78b89a 100644 --- a/lib/studio/studio_providers.dart +++ b/lib/studio/studio_providers.dart @@ -5,7 +5,7 @@ import 'package:otraku/media/media_models.dart'; import 'package:otraku/studio/studio_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; /// Favorite/Unfavorite studio. Returns `true` if successful. Future toggleFavoriteStudio(int studioId) async { @@ -46,7 +46,7 @@ class StudioNotifier extends StateNotifier> { data = data['Studio']; return _initMedia( - StudioState(Studio(data), Pagination(), {}), + StudioState(Studio(data), const Paged(), {}), data['media'], ); }); @@ -103,7 +103,7 @@ class StudioNotifier extends StateNotifier> { return StudioState( s.studio, - s.media.append(items, data['pageInfo']['hasNextPage']), + s.media.withNext(items, data['pageInfo']['hasNextPage']), s.categories, ); } diff --git a/lib/user/friends_provider.dart b/lib/user/friends_provider.dart index f0bcad80..3cc9b2ab 100644 --- a/lib/user/friends_provider.dart +++ b/lib/user/friends_provider.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; final friendsProvider = ChangeNotifierProvider.autoDispose.family( @@ -18,8 +18,8 @@ class FriendsNotifier extends ChangeNotifier { bool _onFollowing = false; int _followingCount = 0; int _followersCount = 0; - var _following = AsyncValue.data(Pagination()); - var _followers = AsyncValue.data(Pagination()); + var _following = const AsyncValue.data(Paged()); + var _followers = const AsyncValue.data(Paged()); int getCount(bool onFollowing) { _onFollowing = onFollowing; @@ -43,8 +43,8 @@ class FriendsNotifier extends ChangeNotifier { return _followersCount; } - AsyncValue> get following => _following; - AsyncValue> get followers => _followers; + AsyncValue> get following => _following; + AsyncValue> get followers => _followers; Future fetch() async { final onFollowing = _onFollowing; @@ -54,7 +54,7 @@ class FriendsNotifier extends ChangeNotifier { var users = onFollowing ? _following : _followers; users = await AsyncValue.guard(() async { - final value = users.valueOrNull ?? Pagination(); + final value = users.valueOrNull ?? const Paged(); final data = await Api.get(GqlQuery.friends, { 'userId': userId, @@ -76,7 +76,7 @@ class FriendsNotifier extends ChangeNotifier { items.add(UserItem(u)); } - return value.append( + return value.withNext( items, data[key]['pageInfo']['hasNextPage'] ?? false, ); diff --git a/lib/widgets/pagination_view.dart b/lib/widgets/pagination_view.dart index 17eb8311..fd93b26a 100644 --- a/lib/widgets/pagination_view.dart +++ b/lib/widgets/pagination_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/utils/consts.dart'; -import 'package:otraku/common/pagination.dart'; +import 'package:otraku/common/paged.dart'; import 'package:otraku/widgets/layouts/constrained_view.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -21,11 +21,11 @@ class PaginationView extends StatelessWidget { required this.onData, }); - final ProviderListenable>> provider; + final ProviderListenable>> provider; final ScrollController scrollCtrl; final Future Function() onRefresh; final String dataType; - final Widget Function(Pagination) onData; + final Widget Function(Paged) onData; @override Widget build(BuildContext context) { @@ -45,28 +45,26 @@ class PaginationView extends StatelessWidget { ); var hasNext = false; - final child = ref - .watch>>(provider) - .unwrapPrevious() - .when( - loading: () => const SliverFillRemaining( - child: Center(child: Loader()), - ), - error: (_, __) => SliverFillRemaining( - child: Center(child: Text('Failed to load $dataType')), - ), - data: (data) { - hasNext = data.hasNext; + final child = + ref.watch>>(provider).unwrapPrevious().when( + loading: () => const SliverFillRemaining( + child: Center(child: Loader()), + ), + error: (_, __) => SliverFillRemaining( + child: Center(child: Text('Failed to load $dataType')), + ), + data: (data) { + hasNext = data.hasNext; - if (data.items.isEmpty) { - return SliverFillRemaining( - child: Center(child: Text('No $dataType')), - ); - } + if (data.items.isEmpty) { + return SliverFillRemaining( + child: Center(child: Text('No $dataType')), + ); + } - return onData(data); - }, - ); + return onData(data); + }, + ); return ConstrainedView( child: CustomScrollView( diff --git a/pubspec.lock b/pubspec.lock index d77902f0..e5334c2c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -428,10 +428,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" path_provider_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fd0d45c3..7e8c75d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: hive: ^2.2.3 # Access to device storage. Used for [hive] setup. - path_provider: ^2.0.13 + path_provider: ^2.0.14 # Secure storage for the access tokens. flutter_secure_storage: ^8.0.0 From 710b0b4571afdc3f5d6a5fa83690990f643686e4 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Mon, 3 Apr 2023 18:46:00 +0300 Subject: [PATCH 11/55] Favorites now use state notifier --- lib/favorites/favorites_model.dart | 58 +++++ lib/favorites/favorites_provider.dart | 162 +++++-------- lib/favorites/favorites_view.dart | 336 +++++--------------------- 3 files changed, 171 insertions(+), 385 deletions(-) create mode 100644 lib/favorites/favorites_model.dart diff --git a/lib/favorites/favorites_model.dart b/lib/favorites/favorites_model.dart new file mode 100644 index 00000000..bef59465 --- /dev/null +++ b/lib/favorites/favorites_model.dart @@ -0,0 +1,58 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/common/paged.dart'; +import 'package:otraku/common/tile_item.dart'; +import 'package:otraku/studio/studio_models.dart'; + +class Favorites { + const Favorites({ + this.anime = const AsyncValue.loading(), + this.manga = const AsyncValue.loading(), + this.characters = const AsyncValue.loading(), + this.staff = const AsyncValue.loading(), + this.studios = const AsyncValue.loading(), + }); + + final AsyncValue> anime; + final AsyncValue> manga; + final AsyncValue> characters; + final AsyncValue> staff; + final AsyncValue> studios; + + int getCount(FavoritesTab tab) { + switch (tab) { + case FavoritesTab.anime: + return anime.valueOrNull?.total ?? 0; + case FavoritesTab.manga: + return manga.valueOrNull?.total ?? 0; + case FavoritesTab.characters: + return characters.valueOrNull?.total ?? 0; + case FavoritesTab.staff: + return staff.valueOrNull?.total ?? 0; + case FavoritesTab.studios: + return studios.valueOrNull?.total ?? 0; + } + } +} + +enum FavoritesTab { + anime, + manga, + characters, + staff, + studios; + + String get title { + switch (this) { + case FavoritesTab.anime: + return 'Favourite Anime'; + case FavoritesTab.manga: + return 'Favourite Manga'; + case FavoritesTab.characters: + return 'Favourite Characters'; + case FavoritesTab.staff: + return 'Favourite Staff'; + case FavoritesTab.studios: + return 'Favourite Studios'; + } + } +} diff --git a/lib/favorites/favorites_provider.dart b/lib/favorites/favorites_provider.dart index f6ec628d..f091b549 100644 --- a/lib/favorites/favorites_provider.dart +++ b/lib/favorites/favorites_provider.dart @@ -1,8 +1,7 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/character/character_models.dart'; import 'package:otraku/common/tile_item.dart'; +import 'package:otraku/favorites/favorites_model.dart'; import 'package:otraku/media/media_models.dart'; import 'package:otraku/staff/staff_models.dart'; import 'package:otraku/studio/studio_models.dart'; @@ -11,81 +10,48 @@ import 'package:otraku/utils/graphql.dart'; import 'package:otraku/common/paged.dart'; final favoritesProvider = - ChangeNotifierProvider.autoDispose.family( + StateNotifierProvider.autoDispose.family( (ref, userId) => FavoritesNotifier(userId), ); -class FavoritesNotifier extends ChangeNotifier { - FavoritesNotifier(this.userId) { - fetch(); +class FavoritesNotifier extends StateNotifier { + FavoritesNotifier(this.userId) : super(const Favorites()) { + _fetch(null); } final int userId; - FavoriteType? _type; - int _animeCount = 0; - int _mangaCount = 0; - int _characterCount = 0; - int _staffCount = 0; - int _studioCount = 0; - var _anime = const AsyncValue>.loading(); - var _manga = const AsyncValue>.loading(); - var _characters = const AsyncValue>.loading(); - var _staff = const AsyncValue>.loading(); - var _studios = const AsyncValue>.loading(); - - int getCount(FavoriteType type) { - _type = type; - switch (type) { - case FavoriteType.anime: - return _animeCount; - case FavoriteType.manga: - return _mangaCount; - case FavoriteType.characters: - return _characterCount; - case FavoriteType.staff: - return _staffCount; - case FavoriteType.studios: - return _studioCount; - } - } + Future fetch(FavoritesTab tab) => _fetch(tab); - AsyncValue> get anime => _anime; - AsyncValue> get manga => _manga; - AsyncValue> get characters => _characters; - AsyncValue> get staff => _staff; - AsyncValue> get studios => _studios; - - Future fetch() async { - final type = _type; + Future _fetch(FavoritesTab? tab) async { final variables = {'userId': userId}; - if (type == null) { + if (tab == null) { variables['withAnime'] = true; variables['withManga'] = true; variables['withCharacters'] = true; variables['withStaff'] = true; variables['withStudios'] = true; - } else if (type == FavoriteType.anime) { - if (!(_anime.valueOrNull?.hasNext ?? true)) return; + } else if (tab == FavoritesTab.anime) { + if (!(state.anime.valueOrNull?.hasNext ?? true)) return; variables['withAnime'] = true; - variables['page'] = _anime.valueOrNull?.next ?? 1; - } else if (type == FavoriteType.manga) { - if (!(_manga.valueOrNull?.hasNext ?? true)) return; + variables['page'] = state.anime.valueOrNull?.next ?? 1; + } else if (tab == FavoritesTab.manga) { + if (!(state.manga.valueOrNull?.hasNext ?? true)) return; variables['withManga'] = true; - variables['page'] = _manga.valueOrNull?.next ?? 1; - } else if (type == FavoriteType.characters) { - if (!(_characters.valueOrNull?.hasNext ?? true)) return; + variables['page'] = state.manga.valueOrNull?.next ?? 1; + } else if (tab == FavoritesTab.characters) { + if (!(state.characters.valueOrNull?.hasNext ?? true)) return; variables['withCharacters'] = true; - variables['page'] = _characters.valueOrNull?.next ?? 1; - } else if (type == FavoriteType.staff) { - if (!(_staff.valueOrNull?.hasNext ?? true)) return; + variables['page'] = state.characters.valueOrNull?.next ?? 1; + } else if (tab == FavoritesTab.staff) { + if (!(state.staff.valueOrNull?.hasNext ?? true)) return; variables['withStaff'] = true; - variables['page'] = _staff.valueOrNull?.next ?? 1; + variables['page'] = state.staff.valueOrNull?.next ?? 1; } else { - if (!(_studios.valueOrNull?.hasNext ?? true)) return; + if (!(state.studios.valueOrNull?.hasNext ?? true)) return; variables['withStudios'] = true; - variables['page'] = _studios.valueOrNull?.next ?? 1; + variables['page'] = state.studios.valueOrNull?.next ?? 1; } final data = await AsyncValue.guard>(() async { @@ -93,13 +59,17 @@ class FavoritesNotifier extends ChangeNotifier { return data['User']['favourites']; }); - if (type == null || type == FavoriteType.anime) { - _anime = await AsyncValue.guard(() { + var anime = state.anime; + var manga = state.manga; + var characters = state.characters; + var staff = state.staff; + var studios = state.studios; + + if (tab == null || tab == FavoritesTab.anime) { + anime = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['anime']; - final value = _anime.valueOrNull ?? const Paged(); - - if (_animeCount == 0) _animeCount = map['pageInfo']['total'] ?? 0; + final value = anime.valueOrNull ?? const PagedWithTotal(); final items = []; for (final a in map['nodes']) { @@ -109,17 +79,16 @@ class FavoritesNotifier extends ChangeNotifier { return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, + map['pageInfo']['total'], )); }); } - if (type == null || type == FavoriteType.manga) { - _manga = await AsyncValue.guard(() { + if (tab == null || tab == FavoritesTab.manga) { + manga = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['manga']; - final value = _manga.valueOrNull ?? const Paged(); - - if (_mangaCount == 0) _mangaCount = map['pageInfo']['total'] ?? 0; + final value = manga.valueOrNull ?? const PagedWithTotal(); final items = []; for (final m in map['nodes']) { @@ -129,19 +98,16 @@ class FavoritesNotifier extends ChangeNotifier { return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, + map['pageInfo']['total'], )); }); } - if (type == null || type == FavoriteType.characters) { - _characters = await AsyncValue.guard(() { + if (tab == null || tab == FavoritesTab.characters) { + characters = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['characters']; - final value = _characters.valueOrNull ?? const Paged(); - - if (_characterCount == 0) { - _characterCount = map['pageInfo']['total'] ?? 0; - } + final value = characters.valueOrNull ?? const PagedWithTotal(); final items = []; for (final c in map['nodes']) { @@ -151,17 +117,16 @@ class FavoritesNotifier extends ChangeNotifier { return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, + map['pageInfo']['total'], )); }); } - if (type == null || type == FavoriteType.staff) { - _staff = await AsyncValue.guard(() { + if (tab == null || tab == FavoritesTab.staff) { + staff = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['staff']; - final value = _staff.valueOrNull ?? const Paged(); - - if (_staffCount == 0) _staffCount = map['pageInfo']['total'] ?? 0; + final value = staff.valueOrNull ?? const PagedWithTotal(); final items = []; for (final s in map['nodes']) { @@ -171,17 +136,16 @@ class FavoritesNotifier extends ChangeNotifier { return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, + map['pageInfo']['total'], )); }); } - if (type == null || type == FavoriteType.studios) { - _studios = await AsyncValue.guard(() { + if (tab == null || tab == FavoritesTab.studios) { + studios = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['studios']; - final value = _studios.valueOrNull ?? const Paged(); - - if (_studioCount == 0) _studioCount = map['pageInfo']['total'] ?? 0; + final value = studios.valueOrNull ?? const PagedWithTotal(); final items = []; for (final s in map['nodes']) { @@ -191,33 +155,17 @@ class FavoritesNotifier extends ChangeNotifier { return Future.value(value.withNext( items, map['pageInfo']['hasNextPage'] ?? false, + map['pageInfo']['total'], )); }); } - notifyListeners(); - } -} - -enum FavoriteType { - anime, - manga, - characters, - staff, - studios; - - String get text { - switch (this) { - case FavoriteType.anime: - return 'Favourite Anime'; - case FavoriteType.manga: - return 'Favourite Manga'; - case FavoriteType.characters: - return 'Favourite Characters'; - case FavoriteType.staff: - return 'Favourite Staff'; - case FavoriteType.studios: - return 'Favourite Studios'; - } + state = Favorites( + anime: anime, + manga: manga, + characters: characters, + staff: staff, + studios: studios, + ); } } diff --git a/lib/favorites/favorites_view.dart b/lib/favorites/favorites_view.dart index 564e2502..f4a6c049 100644 --- a/lib/favorites/favorites_view.dart +++ b/lib/favorites/favorites_view.dart @@ -1,18 +1,18 @@ 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/common/tile_item.dart'; +import 'package:otraku/favorites/favorites_model.dart'; +import 'package:otraku/studio/studio_models.dart'; import 'package:otraku/favorites/favorites_provider.dart'; import 'package:otraku/studio/studio_grid.dart'; 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/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'; +import 'package:otraku/widgets/pagination_view.dart'; class FavoritesView extends ConsumerStatefulWidget { const FavoritesView(this.id); @@ -24,26 +24,33 @@ class FavoritesView extends ConsumerStatefulWidget { } class _FavoritesViewState extends ConsumerState { - FavoriteType _tab = FavoriteType.anime; + FavoritesTab _tab = FavoritesTab.anime; late final _ctrl = PaginationController( - loadMore: () => ref.read(favoritesProvider(widget.id)).fetch(), + loadMore: () => ref.read(favoritesProvider(widget.id).notifier).fetch(_tab), ); + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final count = ref.watch( favoritesProvider(widget.id).select((s) => s.getCount(_tab)), ); - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(favoritesProvider(widget.id)), - ); + final onRefresh = () { + ref.invalidate(favoritesProvider(widget.id)); + return Future.value(); + }; return PageScaffold( bottomBar: BottomBarIconTabs( current: _tab.index, onChanged: (page) { - setState(() => _tab = FavoriteType.values.elementAt(page)); + setState(() => _tab = FavoritesTab.values.elementAt(page)); _ctrl.scrollToTop(); }, onSame: (_) => _ctrl.scrollToTop(), @@ -57,7 +64,7 @@ class _FavoritesViewState extends ConsumerState { ), child: TabScaffold( topBar: TopBar( - title: _tab.text, + title: _tab.title, trailing: [ if (count > 0) Padding( @@ -71,277 +78,50 @@ class _FavoritesViewState extends ConsumerState { ), child: DirectPageView( current: _tab.index, - onChanged: (page) => - setState(() => _tab = FavoriteType.values.elementAt(page)), + onChanged: (page) => setState( + () => _tab = FavoritesTab.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), + PaginationView( + provider: favoritesProvider(widget.id).select((s) => s.anime), + onData: (data) => TileItemGrid(data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + dataType: 'favourite anime', + ), + PaginationView( + provider: favoritesProvider(widget.id).select((s) => s.manga), + onData: (data) => TileItemGrid(data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + dataType: 'favourite manga', + ), + PaginationView( + provider: favoritesProvider(widget.id).select( + (s) => s.characters, + ), + onData: (data) => TileItemGrid(data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + dataType: 'favourite characters', + ), + PaginationView( + provider: favoritesProvider(widget.id).select((s) => s.staff), + onData: (data) => TileItemGrid(data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + dataType: 'favourite staff', + ), + PaginationView( + provider: favoritesProvider(widget.id).select((s) => s.studios), + onData: (data) => StudioGrid(data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + dataType: 'favourite studios', + ), ], ), ), ); } } - -class _AnimeTab extends StatelessWidget { - const _AnimeTab(this.id, this._ctrl, this.refreshControl); - - final int id; - final PaginationController _ctrl; - final Widget refreshControl; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - ref.listen( - favoritesProvider(id), - (_, s) { - s.anime.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load anime', - content: error.toString(), - ), - ), - ); - }, - ); - - return ref.watch(favoritesProvider(id)).anime.when( - loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load favourite anime')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No favourite anime')); - } - - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl, - TileItemGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }); - }, - ); - } -} - -class _MangaTab extends StatelessWidget { - const _MangaTab(this.id, this._ctrl, this.refreshControl); - - final int id; - final PaginationController _ctrl; - final Widget refreshControl; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - ref.listen( - favoritesProvider(id), - (_, s) { - s.manga.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load manga', - content: error.toString(), - ), - ), - ); - }, - ); - - return ref.watch(favoritesProvider(id)).manga.when( - loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load favourite manga')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No favourite manga')); - } - - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl, - TileItemGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }); - }, - ); - } -} - -class _CharactersTab extends StatelessWidget { - const _CharactersTab(this.id, this._ctrl, this.refreshControl); - - final int id; - final PaginationController _ctrl; - final Widget refreshControl; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - ref.listen( - favoritesProvider(id), - (_, s) { - s.characters.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load characters', - content: error.toString(), - ), - ), - ); - }, - ); - - return ref.watch(favoritesProvider(id)).characters.when( - loading: () => const Center(child: Loader()), - error: (_, __) => const Center( - child: Text('Failed to load favourite characters')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No favourite characters')); - } - - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl, - TileItemGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }); - }, - ); - } -} - -class _StaffTab extends StatelessWidget { - const _StaffTab(this.id, this._ctrl, this.refreshControl); - - final int id; - final PaginationController _ctrl; - final Widget refreshControl; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - ref.listen( - favoritesProvider(id), - (_, s) { - s.staff.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load staff', - content: error.toString(), - ), - ), - ); - }, - ); - - return ref.watch(favoritesProvider(id)).staff.when( - loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load favourite staff')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No favourite staff')); - } - - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl, - TileItemGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }); - }, - ); - } -} - -class _StudiosTab extends StatelessWidget { - const _StudiosTab(this.id, this._ctrl, this.refreshControl); - - final int id; - final PaginationController _ctrl; - final Widget refreshControl; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - ref.listen( - favoritesProvider(id), - (_, s) { - s.studios.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load studios', - content: error.toString(), - ), - ), - ); - }, - ); - - return ref.watch(favoritesProvider(id)).studios.when( - loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load favourite studios')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No favourite studios')); - } - - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl, - StudioGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }); - }, - ); - } -} From bb150e305749036af000ee407810a4f859ba16b3 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Tue, 4 Apr 2023 19:02:06 +0300 Subject: [PATCH 12/55] Following/followers now uses state notifier --- lib/user/friends_model.dart | 17 +++++ lib/user/friends_provider.dart | 131 ++++++++++++++++----------------- lib/user/friends_view.dart | 98 +++++------------------- 3 files changed, 101 insertions(+), 145 deletions(-) create mode 100644 lib/user/friends_model.dart diff --git a/lib/user/friends_model.dart b/lib/user/friends_model.dart new file mode 100644 index 00000000..37b39b95 --- /dev/null +++ b/lib/user/friends_model.dart @@ -0,0 +1,17 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/common/paged.dart'; +import 'package:otraku/user/user_models.dart'; + +class Friends { + const Friends({ + this.following = const AsyncValue.loading(), + this.followers = const AsyncValue.loading(), + }); + + final AsyncValue> following; + final AsyncValue> followers; + + int getCount(bool onFollowing) => onFollowing + ? following.valueOrNull?.total ?? 0 + : followers.valueOrNull?.total ?? 0; +} diff --git a/lib/user/friends_provider.dart b/lib/user/friends_provider.dart index 3cc9b2ab..e0dc82fb 100644 --- a/lib/user/friends_provider.dart +++ b/lib/user/friends_provider.dart @@ -1,88 +1,85 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/user/friends_model.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; import 'package:otraku/common/paged.dart'; final friendsProvider = - ChangeNotifierProvider.autoDispose.family( + StateNotifierProvider.autoDispose.family( (ref, userId) => FriendsNotifier(userId), ); -class FriendsNotifier extends ChangeNotifier { - FriendsNotifier(this.userId); +class FriendsNotifier extends StateNotifier { + FriendsNotifier(this.userId) : super(const Friends()) { + _fetch(null); + } final int userId; - bool _onFollowing = false; - int _followingCount = 0; - int _followersCount = 0; - var _following = const AsyncValue.data(Paged()); - var _followers = const AsyncValue.data(Paged()); - - int getCount(bool onFollowing) { - _onFollowing = onFollowing; - if (onFollowing) { - if (_following is AsyncData && - _following.value!.items.isEmpty && - _following.value!.hasNext) { - _following = const AsyncValue.loading(); - fetch(); - } - - return _followingCount; + Future fetch(bool onFollowing) => _fetch(onFollowing); + + Future _fetch(bool? onFollowing) async { + final variables = {'userId': userId}; + + if (onFollowing == null) { + variables['withFollowing'] = true; + variables['withFollowers'] = true; + } else if (onFollowing) { + if (!(state.following.valueOrNull?.hasNext ?? true)) return; + variables['withFollowing'] = true; + variables['page'] = state.following.valueOrNull?.next ?? 1; + } else { + if (!(state.followers.valueOrNull?.hasNext ?? true)) return; + variables['withFollowers'] = true; + variables['page'] = state.followers.valueOrNull?.next ?? 1; } - if (_followers is AsyncData && - _followers.value!.items.isEmpty && - _followers.value!.hasNext) { - _followers = const AsyncValue.loading(); - fetch(); + final data = await AsyncValue.guard( + () => Api.get(GqlQuery.friends, variables), + ); + + var following = state.following; + var followers = state.followers; + + if (onFollowing == null || onFollowing) { + following = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['following']; + final value = following.valueOrNull ?? const PagedWithTotal(); + + final items = []; + for (final u in map['following']) { + items.add(UserItem(u)); + } + + return Future.value(value.withNext( + items, + map['pageInfo']['hasNextPage'] ?? false, + map['pageInfo']['total'], + )); + }); } - return _followersCount; - } - - AsyncValue> get following => _following; - AsyncValue> get followers => _followers; - - Future fetch() async { - final onFollowing = _onFollowing; - if (onFollowing && !(_following.valueOrNull?.hasNext ?? true) || - !onFollowing && !(_followers.valueOrNull?.hasNext ?? true)) return; - var users = onFollowing ? _following : _followers; - - users = await AsyncValue.guard(() async { - final value = users.valueOrNull ?? const Paged(); - - final data = await Api.get(GqlQuery.friends, { - 'userId': userId, - 'page': value.next, - 'withFollowing': onFollowing, - 'withFollowers': !onFollowing, + if (onFollowing == null || !onFollowing) { + followers = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['followers']; + final value = followers.valueOrNull ?? const PagedWithTotal(); + + final items = []; + for (final u in map['followers']) { + items.add(UserItem(u)); + } + + return Future.value(value.withNext( + items, + map['pageInfo']['hasNextPage'] ?? false, + map['pageInfo']['total'], + )); }); + } - final key = onFollowing ? 'following' : 'followers'; - final count = data[key]['pageInfo']['total'] ?? 0; - if (onFollowing) { - if (_followingCount == 0) _followingCount = count; - } else { - if (_followersCount == 0) _followersCount = count; - } - - final items = []; - for (final u in data[key][key]) { - items.add(UserItem(u)); - } - - return value.withNext( - items, - data[key]['pageInfo']['hasNextPage'] ?? false, - ); - }); - - onFollowing ? _following = users : _followers = users; - notifyListeners(); + state = Friends(following: following, followers: followers); } } diff --git a/lib/user/friends_view.dart b/lib/user/friends_view.dart index eff88068..91031a5a 100644 --- a/lib/user/friends_view.dart +++ b/lib/user/friends_view.dart @@ -1,17 +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/user/user_models.dart'; 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/constrained_view.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'; +import 'package:otraku/widgets/pagination_view.dart'; class FriendsView extends ConsumerStatefulWidget { const FriendsView(this.id); @@ -25,7 +23,8 @@ class FriendsView extends ConsumerStatefulWidget { class _FriendsViewState extends ConsumerState { late bool _onFollowing = true; late final _ctrl = PaginationController( - loadMore: () => ref.read(friendsProvider(widget.id).notifier).fetch(), + loadMore: () => + ref.read(friendsProvider(widget.id).notifier).fetch(_onFollowing), ); @override @@ -40,9 +39,10 @@ class _FriendsViewState extends ConsumerState { friendsProvider(widget.id).select((s) => s.getCount(_onFollowing)), ); - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(friendsProvider(widget.id)), - ); + final onRefresh = () { + ref.invalidate(friendsProvider(widget.id)); + return Future.value(); + }; return PageScaffold( bottomBar: BottomBarIconTabs( @@ -76,17 +76,19 @@ class _FriendsViewState extends ConsumerState { setState(() => _onFollowing = page == 0 ? true : false); }, children: [ - _FriendTab( - id: widget.id, - onFollowing: true, - refreshControl: refreshControl, - paginationController: _ctrl, + PaginationView( + provider: friendsProvider(widget.id).select((s) => s.following), + onData: (data) => UserGrid(data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + dataType: 'following', ), - _FriendTab( - id: widget.id, - onFollowing: false, - refreshControl: refreshControl, - paginationController: _ctrl, + PaginationView( + provider: friendsProvider(widget.id).select((s) => s.followers), + onData: (data) => UserGrid(data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + dataType: 'followers', ), ], ), @@ -94,63 +96,3 @@ class _FriendsViewState extends ConsumerState { ); } } - -class _FriendTab extends StatelessWidget { - const _FriendTab({ - required this.id, - required this.onFollowing, - required this.refreshControl, - required this.paginationController, - }); - - final int id; - final bool onFollowing; - final Widget refreshControl; - final PaginationController paginationController; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - ref.listen( - friendsProvider(id), - (_, s) { - final users = onFollowing ? s.following : s.followers; - users.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load users', - content: error.toString(), - ), - ), - ); - }, - ); - - final notifier = ref.watch(friendsProvider(id)); - final users = onFollowing ? notifier.following : notifier.followers; - return users.when( - loading: () => const Center(child: Loader()), - error: (_, __) => const Center(child: Text('Failed to load users')), - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No Users')); - } - - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: paginationController, - slivers: [ - refreshControl, - UserGrid(data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }); - }, - ); - } -} From 2b4f7267122f6f3f7b04bd246dcbced0f95aca52 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Tue, 4 Apr 2023 19:04:58 +0300 Subject: [PATCH 13/55] Renaming --- lib/activity/activities_view.dart | 8 ++++---- lib/activity/activity_view.dart | 4 ++-- lib/character/character_view.dart | 4 ++-- lib/discover/discover_view.dart | 20 +++++++++---------- lib/favorites/favorites_view.dart | 16 +++++++-------- lib/home/home_view.dart | 4 ++-- lib/media/media_people_view.dart | 2 +- lib/media/media_view.dart | 4 ++-- lib/notifications/notifications_view.dart | 6 +++--- lib/review/reviews_view.dart | 8 ++++---- lib/settings/settings_view.dart | 2 +- lib/staff/staff_view.dart | 4 ++-- lib/statistics/statistics_view.dart | 2 +- lib/studio/studio_view.dart | 4 ++-- lib/user/friends_view.dart | 10 +++++----- ..._controller.dart => paged_controller.dart} | 4 ++-- .../{pagination_view.dart => paged_view.dart} | 4 ++-- 17 files changed, 53 insertions(+), 53 deletions(-) rename lib/utils/{pagination_controller.dart => paged_controller.dart} (93%) rename lib/widgets/{pagination_view.dart => paged_view.dart} (97%) diff --git a/lib/activity/activities_view.dart b/lib/activity/activities_view.dart index ad820a33..fc19a395 100644 --- a/lib/activity/activities_view.dart +++ b/lib/activity/activities_view.dart @@ -8,7 +8,7 @@ import 'package:otraku/composition/composition_view.dart'; import 'package:otraku/settings/settings_provider.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/activity/activity_models.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/fields/checkbox_field.dart'; @@ -17,7 +17,7 @@ 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'; +import 'package:otraku/widgets/paged_view.dart'; void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { final filter = ref.read(activityFilterProvider(id)); @@ -81,7 +81,7 @@ class ActivitiesView extends ConsumerStatefulWidget { } class _ActivitiesViewState extends ConsumerState { - late final _ctrl = PaginationController( + late final _ctrl = PagedController( loadMore: () => ref.read(activitiesProvider(widget.id).notifier).fetch(), ); @@ -137,7 +137,7 @@ class ActivitiesSubView extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, ref, _) { - return PaginationView( + return PagedView( provider: activitiesProvider(id), scrollCtrl: scrollCtrl, dataType: 'activities', diff --git a/lib/activity/activity_view.dart b/lib/activity/activity_view.dart index a65c12ca..c456b4e1 100644 --- a/lib/activity/activity_view.dart +++ b/lib/activity/activity_view.dart @@ -9,7 +9,7 @@ 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/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/link_tile.dart'; @@ -31,7 +31,7 @@ class ActivityView extends ConsumerStatefulWidget { } class _ActivityViewState extends ConsumerState { - late final _ctrl = PaginationController( + late final _ctrl = PagedController( loadMore: () => ref.read(activityProvider(widget.id).notifier).fetch(), ); diff --git a/lib/character/character_view.dart b/lib/character/character_view.dart index 8f6c9e19..7914ebbb 100644 --- a/lib/character/character_view.dart +++ b/lib/character/character_view.dart @@ -4,7 +4,7 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/character/character_providers.dart'; 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/utils/paged_controller.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; @@ -23,7 +23,7 @@ class CharacterView extends ConsumerStatefulWidget { class _CharacterViewState extends ConsumerState { int _tab = 0; - late final _ctrl = PaginationController(loadMore: () { + late final _ctrl = PagedController(loadMore: () { if (_tab == 0) return; _tab == 1 ? ref.read(characterMediaProvider(widget.id)).fetchPage(true) diff --git a/lib/discover/discover_view.dart b/lib/discover/discover_view.dart index 365b905a..93f0ed76 100644 --- a/lib/discover/discover_view.dart +++ b/lib/discover/discover_view.dart @@ -15,7 +15,7 @@ import 'package:otraku/user/user_grid.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/utils/convert.dart'; import 'package:otraku/review/review_grid.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_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'; @@ -23,12 +23,12 @@ 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'; +import 'package:otraku/widgets/paged_view.dart'; class DiscoverView extends ConsumerWidget { const DiscoverView(this.scrollCtrl); - final PaginationController scrollCtrl; + final PagedController scrollCtrl; @override Widget build(BuildContext context, WidgetRef ref) { @@ -250,7 +250,7 @@ class _Grid extends StatelessWidget { switch (type) { case DiscoverType.anime: - return PaginationView( + return PagedView( provider: discoverAnimeProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, @@ -260,7 +260,7 @@ class _Grid extends StatelessWidget { : TileItemGrid(data.items), ); case DiscoverType.manga: - return PaginationView( + return PagedView( provider: discoverMangaProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, @@ -270,7 +270,7 @@ class _Grid extends StatelessWidget { : TileItemGrid(data.items), ); case DiscoverType.character: - return PaginationView( + return PagedView( provider: discoverCharacterProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, @@ -278,7 +278,7 @@ class _Grid extends StatelessWidget { onData: (data) => TileItemGrid(data.items), ); case DiscoverType.staff: - return PaginationView( + return PagedView( provider: discoverStaffProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, @@ -286,7 +286,7 @@ class _Grid extends StatelessWidget { onData: (data) => TileItemGrid(data.items), ); case DiscoverType.studio: - return PaginationView( + return PagedView( provider: discoverStudioProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, @@ -294,7 +294,7 @@ class _Grid extends StatelessWidget { onData: (data) => StudioGrid(data.items), ); case DiscoverType.user: - return PaginationView( + return PagedView( provider: discoverUserProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, @@ -302,7 +302,7 @@ class _Grid extends StatelessWidget { onData: (data) => UserGrid(data.items), ); case DiscoverType.review: - return PaginationView( + return PagedView( provider: discoverReviewProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, diff --git a/lib/favorites/favorites_view.dart b/lib/favorites/favorites_view.dart index f4a6c049..90febf8e 100644 --- a/lib/favorites/favorites_view.dart +++ b/lib/favorites/favorites_view.dart @@ -6,13 +6,13 @@ import 'package:otraku/favorites/favorites_model.dart'; import 'package:otraku/studio/studio_models.dart'; import 'package:otraku/favorites/favorites_provider.dart'; import 'package:otraku/studio/studio_grid.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/widgets/grids/tile_item_grid.dart'; import 'package:otraku/widgets/layouts/bottom_bar.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/pagination_view.dart'; +import 'package:otraku/widgets/paged_view.dart'; class FavoritesView extends ConsumerStatefulWidget { const FavoritesView(this.id); @@ -25,7 +25,7 @@ class FavoritesView extends ConsumerStatefulWidget { class _FavoritesViewState extends ConsumerState { FavoritesTab _tab = FavoritesTab.anime; - late final _ctrl = PaginationController( + late final _ctrl = PagedController( loadMore: () => ref.read(favoritesProvider(widget.id).notifier).fetch(_tab), ); @@ -82,21 +82,21 @@ class _FavoritesViewState extends ConsumerState { () => _tab = FavoritesTab.values.elementAt(page), ), children: [ - PaginationView( + PagedView( provider: favoritesProvider(widget.id).select((s) => s.anime), onData: (data) => TileItemGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, dataType: 'favourite anime', ), - PaginationView( + PagedView( provider: favoritesProvider(widget.id).select((s) => s.manga), onData: (data) => TileItemGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, dataType: 'favourite manga', ), - PaginationView( + PagedView( provider: favoritesProvider(widget.id).select( (s) => s.characters, ), @@ -105,14 +105,14 @@ class _FavoritesViewState extends ConsumerState { onRefresh: onRefresh, dataType: 'favourite characters', ), - PaginationView( + PagedView( provider: favoritesProvider(widget.id).select((s) => s.staff), onData: (data) => TileItemGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, dataType: 'favourite staff', ), - PaginationView( + PagedView( provider: favoritesProvider(widget.id).select((s) => s.studios), onData: (data) => StudioGrid(data.items), scrollCtrl: _ctrl, diff --git a/lib/home/home_view.dart b/lib/home/home_view.dart index 3aea2979..747ff04c 100644 --- a/lib/home/home_view.dart +++ b/lib/home/home_view.dart @@ -13,7 +13,7 @@ import 'package:otraku/home/home_provider.dart'; import 'package:otraku/settings/settings_provider.dart'; import 'package:otraku/tag/tag_provider.dart'; import 'package:otraku/user/user_providers.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/discover/discover_view.dart'; import 'package:otraku/collection/collection_view.dart'; @@ -41,7 +41,7 @@ class HomeView extends ConsumerStatefulWidget { } class _HomeViewState extends ConsumerState { - late final _ctrl = PaginationController(loadMore: _scrollListener); + late final _ctrl = PagedController(loadMore: _scrollListener); late final animeCollectionTag = CollectionTag(widget.id, true); late final mangaCollectionTag = CollectionTag(widget.id, false); diff --git a/lib/media/media_people_view.dart b/lib/media/media_people_view.dart index 5ec55a61..59464d4f 100644 --- a/lib/media/media_people_view.dart +++ b/lib/media/media_people_view.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/media/media_providers.dart'; import 'package:otraku/common/relation.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/widgets/grids/relation_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; diff --git a/lib/media/media_view.dart b/lib/media/media_view.dart index 530a79df..8c42de70 100644 --- a/lib/media/media_view.dart +++ b/lib/media/media_view.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/media/media_models.dart'; import 'package:otraku/media/media_providers.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/media/media_info_view.dart'; import 'package:otraku/media/media_other_view.dart'; import 'package:otraku/media/media_people_view.dart'; @@ -95,7 +95,7 @@ class _MediaViewState extends State { } } -/// Due to [NestedScrollView] limitations, the custom [PaginationController] +/// Due to [NestedScrollView] limitations, the custom [PagedController] /// can't be used here and has to be reimplemented temporarely on the inner /// scroll controller of the [NestedScrollView]. /// For more context: https://github.com/flutter/flutter/pull/104166. diff --git a/lib/notifications/notifications_view.dart b/lib/notifications/notifications_view.dart index e864c7ee..28c9b96f 100644 --- a/lib/notifications/notifications_view.dart +++ b/lib/notifications/notifications_view.dart @@ -7,7 +7,7 @@ import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/notifications/notification_model.dart'; import 'package:otraku/notifications/notification_provider.dart'; import 'package:otraku/utils/background_handler.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/edit/edit_view.dart'; import 'package:otraku/widgets/layouts/constrained_view.dart'; @@ -30,13 +30,13 @@ class NotificationsView extends ConsumerStatefulWidget { } class _NotificationsViewState extends ConsumerState { - late final PaginationController _ctrl; + late final PagedController _ctrl; @override void initState() { super.initState(); BackgroundHandler.clearNotifications(); - _ctrl = PaginationController( + _ctrl = PagedController( loadMore: () => ref.read(notificationsProvider).fetch(), ); } diff --git a/lib/review/reviews_view.dart b/lib/review/reviews_view.dart index 8fb57837..beab1e48 100644 --- a/lib/review/reviews_view.dart +++ b/lib/review/reviews_view.dart @@ -3,13 +3,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.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/utils/paged_controller.dart'; import 'package:otraku/review/review_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.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'; +import 'package:otraku/widgets/paged_view.dart'; class ReviewsView extends ConsumerStatefulWidget { const ReviewsView(this.id); @@ -21,7 +21,7 @@ class ReviewsView extends ConsumerStatefulWidget { } class _ReviewsViewState extends ConsumerState { - late final _ctrl = PaginationController( + late final _ctrl = PagedController( loadMore: () => ref.read(reviewsProvider(widget.id).notifier).fetch(), ); @@ -87,7 +87,7 @@ class _ReviewsViewState extends ConsumerState { ], ), child: Consumer( - builder: (context, ref, refreshControl) => PaginationView( + builder: (context, ref, refreshControl) => PagedView( provider: reviewsProvider(widget.id), scrollCtrl: _ctrl, dataType: 'reviews', diff --git a/lib/settings/settings_view.dart b/lib/settings/settings_view.dart index 443a1c16..f839c971 100644 --- a/lib/settings/settings_view.dart +++ b/lib/settings/settings_view.dart @@ -4,7 +4,7 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/collection/collection_models.dart'; import 'package:otraku/collection/collection_providers.dart'; import 'package:otraku/settings/settings_provider.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/settings/settings_app_tab.dart'; import 'package:otraku/settings/settings_content_tab.dart'; diff --git a/lib/staff/staff_view.dart b/lib/staff/staff_view.dart index de6340d0..92de9ada 100644 --- a/lib/staff/staff_view.dart +++ b/lib/staff/staff_view.dart @@ -4,7 +4,7 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/staff/staff_info_tab.dart'; 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/utils/paged_controller.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; @@ -23,7 +23,7 @@ class StaffView extends ConsumerStatefulWidget { class _StaffViewState extends ConsumerState { int _tab = 0; - late final _ctrl = PaginationController(loadMore: () { + late final _ctrl = PagedController(loadMore: () { if (_tab == 0) return; _tab == 1 ? ref.read(staffRelationProvider(widget.id)).fetchPage(true) diff --git a/lib/statistics/statistics_view.dart b/lib/statistics/statistics_view.dart index e682d3dc..5984c195 100644 --- a/lib/statistics/statistics_view.dart +++ b/lib/statistics/statistics_view.dart @@ -4,7 +4,7 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/statistics/user_statistics.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/user/user_providers.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_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'; diff --git a/lib/studio/studio_view.dart b/lib/studio/studio_view.dart index 71a72b1b..bbd21456 100644 --- a/lib/studio/studio_view.dart +++ b/lib/studio/studio_view.dart @@ -8,7 +8,7 @@ import 'package:otraku/staff/staff_providers.dart'; import 'package:otraku/studio/studio_models.dart'; import 'package:otraku/studio/studio_providers.dart'; import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_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'; @@ -30,7 +30,7 @@ class StudioView extends ConsumerStatefulWidget { } class _StudioViewState extends ConsumerState { - late final _ctrl = PaginationController(loadMore: () { + late final _ctrl = PagedController(loadMore: () { ref.read(studioProvider(widget.id).notifier).fetchPage(); }); diff --git a/lib/user/friends_view.dart b/lib/user/friends_view.dart index 91031a5a..b0d77740 100644 --- a/lib/user/friends_view.dart +++ b/lib/user/friends_view.dart @@ -4,12 +4,12 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/user/friends_provider.dart'; import 'package:otraku/user/user_grid.dart'; -import 'package:otraku/utils/pagination_controller.dart'; +import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/widgets/layouts/bottom_bar.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/pagination_view.dart'; +import 'package:otraku/widgets/paged_view.dart'; class FriendsView extends ConsumerStatefulWidget { const FriendsView(this.id); @@ -22,7 +22,7 @@ class FriendsView extends ConsumerStatefulWidget { class _FriendsViewState extends ConsumerState { late bool _onFollowing = true; - late final _ctrl = PaginationController( + late final _ctrl = PagedController( loadMore: () => ref.read(friendsProvider(widget.id).notifier).fetch(_onFollowing), ); @@ -76,14 +76,14 @@ class _FriendsViewState extends ConsumerState { setState(() => _onFollowing = page == 0 ? true : false); }, children: [ - PaginationView( + PagedView( provider: friendsProvider(widget.id).select((s) => s.following), onData: (data) => UserGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, dataType: 'following', ), - PaginationView( + PagedView( provider: friendsProvider(widget.id).select((s) => s.followers), onData: (data) => UserGrid(data.items), scrollCtrl: _ctrl, diff --git a/lib/utils/pagination_controller.dart b/lib/utils/paged_controller.dart similarity index 93% rename from lib/utils/pagination_controller.dart rename to lib/utils/paged_controller.dart index c07dabeb..cbeab691 100644 --- a/lib/utils/pagination_controller.dart +++ b/lib/utils/paged_controller.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; /// A [ScrollController] that can perform and action when /// the bottom of the page is reached. Used for pagination. -class PaginationController extends ScrollController { - PaginationController({required this.loadMore}) { +class PagedController extends ScrollController { + PagedController({required this.loadMore}) { addListener(_listener); } diff --git a/lib/widgets/pagination_view.dart b/lib/widgets/paged_view.dart similarity index 97% rename from lib/widgets/pagination_view.dart rename to lib/widgets/paged_view.dart index fd93b26a..f9d34a14 100644 --- a/lib/widgets/pagination_view.dart +++ b/lib/widgets/paged_view.dart @@ -12,8 +12,8 @@ import 'package:otraku/widgets/overlays/dialogs.dart'; /// [PaginationController], pagination will automatically work. /// [dateType] is a lowercase word for the data being handled (e.g. "reviews"). /// [onData] should return a sliver widget. -class PaginationView extends StatelessWidget { - const PaginationView({ +class PagedView extends StatelessWidget { + const PagedView({ required this.provider, required this.scrollCtrl, required this.onRefresh, From 79b800bb2a9ad80f41eba9e9c3ea32c8ae1a0208 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Tue, 4 Apr 2023 19:09:52 +0300 Subject: [PATCH 14/55] Cleanup --- lib/activity/activities_view.dart | 1 - lib/collection/collection_preview_view.dart | 5 +++-- lib/discover/discover_view.dart | 17 ++++++++--------- lib/favorites/favorites_view.dart | 5 +---- lib/review/reviews_view.dart | 7 ++----- lib/user/friends_view.dart | 5 +---- lib/widgets/paged_view.dart | 2 +- 7 files changed, 16 insertions(+), 26 deletions(-) diff --git a/lib/activity/activities_view.dart b/lib/activity/activities_view.dart index fc19a395..6721fd35 100644 --- a/lib/activity/activities_view.dart +++ b/lib/activity/activities_view.dart @@ -146,7 +146,6 @@ class ActivitiesSubView extends StatelessWidget { if (id == null) { ref.read(settingsProvider.notifier).refetchUnread(); } - return Future.value(); }, onData: (data) => SliverList( delegate: SliverChildBuilderDelegate( diff --git a/lib/collection/collection_preview_view.dart b/lib/collection/collection_preview_view.dart index e3937e3c..8eb2f71e 100644 --- a/lib/collection/collection_preview_view.dart +++ b/lib/collection/collection_preview_view.dart @@ -121,8 +121,9 @@ class _CollectionPreviewViewState extends State { controller: widget.scrollCtrl, slivers: [ SliverRefreshControl( - onRefresh: () => - ref.invalidate(collectionPreviewProvider(widget.tag)), + onRefresh: () => ref.invalidate( + collectionPreviewProvider(widget.tag), + ), ), content, const SliverFooter(), diff --git a/lib/discover/discover_view.dart b/lib/discover/discover_view.dart index 93f0ed76..c9b8c774 100644 --- a/lib/discover/discover_view.dart +++ b/lib/discover/discover_view.dart @@ -37,27 +37,26 @@ class DiscoverView extends ConsumerWidget { switch (type) { case DiscoverType.anime: ref.invalidate(discoverAnimeProvider); - break; + return; case DiscoverType.manga: ref.invalidate(discoverMangaProvider); - break; + return; case DiscoverType.character: ref.invalidate(discoverCharacterProvider); - break; + return; case DiscoverType.staff: ref.invalidate(discoverStaffProvider); - break; + return; case DiscoverType.studio: ref.invalidate(discoverStudioProvider); - break; + return; case DiscoverType.user: ref.invalidate(discoverUserProvider); - break; + return; case DiscoverType.review: ref.invalidate(discoverReviewProvider); - break; + return; } - return Future.value(); }; return TabScaffold( @@ -240,7 +239,7 @@ class _Grid extends StatelessWidget { const _Grid(this.scrollCtrl, this.onRefresh); final ScrollController scrollCtrl; - final Future Function() onRefresh; + final void Function() onRefresh; @override Widget build(BuildContext context) { diff --git a/lib/favorites/favorites_view.dart b/lib/favorites/favorites_view.dart index 90febf8e..420b9648 100644 --- a/lib/favorites/favorites_view.dart +++ b/lib/favorites/favorites_view.dart @@ -41,10 +41,7 @@ class _FavoritesViewState extends ConsumerState { favoritesProvider(widget.id).select((s) => s.getCount(_tab)), ); - final onRefresh = () { - ref.invalidate(favoritesProvider(widget.id)); - return Future.value(); - }; + final onRefresh = () => ref.invalidate(favoritesProvider(widget.id)); return PageScaffold( bottomBar: BottomBarIconTabs( diff --git a/lib/review/reviews_view.dart b/lib/review/reviews_view.dart index beab1e48..a9ac1193 100644 --- a/lib/review/reviews_view.dart +++ b/lib/review/reviews_view.dart @@ -89,13 +89,10 @@ class _ReviewsViewState extends ConsumerState { child: Consumer( builder: (context, ref, refreshControl) => PagedView( provider: reviewsProvider(widget.id), + onData: (data) => ReviewGrid(data.items), + onRefresh: () => ref.invalidate(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/user/friends_view.dart b/lib/user/friends_view.dart index b0d77740..52d6f9f7 100644 --- a/lib/user/friends_view.dart +++ b/lib/user/friends_view.dart @@ -39,10 +39,7 @@ class _FriendsViewState extends ConsumerState { friendsProvider(widget.id).select((s) => s.getCount(_onFollowing)), ); - final onRefresh = () { - ref.invalidate(friendsProvider(widget.id)); - return Future.value(); - }; + final onRefresh = () => ref.invalidate(friendsProvider(widget.id)); return PageScaffold( bottomBar: BottomBarIconTabs( diff --git a/lib/widgets/paged_view.dart b/lib/widgets/paged_view.dart index f9d34a14..ed707bf8 100644 --- a/lib/widgets/paged_view.dart +++ b/lib/widgets/paged_view.dart @@ -23,7 +23,7 @@ class PagedView extends StatelessWidget { final ProviderListenable>> provider; final ScrollController scrollCtrl; - final Future Function() onRefresh; + final void Function() onRefresh; final String dataType; final Widget Function(Paged) onData; From 6c26530e7f060d0f77849e7db51dbad65201380c Mon Sep 17 00:00:00 2001 From: lotusgate Date: Wed, 5 Apr 2023 19:19:23 +0300 Subject: [PATCH 15/55] Renaming --- lib/social/social_model.dart | 36 ++++++++++++++ .../social_provider.dart} | 48 ++++++++++--------- .../social_view.dart} | 36 +++++++------- lib/user/friends_model.dart | 17 ------- lib/utils/route_arg.dart | 4 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 7 files changed, 86 insertions(+), 61 deletions(-) create mode 100644 lib/social/social_model.dart rename lib/{user/friends_provider.dart => social/social_provider.dart} (57%) rename lib/{user/friends_view.dart => social/social_view.dart} (66%) delete mode 100644 lib/user/friends_model.dart diff --git a/lib/social/social_model.dart b/lib/social/social_model.dart new file mode 100644 index 00000000..e9e656e2 --- /dev/null +++ b/lib/social/social_model.dart @@ -0,0 +1,36 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/common/paged.dart'; +import 'package:otraku/user/user_models.dart'; + +class Social { + const Social({ + this.following = const AsyncValue.loading(), + this.followers = const AsyncValue.loading(), + }); + + final AsyncValue> following; + final AsyncValue> followers; + + int getCount(SocialTab tab) { + switch (tab) { + case SocialTab.following: + return following.valueOrNull?.total ?? 0; + case SocialTab.followers: + return followers.valueOrNull?.total ?? 0; + } + } +} + +enum SocialTab { + following, + followers; + + String get title { + switch (this) { + case SocialTab.following: + return 'Following'; + case SocialTab.followers: + return 'Followers'; + } + } +} diff --git a/lib/user/friends_provider.dart b/lib/social/social_provider.dart similarity index 57% rename from lib/user/friends_provider.dart rename to lib/social/social_provider.dart index e0dc82fb..06597565 100644 --- a/lib/user/friends_provider.dart +++ b/lib/social/social_provider.dart @@ -1,38 +1,42 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/user/friends_model.dart'; +import 'package:otraku/social/social_model.dart'; import 'package:otraku/user/user_models.dart'; import 'package:otraku/utils/api.dart'; import 'package:otraku/utils/graphql.dart'; import 'package:otraku/common/paged.dart'; -final friendsProvider = - StateNotifierProvider.autoDispose.family( - (ref, userId) => FriendsNotifier(userId), +final socialProvider = + StateNotifierProvider.autoDispose.family( + (ref, userId) => SocialNotifier(userId), ); -class FriendsNotifier extends StateNotifier { - FriendsNotifier(this.userId) : super(const Friends()) { +class SocialNotifier extends StateNotifier { + SocialNotifier(this.userId) : super(const Social()) { _fetch(null); } final int userId; - Future fetch(bool onFollowing) => _fetch(onFollowing); + Future fetch(SocialTab tab) => _fetch(tab); - Future _fetch(bool? onFollowing) async { + Future _fetch(SocialTab? tab) async { final variables = {'userId': userId}; - if (onFollowing == null) { - variables['withFollowing'] = true; - variables['withFollowers'] = true; - } else if (onFollowing) { - if (!(state.following.valueOrNull?.hasNext ?? true)) return; - variables['withFollowing'] = true; - variables['page'] = state.following.valueOrNull?.next ?? 1; - } else { - if (!(state.followers.valueOrNull?.hasNext ?? true)) return; - variables['withFollowers'] = true; - variables['page'] = state.followers.valueOrNull?.next ?? 1; + switch (tab) { + case null: + variables['withFollowing'] = true; + variables['withFollowers'] = true; + break; + case SocialTab.following: + if (!(state.following.valueOrNull?.hasNext ?? true)) return; + variables['withFollowing'] = true; + variables['page'] = state.following.valueOrNull?.next ?? 1; + break; + case SocialTab.followers: + if (!(state.followers.valueOrNull?.hasNext ?? true)) return; + variables['withFollowers'] = true; + variables['page'] = state.followers.valueOrNull?.next ?? 1; + break; } final data = await AsyncValue.guard( @@ -42,7 +46,7 @@ class FriendsNotifier extends StateNotifier { var following = state.following; var followers = state.followers; - if (onFollowing == null || onFollowing) { + if (tab == null || tab == SocialTab.following) { following = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['following']; @@ -61,7 +65,7 @@ class FriendsNotifier extends StateNotifier { }); } - if (onFollowing == null || !onFollowing) { + if (tab == null || tab == SocialTab.followers) { followers = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['followers']; @@ -80,6 +84,6 @@ class FriendsNotifier extends StateNotifier { }); } - state = Friends(following: following, followers: followers); + state = Social(following: following, followers: followers); } } diff --git a/lib/user/friends_view.dart b/lib/social/social_view.dart similarity index 66% rename from lib/user/friends_view.dart rename to lib/social/social_view.dart index 52d6f9f7..c5b4f776 100644 --- a/lib/user/friends_view.dart +++ b/lib/social/social_view.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; +import 'package:otraku/social/social_model.dart'; import 'package:otraku/user/user_models.dart'; -import 'package:otraku/user/friends_provider.dart'; +import 'package:otraku/social/social_provider.dart'; import 'package:otraku/user/user_grid.dart'; import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; @@ -11,20 +12,19 @@ import 'package:otraku/widgets/layouts/direct_page_view.dart'; import 'package:otraku/widgets/layouts/top_bar.dart'; import 'package:otraku/widgets/paged_view.dart'; -class FriendsView extends ConsumerStatefulWidget { - const FriendsView(this.id); +class SocialView extends ConsumerStatefulWidget { + const SocialView(this.id); final int id; @override - ConsumerState createState() => _FriendsViewState(); + ConsumerState createState() => _SocialViewState(); } -class _FriendsViewState extends ConsumerState { - late bool _onFollowing = true; +class _SocialViewState extends ConsumerState { + late SocialTab _tab = SocialTab.following; late final _ctrl = PagedController( - loadMore: () => - ref.read(friendsProvider(widget.id).notifier).fetch(_onFollowing), + loadMore: () => ref.read(socialProvider(widget.id).notifier).fetch(_tab), ); @override @@ -36,16 +36,17 @@ class _FriendsViewState extends ConsumerState { @override Widget build(BuildContext context) { final count = ref.watch( - friendsProvider(widget.id).select((s) => s.getCount(_onFollowing)), + socialProvider(widget.id).select((s) => s.getCount(_tab)), ); - final onRefresh = () => ref.invalidate(friendsProvider(widget.id)); + final onRefresh = () => ref.invalidate(socialProvider(widget.id)); return PageScaffold( bottomBar: BottomBarIconTabs( - current: _onFollowing ? 0 : 1, + current: _tab.index, onChanged: (page) { - setState(() => _onFollowing = page == 0 ? true : false); + setState(() => _tab = SocialTab.values.elementAt(page)); + _ctrl.scrollToTop(); }, onSame: (_) => _ctrl.scrollToTop(), items: const { @@ -55,7 +56,7 @@ class _FriendsViewState extends ConsumerState { ), child: TabScaffold( topBar: TopBar( - title: _onFollowing ? 'Following' : 'Followers', + title: _tab.title, trailing: [ if (count > 0) Padding( @@ -68,20 +69,21 @@ class _FriendsViewState extends ConsumerState { ], ), child: DirectPageView( - current: _onFollowing ? 0 : 1, + current: _tab.index, onChanged: (page) { - setState(() => _onFollowing = page == 0 ? true : false); + setState(() => _tab = SocialTab.values.elementAt(page)); + _ctrl.scrollToTop(); }, children: [ PagedView( - provider: friendsProvider(widget.id).select((s) => s.following), + provider: socialProvider(widget.id).select((s) => s.following), onData: (data) => UserGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, dataType: 'following', ), PagedView( - provider: friendsProvider(widget.id).select((s) => s.followers), + provider: socialProvider(widget.id).select((s) => s.followers), onData: (data) => UserGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, diff --git a/lib/user/friends_model.dart b/lib/user/friends_model.dart deleted file mode 100644 index 37b39b95..00000000 --- a/lib/user/friends_model.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/user/user_models.dart'; - -class Friends { - const Friends({ - this.following = const AsyncValue.loading(), - this.followers = const AsyncValue.loading(), - }); - - final AsyncValue> following; - final AsyncValue> followers; - - int getCount(bool onFollowing) => onFollowing - ? following.valueOrNull?.total ?? 0 - : followers.valueOrNull?.total ?? 0; -} diff --git a/lib/utils/route_arg.dart b/lib/utils/route_arg.dart index ceaa133e..852cb4d5 100644 --- a/lib/utils/route_arg.dart +++ b/lib/utils/route_arg.dart @@ -6,7 +6,7 @@ import 'package:otraku/favorites/favorites_view.dart'; import 'package:otraku/studio/studio_view.dart'; import 'package:otraku/auth/auth_view.dart'; import 'package:otraku/character/character_view.dart'; -import 'package:otraku/user/friends_view.dart'; +import 'package:otraku/social/social_view.dart'; import 'package:otraku/home/home_view.dart'; import 'package:otraku/media/media_view.dart'; import 'package:otraku/notifications/notifications_view.dart'; @@ -86,7 +86,7 @@ class RouteArg { return MaterialPageRoute(builder: (_) => FavoritesView(arg!.id!)); case friends: if (arg?.id == null) return _unknown; - return MaterialPageRoute(builder: (_) => FriendsView(arg!.id!)); + return MaterialPageRoute(builder: (_) => SocialView(arg!.id!)); case statistics: if (arg?.id == null) return _unknown; return MaterialPageRoute(builder: (_) => StatisticsView(arg!.id!)); diff --git a/pubspec.lock b/pubspec.lock index e5334c2c..dde36b08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b + sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad url: "https://pub.dev" source: hosted - version: "1.6.2" + version: "1.6.3" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7e8c75d4..d67b491f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: app_links: ^3.4.2 # Access to platform theme and easy theme interpolation. - dynamic_color: ^1.6.2 + dynamic_color: ^1.6.3 # Background tasks for notification fetching. workmanager: ^0.5.1 From ff70d85f5d3c706ba030e1a22392c8aa1aefa9e2 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Thu, 6 Apr 2023 23:19:47 +0300 Subject: [PATCH 16/55] Cleanup --- lib/activity/activities_view.dart | 43 +++++++++++++++++-------------- lib/review/reviews_view.dart | 14 +++++----- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/lib/activity/activities_view.dart b/lib/activity/activities_view.dart index 6721fd35..c7314188 100644 --- a/lib/activity/activities_view.dart +++ b/lib/activity/activities_view.dart @@ -164,26 +164,8 @@ class ActivitiesSubView extends StatelessWidget { .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; - } - - ref - .read(activitiesProvider(id).notifier) - .updateActivity(updatedActivity); - }, - ), - ), + onOpenReplies: () => + _onOpenReplies(context, ref, data.items[i]), onEdited: (map) { ref .read(activitiesProvider(id).notifier) @@ -197,4 +179,25 @@ class ActivitiesSubView extends StatelessWidget { }, ); } + + void _onOpenReplies(BuildContext context, WidgetRef ref, Activity activity) { + Navigator.pushNamed( + context, + RouteArg.activity, + arguments: RouteArg( + id: activity.id, + callback: (arg) { + final updatedActivity = arg as Activity?; + if (updatedActivity == null) { + ref.read(activitiesProvider(id).notifier).remove(activity.id); + return; + } + + ref + .read(activitiesProvider(id).notifier) + .updateActivity(updatedActivity); + }, + ), + ); + } } diff --git a/lib/review/reviews_view.dart b/lib/review/reviews_view.dart index a9ac1193..f56595b5 100644 --- a/lib/review/reviews_view.dart +++ b/lib/review/reviews_view.dart @@ -86,14 +86,12 @@ class _ReviewsViewState extends ConsumerState { ), ], ), - child: Consumer( - builder: (context, ref, refreshControl) => PagedView( - provider: reviewsProvider(widget.id), - onData: (data) => ReviewGrid(data.items), - onRefresh: () => ref.invalidate(reviewsProvider(widget.id)), - scrollCtrl: _ctrl, - dataType: 'reviews', - ), + child: PagedView( + provider: reviewsProvider(widget.id), + onData: (data) => ReviewGrid(data.items), + onRefresh: () => ref.invalidate(reviewsProvider(widget.id)), + scrollCtrl: _ctrl, + dataType: 'reviews', ), ), ); From 9ae33ab3785b98aef1f1aeae14a4ebad4197ccb5 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Thu, 6 Apr 2023 23:20:07 +0300 Subject: [PATCH 17/55] Notifications now use StateNotifier --- lib/notifications/notification_provider.dart | 25 ++-- lib/notifications/notifications_view.dart | 148 +++++++------------ 2 files changed, 66 insertions(+), 107 deletions(-) diff --git a/lib/notifications/notification_provider.dart b/lib/notifications/notification_provider.dart index 850edee5..1ae30eef 100644 --- a/lib/notifications/notification_provider.dart +++ b/lib/notifications/notification_provider.dart @@ -1,4 +1,3 @@ -import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/notifications/notification_model.dart'; import 'package:otraku/common/paged.dart'; @@ -9,26 +8,22 @@ final notificationFilterProvider = StateProvider.autoDispose( (ref) => NotificationFilterType.all, ); -final notificationsProvider = ChangeNotifierProvider.autoDispose( +final notificationsProvider = StateNotifierProvider.autoDispose< + NotificationsNotifier, AsyncValue>>( (ref) => NotificationsNotifier(ref.watch(notificationFilterProvider)), ); -class NotificationsNotifier extends ChangeNotifier { - NotificationsNotifier(this.filter) { +class NotificationsNotifier + extends StateNotifier>> { + NotificationsNotifier(this.filter) : super(const AsyncValue.loading()) { fetch(); } final NotificationFilterType filter; - int _unreadCount = 0; - var _notifications = const AsyncValue>.loading(); - - int get unreadCount => _unreadCount; - AsyncValue> get notifications => _notifications; - Future fetch() async { - _notifications = await AsyncValue.guard(() async { - final value = _notifications.valueOrNull ?? const Paged(); + state = await AsyncValue.guard(() async { + final value = state.valueOrNull ?? const PagedWithTotal(); final data = await Api.get(GqlQuery.notifications, { 'page': value.next, @@ -39,9 +34,9 @@ class NotificationsNotifier extends ChangeNotifier { 'filter': filter.vars, }); - _unreadCount = 0; + int? unreadCount; if (filter.index < 1) { - _unreadCount = data['Viewer']?['unreadNotificationCount'] ?? 0; + unreadCount = data['Viewer']['unreadNotificationCount'] ?? 0; } final items = []; @@ -53,8 +48,8 @@ class NotificationsNotifier extends ChangeNotifier { return value.withNext( items, data['Page']['pageInfo']['hasNextPage'] ?? false, + unreadCount, ); }); - notifyListeners(); } } diff --git a/lib/notifications/notifications_view.dart b/lib/notifications/notifications_view.dart index 28c9b96f..a0336c26 100644 --- a/lib/notifications/notifications_view.dart +++ b/lib/notifications/notifications_view.dart @@ -21,6 +21,7 @@ import 'package:otraku/widgets/loaders.dart/loaders.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/paged_view.dart'; class NotificationsView extends ConsumerStatefulWidget { const NotificationsView(); @@ -30,15 +31,14 @@ class NotificationsView extends ConsumerStatefulWidget { } class _NotificationsViewState extends ConsumerState { - late final PagedController _ctrl; + late final _ctrl = PagedController( + loadMore: () => ref.read(notificationsProvider.notifier).fetch(), + ); @override void initState() { super.initState(); BackgroundHandler.clearNotifications(); - _ctrl = PagedController( - loadMore: () => ref.read(notificationsProvider).fetch(), - ); } @override @@ -49,18 +49,20 @@ class _NotificationsViewState extends ConsumerState { @override Widget build(BuildContext context) { + final unreadCount = ref.watch( + notificationsProvider.select((s) => s.valueOrNull?.total ?? 0), + ); + 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, - ), + child: Text( + '${ref.watch(notificationFilterProvider).text} Notifications', + style: Theme.of(context).textTheme.titleLarge, + overflow: TextOverflow.ellipsis, + maxLines: 1, ), ), ], @@ -71,93 +73,55 @@ class _NotificationsViewState extends ConsumerState { 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.textTheme.titleLarge - : theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.primary, - ), - )); - } - - return DynamicGradientDragSheet( - children: tiles, - onTap: (i) => notifier.state = - NotificationFilterType.values.elementAt(i), - ); - }, - ), - ); - }, + onTap: _showFilterSheet, ), ], ), - child: Consumer( - child: SliverRefreshControl( - onRefresh: () => ref.invalidate(notificationsProvider), + child: PagedView( + provider: notificationsProvider, + onData: (data) => SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => _NotificationItem(data.items[i], i < unreadCount), + childCount: data.items.length, + ), ), - builder: (context, ref, refreshControl) { - ref.listen( - notificationsProvider, - (_, s) => s.notifications.whenOrNull( - error: (error, _) => showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load notifications', - content: error.toString(), - ), - ), - ), - ); + onRefresh: () => ref.invalidate(notificationsProvider), + scrollCtrl: _ctrl, + dataType: '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')); - } + void _showFilterSheet() { + showSheet( + context, + Consumer( + builder: (context, ref, _) { + final theme = Theme.of(context); + final index = + ref.read(notificationFilterProvider.notifier).state.index; - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl!, - SliverList( - delegate: SliverChildBuilderDelegate( - (context, i) => _NotificationItem( - data.items[i], - i < notifier.unreadCount, - ), - childCount: data.items.length, - ), - ), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }, - ); - }, - ), + final tiles = []; + for (int i = 0; i < NotificationFilterType.values.length; i++) { + tiles.add(Text( + NotificationFilterType.values.elementAt(i).text, + style: i != index + ? theme.textTheme.titleLarge + : theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.primary, + ), + )); + } + + return DynamicGradientDragSheet( + children: tiles, + onTap: (i) { + final notifier = ref.read(notificationFilterProvider.notifier); + notifier.state = NotificationFilterType.values.elementAt(i); + }, + ); + }, ), ); } From 8c187d0a9848cab5954eb044a2d701ca57949a77 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Thu, 6 Apr 2023 23:25:00 +0300 Subject: [PATCH 18/55] Cleanup --- lib/notifications/notifications_view.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/notifications/notifications_view.dart b/lib/notifications/notifications_view.dart index a0336c26..f066a2f3 100644 --- a/lib/notifications/notifications_view.dart +++ b/lib/notifications/notifications_view.dart @@ -10,14 +10,12 @@ import 'package:otraku/utils/background_handler.dart'; import 'package:otraku/utils/paged_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/cached_image.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/widgets/layouts/floating_bar.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'; import 'package:otraku/widgets/overlays/toast.dart'; From e9bd07b2bf23fc8ce672582446544f53e6e8d1c5 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Thu, 6 Apr 2023 23:43:48 +0300 Subject: [PATCH 19/55] Fixed search field not clearing properly --- lib/filter/filter_search_field.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/filter/filter_search_field.dart b/lib/filter/filter_search_field.dart index 1bd0f4af..ea6f617d 100644 --- a/lib/filter/filter_search_field.dart +++ b/lib/filter/filter_search_field.dart @@ -104,6 +104,7 @@ class _SearchFilterFieldState extends State { hint: widget.title, onChange: (val) { if (val.isEmpty) { + _debounce.cancel(); ref.read(searchProvider(widget.tag).notifier).state = ''; return; From e57c28df5629e747a13a71bbf6b528e9ce763783 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Tue, 11 Apr 2023 14:46:16 +0300 Subject: [PATCH 20/55] StateNotifier is used for all pagination --- lib/activity/activities_view.dart | 1 - lib/character/character_action_buttons.dart | 156 ++++++++ lib/character/character_info_tab.dart | 310 ++++++--------- lib/character/character_media_tab.dart | 295 -------------- lib/character/character_models.dart | 58 +++ lib/character/character_providers.dart | 271 ++++++------- lib/character/character_view.dart | 66 +++- lib/discover/discover_view.dart | 7 - lib/favorites/favorites_view.dart | 5 - lib/media/media_header.dart | 351 ++++++++--------- lib/media/media_info_view.dart | 3 - lib/media/media_models.dart | 66 ++++ lib/media/media_other_view.dart | 237 ++++++------ lib/media/media_people_view.dart | 134 +++---- lib/media/media_providers.dart | 406 ++++++++------------ lib/media/media_social_view.dart | 139 +++---- lib/media/media_view.dart | 40 +- lib/notifications/notifications_view.dart | 3 +- lib/review/reviews_view.dart | 1 - lib/social/social_view.dart | 2 - lib/staff/staff_action_buttons.dart | 122 ++++++ lib/staff/staff_info_tab.dart | 296 ++++++-------- lib/staff/staff_models.dart | 15 + lib/staff/staff_providers.dart | 209 +++++----- lib/staff/staff_relations_tab.dart | 258 ------------- lib/staff/staff_view.dart | 63 ++- lib/studio/studio_models.dart | 22 +- lib/studio/studio_providers.dart | 115 +++--- lib/studio/studio_view.dart | 228 +++++------ lib/user/user_header.dart | 391 +++++++++---------- lib/widgets/grids/relation_grid.dart | 6 +- lib/widgets/layouts/scaffolds.dart | 11 +- lib/widgets/overlays/sheets.dart | 8 +- lib/widgets/paged_view.dart | 53 ++- pubspec.lock | 18 +- pubspec.yaml | 4 +- 36 files changed, 1917 insertions(+), 2453 deletions(-) create mode 100644 lib/character/character_action_buttons.dart delete mode 100644 lib/character/character_media_tab.dart create mode 100644 lib/staff/staff_action_buttons.dart delete mode 100644 lib/staff/staff_relations_tab.dart diff --git a/lib/activity/activities_view.dart b/lib/activity/activities_view.dart index c7314188..2ad25617 100644 --- a/lib/activity/activities_view.dart +++ b/lib/activity/activities_view.dart @@ -140,7 +140,6 @@ class ActivitiesSubView extends StatelessWidget { return PagedView( provider: activitiesProvider(id), scrollCtrl: scrollCtrl, - dataType: 'activities', onRefresh: () { ref.invalidate(activitiesProvider(id)); if (id == null) { diff --git a/lib/character/character_action_buttons.dart b/lib/character/character_action_buttons.dart new file mode 100644 index 00000000..791e67ae --- /dev/null +++ b/lib/character/character_action_buttons.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ionicons/ionicons.dart'; +import 'package:otraku/character/character_models.dart'; +import 'package:otraku/character/character_providers.dart'; +import 'package:otraku/filter/chip_selector.dart'; +import 'package:otraku/media/media_constants.dart'; +import 'package:otraku/utils/consts.dart'; +import 'package:otraku/utils/convert.dart'; +import 'package:otraku/widgets/layouts/floating_bar.dart'; +import 'package:otraku/widgets/overlays/sheets.dart'; + +class CharacterFavoriteButton extends StatefulWidget { + const CharacterFavoriteButton(this.data); + + final Character data; + + @override + State createState() => + _CharacterFavoriteButtonState(); +} + +class _CharacterFavoriteButtonState extends State { + @override + Widget build(BuildContext context) { + return ActionButton( + icon: widget.data.isFavorite ? Icons.favorite : Icons.favorite_border, + tooltip: widget.data.isFavorite ? 'Unfavourite' : 'Favourite', + onTap: () { + setState( + () => widget.data.isFavorite = !widget.data.isFavorite, + ); + toggleFavoriteCharacter(widget.data.id).then((ok) { + if (!ok) { + setState( + () => widget.data.isFavorite = !widget.data.isFavorite, + ); + } + }); + }, + ); + } +} + +class CharacterMediaFilterButton extends StatelessWidget { + const CharacterMediaFilterButton(this.id); + + final int id; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, _) { + return ActionButton( + icon: Ionicons.funnel_outline, + tooltip: 'Filter', + onTap: () { + var filter = ref.read(characterFilterProvider(id)); + + final sortItems = {}; + for (int i = 0; i < MediaSort.values.length; i += 2) { + String key = Convert.clarifyEnum(MediaSort.values[i].name)!; + sortItems[key] = i ~/ 2; + } + + final onDone = (_) => + ref.read(characterFilterProvider(id).notifier).state = filter; + + showSheet( + context, + OpaqueSheet( + initialHeight: Consts.tapTargetSize * 4, + builder: (context, scrollCtrl) => ListView( + controller: scrollCtrl, + physics: Consts.physics, + padding: const EdgeInsets.symmetric(vertical: 20), + children: [ + ChipSelector( + title: 'Sort', + options: MediaSort.values.map((s) => s.label).toList(), + selected: filter.sort.index, + mustHaveSelected: true, + onChanged: (i) => filter = filter.copyWith( + sort: MediaSort.values.elementAt(i!), + ), + ), + ChipSelector( + title: 'List Presence', + options: const ['On List', 'Not on List'], + selected: filter.onList == null + ? null + : filter.onList! + ? 0 + : 1, + onChanged: (val) => filter = filter.copyWith(onList: () { + if (val == null) return null; + return val == 0 ? true : false; + }), + ), + ], + ), + ), + ).then(onDone); + }, + ); + }, + ); + } +} + +class CharacterLanguageSelectionButton extends StatelessWidget { + const CharacterLanguageSelectionButton(this.id); + + final int id; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, _) { + if (ref.watch(characterMediaProvider(id).select( + (s) => s.languages.length < 2, + ))) return const SizedBox(); + + return ActionButton( + tooltip: 'Language', + icon: Ionicons.globe_outline, + onTap: () { + final characterMedia = ref.read(characterMediaProvider(id)); + final languages = characterMedia.languages; + final language = characterMedia.language; + + showSheet( + context, + DynamicGradientDragSheet( + onTap: (i) => ref + .read(characterMediaProvider(id).notifier) + .changeLanguage(languages.elementAt(i)), + children: [ + for (int i = 0; i < languages.length; i++) + Text( + languages.elementAt(i), + style: languages.elementAt(i) != language + ? Theme.of(context).textTheme.titleLarge + : Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/character/character_info_tab.dart b/lib/character/character_info_tab.dart index d55e26ee..459a2189 100644 --- a/lib/character/character_info_tab.dart +++ b/lib/character/character_info_tab.dart @@ -1,86 +1,21 @@ import 'package:flutter/material.dart'; 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/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/scaffolds.dart'; -import 'package:otraku/widgets/layouts/top_bar.dart'; +import 'package:otraku/widgets/layouts/constrained_view.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, this.topBar); + const CharacterInfoTab(this.id, this.imageUrl, this.scrollCtrl); final int id; final String? imageUrl; final ScrollController scrollCtrl; - final TopBar topBar; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(characterProvider(id)), - ); - - return ref.watch(characterProvider(id)).when( - loading: () => _TabContent( - id: id, - data: null, - imageUrl: imageUrl, - scrollCtrl: scrollCtrl, - refreshControl: refreshControl, - topBar: topBar, - loading: true, - ), - error: (_, __) => _TabContent( - id: id, - data: null, - imageUrl: imageUrl, - scrollCtrl: scrollCtrl, - refreshControl: refreshControl, - topBar: topBar, - loading: false, - ), - data: (data) => _TabContent( - id: id, - data: data, - imageUrl: imageUrl, - scrollCtrl: scrollCtrl, - refreshControl: refreshControl, - topBar: topBar, - loading: false, - ), - ); - }, - ); - } -} - -class _TabContent extends StatelessWidget { - const _TabContent({ - required this.id, - required this.data, - required this.imageUrl, - required this.scrollCtrl, - required this.refreshControl, - required this.topBar, - required this.loading, - }); - - final int id; - final Character? data; - final String? imageUrl; - final ScrollController scrollCtrl; - final Widget refreshControl; - final TopBar topBar; - final bool loading; @override Widget build(BuildContext context) { @@ -89,158 +24,147 @@ class _TabContent extends StatelessWidget { : 100.0; final imageHeight = imageWidth * Consts.coverHtoWRatio; - final imageUrl = data?.imageUrl ?? this.imageUrl; - - final headerRow = IntrinsicHeight( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (imageUrl != null) - Hero( - tag: id, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: Container( - width: imageWidth, - height: imageHeight, - color: Theme.of(context).colorScheme.surfaceVariant, - child: GestureDetector( - child: CachedImage(imageUrl), - onTap: () => showPopUp(context, ImageDialog(imageUrl)), - ), - ), - ), - ), - const SizedBox(width: 10), - if (data != null) - Flexible( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, + return Consumer( + builder: (context, ref, _) { + final character = ref.watch(characterProvider(id)); + final imageUrl = character.valueOrNull?.imageUrl ?? this.imageUrl; + + final header = SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: () => Toast.copy(context, data!.name), - child: Text( - data!.name, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - if (data!.altNames.isNotEmpty) - Text(data!.altNames.join(', ')), - if (data!.altNamesSpoilers.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - child: Text( - 'Spoiler names', - style: Theme.of(context).textTheme.bodyLarge, - ), - onTap: () => showPopUp( - context, - TextDialog( - title: 'Spoiler names', - text: data!.altNamesSpoilers.join(', '), + if (imageUrl != null) + Padding( + padding: const EdgeInsets.only(right: 10), + child: Hero( + tag: id, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: Container( + width: imageWidth, + height: imageHeight, + color: Theme.of(context).colorScheme.surfaceVariant, + child: GestureDetector( + child: CachedImage(imageUrl), + onTap: () => + showPopUp(context, ImageDialog(imageUrl)), + ), + ), ), ), ), + character.maybeWhen( + orElse: () => const SizedBox(), + data: (data) => Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: () => Toast.copy(context, data.name), + child: Text( + data.name, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + if (data.altNames.isNotEmpty) + Text(data.altNames.join(', ')), + if (data.altNamesSpoilers.isNotEmpty) + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Text( + 'Spoiler names', + style: Theme.of(context).textTheme.bodyLarge, + ), + onTap: () => showPopUp( + context, + TextDialog( + title: 'Spoiler names', + text: data.altNamesSpoilers.join(', '), + ), + ), + ), + ], + ), + ), + ), ], ), ), - ], - ), - ); + ), + ); - const space = SliverToBoxAdapter(child: SizedBox(height: 10)); + final refreshControl = SliverRefreshControl( + onRefresh: () => ref.invalidate(characterProvider(id)), + ); - return TabScaffold( - topBar: topBar, - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [if (data != null) _FavoriteButton(data!)], - ), - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: CustomScrollView( + return ConstrainedView( + child: character.when( + loading: () => CustomScrollView( + physics: Consts.physics, controller: scrollCtrl, + slivers: [ + refreshControl, + header, + const SliverFillRemaining(child: Center(child: Loader())), + const SliverFooter(), + ], + ), + error: (_, __) => CustomScrollView( physics: Consts.physics, + controller: scrollCtrl, slivers: [ refreshControl, - space, - SliverToBoxAdapter(child: headerRow), - if (data != null) ...[ - space, - SliverGrid( - gridDelegate: - const SliverGridDelegateWithMinWidthAndFixedHeight( - height: Consts.tapTargetSize, - minWidth: 150, - ), - delegate: SliverChildListDelegate([ - _InfoTile('Favourites', data!.favorites.toString()), - if (data!.gender != null) - _InfoTile('Gender', data!.gender!), - if (data!.age != null) _InfoTile('Age', data!.age!), - if (data!.dateOfBirth != null) - _InfoTile('Date of Birth', data!.dateOfBirth!), - if (data!.bloodType != null) - _InfoTile('Blood Type', data!.bloodType!), - ]), + header, + const SliverFillRemaining( + child: Center(child: Text('No data')), + ), + const SliverFooter(), + ], + ), + data: (data) => CustomScrollView( + physics: Consts.physics, + controller: scrollCtrl, + slivers: [ + refreshControl, + header, + SliverGrid( + gridDelegate: + const SliverGridDelegateWithMinWidthAndFixedHeight( + height: Consts.tapTargetSize, + minWidth: 150, ), - space, - if (data!.description.isNotEmpty) - SliverToBoxAdapter( + delegate: SliverChildListDelegate([ + _InfoTile('Favourites', data.favorites.toString()), + if (data.gender != null) _InfoTile('Gender', data.gender!), + if (data.age != null) _InfoTile('Age', data.age!), + if (data.dateOfBirth != null) + _InfoTile('Date of Birth', data.dateOfBirth!), + if (data.bloodType != null) + _InfoTile('Blood Type', data.bloodType!), + ]), + ), + if (data.description.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 10), child: Card( child: Padding( padding: Consts.padding, - child: HtmlContent(data!.description), + child: HtmlContent(data.description), ), ), ), - ] else - SliverFillRemaining( - child: Center( - child: loading ? const Loader() : const Text('No data'), - ), ), const SliverFooter(), ], ), ), - ), - ), - ); - } -} - -class _FavoriteButton extends StatefulWidget { - const _FavoriteButton(this.data); - - final Character data; - - @override - State<_FavoriteButton> createState() => __FavoriteButtonState(); -} - -class __FavoriteButtonState extends State<_FavoriteButton> { - @override - Widget build(BuildContext context) { - return ActionButton( - icon: widget.data.isFavorite ? Icons.favorite : Icons.favorite_border, - tooltip: widget.data.isFavorite ? 'Unfavourite' : 'Favourite', - onTap: () { - setState( - () => widget.data.isFavorite = !widget.data.isFavorite, ); - toggleFavoriteCharacter(widget.data.id).then((ok) { - if (!ok) { - setState( - () => widget.data.isFavorite = !widget.data.isFavorite, - ); - } - }); }, ); } diff --git a/lib/character/character_media_tab.dart b/lib/character/character_media_tab.dart deleted file mode 100644 index 70fff658..00000000 --- a/lib/character/character_media_tab.dart +++ /dev/null @@ -1,295 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ionicons/ionicons.dart'; -import 'package:otraku/character/character_providers.dart'; -import 'package:otraku/filter/chip_selector.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/media/media_constants.dart'; -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/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, this.topBar); - - final int id; - final ScrollController scrollCtrl; - final TopBar topBar; - - @override - Widget build(BuildContext context) { - return TabScaffold( - topBar: topBar, - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [_FilterButton(id), _LanguageButton(id)], - ), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Consumer( - builder: (context, ref, _) { - ref.listen( - characterMediaProvider(id).select((s) => s.anime), - (_, s) { - if (s.hasError) { - showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load anime', - content: s.error.toString(), - ), - ); - } - }, - ); - - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(characterMediaProvider(id)), - ); - - return ref.watch(characterMediaProvider(id)).anime.when( - loading: () => const Center(child: Loader()), - error: (_, __) => CustomScrollView( - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverFillRemaining( - child: Text('Failed to load anime'), - ), - ], - ), - data: (data) { - final anime = []; - final voiceActors = []; - ref - .watch(characterMediaProvider(id).notifier) - .getAnimeAndVoiceActors(anime, voiceActors); - - return CustomScrollView( - controller: scrollCtrl, - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverToBoxAdapter( - child: SizedBox(height: 10), - ), - RelationGrid( - items: anime, - connections: voiceActors, - placeholder: 'No anime', - ), - SliverFooter(loading: data.hasNext), - ], - ); - }, - ); - }, - ), - ), - ), - ), - ); - } -} - -class CharacterMangaTab extends StatelessWidget { - const CharacterMangaTab(this.id, this.scrollCtrl, this.topBar); - - final int id; - final ScrollController scrollCtrl; - final TopBar topBar; - - @override - Widget build(BuildContext context) { - return TabScaffold( - topBar: topBar, - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [_FilterButton(id)], - ), - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: Consumer( - builder: (context, ref, _) { - ref.listen( - characterMediaProvider(id).select((s) => s.manga), - (_, s) { - if (s.hasError) { - showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load manga', - content: s.error.toString(), - ), - ); - } - }, - ); - - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(characterMediaProvider(id)), - ); - - return ref.watch(characterMediaProvider(id)).manga.when( - loading: () => const Center(child: Loader()), - error: (_, __) => CustomScrollView( - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverFillRemaining( - child: Text('Failed to load manga'), - ), - ], - ), - data: (data) { - return CustomScrollView( - controller: scrollCtrl, - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverToBoxAdapter( - child: SizedBox(height: 10), - ), - RelationGrid( - items: data.items, - placeholder: 'No manga', - ), - SliverFooter(loading: data.hasNext), - ], - ); - }, - ); - }, - ), - ), - ), - ), - ); - } -} - -class _LanguageButton extends StatelessWidget { - const _LanguageButton(this.id); - - final int id; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - if (ref.watch(characterMediaProvider(id).select( - (s) => s.languages.isEmpty, - ))) return const SizedBox(); - - return ActionButton( - tooltip: 'Language', - icon: Ionicons.globe_outline, - onTap: () { - final notifier = ref.read(characterMediaProvider(id)); - final languages = notifier.languages; - final language = notifier.language; - - showSheet( - context, - DynamicGradientDragSheet( - onTap: (i) { - ref.read(characterMediaProvider(id)).language = - languages.elementAt(i); - }, - children: [ - for (int i = 0; i < languages.length; i++) - Text( - languages.elementAt(i), - style: languages.elementAt(i) != language - ? Theme.of(context).textTheme.titleLarge - : Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - ); - }, - ); - }, - ); - } -} - -class _FilterButton extends StatelessWidget { - const _FilterButton(this.id); - - final int id; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - return ActionButton( - icon: Ionicons.funnel_outline, - tooltip: 'Filter', - onTap: () { - var filter = ref.read(characterFilterProvider(id)); - - final sortItems = {}; - for (int i = 0; i < MediaSort.values.length; i += 2) { - String key = Convert.clarifyEnum(MediaSort.values[i].name)!; - sortItems[key] = i ~/ 2; - } - - final onDone = (_) => - ref.read(characterFilterProvider(id).notifier).state = filter; - - showSheet( - context, - OpaqueSheet( - initialHeight: Consts.tapTargetSize * 4, - builder: (context, scrollCtrl) => ListView( - controller: scrollCtrl, - physics: Consts.physics, - padding: const EdgeInsets.symmetric(vertical: 20), - children: [ - ChipSelector( - title: 'Sort', - options: MediaSort.values.map((s) => s.label).toList(), - selected: filter.sort.index, - mustHaveSelected: true, - onChanged: (i) => filter = filter.copyWith( - sort: MediaSort.values.elementAt(i!), - ), - ), - ChipSelector( - title: 'List Presence', - options: const ['On List', 'Not on List'], - selected: filter.onList == null - ? null - : filter.onList! - ? 0 - : 1, - onChanged: (val) => filter = filter.copyWith(onList: () { - if (val == null) return null; - return val == 0 ? true : false; - }), - ), - ], - ), - ), - ).then(onDone); - }, - ); - }, - ); - } -} diff --git a/lib/character/character_models.dart b/lib/character/character_models.dart index a3633826..8642e312 100644 --- a/lib/character/character_models.dart +++ b/lib/character/character_models.dart @@ -1,3 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/common/paged.dart'; +import 'package:otraku/common/relation.dart'; import 'package:otraku/common/tile_item.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/media/media_constants.dart'; @@ -79,3 +82,58 @@ class CharacterFilter { onList: onList == null ? this.onList : onList(), ); } + +class CharacterMedia { + const CharacterMedia({ + this.anime = const AsyncValue.loading(), + this.manga = const AsyncValue.loading(), + this.languageToVoiceActors = const {}, + this.language = '', + }); + + final AsyncValue> anime; + final AsyncValue> manga; + + /// For each language, a list of voice actors + /// is mapped to the corresponding media's id. + final Map>> languageToVoiceActors; + + /// The currently selected language. + final String language; + + Iterable get languages => languageToVoiceActors.keys; + + /// Fill [resultingMedia] and [resultingVoiceActors] lists, based on the + /// currently selected [language]. The lists must end up with equal length + /// or if an incorrect [language] is selected, [resultingVoiceActors] should + /// be empty. If there are multiple VAs for a media, add the corresponding + /// media item in [resultingMedia] enough times to compensate. If there are no + /// VAs to a media, compensate with one `null` item in [resultingVoiceActors]. + void getAnimeAndVoiceActors( + List resultingMedia, + List resultingVoiceActors, + ) { + final anime = this.anime.valueOrNull?.items; + if (anime == null || anime.isEmpty) return; + + final actorsPerMedia = languageToVoiceActors[language]; + if (actorsPerMedia == null) { + resultingMedia.addAll(anime); + return; + } + + for (final a in anime) { + final actors = actorsPerMedia[a.id]; + if (actors == null || actors.isEmpty) { + resultingMedia.add(a); + resultingVoiceActors.add(null); + continue; + } + + for (final va in actors) { + resultingMedia.add(a); + resultingVoiceActors.add(va); + } + } + } +} diff --git a/lib/character/character_providers.dart b/lib/character/character_providers.dart index e784ab4d..acbaa57c 100644 --- a/lib/character/character_providers.dart +++ b/lib/character/character_providers.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/character/character_models.dart'; import 'package:otraku/discover/discover_models.dart'; @@ -32,188 +31,144 @@ final characterProvider = FutureProvider.autoDispose.family( final characterFilterProvider = StateProvider.autoDispose.family((ref, _) => CharacterFilter()); -final characterMediaProvider = ChangeNotifierProvider.autoDispose.family( +final characterMediaProvider = StateNotifierProvider.autoDispose + .family( (ref, int id) => CharacterMediaNotifier(id, ref.watch(characterFilterProvider(id))), ); -class CharacterMediaNotifier extends ChangeNotifier { - CharacterMediaNotifier(this.id, this.filter) { - _fetch(); +class CharacterMediaNotifier extends StateNotifier { + CharacterMediaNotifier(this.id, this.filter) : super(const CharacterMedia()) { + _fetch(null); } final int id; final CharacterFilter filter; - var _anime = const AsyncValue>.loading(); - var _manga = const AsyncValue>.loading(); - - /// For each language, a list of voice actors - /// is mapped to the corresponding media's id. - final _languages = >>{}; - - /// The currently selected language. - var _language = ''; - - AsyncValue> get anime => _anime; - AsyncValue> get manga => _manga; - Iterable get languages => _languages.keys; - String get language => _language; - set language(String l) { - _language = l; - notifyListeners(); - } - - /// Fill [media] and [voiceActors] lists, based on the currently selected - /// [language]. The lists must end up with equal [length] or if an incorrect - /// [language] is selected, [voiceActors] should be empty. If there are - /// multiple VAs for a media, add the corresponding media item in [media] - /// enough times to compensate. If there are no VAs to a media, compensate - /// with one `null` item in [voiceActors]. - void getAnimeAndVoiceActors( - List media, - List voiceActors, - ) { - final anime = _anime.valueOrNull?.items; - if (anime == null || anime.isEmpty) return; - - final byLanguage = _languages[_language]; - if (byLanguage == null) { - media.addAll(anime); - return; - } - for (final a in anime) { - final vas = byLanguage[a.id]; - if (vas == null || vas.isEmpty) { - media.add(a); - voiceActors.add(null); - continue; - } - - for (final va in vas) { - media.add(a); - voiceActors.add(va); - } + Future fetch(bool onAnime) => _fetch(onAnime); + + Future _fetch(bool? onAnime) async { + final variables = { + 'id': id, + 'onList': filter.onList, + 'sort': filter.sort.name, + }; + + if (onAnime == null) { + variables['withAnime'] = true; + variables['withManga'] = true; + } else if (onAnime) { + if (!(state.anime.valueOrNull?.hasNext ?? true)) return; + variables['withAnime'] = true; + variables['page'] = state.anime.valueOrNull?.next ?? 1; + } else if (!onAnime) { + if (!(state.manga.valueOrNull?.hasNext ?? true)) return; + variables['withManga'] = true; + variables['page'] = state.manga.valueOrNull?.next ?? 1; } - } - Future _fetch() async { final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.character, { - 'id': id, - 'withAnime': true, - 'withManga': true, - 'onList': filter.onList, - 'sort': filter.sort.name, - }); + final data = await Api.get(GqlQuery.character, variables); return data['Character']; }); - if (data.hasError) { - _anime = AsyncValue.error(data.error!, data.stackTrace!); - _manga = AsyncValue.error(data.error!, data.stackTrace!); - return; - } - - _anime = const AsyncValue.data(Paged()); - _manga = const AsyncValue.data(Paged()); - - _initAnime(data.value!['anime']); - _initManga(data.value!['manga']); + var anime = state.anime; + var manga = state.manga; + var languageToVoiceActors = state.languageToVoiceActors; + var language = state.language; + + if (onAnime == null || onAnime) { + anime = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['anime']; + final value = anime.valueOrNull ?? const Paged(); + + /// The map could be immutable, so a copy is made. + languageToVoiceActors = {...state.languageToVoiceActors}; + + final items = []; + for (final a in map['edges']) { + items.add(Relation( + id: a['node']['id'], + title: a['node']['title']['userPreferred'], + imageUrl: a['node']['coverImage'][Options().imageQuality.value], + subtitle: Convert.clarifyEnum(a['characterRole']), + type: DiscoverType.anime, + )); - if (_languages.isNotEmpty) _language = _languages.keys.first; - notifyListeners(); - } + if (a['voiceActors'] != null) { + for (final va in a['voiceActors']) { + final l = Convert.clarifyEnum(va['languageV2']); + if (l == null) continue; + + final currentLanguage = languageToVoiceActors.putIfAbsent( + l, + () => >{}, + ); + + final currentMedia = currentLanguage.putIfAbsent( + items.last.id, + () => [], + ); + + currentMedia.add(Relation( + id: va['id'], + title: va['name']['userPreferred'], + imageUrl: va['image']['large'], + subtitle: l, + type: DiscoverType.staff, + )); + } + } + } - Future fetchPage(bool ofAnime) async { - final value = ofAnime ? _anime.valueOrNull : _manga.valueOrNull; - if (value == null || !value.hasNext) return; + if (language.isEmpty && languageToVoiceActors.isNotEmpty) { + language = languageToVoiceActors.keys.first; + } - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.character, { - 'id': id, - 'withAnime': ofAnime, - 'withManga': !ofAnime, - 'onList': filter.onList, - 'sort': filter.sort.name, - 'page': value.next, + return Future.value(value.withNext( + items, + map['pageInfo']['hasNextPage'] ?? false, + )); }); - return data['Character']; - }); - - if (data.hasError) { - ofAnime - ? _anime = AsyncValue.error(data.error!, data.stackTrace!) - : _manga = AsyncValue.error(data.error!, data.stackTrace!); - return; } - ofAnime - ? _initAnime(data.value!['anime']) - : _initManga(data.value!['manga']); - notifyListeners(); - } - - void _initAnime(Map data) { - var value = _anime.valueOrNull; - if (value == null) return; - - final items = []; - for (final a in data['edges']) { - items.add(Relation( - id: a['node']['id'], - title: a['node']['title']['userPreferred'], - imageUrl: a['node']['coverImage'][Options().imageQuality.value], - subtitle: Convert.clarifyEnum(a['characterRole']), - type: DiscoverType.anime, - )); - - if (a['voiceActors'] != null) { - for (final va in a['voiceActors']) { - final l = Convert.clarifyEnum(va['languageV2']); - if (l == null) continue; - - final currentLanguage = _languages.putIfAbsent( - l, - () => >{}, - ); - - final currentMedia = currentLanguage.putIfAbsent( - items.last.id, - () => [], - ); - - currentMedia.add(Relation( - id: va['id'], - title: va['name']['userPreferred'], - imageUrl: va['image']['large'], - subtitle: l, - type: DiscoverType.staff, + if (onAnime == null || !onAnime) { + manga = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['manga']; + final value = manga.valueOrNull ?? const Paged(); + + final items = []; + for (final m in map['edges']) { + items.add(Relation( + id: m['node']['id'], + title: m['node']['title']['userPreferred'], + imageUrl: m['node']['coverImage'][Options().imageQuality.value], + subtitle: Convert.clarifyEnum(m['characterRole']), + type: DiscoverType.manga, )); } - } - } - - value = value.withNext(items, data['pageInfo']['hasNextPage']); - _anime = AsyncValue.data(value); - } - void _initManga(Map data) { - var value = _manga.valueOrNull; - if (value == null) return; - - final items = []; - for (final m in data['edges']) { - items.add(Relation( - id: m['node']['id'], - title: m['node']['title']['userPreferred'], - imageUrl: m['node']['coverImage'][Options().imageQuality.value], - subtitle: Convert.clarifyEnum(m['characterRole']), - type: DiscoverType.manga, - )); + return Future.value(value.withNext( + items, + map['pageInfo']['hasNextPage'] ?? false, + )); + }); } - value = value.withNext(items, data['pageInfo']['hasNextPage']); - _manga = AsyncValue.data(value); + state = CharacterMedia( + anime: anime, + manga: manga, + languageToVoiceActors: languageToVoiceActors, + language: language, + ); } + + void changeLanguage(String language) => state = CharacterMedia( + anime: state.anime, + manga: state.manga, + languageToVoiceActors: state.languageToVoiceActors, + language: language, + ); } diff --git a/lib/character/character_view.dart b/lib/character/character_view.dart index 7914ebbb..352b8207 100644 --- a/lib/character/character_view.dart +++ b/lib/character/character_view.dart @@ -1,15 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; +import 'package:otraku/character/character_action_buttons.dart'; import 'package:otraku/character/character_providers.dart'; import 'package:otraku/character/character_info_tab.dart'; -import 'package:otraku/character/character_media_tab.dart'; +import 'package:otraku/common/relation.dart'; import 'package:otraku/utils/paged_controller.dart'; +import 'package:otraku/widgets/grids/relation_grid.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/widgets/layouts/floating_bar.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'; +import 'package:otraku/widgets/paged_view.dart'; class CharacterView extends ConsumerStatefulWidget { const CharacterView(this.id, this.imageUrl); @@ -26,8 +30,8 @@ class _CharacterViewState extends ConsumerState { late final _ctrl = PagedController(loadMore: () { if (_tab == 0) return; _tab == 1 - ? ref.read(characterMediaProvider(widget.id)).fetchPage(true) - : ref.read(characterMediaProvider(widget.id)).fetchPage(false); + ? ref.read(characterMediaProvider(widget.id).notifier).fetch(true) + : ref.read(characterMediaProvider(widget.id).notifier).fetch(false); }); @override @@ -53,9 +57,11 @@ class _CharacterViewState extends ConsumerState { }, ); + final character = ref.watch(characterProvider(widget.id)); + ref.watch(characterMediaProvider(widget.id).select((_) => null)); - final name = ref.watch(characterProvider(widget.id)).valueOrNull?.name; - final topBar = TopBar(title: name); + + final onRefresh = () => ref.invalidate(characterMediaProvider(widget.id)); return PageScaffold( bottomBar: BottomBarIconTabs( @@ -68,14 +74,48 @@ class _CharacterViewState extends ConsumerState { 'Manga': Ionicons.bookmark_outline, }, ), - child: DirectPageView( - current: _tab, - onChanged: (i) => setState(() => _tab = i), - children: [ - CharacterInfoTab(widget.id, widget.imageUrl, _ctrl, topBar), - CharacterAnimeTab(widget.id, _ctrl, topBar), - CharacterMangaTab(widget.id, _ctrl, topBar), - ], + child: TabScaffold( + topBar: TopBar( + title: character.valueOrNull?.name, + ), + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: [ + if (_tab == 0 && character.hasValue) + CharacterFavoriteButton(character.valueOrNull!), + if (_tab > 0) CharacterMediaFilterButton(widget.id), + if (_tab == 1) CharacterLanguageSelectionButton(widget.id), + ], + ), + child: DirectPageView( + current: _tab, + onChanged: (i) => setState(() => _tab = i), + children: [ + CharacterInfoTab(widget.id, widget.imageUrl, _ctrl), + PagedView( + provider: + characterMediaProvider(widget.id).select((s) => s.anime), + onData: (data) { + final anime = []; + final voiceActors = []; + ref + .watch(characterMediaProvider(widget.id)) + .getAnimeAndVoiceActors(anime, voiceActors); + + return RelationGrid(items: anime, connections: voiceActors); + }, + scrollCtrl: _ctrl, + onRefresh: onRefresh, + ), + PagedView( + provider: + characterMediaProvider(widget.id).select((s) => s.manga), + onData: (data) => RelationGrid(items: data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + ), + ], + ), ), ); } diff --git a/lib/discover/discover_view.dart b/lib/discover/discover_view.dart index c9b8c774..30983700 100644 --- a/lib/discover/discover_view.dart +++ b/lib/discover/discover_view.dart @@ -253,7 +253,6 @@ class _Grid extends StatelessWidget { provider: discoverAnimeProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, - dataType: 'anime', onData: (data) => Options().discoverItemView == 0 ? DiscoverMediaGrid(data.items) : TileItemGrid(data.items), @@ -263,7 +262,6 @@ class _Grid extends StatelessWidget { provider: discoverMangaProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, - dataType: 'manga', onData: (data) => Options().discoverItemView == 0 ? DiscoverMediaGrid(data.items) : TileItemGrid(data.items), @@ -273,7 +271,6 @@ class _Grid extends StatelessWidget { provider: discoverCharacterProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, - dataType: 'characters', onData: (data) => TileItemGrid(data.items), ); case DiscoverType.staff: @@ -281,7 +278,6 @@ class _Grid extends StatelessWidget { provider: discoverStaffProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, - dataType: 'staff', onData: (data) => TileItemGrid(data.items), ); case DiscoverType.studio: @@ -289,7 +285,6 @@ class _Grid extends StatelessWidget { provider: discoverStudioProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, - dataType: 'studios', onData: (data) => StudioGrid(data.items), ); case DiscoverType.user: @@ -297,7 +292,6 @@ class _Grid extends StatelessWidget { provider: discoverUserProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, - dataType: 'users', onData: (data) => UserGrid(data.items), ); case DiscoverType.review: @@ -305,7 +299,6 @@ class _Grid extends StatelessWidget { provider: discoverReviewProvider, scrollCtrl: scrollCtrl, onRefresh: onRefresh, - dataType: 'reviews', onData: (data) => ReviewGrid(data.items), ); } diff --git a/lib/favorites/favorites_view.dart b/lib/favorites/favorites_view.dart index 420b9648..40156743 100644 --- a/lib/favorites/favorites_view.dart +++ b/lib/favorites/favorites_view.dart @@ -84,14 +84,12 @@ class _FavoritesViewState extends ConsumerState { onData: (data) => TileItemGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, - dataType: 'favourite anime', ), PagedView( provider: favoritesProvider(widget.id).select((s) => s.manga), onData: (data) => TileItemGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, - dataType: 'favourite manga', ), PagedView( provider: favoritesProvider(widget.id).select( @@ -100,21 +98,18 @@ class _FavoritesViewState extends ConsumerState { onData: (data) => TileItemGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, - dataType: 'favourite characters', ), PagedView( provider: favoritesProvider(widget.id).select((s) => s.staff), onData: (data) => TileItemGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, - dataType: 'favourite staff', ), PagedView( provider: favoritesProvider(widget.id).select((s) => s.studios), onData: (data) => StudioGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, - dataType: 'favourite studios', ), ], ), diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index b094c673..069a7b34 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -1,5 +1,4 @@ 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'; @@ -74,7 +73,7 @@ class MediaHeader extends StatelessWidget { } } -class _Delegate implements SliverPersistentHeaderDelegate { +class _Delegate extends SliverPersistentHeaderDelegate { _Delegate({ required this.id, required this.info, @@ -96,7 +95,6 @@ class _Delegate implements SliverPersistentHeaderDelegate { bool overlapsContent, ) { final height = maxExtent; - final extent = height - shrinkOffset; final opacity = shrinkOffset < (_bannerHeight - minExtent) ? shrinkOffset / (_bannerHeight - minExtent) : 1.0; @@ -115,197 +113,192 @@ class _Delegate implements SliverPersistentHeaderDelegate { ), ], ), - 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), - ], + child: Stack( + fit: StackFit.expand, + children: [ + Column( + children: [ + Flexible( + flex: _bannerHeight.ceil(), + child: info?.banner != null + ? GestureDetector( + child: CachedImage(info!.banner!), + onTap: () => showPopUp( + context, + ImageDialog(info!.banner!), + ), + ) + : const SizedBox(), ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, + Flexible( + flex: (height - _bannerHeight).floor(), + child: const SizedBox(), + ), + ], + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: height - _bannerHeight, + alignment: Alignment.topCenter, + color: theme.colorScheme.background, 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, - ), - ], - ), + 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, - ), + ), + 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, - ), + ), + 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, - ), - ], - ), + ), + 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, - ), - ], - ), + ), + if (textRailItems.isNotEmpty) + TextRail( + textRailItems, + style: theme.textTheme.labelMedium, + ), + ], ), - ], + ), + ], + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background.withAlpha(0), + ], + ), ), ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: Opacity( + opacity: opacity, child: DecoratedBox( decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(0), - ], - ), + color: theme.colorScheme.background, + boxShadow: [ + BoxShadow( + blurRadius: 10, + spreadRadius: 10, + color: theme.colorScheme.background, + ), + ], ), ), ), - 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: [ + TopBarIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: Navigator.of(context).pop, ), - ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, - child: Row( - children: [ - TopBarIcon( - 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, - ), + 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) - TopBarIcon( - tooltip: 'More', - icon: Ionicons.ellipsis_horizontal, - onTap: () => showSheet( - context, - FixedGradientDragSheet.link(context, info!.siteUrl!), - ), + ), + ), + if (info?.siteUrl != null) + TopBarIcon( + tooltip: 'More', + icon: Ionicons.ellipsis_horizontal, + onTap: () => showSheet( + context, + FixedGradientDragSheet.link(context, info!.siteUrl!), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ); } @@ -320,21 +313,7 @@ class _Delegate implements SliverPersistentHeaderDelegate { @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 0dbff466..b4d9e0fe 100644 --- a/lib/media/media_info_view.dart +++ b/lib/media/media_info_view.dart @@ -83,9 +83,6 @@ class MediaInfoView extends StatelessWidget { child: CustomScrollView( controller: scrollCtrl, slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), if (info.description.isNotEmpty) SliverToBoxAdapter( child: Padding( diff --git a/lib/media/media_models.dart b/lib/media/media_models.dart index 5bc632a2..430372ea 100644 --- a/lib/media/media_models.dart +++ b/lib/media/media_models.dart @@ -1,4 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/collection/collection_models.dart'; +import 'package:otraku/common/paged.dart'; +import 'package:otraku/common/relation.dart'; import 'package:otraku/common/tile_item.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/edit/edit_model.dart'; @@ -22,6 +25,69 @@ class Media { final List relations; } +class MediaRelations { + const MediaRelations({ + this.recommended = const AsyncValue.loading(), + this.characters = const AsyncValue.loading(), + this.staff = const AsyncValue.loading(), + this.reviews = const AsyncValue.loading(), + this.languageToVoiceActors = const {}, + this.language = '', + }); + + final AsyncValue> recommended; + final AsyncValue> characters; + final AsyncValue> staff; + final AsyncValue> reviews; + + /// For each language, a list of voice actors + /// is mapped to the corresponding media's id. + final Map>> languageToVoiceActors; + + /// The currently selected language. + final String language; + + Iterable get languages => languageToVoiceActors.keys; + + void getCharactersAndVoiceActors( + List resultingCharacters, + List resultingVoiceActors, + ) { + final chars = characters.valueOrNull?.items; + if (chars == null) return; + + final actorsPerMedia = languageToVoiceActors[language]; + if (actorsPerMedia == null) { + resultingCharacters.addAll(chars); + return; + } + + for (final c in chars) { + final actors = actorsPerMedia[c.id]; + if (actors == null || actors.isEmpty) { + resultingCharacters.add(c); + resultingVoiceActors.add(null); + continue; + } + + for (final va in actors) { + resultingCharacters.add(c); + resultingVoiceActors.add(va); + } + } + } +} + +enum MediaTab { + info, + relations, + recommended, + characters, + staff, + reviews, + statistics, +} + class RelatedMedia { RelatedMedia._({ required this.id, diff --git a/lib/media/media_other_view.dart b/lib/media/media_other_view.dart index 7bf192cb..7a035ba9 100644 --- a/lib/media/media_other_view.dart +++ b/lib/media/media_other_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/media/media_models.dart'; import 'package:otraku/media/media_providers.dart'; +import 'package:otraku/widgets/layouts/constrained_view.dart'; import 'package:otraku/widgets/link_tile.dart'; import 'package:otraku/widgets/cached_image.dart'; import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; @@ -10,6 +11,7 @@ import 'package:otraku/widgets/layouts/floating_bar.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/paged_view.dart'; import 'package:otraku/widgets/text_rail.dart'; class MediaOtherView extends StatelessWidget { @@ -42,39 +44,24 @@ class MediaOtherView extends StatelessWidget { onChanged: null, current: tabToggled ? 1 : 0, children: [ - CustomScrollView( - controller: scrollCtrl, - slivers: [ - SliverOverlapInjector( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - _RelatedGrid(related), - const SliverFooter(), - ], + ConstrainedView( + child: CustomScrollView( + physics: Consts.physics, + controller: scrollCtrl, + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 10)), + _RelatedGrid(related), + const SliverFooter(), + ], + ), ), Consumer( - child: SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(id).select((s) => s.recommended), + onData: (data) => _RecommendationsGrid(id, data.items), + onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), + scrollCtrl: scrollCtrl, ), - builder: (context, ref, overlapInjector) { - return ref - .watch(mediaContentProvider(id).select((s) => s.recommended)) - .when( - loading: () => const Center(child: Loader()), - error: (_, __) => const Center( - child: Text('Failed to load recommendations'), - ), - data: (data) => CustomScrollView( - controller: scrollCtrl, - slivers: [ - overlapInjector!, - _RecommendationsGrid(id, data.items), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }, ), ], ), @@ -95,70 +82,67 @@ class _RelatedGrid extends StatelessWidget { ); } - return SliverPadding( - padding: const EdgeInsets.only(top: 10, left: 10, right: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 230, - height: 100, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (context, i) { - final details = { - if (items[i].relationType != null) items[i].relationType!: true, - if (items[i].format != null) items[i].format!: false, - if (items[i].status != null) items[i].status!: false, - }; + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 230, + height: 100, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) { + final details = { + if (items[i].relationType != null) items[i].relationType!: true, + if (items[i].format != null) items[i].format!: false, + if (items[i].status != null) items[i].status!: false, + }; - return LinkTile( - id: items[i].id, - info: items[i].imageUrl, - discoverType: items[i].type, - child: Card( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Hero( - tag: items[i].id, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: Container( - color: Theme.of(context).colorScheme.surfaceVariant, - child: CachedImage( - items[i].imageUrl, - width: 100 / Consts.coverHtoWRatio, - ), + return LinkTile( + id: items[i].id, + info: items[i].imageUrl, + discoverType: items[i].type, + child: Card( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: items[i].id, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: CachedImage( + items[i].imageUrl, + width: 100 / Consts.coverHtoWRatio, ), ), ), - Expanded( - child: Padding( - padding: Consts.padding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Text( - items[i].title, - overflow: TextOverflow.fade, - ), - ), - const SizedBox(height: 5), - TextRail( - details, - style: Theme.of(context).textTheme.labelMedium, + ), + Expanded( + child: Padding( + padding: Consts.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Text( + items[i].title, + overflow: TextOverflow.fade, ), - ], - ), + ), + const SizedBox(height: 5), + TextRail( + details, + style: Theme.of(context).textTheme.labelMedium, + ), + ], ), ), - ], - ), + ), + ], ), - ); - }, - ), + ), + ); + }, ), ); } @@ -178,53 +162,50 @@ class _RecommendationsGrid extends StatelessWidget { ); } - return SliverPadding( - padding: const EdgeInsets.only(top: 10, left: 10, right: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndExtraHeight( - minWidth: 100, - extraHeight: 70, - rawHWRatio: Consts.coverHtoWRatio, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (context, i) => Card( - child: LinkTile( - id: items[i].id, - discoverType: items[i].type, - info: items[i].imageUrl, - 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!), - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndExtraHeight( + minWidth: 100, + extraHeight: 70, + rawHWRatio: Consts.coverHtoWRatio, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) => Card( + child: LinkTile( + id: items[i].id, + discoverType: items[i].type, + info: items[i].imageUrl, + 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!), ), ), ), - Padding( - padding: const EdgeInsets.only(top: 5, left: 5, right: 5), - child: SizedBox( - height: 35, - child: Text( - items[i].title, - overflow: TextOverflow.fade, - maxLines: 2, - style: Theme.of(context).textTheme.bodyMedium, - ), + ), + Padding( + padding: const EdgeInsets.only(top: 5, left: 5, right: 5), + child: SizedBox( + height: 35, + child: Text( + items[i].title, + overflow: TextOverflow.fade, + maxLines: 2, + style: Theme.of(context).textTheme.bodyMedium, ), ), - Padding( - padding: const EdgeInsets.only(left: 5, right: 5), - child: _Rating(mediaId, items[i]), - ), - ], - ), + ), + Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: _Rating(mediaId, items[i]), + ), + ], ), ), ), diff --git a/lib/media/media_people_view.dart b/lib/media/media_people_view.dart index 59464d4f..67bbc522 100644 --- a/lib/media/media_people_view.dart +++ b/lib/media/media_people_view.dart @@ -3,13 +3,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/media/media_providers.dart'; import 'package:otraku/common/relation.dart'; -import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/widgets/grids/relation_grid.dart'; import 'package:otraku/widgets/layouts/floating_bar.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'; +import 'package:otraku/widgets/paged_view.dart'; class MediaPeopleView extends StatelessWidget { const MediaPeopleView(this.id, this.tabToggled, this.toggleTab); @@ -46,67 +45,20 @@ class MediaPeopleView extends StatelessWidget { current: tabToggled ? 1 : 0, children: [ Consumer( - child: SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(id).select((s) => s.characters), + onData: (data) => _CharacterGrid(id, ref, data.items), + scrollCtrl: scrollCtrl, + onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), ), - builder: (context, ref, overlapInjector) { - return ref - .watch(mediaContentProvider(id).select((s) => s.characters)) - .when( - loading: () => const Center(child: Loader()), - error: (_, __) => const Center( - child: Text('Failed to load characters'), - ), - data: (data) => CustomScrollView( - controller: scrollCtrl, - slivers: [ - overlapInjector!, - SliverPadding( - padding: const EdgeInsets.only( - top: 10, - left: 10, - right: 10, - ), - sliver: _CharacterGrid(id, ref, data.items), - ), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }, ), Consumer( - child: SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(id).select((s) => s.staff), + onData: (data) => RelationGrid(items: data.items), + scrollCtrl: scrollCtrl, + onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), ), - builder: (context, ref, overlapInjector) { - return ref - .watch(mediaContentProvider(id).select((s) => s.staff)) - .when( - loading: () => const Center(child: Loader()), - error: (_, __) => const Center( - child: Text('Failed to load staff'), - ), - data: (data) => CustomScrollView( - controller: scrollCtrl, - slivers: [ - overlapInjector!, - SliverPadding( - padding: const EdgeInsets.only( - top: 10, - left: 10, - right: 10, - ), - sliver: RelationGrid( - placeholder: 'No Staff', - items: data.items, - ), - ), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }, ), ], ), @@ -124,32 +76,38 @@ class _LanguageButton extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, ref, _) { - final notifier = ref.watch(mediaContentProvider(id)); - if (notifier.languages.length < 2) return const SizedBox(); + if (ref.watch(mediaRelationsProvider(id).select( + (s) => s.languages.length < 2, + ))) return const SizedBox(); return ActionButton( tooltip: 'Language', icon: Ionicons.globe_outline, - onTap: () => showSheet( - context, - DynamicGradientDragSheet( - onTap: (i) { - scrollCtrl.scrollToTop(); - ref.read(mediaContentProvider(id)).languageIndex = i; - }, - children: [ - for (int i = 0; i < notifier.languages.length; i++) - Text( - notifier.languages[i], - style: i != notifier.languageIndex - ? Theme.of(context).textTheme.titleLarge - : Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - ), + onTap: () { + final mediaRelations = ref.read(mediaRelationsProvider(id)); + final languages = mediaRelations.languages; + final language = mediaRelations.language; + + showSheet( + context, + DynamicGradientDragSheet( + onTap: (i) => ref + .read(mediaRelationsProvider(id).notifier) + .changeLanguage(languages.elementAt(i)), + children: [ + for (int i = 0; i < languages.length; i++) + Text( + languages.elementAt(i), + style: languages.elementAt(i) != language + ? Theme.of(context).textTheme.titleLarge + : Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ); + }, ); }, ); @@ -165,20 +123,16 @@ class _CharacterGrid extends StatelessWidget { @override Widget build(BuildContext context) { - final notifier = ref.watch(mediaContentProvider(id)); + final mediaRelations = ref.watch(mediaRelationsProvider(id)); - if (notifier.languages.isEmpty) { - return RelationGrid(placeholder: 'No Characters', items: items); + if (mediaRelations.languages.isEmpty) { + return RelationGrid(items: items); } final characters = []; final voiceActors = []; - notifier.selectCharactersAndVoiceActors(characters, voiceActors); + mediaRelations.getCharactersAndVoiceActors(characters, voiceActors); - return RelationGrid( - placeholder: 'No Characters', - items: characters, - connections: voiceActors, - ); + return RelationGrid(items: characters, connections: voiceActors); } } diff --git a/lib/media/media_providers.dart b/lib/media/media_providers.dart index a4dcf9c6..505dd5b7 100644 --- a/lib/media/media_providers.dart +++ b/lib/media/media_providers.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/edit/edit_model.dart'; @@ -58,287 +57,192 @@ final mediaProvider = FutureProvider.autoDispose.family( }, ); -final mediaContentProvider = ChangeNotifierProvider.autoDispose.family( - (ref, int mediaId) => MediaContentNotifier(mediaId), +final mediaRelationsProvider = StateNotifierProvider.autoDispose + .family( + (ref, int mediaId) => MediaRelationsNotifier(mediaId), ); -class MediaContentNotifier extends ChangeNotifier { - MediaContentNotifier(this.mediaId) { - _fetch(); +class MediaRelationsNotifier extends StateNotifier { + MediaRelationsNotifier(this.mediaId) : super(const MediaRelations()) { + _fetch(null); } final int mediaId; - var _recommended = const AsyncValue>.loading(); - var _characters = const AsyncValue>.loading(); - var _staff = const AsyncValue>.loading(); - var _reviews = const AsyncValue>.loading(); + Future fetch(MediaTab tab) => _fetch(tab); - int _languageIndex = 0; - final languages = []; - final _voiceActors = >>{}; - - int get languageIndex => _languageIndex; - set languageIndex(int val) { - if (_languageIndex == val) return; - _languageIndex = val; - notifyListeners(); - } - - AsyncValue> get recommended => _recommended; - AsyncValue> get characters => _characters; - AsyncValue> get staff => _staff; - AsyncValue> get reviews => _reviews; - - void selectCharactersAndVoiceActors( - List characterList, - List voiceActorList, - ) { - final chars = _characters.valueOrNull?.items; - if (chars == null) return; - - final byLanguage = _voiceActors[languages[_languageIndex]]; - if (byLanguage == null) { - characterList.addAll(chars); + Future _fetch(MediaTab? tab) async { + if (tab == MediaTab.info || + tab == MediaTab.relations || + tab == MediaTab.statistics) { return; } - for (final c in chars) { - final vas = byLanguage[c.id]; - if (vas == null || vas.isEmpty) { - characterList.add(c); - voiceActorList.add(null); - continue; - } - - for (final va in vas) { - characterList.add(c); - voiceActorList.add(va); - } - } - } - - Future _fetch() async { - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.media, { - 'id': mediaId, - 'withRecommendations': true, - 'withCharacters': true, - 'withStaff': true, - 'withReviews': true, - }); - return data['Media']; - }); - - if (data.hasError) { - _recommended = AsyncValue.error(data.error!, data.stackTrace!); - _characters = AsyncValue.error(data.error!, data.stackTrace!); - _staff = AsyncValue.error(data.error!, data.stackTrace!); - _reviews = AsyncValue.error(data.error!, data.stackTrace!); - return; + final variables = {'id': mediaId}; + if (tab == null) { + variables['withRecommendations'] = true; + variables['withCharacters'] = true; + variables['withStaff'] = true; + variables['withReviews'] = true; + } else if (tab == MediaTab.recommended) { + if (!(state.recommended.valueOrNull?.hasNext ?? true)) return; + variables['withRecommendations'] = true; + variables['page'] = state.recommended.valueOrNull?.next ?? 1; + } else if (tab == MediaTab.characters) { + if (!(state.characters.valueOrNull?.hasNext ?? true)) return; + variables['withCharacters'] = true; + variables['page'] = state.characters.valueOrNull?.next ?? 1; + } else if (tab == MediaTab.staff) { + if (!(state.staff.valueOrNull?.hasNext ?? true)) return; + variables['withStaff'] = true; + variables['page'] = state.staff.valueOrNull?.next ?? 1; + } else if (tab == MediaTab.reviews) { + if (!(state.reviews.valueOrNull?.hasNext ?? true)) return; + variables['withReviews'] = true; + variables['page'] = state.reviews.valueOrNull?.next ?? 1; } - _recommended = const AsyncValue.data(Paged()); - _characters = const AsyncValue.data(Paged()); - _staff = const AsyncValue.data(Paged()); - _reviews = const AsyncValue.data(Paged()); - - _initRecommended(data.value!['recommendations']); - _initCharacters(data.value!['characters']); - _initStaff(data.value!['staff']); - _initReviews(data.value!['reviews']); - notifyListeners(); - } - - Future fetchRecommended() async { - final value = _recommended.valueOrNull; - if (value == null || !value.hasNext) return; - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.media, { - 'id': mediaId, - 'page': value.next, - 'withRecommendations': true, - }); + final data = await Api.get(GqlQuery.media, variables); return data['Media']; }); - if (data.hasError) { - _recommended = AsyncValue.error(data.error!, data.stackTrace!); - return; - } - - _initRecommended(data.value!['recommendations']); - notifyListeners(); - } - - Future fetchCharacters() async { - final value = _characters.valueOrNull; - if (value == null || !value.hasNext) return; - - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.media, { - 'id': mediaId, - 'page': value.next, - 'withCharacters': true, + var recommended = state.recommended; + var characters = state.characters; + var staff = state.staff; + var reviews = state.reviews; + var languageToVoiceActors = state.languageToVoiceActors; + var language = state.language; + + if (tab == null || tab == MediaTab.recommended) { + recommended = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['recommendations']; + final value = recommended.valueOrNull ?? const Paged(); + + final items = []; + for (final r in map['nodes']) { + if (r['mediaRecommendation'] != null) items.add(Recommendation(r)); + } + + return Future.value( + value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), + ); }); - return data['Media']; - }); - - if (data.hasError) { - _characters = AsyncValue.error(data.error!, data.stackTrace!); - return; } - _initCharacters(data.value!['characters']); - notifyListeners(); - } - - Future fetchStaff() async { - final value = _staff.valueOrNull; - if (value == null || !value.hasNext) return; - - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.media, { - 'id': mediaId, - 'page': value.next, - 'withStaff': true, + if (tab == null || tab == MediaTab.characters) { + characters = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['characters']; + final value = characters.valueOrNull ?? const Paged(); + + /// The map could be immutable, so a copy is made. + languageToVoiceActors = {...state.languageToVoiceActors}; + + final items = []; + for (final c in map['edges']) { + items.add(Relation( + id: c['node']['id'], + title: c['node']['name']['userPreferred'], + imageUrl: c['node']['image']['large'], + subtitle: Convert.clarifyEnum(c['role']), + type: DiscoverType.character, + )); + + if (c['voiceActors'] == null) continue; + + for (final va in c['voiceActors']) { + final l = Convert.clarifyEnum(va['languageV2']); + if (l == null) continue; + + final currentLanguage = languageToVoiceActors.putIfAbsent( + l, + () => >{}, + ); + + final currentCharacter = currentLanguage.putIfAbsent( + items.last.id, + () => [], + ); + + currentCharacter.add(Relation( + id: va['id'], + title: va['name']['userPreferred'], + imageUrl: va['image']['large'], + subtitle: l, + type: DiscoverType.staff, + )); + } + } + + if (language.isEmpty && languageToVoiceActors.isNotEmpty) { + language = languageToVoiceActors.keys.first; + } + + return Future.value( + value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), + ); }); - return data['Media']; - }); - - if (data.hasError) { - _staff = AsyncValue.error(data.error!, data.stackTrace!); - return; } - _initStaff(data.value!['staff']); - notifyListeners(); - } - - Future fetchReviews() async { - final value = _reviews.valueOrNull; - if (value == null || !value.hasNext) return; - - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.media, { - 'id': mediaId, - 'page': value.next, - 'withReviews': true, + if (tab == null || tab == MediaTab.staff) { + staff = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['staff']; + final value = staff.valueOrNull ?? const Paged(); + + final items = []; + for (final s in map['edges']) { + items.add(Relation( + id: s['node']['id'], + title: s['node']['name']['userPreferred'], + imageUrl: s['node']['image']['large'], + subtitle: s['role'], + type: DiscoverType.staff, + )); + } + + return Future.value( + value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), + ); }); - return data['Media']; - }); - - if (data.hasError) { - _reviews = AsyncValue.error(data.error!, data.stackTrace!); - return; } - _initReviews(data.value!['reviews']); - notifyListeners(); - } - - void _initRecommended(Map map) { - var value = _recommended.valueOrNull; - if (value == null) return; - - final items = []; - for (final r in map['nodes']) { - if (r['mediaRecommendation'] != null) items.add(Recommendation(r)); - } - - value = value.withNext( - items, - map['pageInfo']['hasNextPage'], - ); - _recommended = AsyncValue.data(value); - } - - void _initCharacters(Map map) { - var value = _characters.valueOrNull; - if (value == null) return; - - final items = []; - for (final c in map['edges']) { - items.add(Relation( - id: c['node']['id'], - title: c['node']['name']['userPreferred'], - imageUrl: c['node']['image']['large'], - subtitle: Convert.clarifyEnum(c['role']), - type: DiscoverType.character, - )); - - if (c['voiceActors'] == null) continue; + if (tab == null || tab == MediaTab.reviews) { + reviews = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['reviews']; + final value = reviews.valueOrNull ?? const Paged(); - for (final va in c['voiceActors']) { - final l = Convert.clarifyEnum(va['languageV2']); - if (l == null) continue; + final items = []; + for (final r in map['nodes']) { + final item = RelatedReview.maybe(r); + if (item != null) items.add(item); + } - if (!languages.contains(l)) languages.add(l); - - final currentLanguage = _voiceActors.putIfAbsent( - l, - () => >{}, + return Future.value( + value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), ); - - final currentCharacter = currentLanguage.putIfAbsent( - items.last.id, - () => [], - ); - - currentCharacter.add(Relation( - id: va['id'], - title: va['name']['userPreferred'], - imageUrl: va['image']['large'], - subtitle: l, - type: DiscoverType.staff, - )); - } - } - - value = value.withNext( - items, - map['pageInfo']['hasNextPage'], - ); - _characters = AsyncValue.data(value); - } - - void _initStaff(Map map) { - var value = _staff.valueOrNull; - if (value == null) return; - - final items = []; - for (final s in map['edges']) { - items.add(Relation( - id: s['node']['id'], - title: s['node']['name']['userPreferred'], - imageUrl: s['node']['image']['large'], - subtitle: s['role'], - type: DiscoverType.staff, - )); + }); } - value = value.withNext( - items, - map['pageInfo']['hasNextPage'], + state = MediaRelations( + recommended: recommended, + characters: characters, + staff: staff, + reviews: reviews, + languageToVoiceActors: languageToVoiceActors, + language: language, ); - _staff = AsyncValue.data(value); } - void _initReviews(Map map) { - var value = _reviews.valueOrNull; - if (value == null) return; - - final items = []; - for (final r in map['nodes']) { - final item = RelatedReview.maybe(r); - if (item != null) items.add(item); - } - - value = value.withNext( - items, - map['pageInfo']['hasNextPage'], - ); - _reviews = AsyncValue.data(value); - } + void changeLanguage(String language) => state = MediaRelations( + recommended: state.recommended, + characters: state.characters, + staff: state.staff, + reviews: state.reviews, + languageToVoiceActors: state.languageToVoiceActors, + language: language, + ); } diff --git a/lib/media/media_social_view.dart b/lib/media/media_social_view.dart index 968f52f0..4c549d2a 100644 --- a/lib/media/media_social_view.dart +++ b/lib/media/media_social_view.dart @@ -13,6 +13,7 @@ import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/widgets/layouts/floating_bar.dart'; import 'package:otraku/widgets/layouts/direct_page_view.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/widgets/paged_view.dart'; class MediaSocialView extends StatelessWidget { const MediaSocialView(this.id, this.media, this.tabToggled, this.toggleTab); @@ -46,35 +47,16 @@ class MediaSocialView extends StatelessWidget { current: tabToggled ? 1 : 0, children: [ Consumer( - child: SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(id).select((s) => s.reviews), + onData: (data) => _ReviewGrid(data.items, media.info.banner), + scrollCtrl: scrollCtrl, + onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), ), - builder: (context, ref, overlapInjector) { - return ref - .watch(mediaContentProvider(id).select((s) => s.reviews)) - .when( - loading: () => const Center(child: Loader()), - error: (_, __) => const Center( - child: Text('Failed to load reviews'), - ), - data: (data) => CustomScrollView( - controller: scrollCtrl, - slivers: [ - overlapInjector!, - _ReviewGrid(data.items, media.info.banner), - SliverFooter(loading: data.hasNext), - ], - ), - ); - }, ), CustomScrollView( controller: scrollCtrl, slivers: [ - SliverOverlapInjector( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), if (stats.rankTexts.isNotEmpty) _Ranks(stats.rankTexts, stats.rankTypes), if (stats.scoreNames.isNotEmpty) @@ -104,70 +86,67 @@ class _ReviewGrid extends StatelessWidget { ); } - return SliverPadding( - padding: const EdgeInsets.only(top: 10, left: 10, right: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 300, - height: 140, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (context, i) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LinkTile( - id: items[i].userId, - info: items[i].avatar, - discoverType: DiscoverType.user, - child: Row( - children: [ - Hero( - tag: items[i].userId, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: CachedImage( - items[i].avatar, - height: 50, - width: 50, - ), + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 300, + height: 140, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LinkTile( + id: items[i].userId, + info: items[i].avatar, + discoverType: DiscoverType.user, + child: Row( + children: [ + Hero( + tag: items[i].userId, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: CachedImage( + items[i].avatar, + height: 50, + width: 50, ), ), - 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, - ), - ], - ), + ), + 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, + ), + ], ), - const SizedBox(height: 5), - Expanded( - child: LinkTile( - id: items[i].reviewId, - info: bannerUrl, - discoverType: DiscoverType.review, - child: Card( - child: SizedBox( - width: double.infinity, - child: Padding( - padding: Consts.padding, - child: Text( - items[i].summary, - style: Theme.of(context).textTheme.labelMedium, - overflow: TextOverflow.fade, - ), + ), + const SizedBox(height: 5), + Expanded( + child: LinkTile( + id: items[i].reviewId, + info: bannerUrl, + discoverType: DiscoverType.review, + child: Card( + child: SizedBox( + width: double.infinity, + child: Padding( + padding: Consts.padding, + child: Text( + items[i].summary, + style: Theme.of(context).textTheme.labelMedium, + overflow: TextOverflow.fade, ), ), ), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/media/media_view.dart b/lib/media/media_view.dart index 8c42de70..2b1da020 100644 --- a/lib/media/media_view.dart +++ b/lib/media/media_view.dart @@ -54,10 +54,7 @@ class _MediaViewState extends State { child: NestedScrollView( controller: _scrollCtrl, headerSliverBuilder: (context, _) => [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: MediaHeader(widget.id, widget.coverUrl), - ), + MediaHeader(widget.id, widget.coverUrl), ], body: Consumer( builder: (context, ref, _) { @@ -78,9 +75,10 @@ class _MediaViewState extends State { return ref.watch(mediaProvider(widget.id)).when( loading: () => const Center(child: Loader()), - error: (_, __) => - const Center(child: Text('Failed to load media')), - data: (media) => _MediaView( + error: (_, __) => const Center( + child: Text('Failed to load media'), + ), + data: (media) => _MediaSubView( widget.id, _tab, media, @@ -99,8 +97,8 @@ class _MediaViewState extends State { /// can't be used here and has to be reimplemented temporarely on the inner /// scroll controller of the [NestedScrollView]. /// For more context: https://github.com/flutter/flutter/pull/104166. -class _MediaView extends ConsumerStatefulWidget { - const _MediaView(this.id, this.tab, this.media, this.onChanged); +class _MediaSubView extends ConsumerStatefulWidget { + const _MediaSubView(this.id, this.tab, this.media, this.onChanged); final int id; final int tab; @@ -108,10 +106,10 @@ class _MediaView extends ConsumerStatefulWidget { final void Function(int) onChanged; @override - ConsumerState<_MediaView> createState() => __MediaSubViewState(); + ConsumerState<_MediaSubView> createState() => __MediaSubViewState(); } -class __MediaSubViewState extends ConsumerState<_MediaView> { +class __MediaSubViewState extends ConsumerState<_MediaSubView> { late final ScrollController _scrollCtrl; double _lastMaxExtent = 0; bool _otherTabToggled = false; @@ -128,7 +126,7 @@ class __MediaSubViewState extends ConsumerState<_MediaView> { } @override - void didUpdateWidget(covariant _MediaView oldWidget) { + void didUpdateWidget(covariant _MediaSubView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.tab != oldWidget.tab) _lastMaxExtent = 0; } @@ -148,17 +146,25 @@ class __MediaSubViewState extends ConsumerState<_MediaView> { switch (widget.tab) { case 1: if (_otherTabToggled) { - ref.read(mediaContentProvider(widget.id)).fetchRecommended(); + ref + .read(mediaRelationsProvider(widget.id).notifier) + .fetch(MediaTab.recommended); } return; case 2: _peopleTabToggled - ? ref.read(mediaContentProvider(widget.id)).fetchStaff() - : ref.read(mediaContentProvider(widget.id)).fetchCharacters(); + ? ref + .read(mediaRelationsProvider(widget.id).notifier) + .fetch(MediaTab.staff) + : ref + .read(mediaRelationsProvider(widget.id).notifier) + .fetch(MediaTab.characters); return; case 3: if (!_socialTabToggled) { - ref.read(mediaContentProvider(widget.id)).fetchReviews(); + ref + .read(mediaRelationsProvider(widget.id).notifier) + .fetch(MediaTab.reviews); } return; } @@ -166,7 +172,7 @@ class __MediaSubViewState extends ConsumerState<_MediaView> { @override Widget build(BuildContext context) { - ref.watch(mediaContentProvider(widget.id).select((_) => null)); + ref.watch(mediaRelationsProvider(widget.id).select((_) => null)); return DirectPageView( current: widget.tab, diff --git a/lib/notifications/notifications_view.dart b/lib/notifications/notifications_view.dart index f066a2f3..b451f383 100644 --- a/lib/notifications/notifications_view.dart +++ b/lib/notifications/notifications_view.dart @@ -83,9 +83,8 @@ class _NotificationsViewState extends ConsumerState { childCount: data.items.length, ), ), - onRefresh: () => ref.invalidate(notificationsProvider), scrollCtrl: _ctrl, - dataType: 'notifications', + onRefresh: () => ref.invalidate(notificationsProvider), ), ), ); diff --git a/lib/review/reviews_view.dart b/lib/review/reviews_view.dart index f56595b5..a8505648 100644 --- a/lib/review/reviews_view.dart +++ b/lib/review/reviews_view.dart @@ -91,7 +91,6 @@ class _ReviewsViewState extends ConsumerState { onData: (data) => ReviewGrid(data.items), onRefresh: () => ref.invalidate(reviewsProvider(widget.id)), scrollCtrl: _ctrl, - dataType: 'reviews', ), ), ); diff --git a/lib/social/social_view.dart b/lib/social/social_view.dart index c5b4f776..6c460eb1 100644 --- a/lib/social/social_view.dart +++ b/lib/social/social_view.dart @@ -80,14 +80,12 @@ class _SocialViewState extends ConsumerState { onData: (data) => UserGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, - dataType: 'following', ), PagedView( provider: socialProvider(widget.id).select((s) => s.followers), onData: (data) => UserGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, - dataType: 'followers', ), ], ), diff --git a/lib/staff/staff_action_buttons.dart b/lib/staff/staff_action_buttons.dart new file mode 100644 index 00000000..fd7ab93a --- /dev/null +++ b/lib/staff/staff_action_buttons.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ionicons/ionicons.dart'; +import 'package:otraku/filter/chip_selector.dart'; +import 'package:otraku/media/media_constants.dart'; +import 'package:otraku/staff/staff_models.dart'; +import 'package:otraku/staff/staff_providers.dart'; +import 'package:otraku/utils/consts.dart'; +import 'package:otraku/utils/convert.dart'; +import 'package:otraku/widgets/layouts/floating_bar.dart'; +import 'package:otraku/widgets/overlays/sheets.dart'; + +class StaffFavoriteButton extends StatefulWidget { + const StaffFavoriteButton(this.data); + + final Staff data; + + @override + State createState() => _StaffFavoriteButtonState(); +} + +class _StaffFavoriteButtonState extends State { + @override + Widget build(BuildContext context) { + return ActionButton( + icon: widget.data.isFavorite ? Icons.favorite : Icons.favorite_border, + tooltip: widget.data.isFavorite ? 'Unfavourite' : 'Favourite', + onTap: () { + setState(() => widget.data.isFavorite = !widget.data.isFavorite); + toggleFavoriteStaff(widget.data.id).then((ok) { + if (!ok) { + setState(() => widget.data.isFavorite = !widget.data.isFavorite); + } + }); + }, + ); + } +} + +class StaffFilterButton extends StatelessWidget { + const StaffFilterButton(this.id, this.full); + + final int id; + final bool full; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, _) { + return ActionButton( + icon: Ionicons.funnel_outline, + tooltip: 'Filter', + onTap: () { + var filter = ref.read(staffFilterProvider(id)); + + final sortItems = {}; + for (int i = 0; i < MediaSort.values.length; i += 2) { + String key = Convert.clarifyEnum(MediaSort.values[i].name)!; + sortItems[key] = i ~/ 2; + } + + final onDone = (_) => + ref.read(staffFilterProvider(id).notifier).state = filter; + + showSheet( + context, + OpaqueSheet( + initialHeight: Consts.tapTargetSize * (full ? 5.5 : 4), + builder: (context, scrollCtrl) => ListView( + controller: scrollCtrl, + physics: Consts.physics, + padding: const EdgeInsets.symmetric(vertical: 20), + children: [ + ChipSelector( + title: 'Sort', + options: MediaSort.values.map((s) => s.label).toList(), + selected: filter.sort.index, + mustHaveSelected: true, + onChanged: (i) => filter = filter.copyWith( + sort: MediaSort.values.elementAt(i!), + ), + ), + if (full) ...[ + ChipSelector( + title: 'Type', + options: const ['Anime', 'Manga'], + selected: filter.ofAnime == null + ? null + : filter.ofAnime! + ? 0 + : 1, + onChanged: (val) => + filter = filter.copyWith(ofAnime: () { + if (val == null) return null; + return val == 0 ? true : false; + }), + ), + const SizedBox(height: 10), + ], + ChipSelector( + title: 'List Presence', + options: const ['On List', 'Not on List'], + selected: filter.onList == null + ? null + : filter.onList! + ? 0 + : 1, + onChanged: (val) => filter = filter.copyWith(onList: () { + if (val == null) return null; + return val == 0 ? true : false; + }), + ), + ], + ), + ), + ).then(onDone); + }, + ); + }, + ); + } +} diff --git a/lib/staff/staff_info_tab.dart b/lib/staff/staff_info_tab.dart index f6568a51..f0506a3c 100644 --- a/lib/staff/staff_info_tab.dart +++ b/lib/staff/staff_info_tab.dart @@ -1,86 +1,21 @@ import 'package:flutter/material.dart'; 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/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/scaffolds.dart'; -import 'package:otraku/widgets/layouts/top_bar.dart'; +import 'package:otraku/widgets/layouts/constrained_view.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, this.topBar); + const StaffInfoTab(this.id, this.imageUrl, this.scrollCtrl); final int id; final String? imageUrl; final ScrollController scrollCtrl; - final TopBar topBar; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(staffProvider(id)), - ); - - return ref.watch(staffProvider(id)).when( - loading: () => _TabContent( - id: id, - data: null, - imageUrl: imageUrl, - scrollCtrl: scrollCtrl, - refreshControl: refreshControl, - topBar: topBar, - loading: true, - ), - error: (_, __) => _TabContent( - id: id, - data: null, - imageUrl: imageUrl, - scrollCtrl: scrollCtrl, - refreshControl: refreshControl, - topBar: topBar, - loading: false, - ), - data: (data) => _TabContent( - id: id, - data: data, - imageUrl: imageUrl, - scrollCtrl: scrollCtrl, - refreshControl: refreshControl, - topBar: topBar, - loading: false, - ), - ); - }, - ); - } -} - -class _TabContent extends StatelessWidget { - const _TabContent({ - required this.id, - required this.data, - required this.imageUrl, - required this.scrollCtrl, - required this.refreshControl, - required this.topBar, - required this.loading, - }); - - final int id; - final Staff? data; - final String? imageUrl; - final ScrollController scrollCtrl; - final Widget refreshControl; - final TopBar topBar; - final bool loading; @override Widget build(BuildContext context) { @@ -89,147 +24,140 @@ class _TabContent extends StatelessWidget { : 100.0; final imageHeight = imageWidth * Consts.coverHtoWRatio; - final imageUrl = data?.imageUrl ?? this.imageUrl; - - final headerRow = IntrinsicHeight( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (imageUrl != null) - Hero( - tag: id, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: Container( - width: imageWidth, - height: imageHeight, - color: Theme.of(context).colorScheme.surfaceVariant, - child: GestureDetector( - child: CachedImage(imageUrl), - onTap: () => showPopUp(context, ImageDialog(imageUrl)), - ), - ), - ), - ), - const SizedBox(width: 10), - if (data != null) - Flexible( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, + return Consumer( + builder: (context, ref, _) { + final staff = ref.watch(staffProvider(id)); + final imageUrl = staff.valueOrNull?.imageUrl ?? this.imageUrl; + + final header = SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: () => Toast.copy(context, data!.name), - child: Text( - data!.name, - style: Theme.of(context).textTheme.titleLarge, + if (imageUrl != null) + Padding( + padding: const EdgeInsets.only(right: 10), + child: Hero( + tag: id, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: Container( + width: imageWidth, + height: imageHeight, + color: Theme.of(context).colorScheme.surfaceVariant, + child: GestureDetector( + child: CachedImage(imageUrl), + onTap: () => + showPopUp(context, ImageDialog(imageUrl)), + ), + ), + ), + ), + ), + staff.maybeWhen( + orElse: () => const SizedBox(), + data: (data) => Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: () => Toast.copy(context, data.name), + child: Text( + data.name, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + if (data.altNames.isNotEmpty) + Text(data.altNames.join(', ')), + ], + ), ), ), - if (data!.altNames.isNotEmpty) - Text(data!.altNames.join(', ')), ], ), ), - ], - ), - ); + ), + ); - const space = SliverToBoxAdapter(child: SizedBox(height: 10)); + final refreshControl = SliverRefreshControl( + onRefresh: () => ref.invalidate(staffProvider(id)), + ); - return TabScaffold( - topBar: topBar, - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [if (data != null) _FavoriteButton(data!)], - ), - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: CustomScrollView( + return ConstrainedView( + child: staff.when( + loading: () => CustomScrollView( + physics: Consts.physics, controller: scrollCtrl, + slivers: [ + refreshControl, + header, + const SliverFillRemaining(child: Center(child: Loader())), + const SliverFooter(), + ], + ), + error: (_, __) => CustomScrollView( physics: Consts.physics, + controller: scrollCtrl, slivers: [ refreshControl, - space, - SliverToBoxAdapter(child: headerRow), - if (data != null) ...[ - space, - SliverGrid( - gridDelegate: - const SliverGridDelegateWithMinWidthAndFixedHeight( - height: Consts.tapTargetSize, - minWidth: 150, - ), - delegate: SliverChildListDelegate([ - _InfoTile('Favourites', data!.favorites.toString()), - if (data!.gender != null) - _InfoTile('Gender', data!.gender!), - if (data!.age != null) _InfoTile('Age', data!.age!), - if (data!.dateOfBirth != null) - _InfoTile('Date of Birth', data!.dateOfBirth!), - if (data!.dateOfDeath != null) - _InfoTile('Date of Death', data!.dateOfDeath!), - if (data!.startYear != null) - _InfoTile('Active Since', data!.startYear!), - if (data!.endYear != null) - _InfoTile('Active Until', data!.endYear!), - if (data!.homeTown != null) - _InfoTile('Home Town', data!.homeTown!), - if (data!.bloodType != null) - _InfoTile('Blood Type', data!.bloodType!), - ]), + header, + const SliverFillRemaining( + child: Center(child: Text('No data')), + ), + const SliverFooter(), + ], + ), + data: (data) => CustomScrollView( + physics: Consts.physics, + controller: scrollCtrl, + slivers: [ + refreshControl, + header, + SliverGrid( + gridDelegate: + const SliverGridDelegateWithMinWidthAndFixedHeight( + height: Consts.tapTargetSize, + minWidth: 150, ), - space, - if (data!.description.isNotEmpty) - SliverToBoxAdapter( + delegate: SliverChildListDelegate([ + _InfoTile('Favourites', data.favorites.toString()), + if (data.gender != null) _InfoTile('Gender', data.gender!), + if (data.age != null) _InfoTile('Age', data.age!), + if (data.dateOfBirth != null) + _InfoTile('Date of Birth', data.dateOfBirth!), + if (data.dateOfDeath != null) + _InfoTile('Date of Death', data.dateOfDeath!), + if (data.startYear != null) + _InfoTile('Active Since', data.startYear!), + if (data.endYear != null) + _InfoTile('Active Until', data.endYear!), + if (data.homeTown != null) + _InfoTile('Home Town', data.homeTown!), + if (data.bloodType != null) + _InfoTile('Blood Type', data.bloodType!), + ]), + ), + if (data.description.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 10), child: Card( child: Padding( padding: Consts.padding, - child: HtmlContent(data!.description), + child: HtmlContent(data.description), ), ), ), - ] else - SliverFillRemaining( - child: Center( - child: loading ? const Loader() : const Text('No data'), - ), ), const SliverFooter(), ], ), ), - ), - ), - ); - } -} - -class _FavoriteButton extends StatefulWidget { - const _FavoriteButton(this.data); - - final Staff data; - - @override - State<_FavoriteButton> createState() => __FavoriteButtonState(); -} - -class __FavoriteButtonState extends State<_FavoriteButton> { - @override - Widget build(BuildContext context) { - return ActionButton( - icon: widget.data.isFavorite ? Icons.favorite : Icons.favorite_border, - tooltip: widget.data.isFavorite ? 'Unfavourite' : 'Favourite', - onTap: () { - setState(() => widget.data.isFavorite = !widget.data.isFavorite); - toggleFavoriteStaff(widget.data.id).then((ok) { - if (!ok) { - setState(() => widget.data.isFavorite = !widget.data.isFavorite); - } - }); + ); }, ); } diff --git a/lib/staff/staff_models.dart b/lib/staff/staff_models.dart index 48fed137..5c1085d6 100644 --- a/lib/staff/staff_models.dart +++ b/lib/staff/staff_models.dart @@ -1,3 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/common/paged.dart'; +import 'package:otraku/common/relation.dart'; import 'package:otraku/common/tile_item.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/media/media_constants.dart'; @@ -99,3 +102,15 @@ class StaffFilter { onList: onList == null ? this.onList : onList(), ); } + +class StaffRelations { + const StaffRelations({ + this.characters = const AsyncValue.loading(), + this.roles = const AsyncValue.loading(), + this.characterMedia = const [], + }); + + final AsyncValue> characters; + final AsyncValue> roles; + final List characterMedia; +} diff --git a/lib/staff/staff_providers.dart b/lib/staff/staff_providers.dart index c6ee183f..b65d2d58 100644 --- a/lib/staff/staff_providers.dart +++ b/lib/staff/staff_providers.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/discover/discover_models.dart'; import 'package:otraku/common/relation.dart'; @@ -32,136 +31,122 @@ final staffProvider = FutureProvider.autoDispose.family( final staffFilterProvider = StateProvider.autoDispose.family((ref, _) => StaffFilter()); -final staffRelationProvider = ChangeNotifierProvider.autoDispose.family( +final staffRelationsProvider = StateNotifierProvider.autoDispose + .family( (ref, int id) => StaffRelationNotifier(id, ref.watch(staffFilterProvider(id))), ); -class StaffRelationNotifier extends ChangeNotifier { - StaffRelationNotifier(this.id, this.filter) { - _fetch(); +class StaffRelationNotifier extends StateNotifier { + StaffRelationNotifier(this.id, this.filter) : super(const StaffRelations()) { + _fetch(null); } final int id; final StaffFilter filter; - final _characterMedia = []; - var _characters = const AsyncValue>.loading(); - var _roles = const AsyncValue>.loading(); - List get characterMedia => _characterMedia; - AsyncValue> get characters => _characters; - AsyncValue> get roles => _roles; - - Future _fetch() async { - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.staff, { - 'id': id, - 'withCharacters': true, - 'withRoles': true, - 'sort': filter.sort.name, - 'onList': filter.onList, - if (filter.ofAnime != null) 'type': filter.ofAnime! ? 'ANIME' : 'MANGA', - }); - return data['Staff']; - }); - - if (data.hasError) { - _characters = AsyncValue.error(data.error!, data.stackTrace!); - _roles = AsyncValue.error(data.error!, data.stackTrace!); - return; + Future fetch(bool onCharacters) => _fetch(onCharacters); + + Future _fetch(bool? onCharacters) async { + final variables = { + 'id': id, + 'onList': filter.onList, + 'sort': filter.sort.name, + if (filter.ofAnime != null) 'type': filter.ofAnime! ? 'ANIME' : 'MANGA', + }; + + if (onCharacters == null) { + variables['withCharacters'] = true; + variables['withRoles'] = true; + } else if (onCharacters) { + if (!(state.characters.valueOrNull?.hasNext ?? true)) return; + variables['withCharacters'] = true; + variables['page'] = state.characters.valueOrNull?.next ?? 1; + } else { + if (!(state.roles.valueOrNull?.hasNext ?? true)) return; + variables['withRoles'] = true; + variables['page'] = state.roles.valueOrNull?.next ?? 1; } - _characters = const AsyncValue.data(Paged()); - _roles = const AsyncValue.data(Paged()); - - _initCharacters(data.value!['characterMedia']); - _initRoles(data.value!['staffMedia']); - notifyListeners(); - } - - Future fetchPage(bool ofCharacters) async { - final value = ofCharacters ? _characters.valueOrNull : _roles.valueOrNull; - if (value == null || !value.hasNext) return; - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.staff, { - 'id': id, - 'page': value.next, - 'withCharacters': ofCharacters, - 'withRoles': !ofCharacters, - 'sort': filter.sort.name, - 'onList': filter.onList, - if (filter.ofAnime != null) 'type': filter.ofAnime! ? 'ANIME' : 'MANGA', - }); + final data = await Api.get(GqlQuery.staff, variables); return data['Staff']; }); - if (data.hasError) { - ofCharacters - ? _characters = AsyncValue.error(data.error!, data.stackTrace!) - : _roles = AsyncValue.error(data.error!, data.stackTrace!); - return; - } - - ofCharacters - ? _initCharacters(data.value!['characterMedia']) - : _initRoles(data.value!['staffMedia']); - notifyListeners(); - } - - void _initCharacters(Map data) { - var value = _characters.valueOrNull; - if (value == null) return; - - final items = []; - for (final m in data['edges']) { - final media = Relation( - id: m['node']['id'], - title: m['node']['title']['userPreferred'], - imageUrl: m['node']['coverImage'][Options().imageQuality.value], - subtitle: Convert.clarifyEnum(m['node']['format']), - type: m['node']['type'] == 'ANIME' - ? DiscoverType.anime - : DiscoverType.manga, - ); - - for (final c in m['characters']) { - if (c == null) continue; - - _characterMedia.add(media); - - items.add(Relation( - id: c['id'], - title: c['name']['userPreferred'], - imageUrl: c['image']['large'], - type: DiscoverType.character, - subtitle: Convert.clarifyEnum(m['characterRole']), + var characters = state.characters; + var roles = state.roles; + var characterMedia = [...state.characterMedia]; + + if (onCharacters == null || onCharacters) { + characters = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['characterMedia']; + final value = characters.valueOrNull ?? const Paged(); + + final items = []; + for (final m in map['edges']) { + final media = Relation( + id: m['node']['id'], + title: m['node']['title']['userPreferred'], + imageUrl: m['node']['coverImage'][Options().imageQuality.value], + subtitle: Convert.clarifyEnum(m['node']['format']), + type: m['node']['type'] == 'ANIME' + ? DiscoverType.anime + : DiscoverType.manga, + ); + + for (final c in m['characters']) { + if (c == null) continue; + + characterMedia.add(media); + + items.add(Relation( + id: c['id'], + title: c['name']['userPreferred'], + imageUrl: c['image']['large'], + type: DiscoverType.character, + subtitle: Convert.clarifyEnum(m['characterRole']), + )); + } + } + + return Future.value(value.withNext( + items, + map['pageInfo']['hasNextPage'] ?? false, )); - } + }); } - value = value.withNext(items, data['pageInfo']['hasNextPage']); - _characters = AsyncValue.data(value); - } - - void _initRoles(Map data) { - var value = _roles.valueOrNull; - if (value == null) return; - - final items = []; - for (final s in data['edges']) { - items.add(Relation( - id: s['node']['id'], - title: s['node']['title']['userPreferred'], - imageUrl: s['node']['coverImage'][Options().imageQuality.value], - subtitle: s['staffRole'], - type: s['node']['type'] == 'ANIME' - ? DiscoverType.anime - : DiscoverType.manga, - )); + if (onCharacters == null || !onCharacters) { + roles = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['staffMedia']; + final value = roles.valueOrNull ?? const Paged(); + + final items = []; + for (final s in map['edges']) { + items.add(Relation( + id: s['node']['id'], + title: s['node']['title']['userPreferred'], + imageUrl: s['node']['coverImage'][Options().imageQuality.value], + subtitle: s['staffRole'], + type: s['node']['type'] == 'ANIME' + ? DiscoverType.anime + : DiscoverType.manga, + )); + } + + return Future.value(value.withNext( + items, + map['pageInfo']['hasNextPage'] ?? false, + )); + }); } - value = value.withNext(items, data['pageInfo']['hasNextPage']); - _roles = AsyncValue.data(value); + state = StaffRelations( + characters: characters, + roles: roles, + characterMedia: characterMedia, + ); } } diff --git a/lib/staff/staff_relations_tab.dart b/lib/staff/staff_relations_tab.dart deleted file mode 100644 index ca0ba932..00000000 --- a/lib/staff/staff_relations_tab.dart +++ /dev/null @@ -1,258 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ionicons/ionicons.dart'; -import 'package:otraku/filter/chip_selector.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/media/media_constants.dart'; -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/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, this.topBar); - - final int id; - final ScrollController scrollCtrl; - final TopBar topBar; - - @override - Widget build(BuildContext context) { - return TabScaffold( - topBar: topBar, - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [_FilterButton(id, false)], - ), - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: Consumer( - builder: (context, ref, _) { - ref.listen( - staffRelationProvider(id).select((s) => s.characters), - (_, s) { - if (s.hasError) { - showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load characters', - content: s.error.toString(), - ), - ); - } - }, - ); - - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(staffRelationProvider(id)), - ); - - final notifier = ref.watch(staffRelationProvider(id)); - - return notifier.characters.when( - loading: () => const Center(child: Loader()), - error: (_, __) => CustomScrollView( - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverFillRemaining( - child: Text('Failed to load characters'), - ), - ], - ), - data: (data) { - return CustomScrollView( - controller: scrollCtrl, - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverToBoxAdapter(child: SizedBox(height: 10)), - RelationGrid( - items: data.items, - connections: notifier.characterMedia, - placeholder: 'No characters', - ), - SliverFooter(loading: data.hasNext), - ], - ); - }, - ); - }, - ), - ), - ), - ), - ); - } -} - -class StaffRolesTab extends StatelessWidget { - const StaffRolesTab(this.id, this.scrollCtrl, this.topBar); - - final int id; - final ScrollController scrollCtrl; - final TopBar topBar; - - @override - Widget build(BuildContext context) { - return TabScaffold( - topBar: topBar, - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [_FilterButton(id, true)], - ), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Consumer( - builder: (context, ref, _) { - ref.listen( - staffRelationProvider(id).select((s) => s.roles), - (_, s) { - if (s.hasError) { - showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load roles', - content: s.error.toString(), - ), - ); - } - }, - ); - - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(staffRelationProvider(id)), - ); - - return ref.watch(staffRelationProvider(id)).roles.when( - loading: () => const Center(child: Loader()), - error: (_, __) => CustomScrollView( - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverFillRemaining( - child: Text('Failed to load roles'), - ), - ], - ), - data: (data) { - return CustomScrollView( - controller: scrollCtrl, - physics: Consts.physics, - slivers: [ - refreshControl, - const SliverToBoxAdapter( - child: SizedBox(height: 10), - ), - RelationGrid( - items: data.items, - placeholder: 'No roles', - ), - SliverFooter(loading: data.hasNext), - ], - ); - }, - ); - }, - ), - ), - ), - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - const _FilterButton(this.id, this.full); - - final int id; - final bool full; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - return ActionButton( - icon: Ionicons.funnel_outline, - tooltip: 'Filter', - onTap: () { - var filter = ref.read(staffFilterProvider(id)); - - final sortItems = {}; - for (int i = 0; i < MediaSort.values.length; i += 2) { - String key = Convert.clarifyEnum(MediaSort.values[i].name)!; - sortItems[key] = i ~/ 2; - } - - final onDone = (_) => - ref.read(staffFilterProvider(id).notifier).state = filter; - - showSheet( - context, - OpaqueSheet( - initialHeight: Consts.tapTargetSize * (full ? 5.5 : 4), - builder: (context, scrollCtrl) => ListView( - controller: scrollCtrl, - physics: Consts.physics, - padding: const EdgeInsets.symmetric(vertical: 20), - children: [ - ChipSelector( - title: 'Sort', - options: MediaSort.values.map((s) => s.label).toList(), - selected: filter.sort.index, - mustHaveSelected: true, - onChanged: (i) => filter = filter.copyWith( - sort: MediaSort.values.elementAt(i!), - ), - ), - if (full) ...[ - ChipSelector( - title: 'Type', - options: const ['Anime', 'Manga'], - selected: filter.ofAnime == null - ? null - : filter.ofAnime! - ? 0 - : 1, - onChanged: (val) => - filter = filter.copyWith(ofAnime: () { - if (val == null) return null; - return val == 0 ? true : false; - }), - ), - const SizedBox(height: 10), - ], - ChipSelector( - title: 'List Presence', - options: const ['On List', 'Not on List'], - selected: filter.onList == null - ? null - : filter.onList! - ? 0 - : 1, - onChanged: (val) => filter = filter.copyWith(onList: () { - if (val == null) return null; - return val == 0 ? true : false; - }), - ), - ], - ), - ), - ).then(onDone); - }, - ); - }, - ); - } -} diff --git a/lib/staff/staff_view.dart b/lib/staff/staff_view.dart index 92de9ada..d0013b9a 100644 --- a/lib/staff/staff_view.dart +++ b/lib/staff/staff_view.dart @@ -1,15 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; +import 'package:otraku/common/relation.dart'; +import 'package:otraku/staff/staff_action_buttons.dart'; import 'package:otraku/staff/staff_info_tab.dart'; -import 'package:otraku/staff/staff_relations_tab.dart'; import 'package:otraku/staff/staff_providers.dart'; import 'package:otraku/utils/paged_controller.dart'; +import 'package:otraku/widgets/grids/relation_grid.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/widgets/layouts/floating_bar.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'; +import 'package:otraku/widgets/paged_view.dart'; class StaffView extends ConsumerStatefulWidget { const StaffView(this.id, this.imageUrl); @@ -26,8 +30,8 @@ class _StaffViewState extends ConsumerState { late final _ctrl = PagedController(loadMore: () { if (_tab == 0) return; _tab == 1 - ? ref.read(staffRelationProvider(widget.id)).fetchPage(true) - : ref.read(staffRelationProvider(widget.id)).fetchPage(false); + ? ref.read(staffRelationsProvider(widget.id).notifier).fetch(true) + : ref.read(staffRelationsProvider(widget.id).notifier).fetch(false); }); @override @@ -53,9 +57,11 @@ 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); + final staff = ref.watch(staffProvider(widget.id)); + + ref.watch(staffRelationsProvider(widget.id).select((_) => null)); + + final onRefresh = () => ref.invalidate(staffRelationsProvider(widget.id)); return PageScaffold( bottomBar: BottomBarIconTabs( @@ -68,14 +74,43 @@ class _StaffViewState extends ConsumerState { 'Roles': Ionicons.briefcase_outline, }, ), - child: DirectPageView( - current: _tab, - onChanged: (i) => setState(() => _tab = i), - children: [ - StaffInfoTab(widget.id, widget.imageUrl, _ctrl, topBar), - StaffCharactersTab(widget.id, _ctrl, topBar), - StaffRolesTab(widget.id, _ctrl, topBar), - ], + child: TabScaffold( + topBar: TopBar( + title: staff.valueOrNull?.name, + ), + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: [ + if (_tab == 0 && staff.hasValue) + StaffFavoriteButton(staff.valueOrNull!), + if (_tab > 0) StaffFilterButton(widget.id, true), + ], + ), + child: DirectPageView( + current: _tab, + onChanged: (i) => setState(() => _tab = i), + children: [ + StaffInfoTab(widget.id, widget.imageUrl, _ctrl), + PagedView( + provider: + staffRelationsProvider(widget.id).select((s) => s.characters), + onData: (data) => RelationGrid( + items: data.items, + connections: + ref.read(staffRelationsProvider(widget.id)).characterMedia, + ), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + ), + PagedView( + provider: + staffRelationsProvider(widget.id).select((s) => s.roles), + onData: (data) => RelationGrid(items: data.items), + scrollCtrl: _ctrl, + onRefresh: onRefresh, + ), + ], + ), ), ); } diff --git a/lib/studio/studio_models.dart b/lib/studio/studio_models.dart index 21d87e56..4ef0286f 100644 --- a/lib/studio/studio_models.dart +++ b/lib/studio/studio_models.dart @@ -1,3 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/common/tile_item.dart'; import 'package:otraku/media/media_constants.dart'; import 'package:otraku/common/paged.dart'; @@ -12,15 +13,15 @@ class StudioItem { final String name; } -class Studio { - Studio._({ +class StudioInfo { + StudioInfo._({ required this.id, required this.name, required this.favorites, required this.isFavorite, }); - factory Studio(Map map) => Studio._( + factory StudioInfo(Map map) => StudioInfo._( id: map['id'], name: map['name'], favorites: map['favourites'] ?? 0, @@ -33,11 +34,15 @@ class Studio { bool isFavorite; } -class StudioState { - StudioState(this.studio, this.media, this.categories); +class Studio { + const Studio({ + this.info = const AsyncValue.loading(), + this.media = const AsyncValue.loading(), + this.categories = const {}, + }); - final Studio studio; - final Paged media; + final AsyncValue info; + final AsyncValue> media; /// If the items in [media] are sorted by date, [categories] will represent /// each time category (e.g. "2022") and the index of the first item in @@ -45,9 +50,6 @@ class StudioState { /// determined by the starting index of the next category (if there is one). /// If the items in [media] aren't sorted by date, [categories] must be empty. final Map categories; - - StudioState copyWith(Paged media) => - StudioState(studio, media, categories); } class StudioFilter { diff --git a/lib/studio/studio_providers.dart b/lib/studio/studio_providers.dart index 9f78b89a..d4038361 100644 --- a/lib/studio/studio_providers.dart +++ b/lib/studio/studio_providers.dart @@ -20,91 +20,80 @@ Future toggleFavoriteStudio(int studioId) async { final studioFilterProvider = StateProvider.autoDispose.family((ref, _) => StudioFilter()); -final studioProvider = StateNotifierProvider.autoDispose - .family, int>( +final studioProvider = + StateNotifierProvider.autoDispose.family( (ref, int id) => StudioNotifier(id, ref.watch(studioFilterProvider(id))), ); -class StudioNotifier extends StateNotifier> { - StudioNotifier(this.id, this.filter) - : super(const AsyncValue.loading()) { - _fetch(); +class StudioNotifier extends StateNotifier { + StudioNotifier(this.id, this.filter) : super(const Studio()) { + fetch(); } final int id; final StudioFilter filter; - Future _fetch() async { - state = await AsyncValue.guard(() async { - var data = await Api.get(GqlQuery.studio, { - 'id': id, - 'withInfo': true, - 'sort': filter.sort.name, - 'onList': filter.onList, - if (filter.isMain != null) 'isMain': filter.isMain, - }); - data = data['Studio']; - - return _initMedia( - StudioState(Studio(data), const Paged(), {}), - data['media'], - ); - }); - } + Future fetch() async { + var info = state.info; + var media = state.media; + var categories = {...state.categories}; - Future fetchPage() async { - final value = state.valueOrNull; - if (value == null || !value.media.hasNext) return; - - state = await AsyncValue.guard(() async { - var data = await Api.get(GqlQuery.studio, { + final data = await AsyncValue.guard( + () => Api.get(GqlQuery.studio, { 'id': id, + 'withInfo': info.valueOrNull == null, 'sort': filter.sort.name, 'onList': filter.onList, - 'page': value.media.next, + 'page': media.valueOrNull?.next ?? 1, if (filter.isMain != null) 'isMain': filter.isMain, + }), + ); + + if (info.valueOrNull == null) { + info = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + return Future.value(StudioInfo(data.value!['Studio'])); }); - data = data['Studio']; + } - return _initMedia(value, data['media']); - }); - } + media = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['Studio']['media']; + final value = media.valueOrNull ?? const Paged(); - StudioState _initMedia(StudioState s, Map data) { - final items = []; + final items = []; + if (filter.sort != MediaSort.START_DATE && + filter.sort != MediaSort.START_DATE_DESC) { + for (final m in map['nodes']) { + items.add(mediaItem(m)); + } + } else { + final key = filter.sort == MediaSort.START_DATE || + filter.sort == MediaSort.START_DATE_DESC + ? 'startDate' + : 'endDate'; - if (filter.sort != MediaSort.START_DATE && - filter.sort != MediaSort.START_DATE_DESC) { - for (final m in data['nodes']) { - items.add(mediaItem(m)); - } - } else { - final key = filter.sort == MediaSort.START_DATE || - filter.sort == MediaSort.START_DATE_DESC - ? 'startDate' - : 'endDate'; + var index = value.items.length; + for (final m in map['nodes']) { + var category = m[key]?['year']?.toString(); + category ??= + m['status'] == 'CANCELLED' ? 'Cancelled' : 'To Be Announced'; - var index = s.media.items.length; + if (!categories.containsKey(category)) { + categories[category] = index; + } - for (final m in data['nodes']) { - var category = m[key]?['year']?.toString(); - category ??= - m['status'] == 'CANCELLED' ? 'Cancelled' : 'To Be Announced'; + items.add(mediaItem(m)); - if (!s.categories.containsKey(category)) { - s.categories[category] = index; + index++; } - - items.add(mediaItem(m)); - - index++; } - } - return StudioState( - s.studio, - s.media.withNext(items, data['pageInfo']['hasNextPage']), - s.categories, - ); + return Future.value( + value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), + ); + }); + + state = Studio(info: info, media: media, categories: categories); } } diff --git a/lib/studio/studio_view.dart b/lib/studio/studio_view.dart index bbd21456..4e1b9362 100644 --- a/lib/studio/studio_view.dart +++ b/lib/studio/studio_view.dart @@ -4,7 +4,6 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/filter/chip_selector.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/staff/staff_providers.dart'; import 'package:otraku/studio/studio_models.dart'; import 'package:otraku/studio/studio_providers.dart'; import 'package:otraku/utils/convert.dart'; @@ -31,7 +30,7 @@ class StudioView extends ConsumerStatefulWidget { class _StudioViewState extends ConsumerState { late final _ctrl = PagedController(loadMore: () { - ref.read(studioProvider(widget.id).notifier).fetchPage(); + ref.read(studioProvider(widget.id).notifier).fetch(); }); @override @@ -42,47 +41,107 @@ class _StudioViewState extends ConsumerState { @override Widget build(BuildContext context) { - final refreshControl = SliverRefreshControl( - onRefresh: () => ref.invalidate(staffProvider(widget.id)), - ); + return PageScaffold( + child: ConstrainedView( + child: Consumer( + builder: (context, ref, _) { + ref.listen( + studioProvider(widget.id).select((s) => s.info), + (_, s) { + if (s.hasError) { + showPopUp( + context, + ConfirmationDialog( + title: 'Failed to load studio', + content: s.error.toString(), + ), + ); + } + }, + ); - final studio = ref.watch( - studioProvider(widget.id).select((s) => s.valueOrNull?.studio), - ); + final studio = ref.watch(studioProvider(widget.id)); + final info = studio.info.valueOrNull; + final name = info?.name ?? widget.name; + final items = []; + bool? hasNext; - 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(), - ), - ); - } - }, - ); + studio.media.unwrapPrevious().when( + loading: () => items.add( + const SliverFillRemaining(child: Center(child: Loader())), + ), + error: (_, __) => items.add( + const SliverFillRemaining( + child: Center(child: Text('Failed to load studio')), + ), + ), + data: (data) { + hasNext = data.hasNext; + + if (info != null) { + items.add(SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 20, + ), + child: Text( + '${info.favorites.toString()} favourites', + style: Theme.of(context).textTheme.labelMedium, + ), + ), + )); + } + + final sort = + ref.watch(studioFilterProvider(widget.id)).sort; + + if (sort != MediaSort.START_DATE && + sort != MediaSort.START_DATE_DESC) { + items.add(TileItemGrid(data.items)); + return; + } + + for (int i = 0; i < studio.categories.length; i++) { + items.add(SliverToBoxAdapter( + child: Text( + studio.categories.keys.elementAt(i), + style: Theme.of(context).textTheme.titleMedium, + ), + )); - final name = studio?.name ?? widget.name; - final titleWidget = name != null - ? SliverToBoxAdapter( + final beg = studio.categories.values.elementAt(i); + final end = i < studio.categories.length - 1 + ? studio.categories.values.elementAt(i + 1) + : data.items.length; + + items.add( + SliverPadding( + padding: const EdgeInsets.symmetric(vertical: 10), + sliver: TileItemGrid(data.items.sublist(beg, end)), + ), + ); + } + }, + ); + + return TabScaffold( + topBar: const TopBar(), + floatingBar: FloatingBar( + scrollCtrl: _ctrl, + children: info != null + ? [_FavoriteButton(info), _FilterButton(widget.id)] + : const [], + ), + child: CustomScrollView( + physics: Consts.physics, + controller: hasNext != null ? _ctrl : null, + slivers: [ + SliverRefreshControl( + onRefresh: () => ref.invalidate(studioProvider(widget.id)), + ), + if (name != null) + SliverToBoxAdapter( child: GestureDetector( onTap: () => Toast.copy(context, name), child: Hero( @@ -93,88 +152,13 @@ class _StudioViewState extends ConsumerState { ), ), ), - ) - : 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), - child: Text( - '${data.studio.favorites.toString()} favourites', - style: Theme.of(context).textTheme.labelMedium, - ), - ), - ) - ]; - final sort = - ref.watch(studioFilterProvider(widget.id)).sort; - - 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, - ), - )); - - 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)); - } - - return CustomScrollView( - physics: Consts.physics, - controller: _ctrl, - slivers: [ - refreshControl, - titleWidget!, - ...items, - SliverFooter(loading: data.media.hasNext), - ], - ); - }, - ); - }, - ), + ...items, + SliverFooter(loading: hasNext ?? false), + ], + ), + ); + }, ), ), ); @@ -184,7 +168,7 @@ class _StudioViewState extends ConsumerState { class _FavoriteButton extends StatefulWidget { const _FavoriteButton(this.data); - final Studio data; + final StudioInfo data; @override State<_FavoriteButton> createState() => __FavoriteButtonState(); diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index 2bcd85ce..4e67d7f8 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -1,5 +1,4 @@ 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'; @@ -49,7 +48,7 @@ class UserHeader extends StatelessWidget { } } -class _Delegate implements SliverPersistentHeaderDelegate { +class _Delegate extends SliverPersistentHeaderDelegate { _Delegate({ required this.id, required this.isViewer, @@ -78,7 +77,6 @@ class _Delegate implements SliverPersistentHeaderDelegate { : 10.0; final height = maxExtent; - final extent = maxExtent - shrinkOffset; final opacity = shrinkOffset < (_bannerHeight - minExtent) ? shrinkOffset / (_bannerHeight - minExtent) : 1.0; @@ -97,218 +95,213 @@ class _Delegate implements SliverPersistentHeaderDelegate { ), ], ), - 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), - ], + child: Stack( + fit: StackFit.expand, + children: [ + Column( + children: [ + Flexible( + flex: _bannerHeight.ceil(), + child: user?.bannerUrl != null + ? GestureDetector( + child: CachedImage(user!.bannerUrl!), + onTap: () => showPopUp( + context, + ImageDialog(user!.bannerUrl!), + ), + ) + : const SizedBox(), ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, + Flexible( + flex: (height - _bannerHeight).floor(), + child: const SizedBox(), + ), + ], + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: height - _bannerHeight, + alignment: Alignment.topCenter, + color: theme.colorScheme.background, 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, - ), - ], - ), + 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, - ), + ), + 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, - ), - ], - ), + ), + 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, - ), + ), + 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: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background.withAlpha(0), + ], + ), ), ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: minExtent, + child: Opacity( + opacity: opacity, child: DecoratedBox( decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(0), - ], - ), + color: theme.colorScheme.background, + boxShadow: [ + BoxShadow( + blurRadius: 10, + spreadRadius: 10, + color: theme.colorScheme.background, + ), + ], ), ), ), - 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: [ + isViewer + ? const SizedBox(width: 10) + : TopBarIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: Navigator.of(context).pop, ), - ], - ), - ), - ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, - child: Row( - children: [ - isViewer - ? const SizedBox(width: 10) - : TopBarIcon( - 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, - ), + Expanded( + child: user?.name == null + ? const SizedBox() + : Opacity( + opacity: opacity, + child: Text( + user!.name, + style: theme.textTheme.titleMedium, + overflow: TextOverflow.ellipsis, ), - ), - if (!isViewer && user != null) _FollowButton(user!), - if (user?.siteUrl != null) - TopBarIcon( - tooltip: 'More', - icon: Ionicons.ellipsis_horizontal, - onTap: () => showSheet( - context, - FixedGradientDragSheet.link(context, user!.siteUrl!), - ), + ), + ), + if (!isViewer && user != null) _FollowButton(user!), + if (user?.siteUrl != null) + TopBarIcon( + tooltip: 'More', + icon: Ionicons.ellipsis_horizontal, + onTap: () => showSheet( + context, + FixedGradientDragSheet.link(context, user!.siteUrl!), ), - if (isViewer) - TopBarIcon( - tooltip: 'Settings', - icon: Ionicons.cog_outline, - onTap: () => Navigator.pushNamed( - context, - RouteArg.settings, - ), + ), + if (isViewer) + TopBarIcon( + tooltip: 'Settings', + icon: Ionicons.cog_outline, + onTap: () => Navigator.pushNamed( + context, + RouteArg.settings, ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ); } @@ -321,23 +314,9 @@ class _Delegate implements SliverPersistentHeaderDelegate { @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/widgets/grids/relation_grid.dart b/lib/widgets/grids/relation_grid.dart index c5be7f57..4a3fc588 100644 --- a/lib/widgets/grids/relation_grid.dart +++ b/lib/widgets/grids/relation_grid.dart @@ -8,19 +8,15 @@ import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; class RelationGrid extends StatelessWidget { RelationGrid({ required this.items, - required this.placeholder, this.connections = const [], }) : assert(connections.isEmpty || items.length == connections.length); - final String placeholder; final List items; final List connections; @override Widget build(BuildContext context) { - if (items.isEmpty) { - return SliverFillRemaining(child: Center(child: Text(placeholder))); - } + if (items.isEmpty) return const SliverToBoxAdapter(); return SliverGrid( gridDelegate: SliverGridDelegateWithMinWidthAndFixedHeight( diff --git a/lib/widgets/layouts/scaffolds.dart b/lib/widgets/layouts/scaffolds.dart index 624e0881..d3bef342 100644 --- a/lib/widgets/layouts/scaffolds.dart +++ b/lib/widgets/layouts/scaffolds.dart @@ -88,14 +88,15 @@ VerticalOffsets scaffoldOffsets(BuildContext context) { 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; + + if (inner?.topBar != null) { + top += inner!.topBar!.preferredSize.height; + top += outer?._topOffset ?? 0; } + bottom += outer?._bottomOffset ?? 0; + return VerticalOffsets(top, bottom); } diff --git a/lib/widgets/overlays/sheets.dart b/lib/widgets/overlays/sheets.dart index d3bcc055..8434d531 100644 --- a/lib/widgets/overlays/sheets.dart +++ b/lib/widgets/overlays/sheets.dart @@ -111,7 +111,10 @@ class DynamicGradientDragSheet extends StatelessWidget { @override Widget build(BuildContext context) { - final requiredHeight = children.length * Consts.tapTargetSize + 50; + final requiredHeight = children.length * Consts.tapTargetSize + + MediaQuery.of(context).padding.bottom + + 50; + double height = requiredHeight / MediaQuery.of(context).size.height; if (height > 0.6) height = 0.6; @@ -139,10 +142,11 @@ class DynamicGradientDragSheet extends StatelessWidget { constraints: const BoxConstraints(maxWidth: Consts.layoutSmall), child: ListView.builder( controller: scrollCtrl, - padding: const EdgeInsets.only( + padding: EdgeInsets.only( top: 50, left: 10, right: 10, + bottom: MediaQuery.of(context).padding.bottom, ), itemCount: children.length, itemExtent: Consts.tapTargetSize, diff --git a/lib/widgets/paged_view.dart b/lib/widgets/paged_view.dart index ed707bf8..ae1fa760 100644 --- a/lib/widgets/paged_view.dart +++ b/lib/widgets/paged_view.dart @@ -7,24 +7,20 @@ import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; /// Subscribes to a paginated, asynchronous provider. -/// Listens for errors and shows a pop up, if there is one. -/// [onRefresh] allows for refreshing. If [scrollCtrl] is -/// [PaginationController], pagination will automatically work. -/// [dateType] is a lowercase word for the data being handled (e.g. "reviews"). -/// [onData] should return a sliver widget. +/// Shows a pop up, if there is an error. +/// [onData] should return a sliver widget! +/// If [scrollCtrl] is [PagedController], pagination will automatically work. class PagedView extends StatelessWidget { const PagedView({ required this.provider, required this.scrollCtrl, required this.onRefresh, - required this.dataType, required this.onData, }); final ProviderListenable>> provider; final ScrollController scrollCtrl; final void Function() onRefresh; - final String dataType; final Widget Function(Paged) onData; @override @@ -37,43 +33,42 @@ class PagedView extends StatelessWidget { error: (error, _) => showPopUp( context, ConfirmationDialog( - title: 'Failed to load $dataType', + title: 'Failed to load', content: error.toString(), ), ), ), ); - var hasNext = false; - final child = - ref.watch>>(provider).unwrapPrevious().when( - loading: () => const SliverFillRemaining( - child: Center(child: Loader()), - ), - error: (_, __) => SliverFillRemaining( - child: Center(child: Text('Failed to load $dataType')), - ), - data: (data) { - hasNext = data.hasNext; + bool? hasNext; + final child = ref.watch(provider).unwrapPrevious().when( + loading: () => const SliverFillRemaining( + child: Center(child: Loader()), + ), + error: (_, __) => const SliverFillRemaining( + child: Center(child: Text('Failed to load')), + ), + data: (data) { + hasNext = data.hasNext; - if (data.items.isEmpty) { - return SliverFillRemaining( - child: Center(child: Text('No $dataType')), - ); - } + if (data.items.isEmpty) { + return const SliverFillRemaining( + child: Center(child: Text('No results')), + ); + } - return onData(data); - }, - ); + return onData(data); + }, + ); return ConstrainedView( child: CustomScrollView( physics: Consts.physics, - controller: scrollCtrl, + controller: hasNext != null ? scrollCtrl : null, slivers: [ SliverRefreshControl(onRefresh: onRefresh), child, - SliverFooter(loading: hasNext), + SliverFooter(loading: hasNext ?? false), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index dde36b08..281689fb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" clock: dependency: transitive description: @@ -194,10 +194,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "02dcaf49d405f652b7160e882bacfc02cb497041bb2eab2a49b1c393cf9aac12" + sha256: "8546a9b9510e1a260b8d55fb2d07096e8a8552c6a2c2bf529100344894b2b41a" url: "https://pub.dev" source: hosted - version: "0.12.0" + version: "0.13.0" flutter_lints: dependency: "direct dev" description: @@ -234,10 +234,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: b3c3a8a9714b7f88dd2a41e1efbc47f76d620b06ab427c62ae7bc82298cd7dbb + sha256: "812dfbb87af51e73e68ea038bcfd1c732078d6838d3388d03283db7dec0d1e5f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" flutter_secure_storage: dependency: "direct main" description: @@ -524,10 +524,10 @@ packages: dependency: transitive description: name: riverpod - sha256: b0fbf7927333c5c318f7e2c22c8b4fd2542ba294de0373e80ecdb34e0dcd8dc4 + sha256: "77ab3bcd084bb19fa8717a526217787c725d7f5be938404c7839cd760fdf6ae5" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" rxdart: dependency: transitive description: @@ -758,5 +758,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.7.0-0" diff --git a/pubspec.yaml b/pubspec.yaml index d67b491f..2e472b6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter # State management. - flutter_riverpod: ^2.3.2 + flutter_riverpod: ^2.3.4 # Data fetching. http: ^0.13.5 @@ -58,7 +58,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_launcher_icons: ^0.12.0 + flutter_launcher_icons: ^0.13.0 flutter_lints: ^2.0.1 flutter_icons: From e12f20f71c36439c2b96f362b8362ca5c12ec257 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Tue, 11 Apr 2023 23:21:12 +0300 Subject: [PATCH 21/55] Switched to a tab view for the media page --- lib/media/media_action_buttons.dart | 141 +++++++ ...media_other_view.dart => media_grids.dart} | 211 ++++++---- lib/media/media_header.dart | 379 +++++++++--------- lib/media/media_info_view.dart | 277 +++++-------- lib/media/media_models.dart | 24 +- lib/media/media_people_view.dart | 138 ------- lib/media/media_providers.dart | 14 +- lib/media/media_social_view.dart | 233 ----------- lib/media/media_view.dart | 245 ++++++----- 9 files changed, 743 insertions(+), 919 deletions(-) create mode 100644 lib/media/media_action_buttons.dart rename lib/media/{media_other_view.dart => media_grids.dart} (67%) delete mode 100644 lib/media/media_people_view.dart delete mode 100644 lib/media/media_social_view.dart diff --git a/lib/media/media_action_buttons.dart b/lib/media/media_action_buttons.dart new file mode 100644 index 00000000..c18d57d2 --- /dev/null +++ b/lib/media/media_action_buttons.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ionicons/ionicons.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_models.dart'; +import 'package:otraku/media/media_providers.dart'; +import 'package:otraku/widgets/layouts/floating_bar.dart'; +import 'package:otraku/widgets/overlays/sheets.dart'; + +class MediaEditButton extends StatefulWidget { + const MediaEditButton(this.media); + + final Media media; + + @override + State createState() => _MediaEditButtonState(); +} + +class _MediaEditButtonState extends State { + @override + Widget build(BuildContext context) { + final media = widget.media; + return ActionButton( + icon: media.edit.status == null ? Icons.add : Icons.edit_outlined, + tooltip: media.edit.status == null ? 'Add' : 'Edit', + onTap: () => showSheet( + context, + EditView( + EditTag(media.info.id), + callback: (edit) => setState(() => media.edit = edit), + ), + ), + ); + } +} + +class MediaFavoriteButton extends StatefulWidget { + const MediaFavoriteButton(this.info); + + final MediaInfo info; + + @override + State createState() => _MediaFavoriteButtonState(); +} + +class _MediaFavoriteButtonState extends State { + @override + Widget build(BuildContext context) { + return ActionButton( + icon: widget.info.isFavorite ? Icons.favorite : Icons.favorite_border, + tooltip: widget.info.isFavorite ? 'Unfavourite' : 'Favourite', + onTap: () { + setState(() => widget.info.isFavorite = !widget.info.isFavorite); + toggleFavoriteMedia( + widget.info.id, + widget.info.type == DiscoverType.anime, + ).then((ok) { + if (!ok) { + setState(() => widget.info.isFavorite = !widget.info.isFavorite); + } + }); + }, + ); + } +} + +class MediaLanguageButton extends StatefulWidget { + const MediaLanguageButton(this.id, this.tabCtrl); + + final int id; + final TabController tabCtrl; + + @override + State createState() => _MediaLanguageButtonState(); +} + +class _MediaLanguageButtonState extends State { + late bool _hidden = widget.tabCtrl.index != MediaTab.characters.index; + + @override + void initState() { + super.initState(); + widget.tabCtrl.addListener(_listener); + } + + @override + void dispose() { + widget.tabCtrl.removeListener(_listener); + super.dispose(); + } + + void _listener() { + final hidden = widget.tabCtrl.index != MediaTab.characters.index; + if (hidden != _hidden) setState(() => _hidden = hidden); + } + + @override + Widget build(BuildContext context) { + if (_hidden) return const SizedBox(); + + return Consumer( + builder: (context, ref, _) { + if (ref.watch(mediaRelationsProvider(widget.id).select( + (s) => s.languages.length < 2, + ))) return const SizedBox(); + + return ActionButton( + tooltip: 'Language', + icon: Ionicons.globe_outline, + onTap: () { + final mediaRelations = ref.read(mediaRelationsProvider(widget.id)); + final languages = mediaRelations.languages; + final language = mediaRelations.language; + + showSheet( + context, + DynamicGradientDragSheet( + onTap: (i) => ref + .read(mediaRelationsProvider(widget.id).notifier) + .changeLanguage(languages.elementAt(i)), + children: [ + for (int i = 0; i < languages.length; i++) + Text( + languages.elementAt(i), + style: languages.elementAt(i) != language + ? Theme.of(context).textTheme.titleLarge + : Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/media/media_other_view.dart b/lib/media/media_grids.dart similarity index 67% rename from lib/media/media_other_view.dart rename to lib/media/media_grids.dart index 7a035ba9..c69a8893 100644 --- a/lib/media/media_other_view.dart +++ b/lib/media/media_grids.dart @@ -1,76 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/utils/consts.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/widgets/layouts/constrained_view.dart'; -import 'package:otraku/widgets/link_tile.dart'; +import 'package:otraku/utils/consts.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/scaffolds.dart'; -import 'package:otraku/widgets/layouts/direct_page_view.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/paged_view.dart'; +import 'package:otraku/widgets/link_tile.dart'; import 'package:otraku/widgets/text_rail.dart'; -class MediaOtherView extends StatelessWidget { - const MediaOtherView(this.id, this.related, this.tabToggled, this.toggleTab); - - final int id; - final List related; - final bool tabToggled; - final void Function(bool) toggleTab; - - @override - Widget build(BuildContext context) { - final scrollCtrl = context - .findAncestorStateOfType()! - .innerController; - - return TabScaffold( - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - centered: true, - children: [ - ActionTabSwitcher( - items: const ['Related', 'Recommended'], - current: tabToggled ? 1 : 0, - onChanged: (i) => toggleTab(i == 1), - ), - ], - ), - child: DirectPageView( - onChanged: null, - current: tabToggled ? 1 : 0, - children: [ - ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: scrollCtrl, - slivers: [ - const SliverToBoxAdapter(child: SizedBox(height: 10)), - _RelatedGrid(related), - const SliverFooter(), - ], - ), - ), - Consumer( - builder: (context, ref, _) => PagedView( - provider: mediaRelationsProvider(id).select((s) => s.recommended), - onData: (data) => _RecommendationsGrid(id, data.items), - onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), - scrollCtrl: scrollCtrl, - ), - ), - ], - ), - ); - } -} - -class _RelatedGrid extends StatelessWidget { - const _RelatedGrid(this.items); +class MediaRelatedGrid extends StatelessWidget { + const MediaRelatedGrid(this.items); final List items; @@ -148,8 +88,8 @@ class _RelatedGrid extends StatelessWidget { } } -class _RecommendationsGrid extends StatelessWidget { - const _RecommendationsGrid(this.mediaId, this.items); +class MediaRecommendationGrid extends StatelessWidget { + const MediaRecommendationGrid(this.mediaId, this.items); final int mediaId; final List items; @@ -203,7 +143,7 @@ class _RecommendationsGrid extends StatelessWidget { ), Padding( padding: const EdgeInsets.only(left: 5, right: 5), - child: _Rating(mediaId, items[i]), + child: _RecommendationRating(mediaId, items[i]), ), ], ), @@ -214,17 +154,17 @@ class _RecommendationsGrid extends StatelessWidget { } } -class _Rating extends StatefulWidget { - const _Rating(this.mediaId, this.item); +class _RecommendationRating extends StatefulWidget { + const _RecommendationRating(this.mediaId, this.item); final int mediaId; final Recommendation item; @override - State<_Rating> createState() => __RatingState(); + State<_RecommendationRating> createState() => _RecommendationRatingState(); } -class __RatingState extends State<_Rating> { +class _RecommendationRatingState extends State<_RecommendationRating> { @override Widget build(BuildContext context) { return SizedBox( @@ -340,3 +280,128 @@ class __RatingState extends State<_Rating> { ); } } + +class MediaReviewGrid extends StatelessWidget { + const MediaReviewGrid(this.items, this.bannerUrl); + + final List items; + final String? bannerUrl; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) { + return const SliverFillRemaining( + child: Center(child: Text('No reviews')), + ); + } + + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 300, + height: 140, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LinkTile( + id: items[i].userId, + info: items[i].avatar, + discoverType: DiscoverType.user, + child: Row( + children: [ + Hero( + tag: items[i].userId, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: CachedImage( + items[i].avatar, + height: 50, + width: 50, + ), + ), + ), + 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, + ), + ], + ), + ), + const SizedBox(height: 5), + Expanded( + child: LinkTile( + id: items[i].reviewId, + info: bannerUrl, + discoverType: DiscoverType.review, + child: Card( + child: SizedBox( + width: double.infinity, + child: Padding( + padding: Consts.padding, + child: Text( + items[i].summary, + style: Theme.of(context).textTheme.labelMedium, + overflow: TextOverflow.fade, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class MediaRankGrid extends StatelessWidget { + const MediaRankGrid(this.rankTexts, this.rankTypes); + + final List rankTexts; + final List rankTypes; + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.only(top: 10, left: 10, right: 10), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + height: Consts.tapTargetSize, + minWidth: 185, + ), + delegate: SliverChildBuilderDelegate( + (_, i) => Card( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + children: [ + Icon( + rankTypes[i] ? Ionicons.star : Icons.favorite_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 5), + Expanded( + child: Text( + rankTexts[i], + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + ], + ), + ), + ), + childCount: rankTexts.length, + ), + ), + ); + } +} diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index 069a7b34..a0d94492 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -14,10 +14,11 @@ import 'package:otraku/widgets/overlays/toast.dart'; import 'package:otraku/widgets/text_rail.dart'; class MediaHeader extends StatelessWidget { - const MediaHeader(this.id, this.coverUrl); + const MediaHeader(this.id, this.coverUrl, this.tabCtrl); final int id; final String? coverUrl; + final TabController tabCtrl; @override Widget build(BuildContext context) { @@ -60,6 +61,7 @@ class MediaHeader extends StatelessWidget { pinned: true, delegate: _Delegate( id: id, + tabCtrl: tabCtrl, info: data?.info, coverUrl: coverUrl, textRailItems: textRailItems, @@ -80,6 +82,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { required this.imageWidth, required this.coverUrl, required this.textRailItems, + required this.tabCtrl, }); final int id; @@ -87,6 +90,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { final double imageWidth; final String? coverUrl; final Map textRailItems; + final TabController tabCtrl; @override Widget build( @@ -95,206 +99,216 @@ class _Delegate extends SliverPersistentHeaderDelegate { bool overlapsContent, ) { final height = maxExtent; - final opacity = shrinkOffset < (_bannerHeight - minExtent) - ? shrinkOffset / (_bannerHeight - minExtent) - : 1.0; + var opacity = shrinkOffset > _bannerHeight + ? (shrinkOffset - _bannerHeight) / (imageHeight / 4) + : 0.0; + if (opacity > 1) opacity = 1; 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, + final infoContent = 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, + ), ), - ], - ), - child: Stack( - fit: StackFit.expand, - children: [ - Column( + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.end, children: [ - Flexible( - flex: _bannerHeight.ceil(), - child: info?.banner != null - ? GestureDetector( - child: CachedImage(info!.banner!), - onTap: () => showPopUp( - context, - ImageDialog(info!.banner!), + 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, ), - ) - : const SizedBox(), - ), - Flexible( - flex: (height - _bannerHeight).floor(), - child: const SizedBox(), - ), - ], - ), - 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, + ], ), - ], + ), ), + const SizedBox(height: 5), + ], + TextRail( + textRailItems, + style: theme.textTheme.labelMedium, ), - ), + ], ), - 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, - ), - ], + ), + ], + ); + + final topRow = Row( + children: [ + TopBarIcon( + 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) + TopBarIcon( + tooltip: 'More', + icon: Ionicons.ellipsis_horizontal, + onTap: () => showSheet( + context, + FixedGradientDragSheet.link(context, info!.siteUrl!), ), ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, + ], + ); + + return SizedBox( + height: height, + child: Column( + children: [ + Flexible( + flex: (height - Consts.tapTargetSize).floor(), child: DecoratedBox( decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(0), - ], - ), + color: theme.colorScheme.surfaceVariant, ), - ), - ), - 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, + child: Stack( + fit: StackFit.expand, + children: [ + if (info?.banner != null) + Positioned( + top: 0, + left: 0, + right: 0, + bottom: height - _bannerHeight, + child: GestureDetector( + child: CachedImage(info!.banner!), + onTap: () => + showPopUp(context, ImageDialog(info!.banner!)), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + height: height - _bannerHeight, + child: Container( + alignment: Alignment.topCenter, color: theme.colorScheme.background, + child: Container( + height: 0, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: 15, + spreadRadius: 25, + color: theme.colorScheme.background, + ), + ], + ), + ), ), - ], - ), - ), - ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, - child: Row( - children: [ - TopBarIcon( - 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, - ), + ), + Positioned( + bottom: 0, + left: 10, + right: 10, + child: infoContent, + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: Consts.tapTargetSize, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.background, + theme.colorScheme.background.withAlpha(0), + ], ), - ), - if (info?.siteUrl != null) - TopBarIcon( - tooltip: 'More', - icon: Ionicons.ellipsis_horizontal, - onTap: () => showSheet( - context, - FixedGradientDragSheet.link(context, info!.siteUrl!), + ), ), ), + Positioned( + top: 0, + left: 0, + right: 0, + height: Consts.tapTargetSize, + child: Opacity( + opacity: opacity, + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.background, + ), + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: Consts.tapTargetSize, + child: topRow, + ), + ], + ), + ), + ), + Material( + color: Theme.of(context).colorScheme.background, + child: TabBar( + splashBorderRadius: Consts.borderRadiusMin, + controller: tabCtrl, + isScrollable: true, + tabs: const [ + Tab(text: 'Overview'), + Tab(text: 'Related'), + Tab(text: 'Characters'), + Tab(text: 'Staff'), + Tab(text: 'Reviews'), + Tab(text: 'Recommendations'), + Tab(text: 'Statistics'), ], ), ), @@ -308,12 +322,13 @@ class _Delegate extends SliverPersistentHeaderDelegate { double get imageHeight => imageWidth * Consts.coverHtoWRatio; @override - double get maxExtent => _bannerHeight + imageHeight / 2; + double get maxExtent => + _bannerHeight + imageHeight / 2 + Consts.tapTargetSize; @override - double get minExtent => Consts.tapTargetSize; + double get minExtent => Consts.tapTargetSize * 2; @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => - true; + bool shouldRebuild(covariant _Delegate oldDelegate) => + info != oldDelegate.info; } diff --git a/lib/media/media_info_view.dart b/lib/media/media_info_view.dart index b4d9e0fe..8f3a3ddf 100644 --- a/lib/media/media_info_view.dart +++ b/lib/media/media_info_view.dart @@ -1,29 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/edit/edit_providers.dart'; import 'package:otraku/utils/consts.dart'; import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/edit/edit_view.dart'; import 'package:otraku/filter/filter_providers.dart'; 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/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; import 'package:otraku/widgets/overlays/toast.dart'; class MediaInfoView extends StatelessWidget { - const MediaInfoView(this.media); + const MediaInfoView(this.media, this.scrollCtrl); final Media media; + final ScrollController scrollCtrl; @override Widget build(BuildContext context) { @@ -70,188 +65,119 @@ class MediaInfoView extends StatelessWidget { } } - final scrollCtrl = context - .findAncestorStateOfType()! - .innerController; - return Consumer( - builder: (context, ref, _) => TabScaffold( - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - children: [_EditButton(media), _FavoriteButton(info)], - ), - child: CustomScrollView( - controller: scrollCtrl, - slivers: [ - if (info.description.isNotEmpty) - SliverToBoxAdapter( - child: Padding( - padding: Consts.padding, - child: GestureDetector( - child: Card( - child: Padding( - padding: Consts.padding, - child: Text( - info.description, - maxLines: 4, - overflow: TextOverflow.fade, - ), + builder: (context, ref, _) => CustomScrollView( + controller: scrollCtrl, + slivers: [ + if (info.description.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: Consts.padding, + child: GestureDetector( + child: Card( + child: Padding( + padding: Consts.padding, + child: Text( + info.description, + maxLines: 4, + overflow: TextOverflow.fade, ), ), - onTap: () => showPopUp( - context, - TextDialog(title: 'Description', text: info.description), - ), + ), + onTap: () => showPopUp( + context, + TextDialog(title: 'Description', text: info.description), ), ), - ) - else - const SliverToBoxAdapter(child: SizedBox(height: 10)), - SliverPadding( - padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), - sliver: SliverGrid( - gridDelegate: - const SliverGridDelegateWithMinWidthAndFixedHeight( - height: Consts.tapTargetSize, - minWidth: 130, - ), - delegate: SliverChildBuilderDelegate( - (context, i) => Card( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - infoTitles[i], - maxLines: 1, - style: Theme.of(context).textTheme.labelMedium, - ), - Text(infoData[i].toString(), maxLines: 1), - ], - ), + ), + ) + else + const SliverToBoxAdapter(child: SizedBox(height: 10)), + SliverPadding( + padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + height: Consts.tapTargetSize, + minWidth: 130, + ), + delegate: SliverChildBuilderDelegate( + (context, i) => Card( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + infoTitles[i], + maxLines: 1, + style: Theme.of(context).textTheme.labelMedium, + ), + Text(infoData[i].toString(), maxLines: 1), + ], ), ), - childCount: infoData.length, ), + childCount: infoData.length, ), ), - if (info.genres.isNotEmpty) - _PlainScrollCards( - title: 'Genres', - items: info.genres, - onTap: (i) { - ref.read(searchProvider(null).notifier).state = null; - final notifier = ref.read(discoverFilterProvider); - notifier.type = info.type; - - final filter = notifier.filter.clear(); - filter.genreIn.add(info.genres[i]); - notifier.filter = filter; - - ref.read(homeProvider).homeTab = HomeView.DISCOVER; - Navigator.popUntil(context, (r) => r.isFirst); - }, - ), - if (info.tags.isNotEmpty) _TagScrollCards(info, ref), - if (info.studios.isNotEmpty) - _PlainScrollCards( - title: 'Studios', - items: info.studios.keys.toList(), - onTap: (index) => LinkTile.openView( - context: context, - id: info.studios[info.studios.keys.elementAt(index)]!, - imageUrl: info.studios.keys.elementAt(index), - discoverType: DiscoverType.studio, - ), + ), + if (info.genres.isNotEmpty) + _PlainScrollCards( + title: 'Genres', + items: info.genres, + onTap: (i) { + ref.read(searchProvider(null).notifier).state = null; + final notifier = ref.read(discoverFilterProvider); + notifier.type = info.type; + + final filter = notifier.filter.clear(); + filter.genreIn.add(info.genres[i]); + notifier.filter = filter; + + ref.read(homeProvider).homeTab = HomeView.DISCOVER; + Navigator.popUntil(context, (r) => r.isFirst); + }, + ), + if (info.tags.isNotEmpty) _TagScrollCards(info, ref), + if (info.studios.isNotEmpty) + _PlainScrollCards( + title: 'Studios', + items: info.studios.keys.toList(), + onTap: (index) => LinkTile.openView( + context: context, + id: info.studios[info.studios.keys.elementAt(index)]!, + imageUrl: info.studios.keys.elementAt(index), + discoverType: DiscoverType.studio, ), - if (info.producers.isNotEmpty) - _PlainScrollCards( - title: 'Producers', - items: info.producers.keys.toList(), - onTap: (i) => LinkTile.openView( - context: context, - id: info.producers[info.producers.keys.elementAt(i)]!, - imageUrl: info.producers.keys.elementAt(i), - discoverType: DiscoverType.studio, - ), + ), + if (info.producers.isNotEmpty) + _PlainScrollCards( + title: 'Producers', + items: info.producers.keys.toList(), + onTap: (i) => LinkTile.openView( + context: context, + id: info.producers[info.producers.keys.elementAt(i)]!, + imageUrl: info.producers.keys.elementAt(i), + discoverType: DiscoverType.studio, ), - if (info.hashtag != null) _Title('Hashtag', info.hashtag!), - if (info.romajiTitle != null) _Title('Romaji', info.romajiTitle!), - if (info.englishTitle != null) - _Title('English', info.englishTitle!), - if (info.nativeTitle != null) _Title('Native', info.nativeTitle!), - if (info.synonyms.isNotEmpty) - _Title('Synonyms', info.synonyms.join(', ')), - const SliverFooter(), - ], - ), - ), - ); - } -} - -class _EditButton extends StatefulWidget { - const _EditButton(this.media); - - final Media media; - - @override - State<_EditButton> createState() => __EditButtonState(); -} - -class __EditButtonState extends State<_EditButton> { - @override - Widget build(BuildContext context) { - final media = widget.media; - return ActionButton( - icon: media.edit.status == null ? Icons.add : Icons.edit_outlined, - tooltip: media.edit.status == null ? 'Add' : 'Edit', - onTap: () => showSheet( - context, - EditView( - EditTag(media.info.id), - callback: (edit) => setState(() => media.edit = edit), - ), + ), + if (info.hashtag != null) _Title('Hashtag', info.hashtag!), + if (info.romajiTitle != null) _Title('Romaji', info.romajiTitle!), + if (info.englishTitle != null) _Title('English', info.englishTitle!), + if (info.nativeTitle != null) _Title('Native', info.nativeTitle!), + if (info.synonyms.isNotEmpty) + _Title('Synonyms', info.synonyms.join(', ')), + const SliverFooter(), + ], ), ); } } -class _FavoriteButton extends StatefulWidget { - const _FavoriteButton(this.info); - - final MediaInfo info; - - @override - State<_FavoriteButton> createState() => __FavoriteButtonState(); -} - -class __FavoriteButtonState extends State<_FavoriteButton> { - @override - Widget build(BuildContext context) { - return ActionButton( - icon: widget.info.isFavorite ? Icons.favorite : Icons.favorite_border, - tooltip: widget.info.isFavorite ? 'Unfavourite' : 'Favourite', - onTap: () { - setState(() => widget.info.isFavorite = !widget.info.isFavorite); - toggleFavoriteMedia( - widget.info.id, - widget.info.type == DiscoverType.anime, - ).then((ok) { - if (!ok) { - setState(() => widget.info.isFavorite = !widget.info.isFavorite); - } - }); - }, - ); - } -} - class _ScrollCards extends StatelessWidget { const _ScrollCards({ required this.title, @@ -377,16 +303,15 @@ class _TagScrollCardsState extends State<_TagScrollCards> { title: 'Tags', itemCount: tags.length, onTap: (i) { - final ref = widget.ref; - ref.read(searchProvider(null).notifier).state = null; - final notifier = ref.read(discoverFilterProvider); + widget.ref.read(searchProvider(null).notifier).state = null; + final notifier = widget.ref.read(discoverFilterProvider); notifier.type = widget.info.type; final filter = notifier.filter.clear(); filter.tagIn.add(tags[i].name); notifier.filter = filter; - ref.read(homeProvider).homeTab = HomeView.DISCOVER; + widget.ref.read(homeProvider).homeTab = HomeView.DISCOVER; Navigator.popUntil(context, (r) => r.isFirst); }, onLongPress: (i) => showPopUp( diff --git a/lib/media/media_models.dart b/lib/media/media_models.dart index 430372ea..c85f8139 100644 --- a/lib/media/media_models.dart +++ b/lib/media/media_models.dart @@ -25,20 +25,30 @@ class Media { final List relations; } +enum MediaTab { + info, + relations, + characters, + staff, + reviews, + recommendations, + statistics, +} + class MediaRelations { const MediaRelations({ - this.recommended = const AsyncValue.loading(), this.characters = const AsyncValue.loading(), this.staff = const AsyncValue.loading(), this.reviews = const AsyncValue.loading(), + this.recommendations = const AsyncValue.loading(), this.languageToVoiceActors = const {}, this.language = '', }); - final AsyncValue> recommended; final AsyncValue> characters; final AsyncValue> staff; final AsyncValue> reviews; + final AsyncValue> recommendations; /// For each language, a list of voice actors /// is mapped to the corresponding media's id. @@ -78,16 +88,6 @@ class MediaRelations { } } -enum MediaTab { - info, - relations, - recommended, - characters, - staff, - reviews, - statistics, -} - class RelatedMedia { RelatedMedia._({ required this.id, diff --git a/lib/media/media_people_view.dart b/lib/media/media_people_view.dart deleted file mode 100644 index 67bbc522..00000000 --- a/lib/media/media_people_view.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ionicons/ionicons.dart'; -import 'package:otraku/media/media_providers.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/widgets/grids/relation_grid.dart'; -import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/direct_page_view.dart'; -import 'package:otraku/widgets/layouts/scaffolds.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; -import 'package:otraku/widgets/paged_view.dart'; - -class MediaPeopleView extends StatelessWidget { - const MediaPeopleView(this.id, this.tabToggled, this.toggleTab); - - final int id; - final bool tabToggled; - final void Function(bool) toggleTab; - - @override - Widget build(BuildContext context) { - final scrollCtrl = context - .findAncestorStateOfType()! - .innerController; - - return TabScaffold( - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - centered: true, - children: [ - ActionTabSwitcher( - items: const ['Characters', 'Staff'], - current: tabToggled ? 1 : 0, - onChanged: (i) => toggleTab(i == 1), - ), - if (tabToggled) - const SizedBox( - width: floatingBarItemHeight, height: floatingBarItemHeight) - else - _LanguageButton(id, scrollCtrl), - ], - ), - child: DirectPageView( - onChanged: null, - current: tabToggled ? 1 : 0, - children: [ - Consumer( - builder: (context, ref, _) => PagedView( - provider: mediaRelationsProvider(id).select((s) => s.characters), - onData: (data) => _CharacterGrid(id, ref, data.items), - scrollCtrl: scrollCtrl, - onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), - ), - ), - Consumer( - builder: (context, ref, _) => PagedView( - provider: mediaRelationsProvider(id).select((s) => s.staff), - onData: (data) => RelationGrid(items: data.items), - scrollCtrl: scrollCtrl, - onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), - ), - ), - ], - ), - ); - } -} - -class _LanguageButton extends StatelessWidget { - const _LanguageButton(this.id, this.scrollCtrl); - - final int id; - final ScrollController scrollCtrl; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - if (ref.watch(mediaRelationsProvider(id).select( - (s) => s.languages.length < 2, - ))) return const SizedBox(); - - return ActionButton( - tooltip: 'Language', - icon: Ionicons.globe_outline, - onTap: () { - final mediaRelations = ref.read(mediaRelationsProvider(id)); - final languages = mediaRelations.languages; - final language = mediaRelations.language; - - showSheet( - context, - DynamicGradientDragSheet( - onTap: (i) => ref - .read(mediaRelationsProvider(id).notifier) - .changeLanguage(languages.elementAt(i)), - children: [ - for (int i = 0; i < languages.length; i++) - Text( - languages.elementAt(i), - style: languages.elementAt(i) != language - ? Theme.of(context).textTheme.titleLarge - : Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - ); - }, - ); - }, - ); - } -} - -class _CharacterGrid extends StatelessWidget { - const _CharacterGrid(this.id, this.ref, this.items); - - final int id; - final WidgetRef ref; - final List items; - - @override - Widget build(BuildContext context) { - final mediaRelations = ref.watch(mediaRelationsProvider(id)); - - if (mediaRelations.languages.isEmpty) { - return RelationGrid(items: items); - } - - final characters = []; - final voiceActors = []; - mediaRelations.getCharactersAndVoiceActors(characters, voiceActors); - - return RelationGrid(items: characters, connections: voiceActors); - } -} diff --git a/lib/media/media_providers.dart b/lib/media/media_providers.dart index 505dd5b7..f881f7e3 100644 --- a/lib/media/media_providers.dart +++ b/lib/media/media_providers.dart @@ -84,10 +84,10 @@ class MediaRelationsNotifier extends StateNotifier { variables['withCharacters'] = true; variables['withStaff'] = true; variables['withReviews'] = true; - } else if (tab == MediaTab.recommended) { - if (!(state.recommended.valueOrNull?.hasNext ?? true)) return; + } else if (tab == MediaTab.recommendations) { + if (!(state.recommendations.valueOrNull?.hasNext ?? true)) return; variables['withRecommendations'] = true; - variables['page'] = state.recommended.valueOrNull?.next ?? 1; + variables['page'] = state.recommendations.valueOrNull?.next ?? 1; } else if (tab == MediaTab.characters) { if (!(state.characters.valueOrNull?.hasNext ?? true)) return; variables['withCharacters'] = true; @@ -107,14 +107,14 @@ class MediaRelationsNotifier extends StateNotifier { return data['Media']; }); - var recommended = state.recommended; + var recommended = state.recommendations; var characters = state.characters; var staff = state.staff; var reviews = state.reviews; var languageToVoiceActors = state.languageToVoiceActors; var language = state.language; - if (tab == null || tab == MediaTab.recommended) { + if (tab == null || tab == MediaTab.recommendations) { recommended = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['recommendations']; @@ -228,7 +228,7 @@ class MediaRelationsNotifier extends StateNotifier { } state = MediaRelations( - recommended: recommended, + recommendations: recommended, characters: characters, staff: staff, reviews: reviews, @@ -238,7 +238,7 @@ class MediaRelationsNotifier extends StateNotifier { } void changeLanguage(String language) => state = MediaRelations( - recommended: state.recommended, + recommendations: state.recommendations, characters: state.characters, staff: state.staff, reviews: state.reviews, diff --git a/lib/media/media_social_view.dart b/lib/media/media_social_view.dart deleted file mode 100644 index 4c549d2a..00000000 --- a/lib/media/media_social_view.dart +++ /dev/null @@ -1,233 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ionicons/ionicons.dart'; -import 'package:otraku/media/media_models.dart'; -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/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/direct_page_view.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/paged_view.dart'; - -class MediaSocialView extends StatelessWidget { - const MediaSocialView(this.id, this.media, this.tabToggled, this.toggleTab); - - final int id; - final Media media; - final bool tabToggled; - final void Function(bool) toggleTab; - - @override - Widget build(BuildContext context) { - final scrollCtrl = context - .findAncestorStateOfType()! - .innerController; - final stats = media.stats; - - return TabScaffold( - floatingBar: FloatingBar( - scrollCtrl: scrollCtrl, - centered: true, - children: [ - ActionTabSwitcher( - items: const ['Reviews', 'Stats'], - current: tabToggled ? 1 : 0, - onChanged: (i) => toggleTab(i == 1), - ), - ], - ), - child: DirectPageView( - onChanged: null, - current: tabToggled ? 1 : 0, - children: [ - Consumer( - builder: (context, ref, _) => PagedView( - provider: mediaRelationsProvider(id).select((s) => s.reviews), - onData: (data) => _ReviewGrid(data.items, media.info.banner), - scrollCtrl: scrollCtrl, - onRefresh: () => ref.invalidate(mediaRelationsProvider(id)), - ), - ), - CustomScrollView( - controller: scrollCtrl, - slivers: [ - if (stats.rankTexts.isNotEmpty) - _Ranks(stats.rankTexts, stats.rankTypes), - if (stats.scoreNames.isNotEmpty) - _Scores(stats.scoreNames, stats.scoreValues), - if (stats.statusNames.isNotEmpty) - _Statuses(stats.statusNames, stats.statusValues), - const SliverFooter(), - ], - ), - ], - ), - ); - } -} - -class _ReviewGrid extends StatelessWidget { - const _ReviewGrid(this.items, this.bannerUrl); - - final List items; - final String? bannerUrl; - - @override - Widget build(BuildContext context) { - if (items.isEmpty) { - return const SliverFillRemaining( - child: Center(child: Text('No reviews')), - ); - } - - return SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 300, - height: 140, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (context, i) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LinkTile( - id: items[i].userId, - info: items[i].avatar, - discoverType: DiscoverType.user, - child: Row( - children: [ - Hero( - tag: items[i].userId, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: CachedImage( - items[i].avatar, - height: 50, - width: 50, - ), - ), - ), - 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, - ), - ], - ), - ), - const SizedBox(height: 5), - Expanded( - child: LinkTile( - id: items[i].reviewId, - info: bannerUrl, - discoverType: DiscoverType.review, - child: Card( - child: SizedBox( - width: double.infinity, - child: Padding( - padding: Consts.padding, - child: Text( - items[i].summary, - style: Theme.of(context).textTheme.labelMedium, - overflow: TextOverflow.fade, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -class _Ranks extends StatelessWidget { - const _Ranks(this.rankTexts, this.rankTypes); - - final List rankTexts; - final List rankTypes; - - @override - Widget build(BuildContext context) { - return SliverPadding( - padding: const EdgeInsets.only(top: 10, left: 10, right: 10), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - height: Consts.tapTargetSize, - minWidth: 185, - ), - delegate: SliverChildBuilderDelegate( - (_, i) => Card( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - child: Row( - children: [ - Icon( - rankTypes[i] ? Ionicons.star : Icons.favorite_rounded, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 5), - Expanded( - child: Text( - rankTexts[i], - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - ], - ), - ), - ), - childCount: rankTexts.length, - ), - ), - ); - } -} - -class _Scores extends StatelessWidget { - const _Scores(this.scoreNames, this.scoreValues); - - final List scoreNames; - final List scoreValues; - - @override - Widget build(BuildContext context) => SliverToBoxAdapter( - child: BarChart( - title: 'Score Distribution', - names: scoreNames.map((n) => n.toString()).toList(), - values: scoreValues, - ), - ); -} - -class _Statuses extends StatelessWidget { - const _Statuses(this.statusNames, this.statusValues); - - final List statusNames; - final List statusValues; - - @override - Widget build(BuildContext context) => SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: PieChart( - title: 'Status Distribution', - names: statusNames, - values: statusValues, - ), - ), - ); -} diff --git a/lib/media/media_view.dart b/lib/media/media_view.dart index 2b1da020..b24b0777 100644 --- a/lib/media/media_view.dart +++ b/lib/media/media_view.dart @@ -1,19 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ionicons/ionicons.dart'; +import 'package:otraku/common/relation.dart'; +import 'package:otraku/media/media_action_buttons.dart'; +import 'package:otraku/media/media_grids.dart'; import 'package:otraku/media/media_models.dart'; import 'package:otraku/media/media_providers.dart'; +import 'package:otraku/statistics/charts.dart'; import 'package:otraku/utils/paged_controller.dart'; import 'package:otraku/media/media_info_view.dart'; -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/grids/relation_grid.dart'; +import 'package:otraku/widgets/layouts/constrained_view.dart'; +import 'package:otraku/widgets/layouts/floating_bar.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'; import 'package:otraku/widgets/overlays/dialogs.dart'; +import 'package:otraku/widgets/paged_view.dart'; class MediaView extends StatefulWidget { const MediaView(this.id, this.coverUrl); @@ -25,36 +27,30 @@ class MediaView extends StatefulWidget { State createState() => _MediaViewState(); } -class _MediaViewState extends State { +class _MediaViewState extends State + with SingleTickerProviderStateMixin { final _scrollCtrl = ScrollController(); - int _tab = 0; + late final _tabCtrl = TabController( + length: MediaTab.values.length, + vsync: this, + ); @override void dispose() { _scrollCtrl.dispose(); + _tabCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return PageScaffold( - bottomBar: BottomBarIconTabs( - current: _tab, - onChanged: (i) => setState(() => _tab = i), - onSame: (_) => _scrollCtrl.scrollToTop(), - items: const { - 'Info': Ionicons.book_outline, - 'Other': Ionicons.layers_outline, - 'People': Icons.emoji_people_outlined, - 'Social': Ionicons.stats_chart_outline, - }, - ), child: Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), child: NestedScrollView( controller: _scrollCtrl, headerSliverBuilder: (context, _) => [ - MediaHeader(widget.id, widget.coverUrl), + MediaHeader(widget.id, widget.coverUrl, _tabCtrl), ], body: Consumer( builder: (context, ref, _) { @@ -73,16 +69,25 @@ class _MediaViewState extends State { }, ); + final innerScrollCtrl = context + .findAncestorStateOfType()! + .innerController; + return ref.watch(mediaProvider(widget.id)).when( loading: () => const Center(child: Loader()), error: (_, __) => const Center( child: Text('Failed to load media'), ), - data: (media) => _MediaSubView( - widget.id, - _tab, - media, - (i) => setState(() => _tab = i), + data: (media) => TabScaffold( + floatingBar: FloatingBar( + scrollCtrl: innerScrollCtrl, + children: [ + MediaEditButton(media), + MediaFavoriteButton(media.info), + MediaLanguageButton(widget.id, _tabCtrl), + ], + ), + child: _MediaViewContent(widget.id, media, _tabCtrl), ), ); }, @@ -97,24 +102,20 @@ class _MediaViewState extends State { /// can't be used here and has to be reimplemented temporarely on the inner /// scroll controller of the [NestedScrollView]. /// For more context: https://github.com/flutter/flutter/pull/104166. -class _MediaSubView extends ConsumerStatefulWidget { - const _MediaSubView(this.id, this.tab, this.media, this.onChanged); +class _MediaViewContent extends ConsumerStatefulWidget { + const _MediaViewContent(this.id, this.media, this.tabCtrl); final int id; - final int tab; final Media media; - final void Function(int) onChanged; + final TabController tabCtrl; @override - ConsumerState<_MediaSubView> createState() => __MediaSubViewState(); + ConsumerState<_MediaViewContent> createState() => __MediaSubViewState(); } -class __MediaSubViewState extends ConsumerState<_MediaSubView> { +class __MediaSubViewState extends ConsumerState<_MediaViewContent> { late final ScrollController _scrollCtrl; double _lastMaxExtent = 0; - bool _otherTabToggled = false; - bool _peopleTabToggled = false; - bool _socialTabToggled = false; @override void initState() { @@ -122,91 +123,139 @@ class __MediaSubViewState extends ConsumerState<_MediaSubView> { _scrollCtrl = context .findAncestorStateOfType()! .innerController; - _scrollCtrl.addListener(_listener); - } - - @override - void didUpdateWidget(covariant _MediaSubView oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.tab != oldWidget.tab) _lastMaxExtent = 0; + _scrollCtrl.addListener(_scrollListener); + widget.tabCtrl.addListener(_tabListener); } @override void dispose() { - _scrollCtrl.removeListener(_listener); + _scrollCtrl.removeListener(_scrollListener); + widget.tabCtrl.removeListener(_tabListener); super.dispose(); } - void _listener() { + void _tabListener() => _lastMaxExtent = 0; + + void _scrollListener() { final pos = _scrollCtrl.positions.last; if (pos.pixels < pos.maxScrollExtent - 100) return; if (_lastMaxExtent == pos.maxScrollExtent) return; _lastMaxExtent = pos.maxScrollExtent; - switch (widget.tab) { - case 1: - if (_otherTabToggled) { - ref - .read(mediaRelationsProvider(widget.id).notifier) - .fetch(MediaTab.recommended); - } - return; - case 2: - _peopleTabToggled - ? ref - .read(mediaRelationsProvider(widget.id).notifier) - .fetch(MediaTab.staff) - : ref - .read(mediaRelationsProvider(widget.id).notifier) - .fetch(MediaTab.characters); - return; - case 3: - if (!_socialTabToggled) { - ref - .read(mediaRelationsProvider(widget.id).notifier) - .fetch(MediaTab.reviews); - } - return; - } + ref + .read(mediaRelationsProvider(widget.id).notifier) + .fetch(MediaTab.values.elementAt(widget.tabCtrl.index)); + } + + void _refresh(WidgetRef ref) { + ref.invalidate(mediaRelationsProvider(widget.id)); + _lastMaxExtent = 0; } @override Widget build(BuildContext context) { ref.watch(mediaRelationsProvider(widget.id).select((_) => null)); - return DirectPageView( - current: widget.tab, - onChanged: widget.onChanged, + final stats = widget.media.stats; + + return TabBarView( + controller: widget.tabCtrl, children: [ - MediaInfoView(widget.media), - MediaOtherView( - widget.id, - widget.media.relations, - _otherTabToggled, - (val) { - _lastMaxExtent = 0; - _scrollCtrl.scrollToTop(); - setState(() => _otherTabToggled = val); - }, + MediaInfoView(widget.media, _scrollCtrl), + ConstrainedView( + child: CustomScrollView( + controller: _scrollCtrl, + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 10)), + MediaRelatedGrid(widget.media.relations), + const SliverFooter(), + ], + ), + ), + Consumer( + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(widget.id).select( + (s) => s.characters, + ), + scrollCtrl: _scrollCtrl, + onRefresh: () => _refresh(ref), + onData: (data) { + final mediaRelations = ref.watch( + mediaRelationsProvider(widget.id), + ); + + if (mediaRelations.languages.isEmpty) { + return RelationGrid(items: data.items); + } + + final characters = []; + final voiceActors = []; + mediaRelations.getCharactersAndVoiceActors( + characters, + voiceActors, + ); + + return RelationGrid(items: characters, connections: voiceActors); + }, + ), ), - MediaPeopleView( - widget.id, - _peopleTabToggled, - (val) { - _lastMaxExtent = 0; - _scrollCtrl.scrollToTop(); - setState(() => _peopleTabToggled = val); - }, + Consumer( + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(widget.id).select((s) => s.staff), + onData: (data) => RelationGrid(items: data.items), + scrollCtrl: _scrollCtrl, + onRefresh: () => _refresh(ref), + ), ), - MediaSocialView( - widget.id, - widget.media, - _socialTabToggled, - (val) { - _lastMaxExtent = 0; - _scrollCtrl.scrollToTop(); - setState(() => _socialTabToggled = val); - }, + Consumer( + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(widget.id).select( + (s) => s.reviews, + ), + onData: (data) => MediaReviewGrid( + data.items, + widget.media.info.banner, + ), + scrollCtrl: _scrollCtrl, + onRefresh: () => _refresh(ref), + ), + ), + Consumer( + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(widget.id).select( + (s) => s.recommendations, + ), + onData: (data) => MediaRecommendationGrid(widget.id, data.items), + onRefresh: () => _refresh(ref), + scrollCtrl: _scrollCtrl, + ), + ), + CustomScrollView( + controller: _scrollCtrl, + slivers: [ + if (stats.rankTexts.isNotEmpty) + MediaRankGrid(stats.rankTexts, stats.rankTypes), + if (stats.scoreNames.isNotEmpty) + SliverToBoxAdapter( + child: BarChart( + title: 'Score Distribution', + names: stats.scoreNames.map((n) => n.toString()).toList(), + values: stats.scoreValues, + ), + ), + if (stats.statusNames.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: PieChart( + title: 'Status Distribution', + names: stats.statusNames, + values: stats.statusValues, + ), + ), + ), + const SliverFooter(), + ], ), ], ); From b9bdb7d5e2bdf617580006707e4f7ded65a052be Mon Sep 17 00:00:00 2001 From: lotusgate Date: Thu, 13 Apr 2023 00:56:18 +0300 Subject: [PATCH 22/55] Improved media/user header design --- lib/media/media_header.dart | 96 +++++----- lib/user/user_header.dart | 337 ++++++++++++++++++------------------ 2 files changed, 220 insertions(+), 213 deletions(-) diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index a0d94492..a524b01e 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -99,10 +99,10 @@ class _Delegate extends SliverPersistentHeaderDelegate { bool overlapsContent, ) { final height = maxExtent; - var opacity = shrinkOffset > _bannerHeight + var transition = shrinkOffset > _bannerHeight ? (shrinkOffset - _bannerHeight) / (imageHeight / 4) : 0.0; - if (opacity > 1) opacity = 1; + if (transition > 1) transition = 1; final cover = info?.cover ?? coverUrl; final theme = Theme.of(context); @@ -122,9 +122,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { ? GestureDetector( onTap: () => showPopUp( context, - ImageDialog( - info?.extraLargeCover ?? cover, - ), + ImageDialog(info?.extraLargeCover ?? cover), ), child: CachedImage(cover), ) @@ -135,8 +133,8 @@ class _Delegate extends SliverPersistentHeaderDelegate { const SizedBox(width: 10), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (info?.preferredTitle != null) ...[ GestureDetector( @@ -179,7 +177,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { child: info?.preferredTitle == null ? const SizedBox() : Opacity( - opacity: opacity, + opacity: transition, child: Text( info!.preferredTitle!, style: theme.textTheme.titleMedium, @@ -199,31 +197,35 @@ class _Delegate extends SliverPersistentHeaderDelegate { ], ); - return SizedBox( + final body = SizedBox( height: height, child: Column( children: [ Flexible( flex: (height - Consts.tapTargetSize).floor(), - child: DecoratedBox( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceVariant, - ), - child: Stack( - fit: StackFit.expand, - children: [ - if (info?.banner != null) - Positioned( - top: 0, - left: 0, - right: 0, - bottom: height - _bannerHeight, - child: GestureDetector( - child: CachedImage(info!.banner!), - onTap: () => - showPopUp(context, ImageDialog(info!.banner!)), - ), - ), + child: Stack( + fit: StackFit.expand, + children: [ + if (transition < 1) ...[ + Positioned( + top: 0, + left: 0, + right: 0, + bottom: height - _bannerHeight, + child: info?.banner != null + ? GestureDetector( + child: CachedImage(info!.banner!), + onTap: () => showPopUp( + context, + ImageDialog(info!.banner!), + ), + ) + : DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant, + ), + ), + ), Positioned( left: 0, right: 0, @@ -247,7 +249,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { ), ), Positioned( - bottom: 0, + bottom: 5, left: 10, right: 10, child: infoContent, @@ -276,7 +278,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { right: 0, height: Consts.tapTargetSize, child: Opacity( - opacity: opacity, + opacity: transition, child: DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.background, @@ -284,19 +286,19 @@ class _Delegate extends SliverPersistentHeaderDelegate { ), ), ), - Positioned( - top: 0, - left: 0, - right: 0, - height: Consts.tapTargetSize, - child: topRow, - ), ], - ), + Positioned( + top: 0, + left: 0, + right: 0, + height: Consts.tapTargetSize, + child: topRow, + ), + ], ), ), Material( - color: Theme.of(context).colorScheme.background, + color: Colors.transparent, child: TabBar( splashBorderRadius: Consts.borderRadiusMin, controller: tabCtrl, @@ -315,6 +317,18 @@ class _Delegate extends SliverPersistentHeaderDelegate { ], ), ); + + return transition < 1 + ? body + : ClipRect( + child: BackdropFilter( + filter: Consts.filter, + child: DecoratedBox( + decoration: BoxDecoration(color: theme.bottomAppBarTheme.color), + child: body, + ), + ), + ); } static const _bannerHeight = 200.0; @@ -322,11 +336,11 @@ class _Delegate extends SliverPersistentHeaderDelegate { double get imageHeight => imageWidth * Consts.coverHtoWRatio; @override - double get maxExtent => - _bannerHeight + imageHeight / 2 + Consts.tapTargetSize; + double get minExtent => Consts.tapTargetSize * 2; @override - double get minExtent => Consts.tapTargetSize * 2; + double get maxExtent => + _bannerHeight + imageHeight / 2 + Consts.tapTargetSize; @override bool shouldRebuild(covariant _Delegate oldDelegate) => diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index 4e67d7f8..aa4e5912 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -71,53 +71,150 @@ class _Delegate extends SliverPersistentHeaderDelegate { 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 opacity = shrinkOffset < (_bannerHeight - minExtent) - ? shrinkOffset / (_bannerHeight - minExtent) - : 1.0; + var transition = shrinkOffset > _bannerHeight + ? (shrinkOffset - _bannerHeight) / (imageWidth / 4) + : 0.0; + if (transition > 1) transition = 1; 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, + final infoContent = 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, + ), ), - ], - ), - child: Stack( - fit: StackFit.expand, - children: [ - Column( + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible( - flex: _bannerHeight.ceil(), - child: user?.bannerUrl != null - ? GestureDetector( - child: CachedImage(user!.bannerUrl!), - onTap: () => showPopUp( - context, - ImageDialog(user!.bannerUrl!), + 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( + blurRadius: 10, + color: theme.colorScheme.background, ), - ) - : const SizedBox(), - ), - Flexible( - flex: (height - _bannerHeight).floor(), - child: const SizedBox(), + ], + ), + ), + ), + 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, + ), ), ], ), + ), + ], + ); + + final topRow = Row( + children: [ + isViewer + ? const SizedBox(width: 10) + : TopBarIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: Navigator.of(context).pop, + ), + Expanded( + child: user?.name == null + ? const SizedBox() + : Opacity( + opacity: transition, + child: Text( + user!.name, + style: theme.textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (!isViewer && user != null) _FollowButton(user!), + if (user?.siteUrl != null) + TopBarIcon( + tooltip: 'More', + icon: Ionicons.ellipsis_horizontal, + onTap: () => showSheet( + context, + FixedGradientDragSheet.link(context, user!.siteUrl!), + ), + ), + if (isViewer) + TopBarIcon( + tooltip: 'Settings', + icon: Ionicons.cog_outline, + onTap: () => Navigator.pushNamed( + context, + RouteArg.settings, + ), + ), + ], + ); + + final body = Stack( + fit: StackFit.expand, + children: [ + if (transition < 1) ...[ + Positioned( + top: 0, + left: 0, + right: 0, + bottom: height - _bannerHeight, + child: user?.bannerUrl != null + ? GestureDetector( + child: CachedImage(user!.bannerUrl!), + onTap: () => showPopUp( + context, + ImageDialog(user!.bannerUrl!), + ), + ) + : DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant, + ), + ), + ), Positioned( left: 0, right: 0, @@ -142,91 +239,23 @@ class _Delegate extends SliverPersistentHeaderDelegate { ), 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, - ), - ), - ], - ), - ), - ], - ), + left: 10, + right: 10, + child: infoContent, ), Positioned( top: 0, left: 0, right: 0, - height: minExtent, + height: Consts.tapTargetSize, child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(0), + theme.colorScheme.background, + theme.colorScheme.background.withAlpha(0), ], ), ), @@ -236,87 +265,51 @@ class _Delegate extends SliverPersistentHeaderDelegate { top: 0, left: 0, right: 0, - height: minExtent, + height: Consts.tapTargetSize, child: Opacity( - opacity: opacity, + opacity: transition, 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: [ - isViewer - ? const SizedBox(width: 10) - : TopBarIcon( - 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, - ), - ), - ), - if (!isViewer && user != null) _FollowButton(user!), - if (user?.siteUrl != null) - TopBarIcon( - tooltip: 'More', - icon: Ionicons.ellipsis_horizontal, - onTap: () => showSheet( - context, - FixedGradientDragSheet.link(context, user!.siteUrl!), - ), - ), - if (isViewer) - TopBarIcon( - tooltip: 'Settings', - icon: Ionicons.cog_outline, - onTap: () => Navigator.pushNamed( - context, - RouteArg.settings, - ), - ), - ], - ), - ), ], - ), + Positioned( + top: 0, + left: 0, + right: 0, + height: Consts.tapTargetSize, + child: topRow, + ), + ], ); + + return transition < 1 + ? body + : ClipRect( + child: BackdropFilter( + filter: Consts.filter, + child: DecoratedBox( + decoration: BoxDecoration(color: theme.bottomAppBarTheme.color), + child: body, + ), + ), + ); } static const _bannerHeight = 200.0; @override - double get maxExtent => _bannerHeight + imageWidth / 2; + double get minExtent => Consts.tapTargetSize; @override - double get minExtent => Consts.tapTargetSize; + double get maxExtent => _bannerHeight + imageWidth / 2; @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => - true; + bool shouldRebuild(covariant _Delegate oldDelegate) => + user != oldDelegate.user; } class _FollowButton extends StatefulWidget { From 98c05bdf804c3975f53ce67000ab2f7fd37f71fd Mon Sep 17 00:00:00 2001 From: lotusgate Date: Fri, 14 Apr 2023 00:35:46 +0300 Subject: [PATCH 23/55] Visual improvements for media/user headers --- ios/Runner.xcodeproj/project.pbxproj | 2 +- lib/media/media_header.dart | 45 +++++--- lib/media/media_view.dart | 157 +++++++++++++++------------ lib/settings/settings_app_tab.dart | 6 +- lib/settings/settings_view.dart | 23 ++-- lib/statistics/statistics_view.dart | 3 +- lib/user/user_header.dart | 31 ++++-- lib/user/user_view.dart | 123 ++++++++++----------- lib/utils/consts.dart | 1 - lib/widgets/layouts/top_bar.dart | 43 ++++---- lib/widgets/overlays/sheets.dart | 6 +- 11 files changed, 234 insertions(+), 206 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 091b2f55..906691fc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index a524b01e..c854f149 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -22,13 +22,15 @@ class MediaHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final topOffset = MediaQuery.of(context).padding.top; + return Consumer( builder: (context, ref, _) { - final data = ref.watch(mediaProvider(id).select((s) => s.valueOrNull)); + final media = ref.watch(mediaProvider(id).select((s) => s.valueOrNull)); final textRailItems = {}; - if (data != null) { - final info = data.info; + if (media != null) { + final info = media.info; if (info.isAdult) textRailItems['Adult'] = true; @@ -36,9 +38,9 @@ class MediaHeader extends StatelessWidget { textRailItems[Convert.clarifyEnum(info.format)!] = false; } - if (data.edit.status != null) { + if (media.edit.status != null) { textRailItems[Convert.adaptListStatus( - data.edit.status!, + media.edit.status!, info.type == DiscoverType.anime, )] = false; } @@ -48,8 +50,8 @@ class MediaHeader extends StatelessWidget { '${Convert.timeUntilTimestamp(info.airingAt)}'] = true; } - if (data.edit.status != null) { - final progress = data.edit.progress; + if (media.edit.status != null) { + final progress = media.edit.progress; if (info.nextEpisode != null && info.nextEpisode! - 1 > progress) { textRailItems['${info.nextEpisode! - 1 - progress}' ' ep behind'] = true; @@ -62,8 +64,9 @@ class MediaHeader extends StatelessWidget { delegate: _Delegate( id: id, tabCtrl: tabCtrl, - info: data?.info, + info: media?.info, coverUrl: coverUrl, + topOffset: topOffset, textRailItems: textRailItems, imageWidth: MediaQuery.of(context).size.width < 430.0 ? MediaQuery.of(context).size.width * 0.30 @@ -81,6 +84,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { required this.info, required this.imageWidth, required this.coverUrl, + required this.topOffset, required this.textRailItems, required this.tabCtrl, }); @@ -89,6 +93,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { final MediaInfo? info; final double imageWidth; final String? coverUrl; + final double topOffset; final Map textRailItems; final TabController tabCtrl; @@ -99,8 +104,11 @@ class _Delegate extends SliverPersistentHeaderDelegate { bool overlapsContent, ) { final height = maxExtent; - var transition = shrinkOffset > _bannerHeight - ? (shrinkOffset - _bannerHeight) / (imageHeight / 4) + final bannerOffset = + height - _bannerBaseHeight - topOffset - imageHeight / 4; + + var transition = shrinkOffset > _bannerBaseHeight + ? (shrinkOffset - _bannerBaseHeight) / (imageHeight / 4) : 0.0; if (transition > 1) transition = 1; @@ -211,7 +219,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { top: 0, left: 0, right: 0, - bottom: height - _bannerHeight, + bottom: bannerOffset, child: info?.banner != null ? GestureDetector( child: CachedImage(info!.banner!), @@ -230,7 +238,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { left: 0, right: 0, bottom: 0, - height: height - _bannerHeight, + height: bannerOffset, child: Container( alignment: Alignment.topCenter, color: theme.colorScheme.background, @@ -258,7 +266,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { top: 0, left: 0, right: 0, - height: Consts.tapTargetSize, + height: topOffset + Consts.tapTargetSize, child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( @@ -266,6 +274,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { end: Alignment.bottomCenter, colors: [ theme.colorScheme.background, + theme.colorScheme.background.withAlpha(200), theme.colorScheme.background.withAlpha(0), ], ), @@ -276,7 +285,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { top: 0, left: 0, right: 0, - height: Consts.tapTargetSize, + height: topOffset + Consts.tapTargetSize, child: Opacity( opacity: transition, child: DecoratedBox( @@ -288,9 +297,9 @@ class _Delegate extends SliverPersistentHeaderDelegate { ), ], Positioned( - top: 0, left: 0, right: 0, + top: topOffset, height: Consts.tapTargetSize, child: topRow, ), @@ -331,16 +340,16 @@ class _Delegate extends SliverPersistentHeaderDelegate { ); } - static const _bannerHeight = 200.0; + static const _bannerBaseHeight = 200.0; double get imageHeight => imageWidth * Consts.coverHtoWRatio; @override - double get minExtent => Consts.tapTargetSize * 2; + double get minExtent => topOffset + Consts.tapTargetSize * 2; @override double get maxExtent => - _bannerHeight + imageHeight / 2 + Consts.tapTargetSize; + topOffset + Consts.tapTargetSize + _bannerBaseHeight + imageHeight / 2; @override bool shouldRebuild(covariant _Delegate oldDelegate) => diff --git a/lib/media/media_view.dart b/lib/media/media_view.dart index b24b0777..73f4739b 100644 --- a/lib/media/media_view.dart +++ b/lib/media/media_view.dart @@ -45,53 +45,50 @@ class _MediaViewState extends State @override Widget build(BuildContext context) { return PageScaffold( - child: Padding( - padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), - child: NestedScrollView( - controller: _scrollCtrl, - headerSliverBuilder: (context, _) => [ - MediaHeader(widget.id, widget.coverUrl, _tabCtrl), - ], - body: Consumer( - builder: (context, ref, _) { - ref.listen( - mediaProvider(widget.id), - (_, s) { - if (s.hasError) { - showPopUp( - context, - ConfirmationDialog( - title: 'Failed to load media', - content: s.error.toString(), - ), - ); - } - }, - ); + child: NestedScrollView( + controller: _scrollCtrl, + headerSliverBuilder: (context, _) => [ + MediaHeader(widget.id, widget.coverUrl, _tabCtrl), + ], + body: Consumer( + builder: (context, ref, _) { + ref.listen( + mediaProvider(widget.id), + (_, s) { + if (s.hasError) { + showPopUp( + context, + ConfirmationDialog( + title: 'Failed to load media', + content: s.error.toString(), + ), + ); + } + }, + ); - final innerScrollCtrl = context - .findAncestorStateOfType()! - .innerController; + final innerScrollCtrl = context + .findAncestorStateOfType()! + .innerController; - return ref.watch(mediaProvider(widget.id)).when( - loading: () => const Center(child: Loader()), - error: (_, __) => const Center( - child: Text('Failed to load media'), - ), - data: (media) => TabScaffold( - floatingBar: FloatingBar( - scrollCtrl: innerScrollCtrl, - children: [ - MediaEditButton(media), - MediaFavoriteButton(media.info), - MediaLanguageButton(widget.id, _tabCtrl), - ], - ), - child: _MediaViewContent(widget.id, media, _tabCtrl), + return ref.watch(mediaProvider(widget.id)).when( + loading: () => const Center(child: Loader()), + error: (_, __) => const Center( + child: Text('Failed to load media'), + ), + data: (media) => TabScaffold( + floatingBar: FloatingBar( + scrollCtrl: innerScrollCtrl, + children: [ + MediaEditButton(media), + MediaFavoriteButton(media.info), + MediaLanguageButton(widget.id, _tabCtrl), + ], ), - ); - }, - ), + child: _MediaViewContent(widget.id, media, _tabCtrl), + ), + ); + }, ), ), ); @@ -134,7 +131,19 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { super.dispose(); } - void _tabListener() => _lastMaxExtent = 0; + void _tabListener() { + _lastMaxExtent = 0; + + // This is a workaround for an issue with [NestedScrollView]. + // If you switch to a tab with pagination, where the content + // doesn't fill the view, the scroll controller has it's maximum + // extent set to 0 and the loading of a next page of items is not triggered. + // This is why we need to manually load the second page. + if (!widget.tabCtrl.indexIsChanging) { + final pos = _scrollCtrl.positions.last; + if (pos.minScrollExtent == pos.maxScrollExtent) _loadNextPage(); + } + } void _scrollListener() { final pos = _scrollCtrl.positions.last; @@ -142,11 +151,13 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { if (_lastMaxExtent == pos.maxScrollExtent) return; _lastMaxExtent = pos.maxScrollExtent; - ref - .read(mediaRelationsProvider(widget.id).notifier) - .fetch(MediaTab.values.elementAt(widget.tabCtrl.index)); + _loadNextPage(); } + void _loadNextPage() => ref + .read(mediaRelationsProvider(widget.id).notifier) + .fetch(MediaTab.values.elementAt(widget.tabCtrl.index)); + void _refresh(WidgetRef ref) { ref.invalidate(mediaRelationsProvider(widget.id)); _lastMaxExtent = 0; @@ -161,7 +172,7 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { return TabBarView( controller: widget.tabCtrl, children: [ - MediaInfoView(widget.media, _scrollCtrl), + ConstrainedView(child: MediaInfoView(widget.media, _scrollCtrl)), ConstrainedView( child: CustomScrollView( controller: _scrollCtrl, @@ -230,32 +241,34 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { scrollCtrl: _scrollCtrl, ), ), - CustomScrollView( - controller: _scrollCtrl, - slivers: [ - if (stats.rankTexts.isNotEmpty) - MediaRankGrid(stats.rankTexts, stats.rankTypes), - if (stats.scoreNames.isNotEmpty) - SliverToBoxAdapter( - child: BarChart( - title: 'Score Distribution', - names: stats.scoreNames.map((n) => n.toString()).toList(), - values: stats.scoreValues, + ConstrainedView( + child: CustomScrollView( + controller: _scrollCtrl, + slivers: [ + if (stats.rankTexts.isNotEmpty) + MediaRankGrid(stats.rankTexts, stats.rankTypes), + if (stats.scoreNames.isNotEmpty) + SliverToBoxAdapter( + child: BarChart( + title: 'Score Distribution', + names: stats.scoreNames.map((n) => n.toString()).toList(), + values: stats.scoreValues, + ), ), - ), - if (stats.statusNames.isNotEmpty) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: PieChart( - title: 'Status Distribution', - names: stats.statusNames, - values: stats.statusValues, + if (stats.statusNames.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: PieChart( + title: 'Status Distribution', + names: stats.statusNames, + values: stats.statusValues, + ), ), ), - ), - const SliverFooter(), - ], + const SliverFooter(), + ], + ), ), ], ); diff --git a/lib/settings/settings_app_tab.dart b/lib/settings/settings_app_tab.dart index 7cd24fd8..f84bdfae 100644 --- a/lib/settings/settings_app_tab.dart +++ b/lib/settings/settings_app_tab.dart @@ -31,8 +31,10 @@ class SettingsAppTab extends StatelessWidget { SliverPadding( padding: const EdgeInsets.only(left: 10, top: 10), sliver: SliverToBoxAdapter( - child: - Text('Theme', style: Theme.of(context).textTheme.labelMedium), + child: Text( + 'Theme', + style: Theme.of(context).textTheme.labelMedium, + ), ), ), SliverPadding( diff --git a/lib/settings/settings_view.dart b/lib/settings/settings_view.dart index f839c971..1b41a892 100644 --- a/lib/settings/settings_view.dart +++ b/lib/settings/settings_view.dart @@ -11,6 +11,7 @@ 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/constrained_view.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'; @@ -63,22 +64,30 @@ class _SettingsViewState extends ConsumerState { const errorWidget = Center(child: Text('Failed to load settings')); final tabs = [ - SettingsAppTab(_ctrl), + ConstrainedView(child: SettingsAppTab(_ctrl)), if (_settings.hasError) ...[ errorWidget, errorWidget, ] else if (_settings.hasValue) ...[ - SettingsContentTab(_ctrl, _settings.value!, () => _shouldUpdate = true), - SettingsNotificationsTab( - _ctrl, - _settings.value!, - () => _shouldUpdate = true, + ConstrainedView( + child: SettingsContentTab( + _ctrl, + _settings.value!, + () => _shouldUpdate = true, + ), + ), + ConstrainedView( + child: SettingsNotificationsTab( + _ctrl, + _settings.value!, + () => _shouldUpdate = true, + ), ), ] else ...[ loadWidget, loadWidget, ], - SettingsAboutTab(_ctrl), + ConstrainedView(child: SettingsAboutTab(_ctrl)), ]; return WillPopScope( diff --git a/lib/statistics/statistics_view.dart b/lib/statistics/statistics_view.dart index 5984c195..704e7dd2 100644 --- a/lib/statistics/statistics_view.dart +++ b/lib/statistics/statistics_view.dart @@ -8,6 +8,7 @@ import 'package:otraku/utils/paged_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/constrained_view.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'; @@ -105,7 +106,7 @@ class _StatisticsViewState extends State { topBar: TopBar( title: _onAnime ? 'Anime Statistics' : 'Manga Statistics', ), - child: content, + child: ConstrainedView(child: content), ), ); } diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index aa4e5912..db33942e 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -26,6 +26,7 @@ class UserHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final topOffset = MediaQuery.of(context).padding.top; final textRailItems = {}; if (user != null) { if (user!.modRoles.isNotEmpty) textRailItems[user!.modRoles[0]] = false; @@ -39,6 +40,7 @@ class UserHeader extends StatelessWidget { isViewer: isViewer, user: user, imageUrl: imageUrl, + topOffset: topOffset, textRailItems: textRailItems, imageWidth: MediaQuery.of(context).size.width < 430.0 ? MediaQuery.of(context).size.width * 0.30 @@ -54,6 +56,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { required this.isViewer, required this.user, required this.imageUrl, + required this.topOffset, required this.imageWidth, required this.textRailItems, }); @@ -62,6 +65,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { final bool isViewer; final User? user; final String? imageUrl; + final double topOffset; final double imageWidth; final Map textRailItems; @@ -72,8 +76,10 @@ class _Delegate extends SliverPersistentHeaderDelegate { bool overlapsContent, ) { final height = maxExtent; - var transition = shrinkOffset > _bannerHeight - ? (shrinkOffset - _bannerHeight) / (imageWidth / 4) + final bannerOffset = height - _bannerBaseHeight - topOffset; + + var transition = shrinkOffset > _bannerBaseHeight + ? (shrinkOffset - _bannerBaseHeight) / (imageWidth / 4) : 0.0; if (transition > 1) transition = 1; @@ -108,7 +114,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (user != null) + if (user != null) ...[ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Toast.copy(context, user!.name), @@ -125,6 +131,8 @@ class _Delegate extends SliverPersistentHeaderDelegate { ), ), ), + const SizedBox(height: 5), + ], GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { @@ -200,7 +208,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { top: 0, left: 0, right: 0, - bottom: height - _bannerHeight, + bottom: bannerOffset, child: user?.bannerUrl != null ? GestureDetector( child: CachedImage(user!.bannerUrl!), @@ -219,8 +227,8 @@ class _Delegate extends SliverPersistentHeaderDelegate { left: 0, right: 0, bottom: 0, + height: bannerOffset, child: Container( - height: height - _bannerHeight, alignment: Alignment.topCenter, color: theme.colorScheme.background, child: Container( @@ -247,7 +255,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { top: 0, left: 0, right: 0, - height: Consts.tapTargetSize, + height: topOffset + Consts.tapTargetSize, child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( @@ -255,6 +263,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { end: Alignment.bottomCenter, colors: [ theme.colorScheme.background, + theme.colorScheme.background.withAlpha(200), theme.colorScheme.background.withAlpha(0), ], ), @@ -265,7 +274,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { top: 0, left: 0, right: 0, - height: Consts.tapTargetSize, + height: topOffset + Consts.tapTargetSize, child: Opacity( opacity: transition, child: DecoratedBox( @@ -277,9 +286,9 @@ class _Delegate extends SliverPersistentHeaderDelegate { ), ], Positioned( - top: 0, left: 0, right: 0, + top: topOffset, height: Consts.tapTargetSize, child: topRow, ), @@ -299,13 +308,13 @@ class _Delegate extends SliverPersistentHeaderDelegate { ); } - static const _bannerHeight = 200.0; + static const _bannerBaseHeight = 200.0; @override - double get minExtent => Consts.tapTargetSize; + double get minExtent => topOffset + Consts.tapTargetSize; @override - double get maxExtent => _bannerHeight + imageWidth / 2; + double get maxExtent => topOffset + _bannerBaseHeight + imageWidth / 2; @override bool shouldRebuild(covariant _Delegate oldDelegate) => diff --git a/lib/user/user_view.dart b/lib/user/user_view.dart index c235513c..a71cde5d 100644 --- a/lib/user/user_view.dart +++ b/lib/user/user_view.dart @@ -8,6 +8,7 @@ import 'package:otraku/utils/options.dart'; import 'package:otraku/utils/route_arg.dart'; import 'package:otraku/widgets/html_content.dart'; import 'package:otraku/utils/consts.dart'; +import 'package:otraku/widgets/layouts/constrained_view.dart'; import 'package:otraku/widgets/layouts/scaffolds.dart'; import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; @@ -32,75 +33,63 @@ class UserSubView extends StatelessWidget { @override Widget build(BuildContext context) { - final sidePadding = MediaQuery.of(context).size.width > Consts.layoutBig - ? (MediaQuery.of(context).size.width - Consts.layoutBig) / 2 - : 10.0; - - return SafeArea( - child: 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(), + 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(), + ), + ), + ), + ); + + final user = ref.watch(userProvider(id)); + + final header = UserHeader( + id: id, + isViewer: id == Options().id, + user: user.valueOrNull, + imageUrl: avatarUrl ?? user.valueOrNull?.imageUrl, + ); + + return user.when( + error: (_, __) => CustomScrollView( + controller: scrollCtrl, + slivers: [ + header, + const SliverFillRemaining( + child: Center(child: Text('Failed to load user')), + ) + ], + ), + loading: () => CustomScrollView( + controller: scrollCtrl, + slivers: [ + header, + const SliverFillRemaining(child: Center(child: Loader())) + ], + ), + data: (data) => CustomScrollView( + controller: scrollCtrl, + slivers: [ + header, + _ButtonRow(id), + if (data.description.isNotEmpty) + SliverToBoxAdapter( + child: ConstrainedView( + child: Card(child: HtmlContent(data.description)), ), ), - ), - ); - - final user = ref.watch(userProvider(id)); - - final header = UserHeader( - id: id, - isViewer: id == Options().id, - user: user.valueOrNull, - imageUrl: avatarUrl ?? user.valueOrNull?.imageUrl, - ); - - return user.when( - error: (_, __) => CustomScrollView( - controller: scrollCtrl, - slivers: [ - header, - const SliverFillRemaining( - child: Center(child: Text('Failed to load user')), - ) - ], - ), - loading: () => CustomScrollView( - controller: scrollCtrl, - slivers: [ - header, - const SliverFillRemaining(child: Center(child: Loader())) - ], - ), - data: (data) => CustomScrollView( - controller: scrollCtrl, - slivers: [ - header, - _ButtonRow(id), - if (data.description.isNotEmpty) - SliverToBoxAdapter( - child: Card( - margin: EdgeInsets.symmetric(horizontal: sidePadding), - child: Padding( - padding: Consts.padding, - child: HtmlContent(data.description), - ), - ), - ), - const SliverFooter(), - ], - ), - ); - }, - ), - ), + const SliverFooter(), + ], + ), + ); + }, ); } } diff --git a/lib/utils/consts.dart b/lib/utils/consts.dart index 8c83994c..30f6caab 100644 --- a/lib/utils/consts.dart +++ b/lib/utils/consts.dart @@ -22,7 +22,6 @@ abstract class Consts { // Layout sizes. static const layoutBig = 1000.0; static const layoutMedium = 600.0; - static const layoutSmall = 400.0; // Font sizes. static const fontBig = 20.0; diff --git a/lib/widgets/layouts/top_bar.dart b/lib/widgets/layouts/top_bar.dart index df969b84..8ee9cc84 100644 --- a/lib/widgets/layouts/top_bar.dart +++ b/lib/widgets/layouts/top_bar.dart @@ -31,30 +31,27 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { top: MediaQuery.of(context).viewPadding.top, ), child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutBig), - child: Material( - color: Colors.transparent, - child: Row( - children: [ - if (canPop) - TopBarIcon( - tooltip: 'Close', - icon: Ionicons.chevron_back_outline, - onTap: () => Navigator.maybePop(context), - ) - else - const SizedBox(width: 10), - if (title != null) - Expanded( - child: Text( - title!, - style: Theme.of(context).textTheme.titleLarge, - ), + child: Material( + color: Colors.transparent, + child: Row( + children: [ + if (canPop) + TopBarIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: () => Navigator.maybePop(context), + ) + else + const SizedBox(width: 10), + if (title != null) + Expanded( + child: Text( + title!, + style: Theme.of(context).textTheme.titleLarge, ), - ...trailing, - ], - ), + ), + ...trailing, + ], ), ), ), diff --git a/lib/widgets/overlays/sheets.dart b/lib/widgets/overlays/sheets.dart index 8434d531..b252c77a 100644 --- a/lib/widgets/overlays/sheets.dart +++ b/lib/widgets/overlays/sheets.dart @@ -40,7 +40,7 @@ class OpaqueSheet extends StatelessWidget { builder: (context, scrollCtrl) { sheet ??= Center( child: Container( - constraints: const BoxConstraints(maxWidth: Consts.layoutSmall), + constraints: const BoxConstraints(maxWidth: Consts.layoutMedium), decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: @@ -139,7 +139,7 @@ class DynamicGradientDragSheet extends StatelessWidget { ), ), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutSmall), + constraints: const BoxConstraints(maxWidth: Consts.layoutMedium), child: ListView.builder( controller: scrollCtrl, padding: EdgeInsets.only( @@ -220,7 +220,7 @@ class FixedGradientDragSheet extends StatelessWidget { ), ), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutSmall), + constraints: const BoxConstraints(maxWidth: Consts.layoutMedium), child: ListView( controller: scrollCtrl, padding: const EdgeInsets.only( From a86f66db8deeb63894aea3cb20c77456e91a49eb Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 29 Apr 2023 22:34:27 +0300 Subject: [PATCH 24/55] Visual improvements --- lib/character/character_view.dart | 2 +- lib/composition/composition_view.dart | 172 +++++++++++++------------- lib/edit/edit_buttons.dart | 13 +- lib/favorites/favorites_view.dart | 2 +- lib/filter/filter_view.dart | 12 +- lib/home/home_view.dart | 2 +- lib/media/media_header.dart | 2 +- lib/settings/settings_view.dart | 2 +- lib/social/social_view.dart | 2 +- lib/staff/staff_view.dart | 2 +- lib/statistics/statistics_view.dart | 2 +- lib/user/user_header.dart | 2 +- lib/utils/consts.dart | 2 +- lib/utils/theming.dart | 5 +- lib/widgets/layouts/bottom_bar.dart | 171 ++++++++++--------------- lib/widgets/layouts/top_bar.dart | 4 +- 16 files changed, 175 insertions(+), 222 deletions(-) diff --git a/lib/character/character_view.dart b/lib/character/character_view.dart index 352b8207..a86eb90f 100644 --- a/lib/character/character_view.dart +++ b/lib/character/character_view.dart @@ -64,7 +64,7 @@ class _CharacterViewState extends ConsumerState { final onRefresh = () => ref.invalidate(characterMediaProvider(widget.id)); return PageScaffold( - bottomBar: BottomBarIconTabs( + bottomBar: BottomNavBar( current: _tab, onChanged: (i) => setState(() => _tab = i), onSame: (_) => _ctrl.scrollToTop(), diff --git a/lib/composition/composition_view.dart b/lib/composition/composition_view.dart index 74001bba..a44c55c9 100644 --- a/lib/composition/composition_view.dart +++ b/lib/composition/composition_view.dart @@ -55,30 +55,28 @@ class _CompositionViewState extends State { textCtrl: _ctrl, scrollCtrl: scrollCtrl, ), - buttons: BottomBar( - child: _ButtonRow( - tab: _tab, - textCtrl: _ctrl, - composition: widget.composition, - onSave: () async { - try { - widget.onDone( - await saveComposition(widget.composition), - ); - if (mounted) Navigator.pop(context); - } catch (e) { - if (!mounted) return; - Navigator.pop(context); - showPopUp( - context, - ConfirmationDialog( - title: 'Could not post', - content: e.toString(), - ), - ); - } - }, - ), + buttons: _BottomBar( + tab: _tab, + textCtrl: _ctrl, + composition: widget.composition, + onSave: () async { + try { + widget.onDone( + await saveComposition(widget.composition), + ); + if (mounted) Navigator.pop(context); + } catch (e) { + if (!mounted) return; + Navigator.pop(context); + showPopUp( + context, + ConfirmationDialog( + title: 'Could not post', + content: e.toString(), + ), + ); + } + }, ), ); } @@ -154,8 +152,8 @@ class _CompositionView extends StatelessWidget { /// A button menu. Some of the buttons are hidden, /// when the user isn't on the editing tab. -class _ButtonRow extends StatefulWidget { - const _ButtonRow({ +class _BottomBar extends StatefulWidget { + const _BottomBar({ required this.tab, required this.composition, required this.textCtrl, @@ -168,80 +166,76 @@ class _ButtonRow extends StatefulWidget { final void Function() onSave; @override - State<_ButtonRow> createState() => _ButtonRowState(); + State<_BottomBar> createState() => _BottomBarState(); } -class _ButtonRowState extends State<_ButtonRow> { +class _BottomBarState extends State<_BottomBar> { bool _loading = false; @override Widget build(BuildContext context) { if (_loading) return const Center(child: Loader()); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ValueListenableBuilder( - valueListenable: widget.tab, - builder: (context, i, child) => i == 0 ? child! : const SizedBox(), - child: Expanded( - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - _FormatButton( - tag: 'b', - name: 'Bold', - icon: Icons.format_bold_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'i', - name: 'Italic', - icon: Icons.format_italic_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'del', - name: 'Strikethrough', - icon: Icons.format_strikethrough_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'center', - name: 'Center', - icon: Icons.align_horizontal_center_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'code', - name: 'Code', - icon: Icons.code_outlined, - textCtrl: widget.textCtrl, - ), - ], - ), + return BottomBar([ + ValueListenableBuilder( + valueListenable: widget.tab, + builder: (context, i, child) => i == 0 ? child! : const SizedBox(), + child: Expanded( + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + _FormatButton( + tag: 'b', + name: 'Bold', + icon: Icons.format_bold_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'i', + name: 'Italic', + icon: Icons.format_italic_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'del', + name: 'Strikethrough', + icon: Icons.format_strikethrough_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'center', + name: 'Center', + icon: Icons.align_horizontal_center_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'code', + name: 'Code', + icon: Icons.code_outlined, + textCtrl: widget.textCtrl, + ), + ], ), ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.composition.isPrivate != null) - _PrivateButton( - widget.composition.isPrivate!, - (v) => widget.composition.isPrivate = v, - ), - TopBarIcon( - tooltip: 'Post', - icon: Ionicons.send_outline, - onTap: () async { - setState(() => _loading = true); - widget.onSave(); - }, + ), + Row( + children: [ + if (widget.composition.isPrivate != null) + _PrivateButton( + widget.composition.isPrivate!, + (v) => widget.composition.isPrivate = v, ), - ], - ), - ], - ); + TopBarIcon( + tooltip: 'Post', + icon: Ionicons.send_outline, + onTap: () async { + setState(() => _loading = true); + widget.onSave(); + }, + ), + ], + ), + ]); } } diff --git a/lib/edit/edit_buttons.dart b/lib/edit/edit_buttons.dart index 825f963c..bfd2b702 100644 --- a/lib/edit/edit_buttons.dart +++ b/lib/edit/edit_buttons.dart @@ -10,6 +10,7 @@ import 'package:otraku/filter/filter_providers.dart'; import 'package:otraku/home/home_provider.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/widgets/loaders.dart/loaders.dart'; import 'package:otraku/widgets/overlays/dialogs.dart'; class EditButtons extends StatefulWidget { @@ -29,9 +30,9 @@ class _EditButtonsState extends State { @override Widget build(BuildContext context) { return Consumer( - builder: (context, ref, __) => BottomBarDualButtonRow( - primary: _loading - ? null + builder: (context, ref, __) => BottomBar([ + _loading + ? const Expanded(child: Center(child: Loader())) : BottomBarButton( text: 'Save', icon: Ionicons.save_outline, @@ -86,8 +87,8 @@ class _EditButtonsState extends State { } }, ), - secondary: widget.oldEdit.entryId == null - ? null + widget.oldEdit.entryId == null + ? const Spacer() : BottomBarButton( text: 'Remove', icon: Ionicons.trash_bin_outline, @@ -138,7 +139,7 @@ class _EditButtonsState extends State { ), ), ), - ), + ]), ); } } diff --git a/lib/favorites/favorites_view.dart b/lib/favorites/favorites_view.dart index 40156743..af355906 100644 --- a/lib/favorites/favorites_view.dart +++ b/lib/favorites/favorites_view.dart @@ -44,7 +44,7 @@ class _FavoritesViewState extends ConsumerState { final onRefresh = () => ref.invalidate(favoritesProvider(widget.id)); return PageScaffold( - bottomBar: BottomBarIconTabs( + bottomBar: BottomNavBar( current: _tab.index, onChanged: (page) { setState(() => _tab = FavoritesTab.values.elementAt(page)); diff --git a/lib/filter/filter_view.dart b/lib/filter/filter_view.dart index e6fcef04..5f44a600 100644 --- a/lib/filter/filter_view.dart +++ b/lib/filter/filter_view.dart @@ -37,8 +37,8 @@ class __FilterViewState> @override Widget build(BuildContext context) { return OpaqueSheetView( - buttons: BottomBarDualButtonRow( - primary: BottomBarButton( + buttons: BottomBar([ + BottomBarButton( text: 'Apply', icon: Icons.done_rounded, onTap: () { @@ -46,7 +46,7 @@ class __FilterViewState> Navigator.pop(context); }, ), - secondary: BottomBarButton( + BottomBarButton( text: 'Clear', icon: Icons.close, warning: true, @@ -55,7 +55,7 @@ class __FilterViewState> Navigator.pop(context); }, ), - ), + ]), builder: (context, scrollCtrl) => widget.builder(context, scrollCtrl, _filter), ); @@ -413,10 +413,10 @@ class TagSheetBodyState extends ConsumerState { ClipRRect( borderRadius: const BorderRadius.vertical(top: Consts.radiusMax), child: BackdropFilter( - filter: Consts.filter, + filter: Consts.blurFilter, child: Container( height: 95, - color: Theme.of(context).bottomAppBarTheme.color, + color: Theme.of(context).navigationBarTheme.backgroundColor, padding: const EdgeInsets.only(top: 10, bottom: 5), child: Column( children: [ diff --git a/lib/home/home_view.dart b/lib/home/home_view.dart index 747ff04c..0fbd32a3 100644 --- a/lib/home/home_view.dart +++ b/lib/home/home_view.dart @@ -138,7 +138,7 @@ class _HomeViewState extends ConsumerState { return WillPopScope( onWillPop: () => _onWillPop(context), child: PageScaffold( - bottomBar: BottomBarIconTabs( + bottomBar: BottomNavBar( current: notifier.homeTab, onChanged: (i) => ref.read(homeProvider).homeTab = i, items: const { diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index c854f149..7cb07deb 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -331,7 +331,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { ? body : ClipRect( child: BackdropFilter( - filter: Consts.filter, + filter: Consts.blurFilter, child: DecoratedBox( decoration: BoxDecoration(color: theme.bottomAppBarTheme.color), child: body, diff --git a/lib/settings/settings_view.dart b/lib/settings/settings_view.dart index 1b41a892..9d01272e 100644 --- a/lib/settings/settings_view.dart +++ b/lib/settings/settings_view.dart @@ -98,7 +98,7 @@ class _SettingsViewState extends ConsumerState { return Future.value(true); }, child: PageScaffold( - bottomBar: BottomBarIconTabs( + bottomBar: BottomNavBar( current: _tabIndex, onSame: (_) => _ctrl.scrollToTop(), onChanged: (i) => setState(() => _tabIndex = i), diff --git a/lib/social/social_view.dart b/lib/social/social_view.dart index 6c460eb1..7df09dc9 100644 --- a/lib/social/social_view.dart +++ b/lib/social/social_view.dart @@ -42,7 +42,7 @@ class _SocialViewState extends ConsumerState { final onRefresh = () => ref.invalidate(socialProvider(widget.id)); return PageScaffold( - bottomBar: BottomBarIconTabs( + bottomBar: BottomNavBar( current: _tab.index, onChanged: (page) { setState(() => _tab = SocialTab.values.elementAt(page)); diff --git a/lib/staff/staff_view.dart b/lib/staff/staff_view.dart index d0013b9a..142f3824 100644 --- a/lib/staff/staff_view.dart +++ b/lib/staff/staff_view.dart @@ -64,7 +64,7 @@ class _StaffViewState extends ConsumerState { final onRefresh = () => ref.invalidate(staffRelationsProvider(widget.id)); return PageScaffold( - bottomBar: BottomBarIconTabs( + bottomBar: BottomNavBar( current: _tab, onChanged: (i) => setState(() => _tab = i), onSame: (_) => _ctrl.scrollToTop(), diff --git a/lib/statistics/statistics_view.dart b/lib/statistics/statistics_view.dart index 704e7dd2..fdbce616 100644 --- a/lib/statistics/statistics_view.dart +++ b/lib/statistics/statistics_view.dart @@ -92,7 +92,7 @@ class _StatisticsViewState extends State { ); return PageScaffold( - bottomBar: BottomBarIconTabs( + bottomBar: BottomNavBar( current: _onAnime ? 0 : 1, onChanged: (page) => setState(() => _onAnime = page == 0 ? true : false), diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index db33942e..2b2873f4 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -299,7 +299,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { ? body : ClipRect( child: BackdropFilter( - filter: Consts.filter, + filter: Consts.blurFilter, child: DecoratedBox( decoration: BoxDecoration(color: theme.bottomAppBarTheme.color), child: body, diff --git a/lib/utils/consts.dart b/lib/utils/consts.dart index 30f6caab..df7c646d 100644 --- a/lib/utils/consts.dart +++ b/lib/utils/consts.dart @@ -14,7 +14,7 @@ abstract class Consts { static const physics = AlwaysScrollableScrollPhysics( parent: BouncingScrollPhysics(), ); - static final filter = ImageFilter.blur(sigmaX: 5, sigmaY: 5); + static final blurFilter = ImageFilter.blur(sigmaX: 5, sigmaY: 5); // Optimal height to width ratio for a media cover. static const coverHtoWRatio = 1.53; diff --git a/lib/utils/theming.dart b/lib/utils/theming.dart index 7fd77cbd..965eadc9 100644 --- a/lib/utils/theming.dart +++ b/lib/utils/theming.dart @@ -40,8 +40,9 @@ ThemeData themeDataFrom(ColorScheme scheme) => ThemeData( color: scheme.onSurfaceVariant, size: Consts.iconBig, ), - bottomAppBarTheme: BottomAppBarTheme( - color: scheme.background.withAlpha(190), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: scheme.background.withAlpha(190), + labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( diff --git a/lib/widgets/layouts/bottom_bar.dart b/lib/widgets/layouts/bottom_bar.dart index 06717647..80a51eef 100644 --- a/lib/widgets/layouts/bottom_bar.dart +++ b/lib/widgets/layouts/bottom_bar.dart @@ -1,36 +1,8 @@ import 'package:flutter/material.dart'; import 'package:otraku/utils/consts.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -/// A bottom app bar implementation that uses a blurred, translucent background. -class BottomBar extends StatelessWidget { - const BottomBar({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - final bottomPadding = MediaQuery.of(context).viewPadding.bottom; - - return ClipRect( - child: BackdropFilter( - filter: Consts.filter, - child: Container( - height: Consts.tapTargetSize + bottomPadding, - padding: EdgeInsets.only(bottom: bottomPadding), - color: Theme.of(context).bottomAppBarTheme.color, - child: Material(color: Colors.transparent, child: child), - ), - ), - ); - } -} - -/// A [BottomBar] implementation with icons for tab switching. If the screen -/// is wide enough, next to each icon will be the name of the tab. -class BottomBarIconTabs extends StatelessWidget { - const BottomBarIconTabs({ +class BottomNavBar extends StatefulWidget { + const BottomNavBar({ required this.current, required this.items, required this.onChanged, @@ -39,97 +11,78 @@ class BottomBarIconTabs extends StatelessWidget { final int current; final Map items; - - /// Called when a new tab is selected. final void Function(int) onChanged; - - /// Called when the currently selected tab is pressed. - /// Usually this toggles special functionality like search. final void Function(int) onSame; @override - Widget build(BuildContext context) { - final width = - MediaQuery.of(context).size.width > items.length * 130 ? 130.0 : 50.0; + State createState() => _BottomNavBarState(); +} - return BottomBar( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - for (int i = 0; i < items.length; i++) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => i != current ? onChanged(i) : onSame(i), - child: SizedBox( - height: double.infinity, - width: width, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - items.values.elementAt(i), - color: i != current - ? Theme.of(context).colorScheme.onSurfaceVariant - : Theme.of(context).colorScheme.primary, - ), - if (width > 50) ...[ - const SizedBox(width: 5), - Text( - items.keys.elementAt(i), - style: i != current - ? Theme.of(context).textTheme.labelMedium - : Theme.of(context).textTheme.bodyLarge, - ), - ], - ], - ), - ), - ), - ], +class _BottomNavBarState extends State { + late int _selected = widget.current; + + @override + void didUpdateWidget(covariant BottomNavBar oldWidget) { + super.didUpdateWidget(oldWidget); + _selected = widget.current; + } + + @override + Widget build(BuildContext context) { + return ClipRect( + child: BackdropFilter( + filter: Consts.blurFilter, + child: NavigationBar( + height: Consts.tapTargetSize, + selectedIndex: _selected, + onDestinationSelected: (i) { + if (_selected == i) { + widget.onSame(i); + } else { + _selected = i; + widget.onChanged(_selected); + } + }, + destinations: [ + for (final t in widget.items.entries) + NavigationDestination(label: t.key, icon: Icon(t.value)) + ], + ), ), ); } } -/// A [BottomBar] implementation with 2 buttons. If [primary] is `null`, -/// there will be a [Loader] in its place. If [secondary] is `null`, -/// there won't be anything in its place. -class BottomBarDualButtonRow extends StatelessWidget { - const BottomBarDualButtonRow({ - required this.primary, - required this.secondary, - }); +class BottomBar extends StatelessWidget { + const BottomBar(this.items); - final BottomBarButton? primary; - final BottomBarButton? secondary; + final List items; @override Widget build(BuildContext context) { - final primary = this.primary != null - ? this.primary! - : const Expanded(child: Center(child: Loader())); - final secondary = this.secondary != null ? this.secondary! : const Spacer(); + final bottomPadding = MediaQuery.of(context).viewPadding.bottom; - return BottomBar( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - children: [ - if (Options().leftHanded) ...[ - primary, - secondary, - ] else ...[ - secondary, - primary, - ], - ], + return ClipRect( + child: BackdropFilter( + filter: Consts.blurFilter, + child: SizedBox( + height: Consts.tapTargetSize + bottomPadding, + child: Material( + elevation: 3, + color: Theme.of(context).navigationBarTheme.backgroundColor, + surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, + shadowColor: Colors.transparent, + child: Padding( + padding: EdgeInsets.only(bottom: bottomPadding), + child: Row(children: items), + ), + ), ), ), ); } } -/// A [TextButton] implementation. class BottomBarButton extends StatelessWidget { const BottomBarButton({ required this.text, @@ -142,18 +95,22 @@ class BottomBarButton extends StatelessWidget { final IconData icon; final void Function() onTap; - // If the icon/text should be in the error colour. + /// If the icon/text should be in the error colour. final bool warning; @override Widget build(BuildContext context) { return Expanded( - child: TextButton.icon( - label: Text(text), - icon: Icon(icon), - onPressed: onTap, - style: TextButton.styleFrom( - foregroundColor: warning ? Theme.of(context).colorScheme.error : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextButton.icon( + label: Text(text), + icon: Icon(icon), + onPressed: onTap, + style: TextButton.styleFrom( + foregroundColor: + warning ? Theme.of(context).colorScheme.error : null, + ), ), ), ); diff --git a/lib/widgets/layouts/top_bar.dart b/lib/widgets/layouts/top_bar.dart index 8ee9cc84..951ac3c4 100644 --- a/lib/widgets/layouts/top_bar.dart +++ b/lib/widgets/layouts/top_bar.dart @@ -21,10 +21,10 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { return ClipRect( child: BackdropFilter( - filter: Consts.filter, + filter: Consts.blurFilter, child: DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).bottomAppBarTheme.color, + color: Theme.of(context).navigationBarTheme.backgroundColor, ), child: Padding( padding: EdgeInsets.only( From a4d3de68f31a8127d93267b1dd8c280c561671e9 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Fri, 5 May 2023 23:27:44 +0300 Subject: [PATCH 25/55] Dependencies and notifications cleanup --- lib/notifications/notification_model.dart | 38 +++++++++++++---------- lib/utils/background_handler.dart | 10 ++---- pubspec.lock | 28 ++++++++--------- pubspec.yaml | 8 ++--- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/notifications/notification_model.dart b/lib/notifications/notification_model.dart index c24af93d..5d6f7818 100644 --- a/lib/notifications/notification_model.dart +++ b/lib/notifications/notification_model.dart @@ -357,21 +357,25 @@ class SiteNotification { } enum NotificationType { - FOLLOWING, - ACTIVITY_MESSAGE, - ACTIVITY_REPLY, - ACTIVITY_REPLY_SUBSCRIBED, - ACTIVITY_MENTION, - ACTIVITY_LIKE, - ACTIVITY_REPLY_LIKE, - THREAD_COMMENT_REPLY, - THREAD_COMMENT_MENTION, - THREAD_SUBSCRIBED, - THREAD_LIKE, - THREAD_COMMENT_LIKE, - RELATED_MEDIA_ADDITION, - MEDIA_DATA_CHANGE, - MEDIA_MERGE, - MEDIA_DELETION, - AIRING, + FOLLOWING('Follows'), + ACTIVITY_MESSAGE('Messages'), + ACTIVITY_REPLY('Activity replies'), + ACTIVITY_REPLY_SUBSCRIBED('Subscribed activity replies'), + ACTIVITY_MENTION('Activity mentions'), + ACTIVITY_LIKE('Activity likes'), + ACTIVITY_REPLY_LIKE('Activity reply likes'), + THREAD_COMMENT_REPLY('Thread comments'), + THREAD_COMMENT_MENTION('Thread mentions'), + THREAD_SUBSCRIBED('Subscribed thread replies'), + THREAD_LIKE('Thread likes'), + THREAD_COMMENT_LIKE('Thread comment likes'), + RELATED_MEDIA_ADDITION('Related media additions'), + MEDIA_DATA_CHANGE('Media changes'), + MEDIA_MERGE('Media merges'), + MEDIA_DELETION('Media deletions'), + AIRING('Episode releases'); + + const NotificationType(this.text); + + final String text; } diff --git a/lib/utils/background_handler.dart b/lib/utils/background_handler.dart index 3007fa49..033dd358 100644 --- a/lib/utils/background_handler.dart +++ b/lib/utils/background_handler.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:otraku/notifications/notification_model.dart'; import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/convert.dart'; import 'package:otraku/utils/graphql.dart'; import 'package:otraku/utils/options.dart'; import 'package:otraku/utils/route_arg.dart'; @@ -259,18 +258,15 @@ void _fetch() => Workmanager().executeTask((_, __) async { }); void _show(SiteNotification notification, String title, String payload) { - final id = notification.type.name; - final name = Convert.clarifyEnum(id)!; - _notificationPlugin.show( notification.id, title, notification.texts.join(), NotificationDetails( android: AndroidNotificationDetails( - id, - name, - channelDescription: name, + notification.type.name, + notification.type.text, + channelDescription: notification.type.text, color: const Color(0xFF45A0F2), ), ), diff --git a/pubspec.lock b/pubspec.lock index 281689fb..8114ae0d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "8546a9b9510e1a260b8d55fb2d07096e8a8552c6a2c2bf529100344894b2b41a" + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -210,34 +210,34 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + sha256: "2876372952b65ca7f684e698eba22bda1cf581fa071dd30ba2f01900f507d0d1" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.0.0+1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + sha256: "909bb95de05a2e793503a2437146285a2f600cd0b3f826e26b870a334d8586d7" url: "https://pub.dev" source: hosted - version: "3.0.0+1" + version: "4.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "63235c42de5b6c99846969a27ad0209c401e6b77b0498939813725b5791c107c" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "812dfbb87af51e73e68ea038bcfd1c732078d6838d3388d03283db7dec0d1e5f" + sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" flutter_secure_storage: dependency: "direct main" description: @@ -332,10 +332,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_parser: dependency: transitive description: @@ -524,10 +524,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "77ab3bcd084bb19fa8717a526217787c725d7f5be938404c7839cd760fdf6ae5" + sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2e472b6c..9d69b7d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,10 +13,10 @@ dependencies: sdk: flutter # State management. - flutter_riverpod: ^2.3.4 + flutter_riverpod: ^2.3.6 # Data fetching. - http: ^0.13.5 + http: ^0.13.6 # Settings storage. hive: ^2.2.3 @@ -46,7 +46,7 @@ dependencies: workmanager: ^0.5.1 # Sending device notifications. - flutter_local_notifications: ^13.0.0 + flutter_local_notifications: ^14.0.0+1 # Translating html into flutter widgets. flutter_widget_from_html_core: ^0.10.0 @@ -58,7 +58,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_launcher_icons: ^0.13.0 + flutter_launcher_icons: ^0.13.1 flutter_lints: ^2.0.1 flutter_icons: From b69b539ccc21b75f6875b0eb27eb9a52e97800b1 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 6 May 2023 22:59:48 +0300 Subject: [PATCH 26/55] Viewer activities in the main feed can be toggled --- lib/activity/activities_providers.dart | 51 ++++++++++++++--------- lib/activity/activities_view.dart | 56 +++++++++++++++++--------- lib/activity/activity_models.dart | 13 ++++-- lib/utils/options.dart | 10 +++++ 4 files changed, 87 insertions(+), 43 deletions(-) diff --git a/lib/activity/activities_providers.dart b/lib/activity/activities_providers.dart index 1ee6ca9d..5c6f38a6 100644 --- a/lib/activity/activities_providers.dart +++ b/lib/activity/activities_providers.dart @@ -21,17 +21,20 @@ final activityFilterProvider = StateNotifierProvider.autoDispose .family( (ref, userId) { var typeIn = ActivityType.values; - bool? onFollowing; + FeedFilter? feedFilter; if (userId == null) { - onFollowing = Options().feedOnFollowing; + feedFilter = FeedFilter( + Options().feedOnFollowing, + Options().viewerActivitiesInFeed, + ); typeIn = Options() .feedActivityFilters .map((e) => ActivityType.values.elementAt(e)) .toList(); } - return ActivityFilterNotifier(typeIn, onFollowing); + return ActivityFilterNotifier(typeIn, feedFilter); }, ); @@ -61,14 +64,11 @@ class ActivitiesNotifier extends StateNotifier>> { final data = await Api.get(GqlQuery.activities, { 'typeIn': filter.typeIn.map((t) => t.name).toList(), - if (userId != null) ...{ - 'userId': userId, - } else ...{ - 'isFollowing': filter.onFollowing, - if ((filter.onFollowing ?? false)) - 'userIdNot': viewerId - else - 'hasRepliesOrText': true, + if (userId != null) 'userId': userId, + if (filter.feedFilter != null) ...{ + 'isFollowing': filter.feedFilter!.onFollowing, + if (!filter.feedFilter!.withViewerActivities) 'userIdNot': viewerId, + if (!filter.feedFilter!.onFollowing) 'hasRepliesOrText': true, }, if (_lastCreatedAt != null) 'createdBefore': _lastCreatedAt! }); @@ -187,16 +187,27 @@ class ActivitiesNotifier extends StateNotifier>> { } class ActivityFilterNotifier extends StateNotifier { - ActivityFilterNotifier(List typeIn, bool? onFollowing) - : super(ActivityFilter(typeIn, onFollowing)); - - void update(List typeIn, bool? onFollowing) { - state = state.onFollowing == null + ActivityFilterNotifier(List typeIn, FeedFilter? feedFilter) + : super(ActivityFilter(typeIn, feedFilter)); + + void update( + List typeIn, + bool? onFollowing, + bool? withViewerActivities, + ) { + state = state.feedFilter == null ? ActivityFilter(typeIn, null) - : ActivityFilter(typeIn, onFollowing ?? state.onFollowing); - - if (onFollowing == null) return; + : ActivityFilter( + typeIn, + FeedFilter( + onFollowing ?? state.feedFilter!.onFollowing, + withViewerActivities ?? state.feedFilter!.withViewerActivities, + ), + ); + + if (state.feedFilter == null) return; Options().feedActivityFilters = typeIn.map((e) => e.index).toList(); - Options().feedOnFollowing = onFollowing; + Options().feedOnFollowing = state.feedFilter!.onFollowing; + Options().viewerActivitiesInFeed = state.feedFilter!.withViewerActivities; } } diff --git a/lib/activity/activities_view.dart b/lib/activity/activities_view.dart index 2ad25617..9a4a21b6 100644 --- a/lib/activity/activities_view.dart +++ b/lib/activity/activities_view.dart @@ -22,11 +22,19 @@ import 'package:otraku/widgets/paged_view.dart'; void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { final filter = ref.read(activityFilterProvider(id)); final typeIn = [...filter.typeIn]; - bool? onFollowing = filter.onFollowing; bool changed = false; - double initialHeight = Consts.tapTargetSize * ActivityType.values.length + 20; - if (onFollowing != null) initialHeight += Consts.tapTargetSize; + bool? onFollowing; + bool? withViewerActivities; + if (filter.feedFilter != null) { + onFollowing = filter.feedFilter!.onFollowing; + withViewerActivities = filter.feedFilter!.withViewerActivities; + } + + double initialHeight = MediaQuery.of(context).padding.bottom + + Consts.tapTargetSize * ActivityType.values.length + + 20; + if (onFollowing != null) initialHeight += Consts.tapTargetSize * 1.5; showSheet( context, @@ -37,22 +45,25 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { physics: Consts.physics, padding: Consts.padding, children: [ - ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - for (final a in ActivityType.values) - CheckBoxField( - title: a.text, - initial: typeIn.contains(a), - onChanged: (val) { - val ? typeIn.add(a) : typeIn.remove(a); - changed = true; - }, - ) - ], - ), - if (onFollowing != null) + for (final a in ActivityType.values) + CheckBoxField( + title: a.text, + initial: typeIn.contains(a), + onChanged: (val) { + val ? typeIn.add(a) : typeIn.remove(a); + changed = true; + }, + ), + if (onFollowing != null) ...[ + const Divider(), + CheckBoxField( + title: 'Your Activities', + initial: withViewerActivities!, + onChanged: (val) { + withViewerActivities = val; + changed = true; + }, + ), SegmentSwitcher( items: const ['Following', 'Global'], current: onFollowing! ? 0 : 1, @@ -61,12 +72,17 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { changed = true; }, ), + ], ], ), ), ).then((_) { if (changed) { - ref.read(activityFilterProvider(id).notifier).update(typeIn, onFollowing); + ref.read(activityFilterProvider(id).notifier).update( + typeIn, + onFollowing, + withViewerActivities, + ); } }); } diff --git a/lib/activity/activity_models.dart b/lib/activity/activity_models.dart index ec181a21..14ed7985 100644 --- a/lib/activity/activity_models.dart +++ b/lib/activity/activity_models.dart @@ -205,10 +205,17 @@ enum ActivityType { } class ActivityFilter { - const ActivityFilter(this.typeIn, this.onFollowing); + const ActivityFilter(this.typeIn, this.feedFilter); final List typeIn; - /// Not `null` only for the main feed. Switches between following/global. - final bool? onFollowing; + /// Not `null` only for the main feed. + final FeedFilter? feedFilter; +} + +class FeedFilter { + const FeedFilter(this.onFollowing, this.withViewerActivities); + + final bool onFollowing; + final bool withViewerActivities; } diff --git a/lib/utils/options.dart b/lib/utils/options.dart index c049d77a..e3d4113b 100644 --- a/lib/utils/options.dart +++ b/lib/utils/options.dart @@ -27,6 +27,7 @@ enum _OptionKey { leftHanded, analogueClock, feedOnFollowing, + viewerActivitiesInFeed, discoverItemView, collectionItemView, collectionPreviewItemView, @@ -77,6 +78,7 @@ class Options extends ChangeNotifier { this._leftHanded, this._analogueClock, this._feedOnFollowing, + this._viewerActivitiesInFeed, this._discoverItemView, this._collectionItemView, this._collectionPreviewItemView, @@ -156,6 +158,7 @@ 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.viewerActivitiesInFeed.name) ?? false, discoverItemView, collectionItemView, collectionPreviewItemView, @@ -227,6 +230,7 @@ class Options extends ChangeNotifier { bool _leftHanded; bool _analogueClock; bool _feedOnFollowing; + bool _viewerActivitiesInFeed; int _discoverItemView; int _collectionItemView; int _collectionPreviewItemView; @@ -253,6 +257,7 @@ class Options extends ChangeNotifier { bool get leftHanded => _leftHanded; bool get analogueClock => _analogueClock; bool get feedOnFollowing => _feedOnFollowing; + bool get viewerActivitiesInFeed => _viewerActivitiesInFeed; int get discoverItemView => _discoverItemView; int get collectionItemView => _collectionItemView; int get collectionPreviewItemView => _collectionPreviewItemView; @@ -397,6 +402,11 @@ class Options extends ChangeNotifier { _optionBox.put(_OptionKey.feedOnFollowing.name, v); } + set viewerActivitiesInFeed(bool v) { + _viewerActivitiesInFeed = v; + _optionBox.put(_OptionKey.viewerActivitiesInFeed.name, v); + } + set discoverItemView(int v) { _discoverItemView = v; _optionBox.put(_OptionKey.discoverItemView.name, v); From f1a307e53642691a2a9f2273ee578ce06fd7627e Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sun, 7 May 2023 16:09:29 +0300 Subject: [PATCH 27/55] Reorganised sheets --- lib/activity/activity_card.dart | 10 +- lib/activity/reply_card.dart | 83 +++++---- lib/character/character_action_buttons.dart | 27 ++- lib/collection/collection_view.dart | 19 ++- lib/discover/discover_view.dart | 73 +++----- lib/filter/filter_view.dart | 6 +- lib/media/media_action_buttons.dart | 26 ++- lib/media/media_header.dart | 2 +- lib/notifications/notifications_view.dart | 30 ++-- lib/review/review_header.dart | 2 +- lib/review/reviews_view.dart | 33 ++-- lib/user/user_header.dart | 2 +- lib/widgets/overlays/sheets.dart | 180 +++++++------------- 13 files changed, 194 insertions(+), 299 deletions(-) diff --git a/lib/activity/activity_card.dart b/lib/activity/activity_card.dart index aa0767e5..5c073896 100644 --- a/lib/activity/activity_card.dart +++ b/lib/activity/activity_card.dart @@ -329,12 +329,12 @@ class _ActivityFooterState extends State { context, Consumer( builder: (context, ref, __) => - FixedGradientDragSheet.link(context, activity.siteUrl!, [ + GradientSheet.link(context, activity.siteUrl!, [ if (activity.isOwned) ...[ if (activity.type == ActivityType.TEXT || activity.type == ActivityType.MESSAGE && activity.agent.id == Options().id) - FixedGradientSheetTile( + GradientSheetButton( text: 'Edit', icon: Icons.edit_outlined, onTap: () => showSheet( @@ -351,7 +351,7 @@ class _ActivityFooterState extends State { ), ), ), - FixedGradientSheetTile( + GradientSheetButton( text: 'Delete', icon: Ionicons.trash_outline, onTap: () => showPopUp( @@ -380,7 +380,7 @@ class _ActivityFooterState extends State { if (widget.onPinned != null && activity.isOwned && activity.type != ActivityType.MESSAGE) - FixedGradientSheetTile( + GradientSheetButton( text: activity.isPinned ? 'Unpin' : 'Pin', icon: activity.isPinned ? Icons.push_pin : Icons.push_pin_outlined, @@ -405,7 +405,7 @@ class _ActivityFooterState extends State { }); }, ), - FixedGradientSheetTile( + GradientSheetButton( text: !activity.isSubscribed ? 'Subscribe' : 'Unsubscribe', icon: !activity.isSubscribed ? Ionicons.notifications_outline diff --git a/lib/activity/reply_card.dart b/lib/activity/reply_card.dart index 13fed623..ac342e3f 100644 --- a/lib/activity/reply_card.dart +++ b/lib/activity/reply_card.dart @@ -98,53 +98,50 @@ class ReplyCard extends StatelessWidget { void _showMoreSheet(BuildContext context, WidgetRef ref) { showSheet( context, - FixedGradientDragSheet( - children: [ - FixedGradientSheetTile( - text: 'Edit', - icon: Icons.edit_outlined, - onTap: () => showSheet( - context, - CompositionView( - composition: - Composition.reply(reply.id, reply.text, activityId), - onDone: (map) => ref - .read(activityProvider(activityId).notifier) - .replaceReply(map), - ), + GradientSheet([ + GradientSheetButton( + text: 'Edit', + icon: Icons.edit_outlined, + onTap: () => showSheet( + context, + CompositionView( + composition: Composition.reply(reply.id, reply.text, activityId), + onDone: (map) => ref + .read(activityProvider(activityId).notifier) + .replaceReply(map), ), ), - FixedGradientSheetTile( - text: 'Delete', - icon: Ionicons.trash_outline, - onTap: () => showPopUp( - context, - ConfirmationDialog( - title: 'Delete?', - mainAction: 'Yes', - secondaryAction: 'No', - onConfirm: () { - deleteActivityReply(reply.id).then((err) { - if (err == null) { - ref - .read(activityProvider(activityId).notifier) - .removeReply(reply.id); - } else { - showPopUp( - context, - ConfirmationDialog( - title: 'Could not delete reply', - content: err.toString(), - ), - ); - } - }); - }, - ), + ), + GradientSheetButton( + text: 'Delete', + icon: Ionicons.trash_outline, + onTap: () => showPopUp( + context, + ConfirmationDialog( + title: 'Delete?', + mainAction: 'Yes', + secondaryAction: 'No', + onConfirm: () { + deleteActivityReply(reply.id).then((err) { + if (err == null) { + ref + .read(activityProvider(activityId).notifier) + .removeReply(reply.id); + } else { + showPopUp( + context, + ConfirmationDialog( + title: 'Could not delete reply', + content: err.toString(), + ), + ); + } + }); + }, ), ), - ], - ), + ), + ]), ); } } diff --git a/lib/character/character_action_buttons.dart b/lib/character/character_action_buttons.dart index 791e67ae..674801d6 100644 --- a/lib/character/character_action_buttons.dart +++ b/lib/character/character_action_buttons.dart @@ -131,22 +131,17 @@ class CharacterLanguageSelectionButton extends StatelessWidget { showSheet( context, - DynamicGradientDragSheet( - onTap: (i) => ref - .read(characterMediaProvider(id).notifier) - .changeLanguage(languages.elementAt(i)), - children: [ - for (int i = 0; i < languages.length; i++) - Text( - languages.elementAt(i), - style: languages.elementAt(i) != language - ? Theme.of(context).textTheme.titleLarge - : Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), + GradientSheet([ + for (int i = 0; i < languages.length; i++) ...[ + GradientSheetButton( + text: languages.elementAt(i), + selected: languages.elementAt(i) == language, + onTap: () => ref + .read(characterMediaProvider(id).notifier) + .changeLanguage(languages.elementAt(i)), + ), + ] + ]), ); }, ); diff --git a/lib/collection/collection_view.dart b/lib/collection/collection_view.dart index aae135e5..cfaa928e 100644 --- a/lib/collection/collection_view.dart +++ b/lib/collection/collection_view.dart @@ -189,11 +189,16 @@ class _ActionButton extends StatelessWidget { showSheet( context, - DynamicGradientDragSheet( - onTap: (i) => notifier.index = i, - children: [ - for (int i = 0; i < notifier.lists.length; i++) - Row( + GradientSheet([ + for (int i = 0; i < notifier.lists.length; i++) + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.pop(context); + notifier.index = i; + }, + child: Row( + mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( @@ -213,8 +218,8 @@ class _ActionButton extends StatelessWidget { ), ], ), - ], - ), + ), + ]), ); }, onSwipe: (goRight) { diff --git a/lib/discover/discover_view.dart b/lib/discover/discover_view.dart index 30983700..96d97112 100644 --- a/lib/discover/discover_view.dart +++ b/lib/discover/discover_view.dart @@ -107,29 +107,21 @@ class _TopBarContent extends StatelessWidget { tooltip: 'Sort', icon: Ionicons.funnel_outline, onTap: () { - final notifier = - ref.read(reviewSortProvider(null).notifier); - final theme = Theme.of(context); + final index = + ref.read(reviewSortProvider(null).notifier).state.index; 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, - ), - ), - ], - ), + GradientSheet([ + for (int i = 0; i < ReviewSort.values.length; i++) + GradientSheetButton( + text: ReviewSort.values.elementAt(i).text, + selected: index == i, + onTap: () => ref + .read(reviewSortProvider(null).notifier) + .state = ReviewSort.values.elementAt(i), + ), + ]), ); }, ) @@ -156,39 +148,18 @@ class _ActionButton extends StatelessWidget { tooltip: 'Types', icon: type.icon, onTap: () { - final theme = Theme.of(context); - showSheet( context, - DynamicGradientDragSheet( - onTap: (i) { - ref.read(discoverFilterProvider).type = - DiscoverType.values[i]; - }, - children: [ - for (int i = 0; i < DiscoverType.values.length; i++) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - DiscoverType.values[i].icon, - color: i != type.index - ? Theme.of(context).colorScheme.onBackground - : Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 5), - Text( - Convert.clarifyEnum(DiscoverType.values[i].name)!, - style: i != type.index - ? theme.textTheme.titleLarge - : theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.primary, - ), - ), - ], - ), - ], - ), + GradientSheet([ + for (int i = 0; i < DiscoverType.values.length; i++) + GradientSheetButton( + text: Convert.clarifyEnum(DiscoverType.values[i].name)!, + icon: DiscoverType.values[i].icon, + selected: type.index == i, + onTap: () => ref.read(discoverFilterProvider).type = + DiscoverType.values[i], + ), + ]), ); }, onSwipe: (goRight) { diff --git a/lib/filter/filter_view.dart b/lib/filter/filter_view.dart index 5f44a600..7e3e8cf8 100644 --- a/lib/filter/filter_view.dart +++ b/lib/filter/filter_view.dart @@ -376,11 +376,11 @@ class TagSheetBodyState extends ConsumerState { children: [ if (_itemIndices.isNotEmpty) ListView.builder( - padding: const EdgeInsets.only( + padding: EdgeInsets.only( + top: 90, left: 20, right: 20, - bottom: 10, - top: 90, + bottom: MediaQuery.of(context).padding.bottom, ), controller: widget.scrollCtrl, itemExtent: Consts.tapTargetSize, diff --git a/lib/media/media_action_buttons.dart b/lib/media/media_action_buttons.dart index c18d57d2..62c2fa61 100644 --- a/lib/media/media_action_buttons.dart +++ b/lib/media/media_action_buttons.dart @@ -116,22 +116,16 @@ class _MediaLanguageButtonState extends State { showSheet( context, - DynamicGradientDragSheet( - onTap: (i) => ref - .read(mediaRelationsProvider(widget.id).notifier) - .changeLanguage(languages.elementAt(i)), - children: [ - for (int i = 0; i < languages.length; i++) - Text( - languages.elementAt(i), - style: languages.elementAt(i) != language - ? Theme.of(context).textTheme.titleLarge - : Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), + GradientSheet([ + for (int i = 0; i < languages.length; i++) + GradientSheetButton( + text: languages.elementAt(i), + selected: languages.elementAt(i) == language, + onTap: () => ref + .read(mediaRelationsProvider(widget.id).notifier) + .changeLanguage(languages.elementAt(i)), + ), + ]), ); }, ); diff --git a/lib/media/media_header.dart b/lib/media/media_header.dart index 7cb07deb..e8348f51 100644 --- a/lib/media/media_header.dart +++ b/lib/media/media_header.dart @@ -199,7 +199,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { icon: Ionicons.ellipsis_horizontal, onTap: () => showSheet( context, - FixedGradientDragSheet.link(context, info!.siteUrl!), + GradientSheet.link(context, info!.siteUrl!), ), ), ], diff --git a/lib/notifications/notifications_view.dart b/lib/notifications/notifications_view.dart index b451f383..21a8d794 100644 --- a/lib/notifications/notifications_view.dart +++ b/lib/notifications/notifications_view.dart @@ -95,29 +95,19 @@ class _NotificationsViewState extends ConsumerState { context, Consumer( builder: (context, ref, _) { - final theme = Theme.of(context); final index = ref.read(notificationFilterProvider.notifier).state.index; - final tiles = []; - for (int i = 0; i < NotificationFilterType.values.length; i++) { - tiles.add(Text( - NotificationFilterType.values.elementAt(i).text, - style: i != index - ? theme.textTheme.titleLarge - : theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.primary, - ), - )); - } - - return DynamicGradientDragSheet( - children: tiles, - onTap: (i) { - final notifier = ref.read(notificationFilterProvider.notifier); - notifier.state = NotificationFilterType.values.elementAt(i); - }, - ); + return GradientSheet([ + for (int i = 0; i < NotificationFilterType.values.length; i++) + GradientSheetButton( + text: NotificationFilterType.values.elementAt(i).text, + selected: index == i, + onTap: () => ref + .read(notificationFilterProvider.notifier) + .state = NotificationFilterType.values.elementAt(i), + ), + ]); }, ), ); diff --git a/lib/review/review_header.dart b/lib/review/review_header.dart index 349e8e8d..72d7fed0 100644 --- a/lib/review/review_header.dart +++ b/lib/review/review_header.dart @@ -168,7 +168,7 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate { icon: Ionicons.ellipsis_horizontal, onTap: () => showSheet( context, - FixedGradientDragSheet.link(context, siteUrl!), + GradientSheet.link(context, siteUrl!), ), ), ], diff --git a/lib/review/reviews_view.dart b/lib/review/reviews_view.dart index a8505648..51e46725 100644 --- a/lib/review/reviews_view.dart +++ b/lib/review/reviews_view.dart @@ -59,28 +59,23 @@ class _ReviewsViewState extends ConsumerState { tooltip: 'Sort', icon: Ionicons.funnel_outline, onTap: () { - final theme = Theme.of(context); - final notifier = - ref.read(reviewSortProvider(widget.id).notifier); + final index = ref + .read(reviewSortProvider(widget.id).notifier) + .state + .index; 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), - ), - ], - ), + GradientSheet([ + for (int i = 0; i < ReviewSort.values.length; i++) + GradientSheetButton( + text: ReviewSort.values.elementAt(i).text, + selected: index == i, + onTap: () => ref + .read(reviewSortProvider(widget.id).notifier) + .state = ReviewSort.values.elementAt(i), + ), + ]), ); }, ), diff --git a/lib/user/user_header.dart b/lib/user/user_header.dart index 2b2873f4..c22b358d 100644 --- a/lib/user/user_header.dart +++ b/lib/user/user_header.dart @@ -185,7 +185,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { icon: Ionicons.ellipsis_horizontal, onTap: () => showSheet( context, - FixedGradientDragSheet.link(context, user!.siteUrl!), + GradientSheet.link(context, user!.siteUrl!), ), ), if (isViewer) diff --git a/lib/widgets/overlays/sheets.dart b/lib/widgets/overlays/sheets.dart index b252c77a..d47b0bdf 100644 --- a/lib/widgets/overlays/sheets.dart +++ b/lib/widgets/overlays/sheets.dart @@ -30,29 +30,25 @@ class OpaqueSheet extends StatelessWidget { : 0.5; if (initialSize > 0.9) initialSize = 0.9; - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: DraggableScrollableSheet( - expand: false, - maxChildSize: 0.9, - initialChildSize: initialSize, - minChildSize: initialSize < 0.25 ? initialSize : 0.25, - builder: (context, scrollCtrl) { - sheet ??= Center( - child: Container( - constraints: const BoxConstraints(maxWidth: Consts.layoutMedium), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: - const BorderRadius.vertical(top: Consts.radiusMax), - ), - child: builder(context, scrollCtrl), + return DraggableScrollableSheet( + expand: false, + maxChildSize: 0.9, + initialChildSize: initialSize, + minChildSize: initialSize < 0.25 ? initialSize : 0.25, + builder: (context, scrollCtrl) { + sheet ??= Center( + child: Container( + constraints: const BoxConstraints(maxWidth: Consts.layoutMedium), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: const BorderRadius.vertical(top: Consts.radiusMax), ), - ); + child: builder(context, scrollCtrl), + ), + ); - return sheet!; - }, - ), + return sheet!; + }, ); } } @@ -101,13 +97,29 @@ class OpaqueSheetView extends StatelessWidget { ); } -/// An implementation of [DraggableScrollableSheet] with -/// gradient background that builds its children dynamically. -class DynamicGradientDragSheet extends StatelessWidget { - const DynamicGradientDragSheet({required this.children, required this.onTap}); +class GradientSheet extends StatelessWidget { + const GradientSheet(this.children); + + factory GradientSheet.link( + BuildContext context, + String link, [ + List children = const [], + ]) => + GradientSheet([ + ...children, + GradientSheetButton( + text: 'Copy Link', + icon: Ionicons.clipboard_outline, + onTap: () => Toast.copy(context, link), + ), + GradientSheetButton( + text: 'Open in Browser', + icon: Ionicons.link_outline, + onTap: () => Toast.launch(context, link), + ), + ]); final List children; - final void Function(int) onTap; @override Widget build(BuildContext context) { @@ -140,7 +152,7 @@ class DynamicGradientDragSheet extends StatelessWidget { ), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: Consts.layoutMedium), - child: ListView.builder( + child: ListView( controller: scrollCtrl, padding: EdgeInsets.only( top: 50, @@ -148,87 +160,6 @@ class DynamicGradientDragSheet extends StatelessWidget { right: 10, bottom: MediaQuery.of(context).padding.bottom, ), - itemCount: children.length, - itemExtent: Consts.tapTargetSize, - itemBuilder: (context, i) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: children[i], - onTap: () { - onTap(i); - Navigator.pop(context); - }, - ), - ), - ), - ), - ); - } -} - -/// An implementation of [DraggableScrollableSheet] -/// with gradient background and fixed children. -class FixedGradientDragSheet extends StatelessWidget { - const FixedGradientDragSheet({required this.children}); - - // A version with the given buttons, along with copy/open link buttons. - factory FixedGradientDragSheet.link( - BuildContext context, - String link, [ - List children = const [], - ]) => - FixedGradientDragSheet( - children: [ - ...children, - FixedGradientSheetTile( - text: 'Copy Link', - icon: Ionicons.clipboard_outline, - onTap: () => Toast.copy(context, link), - ), - FixedGradientSheetTile( - text: 'Open in Browser', - icon: Ionicons.link_outline, - onTap: () => Toast.launch(context, link), - ), - ], - ); - - final List children; - - @override - Widget build(BuildContext context) { - final requiredHeight = children.length * Consts.tapTargetSize + 60; - double height = requiredHeight / MediaQuery.of(context).size.height; - if (height > 0.9) height = 0.9; - - return DraggableScrollableSheet( - expand: false, - initialChildSize: height, - minChildSize: height < 0.25 ? height : 0.25, - builder: (context, scrollCtrl) => Container( - alignment: Alignment.bottomCenter, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: const [0, 0.6, 0.9, 1], - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(200), - Theme.of(context).colorScheme.background.withAlpha(150), - Theme.of(context).colorScheme.background.withAlpha(0), - ], - ), - ), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: Consts.layoutMedium), - child: ListView( - controller: scrollCtrl, - padding: const EdgeInsets.only( - top: 50, - bottom: 10, - left: 10, - right: 10, - ), itemExtent: Consts.tapTargetSize, children: children, ), @@ -238,20 +169,23 @@ class FixedGradientDragSheet extends StatelessWidget { } } -/// Sometimes used by [FixedGradientDragSheet]. -class FixedGradientSheetTile extends StatelessWidget { - const FixedGradientSheetTile({ +class GradientSheetButton extends StatelessWidget { + const GradientSheetButton({ required this.text, required this.onTap, - required this.icon, + this.selected = false, + this.icon, }); final String text; - final IconData icon; + final IconData? icon; final void Function() onTap; + final bool selected; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { @@ -261,9 +195,23 @@ class FixedGradientSheetTile extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, color: Theme.of(context).colorScheme.onBackground), - const SizedBox(width: 10), - Text(text, style: Theme.of(context).textTheme.titleLarge), + if (icon != null) ...[ + Icon( + icon, + color: selected + ? theme.colorScheme.primary + : theme.colorScheme.onBackground, + ), + const SizedBox(width: 10), + ], + Text( + text, + style: selected + ? theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.primary, + ) + : theme.textTheme.titleLarge, + ), ], ), ); From bf4c7cb043c16f359b4b3dd0e57c18ab56fed7c6 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sun, 7 May 2023 16:46:51 +0300 Subject: [PATCH 28/55] Restructured project --- lib/common/{ => models}/paged.dart | 0 lib/common/{ => models}/relation.dart | 2 +- lib/common/{ => models}/tile_item.dart | 2 +- lib/{ => common}/utils/api.dart | 4 +- .../utils/background_handler.dart | 12 ++--- lib/{ => common}/utils/consts.dart | 0 lib/{ => common}/utils/convert.dart | 4 +- lib/{ => common}/utils/graphql.dart | 0 lib/{ => common}/utils/options.dart | 6 +-- lib/{ => common}/utils/paged_controller.dart | 0 lib/{ => common}/utils/route_arg.dart | 34 +++++++------- lib/{ => common}/utils/theming.dart | 2 +- lib/{ => common}/widgets/cached_image.dart | 2 +- lib/{ => common}/widgets/drag_detector.dart | 0 .../widgets/fields/checkbox_field.dart | 2 +- .../widgets/fields/date_field.dart | 0 .../widgets/fields/drop_down_field.dart | 4 +- .../widgets/fields/growable_text_field.dart | 2 +- .../widgets/fields/labeled_field.dart | 0 .../widgets/fields/number_field.dart | 0 .../widgets/fields/search_field.dart | 2 +- .../widgets/grids/chip_grids.dart | 12 ++--- .../widgets/grids/relation_grid.dart | 10 ++-- .../widgets/grids/sliver_grid_delegates.dart | 0 .../widgets/grids/tile_item_grid.dart | 10 ++-- lib/{ => common}/widgets/html_content.dart | 8 ++-- .../widgets/layouts/bottom_bar.dart | 2 +- .../widgets/layouts/constrained_view.dart | 2 +- .../widgets/layouts/direct_page_view.dart | 0 .../widgets/layouts/floating_bar.dart | 8 ++-- .../widgets/layouts/scaffolds.dart | 6 +-- .../widgets/layouts/segment_switcher.dart | 2 +- lib/{ => common}/widgets/layouts/top_bar.dart | 2 +- lib/{ => common}/widgets/link_tile.dart | 10 ++-- .../widgets/loaders.dart/loaders.dart | 6 +-- .../widgets/loaders.dart/shimmer.dart | 0 .../widgets/overlays/dialogs.dart | 2 +- lib/{ => common}/widgets/overlays/sheets.dart | 4 +- lib/{ => common}/widgets/overlays/toast.dart | 2 +- lib/{ => common}/widgets/paged_view.dart | 10 ++-- lib/{ => common}/widgets/text_rail.dart | 0 lib/main.dart | 10 ++-- .../activity/activities_providers.dart | 12 ++--- .../activity/activities_view.dart | 34 +++++++------- lib/{ => modules}/activity/activity_card.dart | 24 +++++----- .../activity/activity_models.dart | 6 +-- .../activity/activity_provider.dart | 10 ++-- lib/{ => modules}/activity/activity_view.dart | 36 +++++++-------- lib/{ => modules}/activity/reply_card.dart | 24 +++++----- lib/{ => modules}/auth/auth_view.dart | 16 +++---- .../character/character_action_buttons.dart | 16 +++---- .../character/character_info_tab.dart | 18 ++++---- .../character/character_models.dart | 12 ++--- .../character/character_providers.dart | 16 +++---- .../character/character_view.dart | 26 +++++------ .../collection/collection_grid.dart | 22 ++++----- .../collection/collection_list.dart | 24 +++++----- .../collection/collection_models.dart | 6 +-- .../collection_preview_provider.dart | 10 ++-- .../collection/collection_preview_view.dart | 28 +++++------ .../collection/collection_providers.dart | 12 ++--- .../collection/collection_view.dart | 34 +++++++------- .../composition/composition_model.dart | 6 +-- .../composition/composition_view.dart | 20 ++++---- .../discover/discover_media_grid.dart | 12 ++--- .../discover/discover_models.dart | 6 +-- .../discover/discover_providers.dart | 28 +++++------ lib/{ => modules}/discover/discover_view.dart | 46 +++++++++---------- lib/{ => modules}/edit/edit_buttons.dart | 22 ++++----- lib/{ => modules}/edit/edit_model.dart | 6 +-- lib/{ => modules}/edit/edit_providers.dart | 12 ++--- lib/{ => modules}/edit/edit_view.dart | 40 ++++++++-------- lib/{ => modules}/edit/score_field.dart | 6 +-- .../favorites/favorites_model.dart | 6 +-- .../favorites/favorites_provider.dart | 18 ++++---- .../favorites/favorites_view.dart | 24 +++++----- lib/{ => modules}/feed/feed_view.dart | 22 ++++----- lib/{ => modules}/filter/chip_selector.dart | 2 +- lib/{ => modules}/filter/filter_models.dart | 4 +- .../filter/filter_providers.dart | 8 ++-- .../filter/filter_search_field.dart | 8 ++-- lib/{ => modules}/filter/filter_view.dart | 28 +++++------ .../filter/year_range_picker.dart | 0 lib/{ => modules}/home/home_provider.dart | 6 +-- lib/{ => modules}/home/home_view.dart | 46 +++++++++---------- .../media/media_action_buttons.dart | 14 +++--- lib/{ => modules}/media/media_constants.dart | 0 lib/{ => modules}/media/media_grids.dart | 16 +++---- lib/{ => modules}/media/media_header.dart | 22 ++++----- lib/{ => modules}/media/media_info_view.dart | 24 +++++----- lib/{ => modules}/media/media_models.dart | 18 ++++---- lib/{ => modules}/media/media_providers.dart | 18 ++++---- lib/{ => modules}/media/media_view.dart | 32 ++++++------- .../notification}/notification_model.dart | 6 +-- .../notification}/notification_provider.dart | 8 ++-- .../notification}/notifications_view.dart | 38 +++++++-------- lib/{ => modules}/review/review_grid.dart | 12 ++--- lib/{ => modules}/review/review_header.dart | 10 ++-- lib/{ => modules}/review/review_models.dart | 4 +- .../review/review_providers.dart | 8 ++-- lib/{ => modules}/review/review_view.dart | 12 ++--- lib/{ => modules}/review/reviews_view.dart | 18 ++++---- .../settings/settings_about_tab.dart | 12 ++--- .../settings/settings_app_tab.dart | 30 ++++++------ .../settings/settings_content_tab.dart | 20 ++++---- .../settings/settings_notifications_tab.dart | 10 ++-- .../settings/settings_provider.dart | 10 ++-- lib/{ => modules}/settings/settings_view.dart | 30 ++++++------ lib/{ => modules}/settings/theme_preview.dart | 8 ++-- lib/{ => modules}/social/social_model.dart | 4 +- lib/{ => modules}/social/social_provider.dart | 10 ++-- lib/{ => modules}/social/social_view.dart | 20 ++++---- .../staff/staff_action_buttons.dart | 16 +++---- lib/{ => modules}/staff/staff_info_tab.dart | 18 ++++---- lib/{ => modules}/staff/staff_models.dart | 12 ++--- lib/{ => modules}/staff/staff_providers.dart | 16 +++---- lib/{ => modules}/staff/staff_view.dart | 26 +++++------ lib/{ => modules}/statistics/charts.dart | 2 +- .../statistics/statistics_view.dart | 28 +++++------ .../statistics/user_statistics.dart | 2 +- lib/{ => modules}/studio/studio_grid.dart | 8 ++-- lib/{ => modules}/studio/studio_models.dart | 6 +-- .../studio/studio_providers.dart | 14 +++--- lib/{ => modules}/studio/studio_view.dart | 32 ++++++------- lib/{ => modules}/tag/tag_models.dart | 0 lib/{ => modules}/tag/tag_provider.dart | 4 +- lib/{ => modules}/user/user_grid.dart | 12 ++--- lib/{ => modules}/user/user_header.dart | 20 ++++---- lib/{ => modules}/user/user_models.dart | 4 +- lib/{ => modules}/user/user_providers.dart | 6 +-- lib/{ => modules}/user/user_view.dart | 22 ++++----- 131 files changed, 804 insertions(+), 804 deletions(-) rename lib/common/{ => models}/paged.dart (100%) rename lib/common/{ => models}/relation.dart (81%) rename lib/common/{ => models}/tile_item.dart (78%) rename lib/{ => common}/utils/api.dart (97%) rename lib/{ => common}/utils/background_handler.dart (96%) rename lib/{ => common}/utils/consts.dart (100%) rename lib/{ => common}/utils/convert.dart (97%) rename lib/{ => common}/utils/graphql.dart (100%) rename lib/{ => common}/utils/options.dart (98%) rename lib/{ => common}/utils/paged_controller.dart (100%) rename lib/{ => common}/utils/route_arg.dart (80%) rename lib/{ => common}/utils/theming.dart (98%) rename lib/{ => common}/widgets/cached_image.dart (95%) rename lib/{ => common}/widgets/drag_detector.dart (100%) rename lib/{ => common}/widgets/fields/checkbox_field.dart (98%) rename lib/{ => common}/widgets/fields/date_field.dart (100%) rename lib/{ => common}/widgets/fields/drop_down_field.dart (94%) rename lib/{ => common}/widgets/fields/growable_text_field.dart (95%) rename lib/{ => common}/widgets/fields/labeled_field.dart (100%) rename lib/{ => common}/widgets/fields/number_field.dart (100%) rename lib/{ => common}/widgets/fields/search_field.dart (98%) rename lib/{ => common}/widgets/grids/chip_grids.dart (96%) rename lib/{ => common}/widgets/grids/relation_grid.dart (94%) rename lib/{ => common}/widgets/grids/sliver_grid_delegates.dart (100%) rename lib/{ => common}/widgets/grids/tile_item_grid.dart (84%) rename lib/{ => common}/widgets/html_content.dart (88%) rename lib/{ => common}/widgets/layouts/bottom_bar.dart (98%) rename lib/{ => common}/widgets/layouts/constrained_view.dart (91%) rename lib/{ => common}/widgets/layouts/direct_page_view.dart (100%) rename lib/{ => common}/widgets/layouts/floating_bar.dart (97%) rename lib/{ => common}/widgets/layouts/scaffolds.dart (94%) rename lib/{ => common}/widgets/layouts/segment_switcher.dart (98%) rename lib/{ => common}/widgets/layouts/top_bar.dart (98%) rename lib/{ => common}/widgets/link_tile.dart (85%) rename lib/{ => common}/widgets/loaders.dart/loaders.dart (93%) rename lib/{ => common}/widgets/loaders.dart/shimmer.dart (100%) rename lib/{ => common}/widgets/overlays/dialogs.dart (99%) rename lib/{ => common}/widgets/overlays/sheets.dart (98%) rename lib/{ => common}/widgets/overlays/toast.dart (97%) rename lib/{ => common}/widgets/paged_view.dart (87%) rename lib/{ => common}/widgets/text_rail.dart (100%) rename lib/{ => modules}/activity/activities_providers.dart (95%) rename lib/{ => modules}/activity/activities_view.dart (85%) rename lib/{ => modules}/activity/activity_card.dart (94%) rename lib/{ => modules}/activity/activity_models.dart (97%) rename lib/{ => modules}/activity/activity_provider.dart (95%) rename lib/{ => modules}/activity/activity_view.dart (86%) rename lib/{ => modules}/activity/reply_card.dart (89%) rename lib/{ => modules}/auth/auth_view.dart (94%) rename lib/{ => modules}/character/character_action_buttons.dart (90%) rename lib/{ => modules}/character/character_info_tab.dart (92%) rename lib/{ => modules}/character/character_models.dart (92%) rename lib/{ => modules}/character/character_providers.dart (92%) rename lib/{ => modules}/character/character_view.dart (79%) rename lib/{ => modules}/collection/collection_grid.dart (87%) rename lib/{ => modules}/collection/collection_list.dart (93%) rename lib/{ => modules}/collection/collection_models.dart (98%) rename lib/{ => modules}/collection/collection_preview_provider.dart (89%) rename lib/{ => modules}/collection/collection_preview_view.dart (81%) rename lib/{ => modules}/collection/collection_providers.dart (96%) rename lib/{ => modules}/collection/collection_view.dart (89%) rename lib/{ => modules}/composition/composition_model.dart (94%) rename lib/{ => modules}/composition/composition_view.dart (92%) rename lib/{ => modules}/discover/discover_media_grid.dart (92%) rename lib/{ => modules}/discover/discover_models.dart (92%) rename lib/{ => modules}/discover/discover_providers.dart (91%) rename lib/{ => modules}/discover/discover_view.dart (85%) rename lib/{ => modules}/edit/edit_buttons.dart (87%) rename lib/{ => modules}/edit/edit_model.dart (97%) rename lib/{ => modules}/edit/edit_providers.dart (87%) rename lib/{ => modules}/edit/edit_view.dart (92%) rename lib/{ => modules}/edit/score_field.dart (96%) rename lib/{ => modules}/favorites/favorites_model.dart (90%) rename lib/{ => modules}/favorites/favorites_provider.dart (90%) rename lib/{ => modules}/favorites/favorites_view.dart (81%) rename lib/{ => modules}/feed/feed_view.dart (76%) rename lib/{ => modules}/filter/chip_selector.dart (98%) rename lib/{ => modules}/filter/filter_models.dart (96%) rename lib/{ => modules}/filter/filter_providers.dart (84%) rename lib/{ => modules}/filter/filter_search_field.dart (93%) rename lib/{ => modules}/filter/filter_view.dart (95%) rename lib/{ => modules}/filter/year_range_picker.dart (100%) rename lib/{ => modules}/home/home_provider.dart (92%) rename lib/{ => modules}/home/home_view.dart (84%) rename lib/{ => modules}/media/media_action_buttons.dart (89%) rename lib/{ => modules}/media/media_constants.dart (100%) rename lib/{ => modules}/media/media_grids.dart (96%) rename lib/{ => modules}/media/media_header.dart (94%) rename lib/{ => modules}/media/media_info_view.dart (94%) rename lib/{ => modules}/media/media_models.dart (95%) rename lib/{ => modules}/media/media_providers.dart (93%) rename lib/{ => modules}/media/media_view.dart (89%) rename lib/{notifications => modules/notification}/notification_model.dart (98%) rename lib/{notifications => modules/notification}/notification_provider.dart (87%) rename lib/{notifications => modules/notification}/notifications_view.dart (91%) rename lib/{ => modules}/review/review_grid.dart (90%) rename lib/{ => modules}/review/review_header.dart (95%) rename lib/{ => modules}/review/review_models.dart (97%) rename lib/{ => modules}/review/review_providers.dart (92%) rename lib/{ => modules}/review/review_view.dart (95%) rename lib/{ => modules}/review/reviews_view.dart (81%) rename lib/{ => modules}/settings/settings_about_tab.dart (92%) rename lib/{ => modules}/settings/settings_app_tab.dart (90%) rename lib/{ => modules}/settings/settings_content_tab.dart (91%) rename lib/{ => modules}/settings/settings_notifications_tab.dart (80%) rename lib/{ => modules}/settings/settings_provider.dart (96%) rename lib/{ => modules}/settings/settings_view.dart (77%) rename lib/{ => modules}/settings/theme_preview.dart (97%) rename lib/{ => modules}/social/social_model.dart (87%) rename lib/{ => modules}/social/social_provider.dart (90%) rename lib/{ => modules}/social/social_view.dart (80%) rename lib/{ => modules}/staff/staff_action_buttons.dart (89%) rename lib/{ => modules}/staff/staff_info_tab.dart (92%) rename lib/{ => modules}/staff/staff_models.dart (89%) rename lib/{ => modules}/staff/staff_providers.dart (91%) rename lib/{ => modules}/staff/staff_view.dart (78%) rename lib/{ => modules}/statistics/charts.dart (99%) rename lib/{ => modules}/statistics/statistics_view.dart (91%) rename lib/{ => modules}/statistics/user_statistics.dart (98%) rename lib/{ => modules}/studio/studio_grid.dart (77%) rename lib/{ => modules}/studio/studio_models.dart (91%) rename lib/{ => modules}/studio/studio_providers.dart (87%) rename lib/{ => modules}/studio/studio_view.dart (90%) rename lib/{ => modules}/tag/tag_models.dart (100%) rename lib/{ => modules}/tag/tag_provider.dart (78%) rename lib/{ => modules}/user/user_grid.dart (79%) rename lib/{ => modules}/user/user_header.dart (94%) rename lib/{ => modules}/user/user_models.dart (94%) rename lib/{ => modules}/user/user_providers.dart (76%) rename lib/{ => modules}/user/user_view.dart (89%) diff --git a/lib/common/paged.dart b/lib/common/models/paged.dart similarity index 100% rename from lib/common/paged.dart rename to lib/common/models/paged.dart diff --git a/lib/common/relation.dart b/lib/common/models/relation.dart similarity index 81% rename from lib/common/relation.dart rename to lib/common/models/relation.dart index affc39f6..97989008 100644 --- a/lib/common/relation.dart +++ b/lib/common/models/relation.dart @@ -1,4 +1,4 @@ -import 'package:otraku/discover/discover_models.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; class Relation { Relation({ diff --git a/lib/common/tile_item.dart b/lib/common/models/tile_item.dart similarity index 78% rename from lib/common/tile_item.dart rename to lib/common/models/tile_item.dart index 8a0f99bc..6d14f46f 100644 --- a/lib/common/tile_item.dart +++ b/lib/common/models/tile_item.dart @@ -1,4 +1,4 @@ -import 'package:otraku/discover/discover_models.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; class TileItem { const TileItem({ diff --git a/lib/utils/api.dart b/lib/common/utils/api.dart similarity index 97% rename from lib/utils/api.dart rename to lib/common/utils/api.dart index e425d6ab..8d68af16 100644 --- a/lib/utils/api.dart +++ b/lib/common/utils/api.dart @@ -4,8 +4,8 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/utils/route_arg.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/utils/route_arg.dart'; abstract class Api { static final _url = Uri.parse('https://graphql.anilist.co'); diff --git a/lib/utils/background_handler.dart b/lib/common/utils/background_handler.dart similarity index 96% rename from lib/utils/background_handler.dart rename to lib/common/utils/background_handler.dart index 033dd358..f32b3f8c 100644 --- a/lib/utils/background_handler.dart +++ b/lib/common/utils/background_handler.dart @@ -2,12 +2,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:otraku/notifications/notification_model.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/utils/route_arg.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; +import 'package:otraku/modules/notification/notification_model.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; import 'package:workmanager/workmanager.dart'; final _notificationPlugin = FlutterLocalNotificationsPlugin(); diff --git a/lib/utils/consts.dart b/lib/common/utils/consts.dart similarity index 100% rename from lib/utils/consts.dart rename to lib/common/utils/consts.dart diff --git a/lib/utils/convert.dart b/lib/common/utils/convert.dart similarity index 97% rename from lib/utils/convert.dart rename to lib/common/utils/convert.dart index b8213e1d..3dfd854f 100644 --- a/lib/utils/convert.dart +++ b/lib/common/utils/convert.dart @@ -1,5 +1,5 @@ -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/common/utils/options.dart'; abstract class Convert { /// Code points of some characters. diff --git a/lib/utils/graphql.dart b/lib/common/utils/graphql.dart similarity index 100% rename from lib/utils/graphql.dart rename to lib/common/utils/graphql.dart diff --git a/lib/utils/options.dart b/lib/common/utils/options.dart similarity index 98% rename from lib/utils/options.dart rename to lib/common/utils/options.dart index e3d4113b..d41e9596 100644 --- a/lib/utils/options.dart +++ b/lib/common/utils/options.dart @@ -1,9 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/theming.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/theming.dart'; import 'package:path_provider/path_provider.dart'; /// Current app version. diff --git a/lib/utils/paged_controller.dart b/lib/common/utils/paged_controller.dart similarity index 100% rename from lib/utils/paged_controller.dart rename to lib/common/utils/paged_controller.dart diff --git a/lib/utils/route_arg.dart b/lib/common/utils/route_arg.dart similarity index 80% rename from lib/utils/route_arg.dart rename to lib/common/utils/route_arg.dart index 852cb4d5..aed862a4 100644 --- a/lib/utils/route_arg.dart +++ b/lib/common/utils/route_arg.dart @@ -1,21 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:otraku/activity/activities_view.dart'; -import 'package:otraku/activity/activity_view.dart'; -import 'package:otraku/collection/collection_view.dart'; -import 'package:otraku/favorites/favorites_view.dart'; -import 'package:otraku/studio/studio_view.dart'; -import 'package:otraku/auth/auth_view.dart'; -import 'package:otraku/character/character_view.dart'; -import 'package:otraku/social/social_view.dart'; -import 'package:otraku/home/home_view.dart'; -import 'package:otraku/media/media_view.dart'; -import 'package:otraku/notifications/notifications_view.dart'; -import 'package:otraku/review/review_view.dart'; -import 'package:otraku/settings/settings_view.dart'; -import 'package:otraku/staff/staff_view.dart'; -import 'package:otraku/statistics/statistics_view.dart'; -import 'package:otraku/review/reviews_view.dart'; -import 'package:otraku/user/user_view.dart'; +import 'package:otraku/modules/activity/activities_view.dart'; +import 'package:otraku/modules/activity/activity_view.dart'; +import 'package:otraku/modules/collection/collection_view.dart'; +import 'package:otraku/modules/favorites/favorites_view.dart'; +import 'package:otraku/modules/studio/studio_view.dart'; +import 'package:otraku/modules/auth/auth_view.dart'; +import 'package:otraku/modules/character/character_view.dart'; +import 'package:otraku/modules/social/social_view.dart'; +import 'package:otraku/modules/home/home_view.dart'; +import 'package:otraku/modules/media/media_view.dart'; +import 'package:otraku/modules/notification/notifications_view.dart'; +import 'package:otraku/modules/review/review_view.dart'; +import 'package:otraku/modules/settings/settings_view.dart'; +import 'package:otraku/modules/staff/staff_view.dart'; +import 'package:otraku/modules/statistics/statistics_view.dart'; +import 'package:otraku/modules/review/reviews_view.dart'; +import 'package:otraku/modules/user/user_view.dart'; /// A routing helper. When passing arguments to named routes, they should always /// be an instance of [RouteArg] or `null`. diff --git a/lib/utils/theming.dart b/lib/common/utils/theming.dart similarity index 98% rename from lib/utils/theming.dart rename to lib/common/utils/theming.dart index 965eadc9..c17a24c5 100644 --- a/lib/utils/theming.dart +++ b/lib/common/utils/theming.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; class ColorSeed { const ColorSeed(this.seed); diff --git a/lib/widgets/cached_image.dart b/lib/common/widgets/cached_image.dart similarity index 95% rename from lib/widgets/cached_image.dart rename to lib/common/widgets/cached_image.dart index 635e960c..79c74cd3 100644 --- a/lib/widgets/cached_image.dart +++ b/lib/common/widgets/cached_image.dart @@ -1,7 +1,7 @@ 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'; +import 'package:otraku/common/widgets/overlays/toast.dart'; /// A custom cache manager is needed to define exact image cap and stale period. final _cacheManager = CacheManager( diff --git a/lib/widgets/drag_detector.dart b/lib/common/widgets/drag_detector.dart similarity index 100% rename from lib/widgets/drag_detector.dart rename to lib/common/widgets/drag_detector.dart diff --git a/lib/widgets/fields/checkbox_field.dart b/lib/common/widgets/fields/checkbox_field.dart similarity index 98% rename from lib/widgets/fields/checkbox_field.dart rename to lib/common/widgets/fields/checkbox_field.dart index d42c1aed..6b263430 100644 --- a/lib/widgets/fields/checkbox_field.dart +++ b/lib/common/widgets/fields/checkbox_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; class CheckBoxField extends StatefulWidget { const CheckBoxField({ diff --git a/lib/widgets/fields/date_field.dart b/lib/common/widgets/fields/date_field.dart similarity index 100% rename from lib/widgets/fields/date_field.dart rename to lib/common/widgets/fields/date_field.dart diff --git a/lib/widgets/fields/drop_down_field.dart b/lib/common/widgets/fields/drop_down_field.dart similarity index 94% rename from lib/widgets/fields/drop_down_field.dart rename to lib/common/widgets/fields/drop_down_field.dart index 5e7badc0..e987bf2d 100644 --- a/lib/widgets/fields/drop_down_field.dart +++ b/lib/common/widgets/fields/drop_down_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/fields/labeled_field.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/fields/labeled_field.dart'; class DropDownField extends StatefulWidget { const DropDownField({ diff --git a/lib/widgets/fields/growable_text_field.dart b/lib/common/widgets/fields/growable_text_field.dart similarity index 95% rename from lib/widgets/fields/growable_text_field.dart rename to lib/common/widgets/fields/growable_text_field.dart index ebbed887..d9a9d0da 100644 --- a/lib/widgets/fields/growable_text_field.dart +++ b/lib/common/widgets/fields/growable_text_field.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; // A text field that grows to up to 10 lines, if necessary. class GrowableTextField extends StatefulWidget { diff --git a/lib/widgets/fields/labeled_field.dart b/lib/common/widgets/fields/labeled_field.dart similarity index 100% rename from lib/widgets/fields/labeled_field.dart rename to lib/common/widgets/fields/labeled_field.dart diff --git a/lib/widgets/fields/number_field.dart b/lib/common/widgets/fields/number_field.dart similarity index 100% rename from lib/widgets/fields/number_field.dart rename to lib/common/widgets/fields/number_field.dart diff --git a/lib/widgets/fields/search_field.dart b/lib/common/widgets/fields/search_field.dart similarity index 98% rename from lib/widgets/fields/search_field.dart rename to lib/common/widgets/fields/search_field.dart index 04c18926..8cbd99dc 100644 --- a/lib/widgets/fields/search_field.dart +++ b/lib/common/widgets/fields/search_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; class SearchField extends StatefulWidget { const SearchField({ diff --git a/lib/widgets/grids/chip_grids.dart b/lib/common/widgets/grids/chip_grids.dart similarity index 96% rename from lib/widgets/grids/chip_grids.dart rename to lib/common/widgets/grids/chip_grids.dart index aeb0e82f..2cd1bb3a 100644 --- a/lib/widgets/grids/chip_grids.dart +++ b/lib/common/widgets/grids/chip_grids.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/tag/tag_models.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/filter/filter_view.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/tag/tag_models.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/modules/filter/filter_view.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class ChipOptionField extends StatelessWidget { const ChipOptionField({ diff --git a/lib/widgets/grids/relation_grid.dart b/lib/common/widgets/grids/relation_grid.dart similarity index 94% rename from lib/widgets/grids/relation_grid.dart rename to lib/common/widgets/grids/relation_grid.dart index 4a3fc588..c5389ac0 100644 --- a/lib/widgets/grids/relation_grid.dart +++ b/lib/common/widgets/grids/relation_grid.dart @@ -1,9 +1,9 @@ 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/cached_image.dart'; -import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; class RelationGrid extends StatelessWidget { RelationGrid({ diff --git a/lib/widgets/grids/sliver_grid_delegates.dart b/lib/common/widgets/grids/sliver_grid_delegates.dart similarity index 100% rename from lib/widgets/grids/sliver_grid_delegates.dart rename to lib/common/widgets/grids/sliver_grid_delegates.dart diff --git a/lib/widgets/grids/tile_item_grid.dart b/lib/common/widgets/grids/tile_item_grid.dart similarity index 84% rename from lib/widgets/grids/tile_item_grid.dart rename to lib/common/widgets/grids/tile_item_grid.dart index 573d0467..ea7133fc 100644 --- a/lib/widgets/grids/tile_item_grid.dart +++ b/lib/common/widgets/grids/tile_item_grid.dart @@ -1,9 +1,9 @@ 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/cached_image.dart'; -import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; class TileItemGrid extends StatelessWidget { const TileItemGrid(this.items); diff --git a/lib/widgets/html_content.dart b/lib/common/widgets/html_content.dart similarity index 88% rename from lib/widgets/html_content.dart rename to lib/common/widgets/html_content.dart index d3c1c51c..b62e3e0a 100644 --- a/lib/widgets/html_content.dart +++ b/lib/common/widgets/html_content.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class HtmlContent extends StatelessWidget { const HtmlContent(this.text); diff --git a/lib/widgets/layouts/bottom_bar.dart b/lib/common/widgets/layouts/bottom_bar.dart similarity index 98% rename from lib/widgets/layouts/bottom_bar.dart rename to lib/common/widgets/layouts/bottom_bar.dart index 80a51eef..9cf69f44 100644 --- a/lib/widgets/layouts/bottom_bar.dart +++ b/lib/common/widgets/layouts/bottom_bar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; class BottomNavBar extends StatefulWidget { const BottomNavBar({ diff --git a/lib/widgets/layouts/constrained_view.dart b/lib/common/widgets/layouts/constrained_view.dart similarity index 91% rename from lib/widgets/layouts/constrained_view.dart rename to lib/common/widgets/layouts/constrained_view.dart index e80d8ea2..b3453b22 100644 --- a/lib/widgets/layouts/constrained_view.dart +++ b/lib/common/widgets/layouts/constrained_view.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; /// Horizontally constrains [child] into the center. class ConstrainedView extends StatelessWidget { diff --git a/lib/widgets/layouts/direct_page_view.dart b/lib/common/widgets/layouts/direct_page_view.dart similarity index 100% rename from lib/widgets/layouts/direct_page_view.dart rename to lib/common/widgets/layouts/direct_page_view.dart diff --git a/lib/widgets/layouts/floating_bar.dart b/lib/common/widgets/layouts/floating_bar.dart similarity index 97% rename from lib/widgets/layouts/floating_bar.dart rename to lib/common/widgets/layouts/floating_bar.dart index e01e522f..4d75db79 100644 --- a/lib/widgets/layouts/floating_bar.dart +++ b/lib/common/widgets/layouts/floating_bar.dart @@ -1,8 +1,8 @@ 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/scaffolds.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/drag_detector.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; /// Hides the [child] on scroll-down and reveals it on scroll-up. class FloatingBar extends StatefulWidget { diff --git a/lib/widgets/layouts/scaffolds.dart b/lib/common/widgets/layouts/scaffolds.dart similarity index 94% rename from lib/widgets/layouts/scaffolds.dart rename to lib/common/widgets/layouts/scaffolds.dart index d3bef342..725854d5 100644 --- a/lib/widgets/layouts/scaffolds.dart +++ b/lib/common/widgets/layouts/scaffolds.dart @@ -1,7 +1,7 @@ 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'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; /// Simple wrapper around [Scaffold], only supporting a bottom bar. /// For top bars and floating bars, use [TabScaffold]. diff --git a/lib/widgets/layouts/segment_switcher.dart b/lib/common/widgets/layouts/segment_switcher.dart similarity index 98% rename from lib/widgets/layouts/segment_switcher.dart rename to lib/common/widgets/layouts/segment_switcher.dart index febc7139..84c4e05d 100644 --- a/lib/widgets/layouts/segment_switcher.dart +++ b/lib/common/widgets/layouts/segment_switcher.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; class SegmentSwitcher extends StatefulWidget { const SegmentSwitcher({ diff --git a/lib/widgets/layouts/top_bar.dart b/lib/common/widgets/layouts/top_bar.dart similarity index 98% rename from lib/widgets/layouts/top_bar.dart rename to lib/common/widgets/layouts/top_bar.dart index 951ac3c4..7e602cb6 100644 --- a/lib/widgets/layouts/top_bar.dart +++ b/lib/common/widgets/layouts/top_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; /// A top app bar implementation that uses a blurred, translucent background. /// It has (in order): diff --git a/lib/widgets/link_tile.dart b/lib/common/widgets/link_tile.dart similarity index 85% rename from lib/widgets/link_tile.dart rename to lib/common/widgets/link_tile.dart index c827129f..1ea8814b 100644 --- a/lib/widgets/link_tile.dart +++ b/lib/common/widgets/link_tile.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/edit/edit_providers.dart'; -import 'package:otraku/utils/route_arg.dart'; -import 'package:otraku/edit/edit_view.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/modules/edit/edit_view.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class LinkTile extends StatelessWidget { const LinkTile({ diff --git a/lib/widgets/loaders.dart/loaders.dart b/lib/common/widgets/loaders.dart/loaders.dart similarity index 93% rename from lib/widgets/loaders.dart/loaders.dart rename to lib/common/widgets/loaders.dart/loaders.dart index 11710234..c91c2eb1 100644 --- a/lib/widgets/loaders.dart/loaders.dart +++ b/lib/common/widgets/loaders.dart/loaders.dart @@ -1,8 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/layouts/scaffolds.dart'; -import 'package:otraku/widgets/loaders.dart/shimmer.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/loaders.dart/shimmer.dart'; class Loader extends StatelessWidget { const Loader(); diff --git a/lib/widgets/loaders.dart/shimmer.dart b/lib/common/widgets/loaders.dart/shimmer.dart similarity index 100% rename from lib/widgets/loaders.dart/shimmer.dart rename to lib/common/widgets/loaders.dart/shimmer.dart diff --git a/lib/widgets/overlays/dialogs.dart b/lib/common/widgets/overlays/dialogs.dart similarity index 99% rename from lib/widgets/overlays/dialogs.dart rename to lib/common/widgets/overlays/dialogs.dart index 5cc5d39c..dc47471e 100644 --- a/lib/widgets/overlays/dialogs.dart +++ b/lib/common/widgets/overlays/dialogs.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:otraku/widgets/html_content.dart'; +import 'package:otraku/common/widgets/html_content.dart'; Future showPopUp(BuildContext context, Widget child) => showDialog( context: context, diff --git a/lib/widgets/overlays/sheets.dart b/lib/common/widgets/overlays/sheets.dart similarity index 98% rename from lib/widgets/overlays/sheets.dart rename to lib/common/widgets/overlays/sheets.dart index d47b0bdf..9f32fc05 100644 --- a/lib/widgets/overlays/sheets.dart +++ b/lib/common/widgets/overlays/sheets.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; /// Used to open [DraggableScrollableSheet]. Future showSheet(BuildContext context, Widget sheet) => diff --git a/lib/widgets/overlays/toast.dart b/lib/common/widgets/overlays/toast.dart similarity index 97% rename from lib/widgets/overlays/toast.dart rename to lib/common/widgets/overlays/toast.dart index 8b0d5209..d7c71b36 100644 --- a/lib/widgets/overlays/toast.dart +++ b/lib/common/widgets/overlays/toast.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; import 'package:url_launcher/url_launcher.dart'; class Toast { diff --git a/lib/widgets/paged_view.dart b/lib/common/widgets/paged_view.dart similarity index 87% rename from lib/widgets/paged_view.dart rename to lib/common/widgets/paged_view.dart index ae1fa760..269d35f4 100644 --- a/lib/widgets/paged_view.dart +++ b/lib/common/widgets/paged_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/widgets/layouts/constrained_view.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; /// Subscribes to a paginated, asynchronous provider. /// Shows a pop up, if there is an error. diff --git a/lib/widgets/text_rail.dart b/lib/common/widgets/text_rail.dart similarity index 100% rename from lib/widgets/text_rail.dart rename to lib/common/widgets/text_rail.dart diff --git a/lib/main.dart b/lib/main.dart index 39e0962d..2cab31e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/home/home_provider.dart'; -import 'package:otraku/utils/background_handler.dart'; -import 'package:otraku/utils/route_arg.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/utils/theming.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/common/utils/background_handler.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/utils/theming.dart'; Future main() async { await Options.init(); diff --git a/lib/activity/activities_providers.dart b/lib/modules/activity/activities_providers.dart similarity index 95% rename from lib/activity/activities_providers.dart rename to lib/modules/activity/activities_providers.dart index 5c6f38a6..1eb075d4 100644 --- a/lib/activity/activities_providers.dart +++ b/lib/modules/activity/activities_providers.dart @@ -1,10 +1,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/activity/activity_models.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/home/home_provider.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/activity/activity_models.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/utils/options.dart'; final activitiesProvider = StateNotifierProvider.autoDispose .family>, int?>( diff --git a/lib/activity/activities_view.dart b/lib/modules/activity/activities_view.dart similarity index 85% rename from lib/activity/activities_view.dart rename to lib/modules/activity/activities_view.dart index 9a4a21b6..e566dae5 100644 --- a/lib/activity/activities_view.dart +++ b/lib/modules/activity/activities_view.dart @@ -1,23 +1,23 @@ 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/activity_card.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/consts.dart'; -import 'package:otraku/activity/activity_models.dart'; -import 'package:otraku/utils/paged_controller.dart'; -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/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/paged_view.dart'; +import 'package:otraku/modules/activity/activities_providers.dart'; +import 'package:otraku/modules/activity/activity_card.dart'; +import 'package:otraku/modules/composition/composition_model.dart'; +import 'package:otraku/modules/composition/composition_view.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/activity/activity_models.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/fields/checkbox_field.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/segment_switcher.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { final filter = ref.read(activityFilterProvider(id)); diff --git a/lib/activity/activity_card.dart b/lib/modules/activity/activity_card.dart similarity index 94% rename from lib/activity/activity_card.dart rename to lib/modules/activity/activity_card.dart index 5c073896..7af653cc 100644 --- a/lib/activity/activity_card.dart +++ b/lib/modules/activity/activity_card.dart @@ -1,18 +1,18 @@ 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_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/cached_image.dart'; -import 'package:otraku/widgets/html_content.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/activity/activity_models.dart'; +import 'package:otraku/modules/activity/activity_provider.dart'; +import 'package:otraku/modules/composition/composition_model.dart'; +import 'package:otraku/modules/composition/composition_view.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/html_content.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class ActivityCard extends StatelessWidget { const ActivityCard({ diff --git a/lib/activity/activity_models.dart b/lib/modules/activity/activity_models.dart similarity index 97% rename from lib/activity/activity_models.dart rename to lib/modules/activity/activity_models.dart index 14ed7985..c628e64a 100644 --- a/lib/activity/activity_models.dart +++ b/lib/modules/activity/activity_models.dart @@ -1,6 +1,6 @@ -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/utils/options.dart'; class ActivityState { ActivityState(this.activity, this.replies); diff --git a/lib/activity/activity_provider.dart b/lib/modules/activity/activity_provider.dart similarity index 95% rename from lib/activity/activity_provider.dart rename to lib/modules/activity/activity_provider.dart index 0937e342..dbe0b343 100644 --- a/lib/activity/activity_provider.dart +++ b/lib/modules/activity/activity_provider.dart @@ -1,9 +1,9 @@ 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/paged.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/activity/activity_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/utils/options.dart'; /// Toggles an activity like and returns an error if unsuccessful. Future toggleActivityLike(Activity activity) async { diff --git a/lib/activity/activity_view.dart b/lib/modules/activity/activity_view.dart similarity index 86% rename from lib/activity/activity_view.dart rename to lib/modules/activity/activity_view.dart index c456b4e1..96a7c22f 100644 --- a/lib/activity/activity_view.dart +++ b/lib/modules/activity/activity_view.dart @@ -1,24 +1,24 @@ 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_provider.dart'; -import 'package:otraku/activity/activity_card.dart'; -import 'package:otraku/activity/reply_card.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/paged_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/cached_image.dart'; -import 'package:otraku/widgets/layouts/floating_bar.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'; +import 'package:otraku/modules/activity/activity_models.dart'; +import 'package:otraku/modules/activity/activity_provider.dart'; +import 'package:otraku/modules/activity/activity_card.dart'; +import 'package:otraku/modules/activity/reply_card.dart'; +import 'package:otraku/modules/composition/composition_model.dart'; +import 'package:otraku/modules/composition/composition_view.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class ActivityView extends ConsumerStatefulWidget { const ActivityView(this.id, this.onChanged); diff --git a/lib/activity/reply_card.dart b/lib/modules/activity/reply_card.dart similarity index 89% rename from lib/activity/reply_card.dart rename to lib/modules/activity/reply_card.dart index ac342e3f..bf0ef41e 100644 --- a/lib/activity/reply_card.dart +++ b/lib/modules/activity/reply_card.dart @@ -1,18 +1,18 @@ 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_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/cached_image.dart'; -import 'package:otraku/widgets/html_content.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/activity/activity_models.dart'; +import 'package:otraku/modules/activity/activity_provider.dart'; +import 'package:otraku/modules/composition/composition_model.dart'; +import 'package:otraku/modules/composition/composition_view.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/html_content.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class ReplyCard extends StatelessWidget { const ReplyCard(this.activityId, this.reply); diff --git a/lib/auth/auth_view.dart b/lib/modules/auth/auth_view.dart similarity index 94% rename from lib/auth/auth_view.dart rename to lib/modules/auth/auth_view.dart index bca59cc0..879aa183 100644 --- a/lib/auth/auth_view.dart +++ b/lib/modules/auth/auth_view.dart @@ -3,14 +3,14 @@ import 'dart:async'; import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -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/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'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class AuthView extends StatefulWidget { const AuthView(); diff --git a/lib/character/character_action_buttons.dart b/lib/modules/character/character_action_buttons.dart similarity index 90% rename from lib/character/character_action_buttons.dart rename to lib/modules/character/character_action_buttons.dart index 674801d6..e7683edc 100644 --- a/lib/character/character_action_buttons.dart +++ b/lib/modules/character/character_action_buttons.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/character/character_models.dart'; -import 'package:otraku/character/character_providers.dart'; -import 'package:otraku/filter/chip_selector.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/character/character_models.dart'; +import 'package:otraku/modules/character/character_providers.dart'; +import 'package:otraku/modules/filter/chip_selector.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class CharacterFavoriteButton extends StatefulWidget { const CharacterFavoriteButton(this.data); diff --git a/lib/character/character_info_tab.dart b/lib/modules/character/character_info_tab.dart similarity index 92% rename from lib/character/character_info_tab.dart rename to lib/modules/character/character_info_tab.dart index 459a2189..c1de7e6b 100644 --- a/lib/character/character_info_tab.dart +++ b/lib/modules/character/character_info_tab.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/character/character_providers.dart'; -import 'package:otraku/utils/consts.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/constrained_view.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/modules/character/character_providers.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/html_content.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class CharacterInfoTab extends StatelessWidget { const CharacterInfoTab(this.id, this.imageUrl, this.scrollCtrl); diff --git a/lib/character/character_models.dart b/lib/modules/character/character_models.dart similarity index 92% rename from lib/character/character_models.dart rename to lib/modules/character/character_models.dart index 8642e312..5189f89c 100644 --- a/lib/character/character_models.dart +++ b/lib/modules/character/character_models.dart @@ -1,10 +1,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/convert.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/convert.dart'; TileItem characterItem(Map map) => TileItem( id: map['id'], diff --git a/lib/character/character_providers.dart b/lib/modules/character/character_providers.dart similarity index 92% rename from lib/character/character_providers.dart rename to lib/modules/character/character_providers.dart index acbaa57c..1b246060 100644 --- a/lib/character/character_providers.dart +++ b/lib/modules/character/character_providers.dart @@ -1,12 +1,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/character/character_models.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/character/character_models.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/utils/options.dart'; /// Favorite/Unfavorite character. Returns `true` if successful. Future toggleFavoriteCharacter(int characterId) async { diff --git a/lib/character/character_view.dart b/lib/modules/character/character_view.dart similarity index 79% rename from lib/character/character_view.dart rename to lib/modules/character/character_view.dart index a86eb90f..e40acb21 100644 --- a/lib/character/character_view.dart +++ b/lib/modules/character/character_view.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/character/character_action_buttons.dart'; -import 'package:otraku/character/character_providers.dart'; -import 'package:otraku/character/character_info_tab.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/widgets/grids/relation_grid.dart'; -import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/floating_bar.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'; -import 'package:otraku/widgets/paged_view.dart'; +import 'package:otraku/modules/character/character_action_buttons.dart'; +import 'package:otraku/modules/character/character_providers.dart'; +import 'package:otraku/modules/character/character_info_tab.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/widgets/grids/relation_grid.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class CharacterView extends ConsumerStatefulWidget { const CharacterView(this.id, this.imageUrl); diff --git a/lib/collection/collection_grid.dart b/lib/modules/collection/collection_grid.dart similarity index 87% rename from lib/collection/collection_grid.dart rename to lib/modules/collection/collection_grid.dart index e2687ad7..8453a62d 100644 --- a/lib/collection/collection_grid.dart +++ b/lib/modules/collection/collection_grid.dart @@ -1,16 +1,16 @@ 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/edit/edit_view.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/consts.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/modules/collection/collection_models.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/modules/edit/edit_view.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class CollectionGrid extends StatelessWidget { const CollectionGrid({ diff --git a/lib/collection/collection_list.dart b/lib/modules/collection/collection_list.dart similarity index 93% rename from lib/collection/collection_list.dart rename to lib/modules/collection/collection_list.dart index 22f45e87..cb93d231 100644 --- a/lib/collection/collection_list.dart +++ b/lib/modules/collection/collection_list.dart @@ -1,17 +1,17 @@ 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'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/edit/edit_view.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/text_rail.dart'; const _TILE_HEIGHT = 140.0; diff --git a/lib/collection/collection_models.dart b/lib/modules/collection/collection_models.dart similarity index 98% rename from lib/collection/collection_models.dart rename to lib/modules/collection/collection_models.dart index 3367a07b..aa32d167 100644 --- a/lib/collection/collection_models.dart +++ b/lib/modules/collection/collection_models.dart @@ -1,6 +1,6 @@ -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/options.dart'; /// Used as an argument for a [collectionProvider] `family` instance. class CollectionTag { diff --git a/lib/collection/collection_preview_provider.dart b/lib/modules/collection/collection_preview_provider.dart similarity index 89% rename from lib/collection/collection_preview_provider.dart rename to lib/modules/collection/collection_preview_provider.dart index 59bdb057..ca3ab4b8 100644 --- a/lib/collection/collection_preview_provider.dart +++ b/lib/modules/collection/collection_preview_provider.dart @@ -1,10 +1,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/utils/options.dart'; final collectionPreviewProvider = ChangeNotifierProvider.autoDispose.family( (ref, CollectionTag tag) => CollectionPreviewNotifier(tag), diff --git a/lib/collection/collection_preview_view.dart b/lib/modules/collection/collection_preview_view.dart similarity index 81% rename from lib/collection/collection_preview_view.dart rename to lib/modules/collection/collection_preview_view.dart index 8eb2f71e..5d8dda8b 100644 --- a/lib/collection/collection_preview_view.dart +++ b/lib/modules/collection/collection_preview_view.dart @@ -3,20 +3,20 @@ 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_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/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/modules/collection/collection_grid.dart'; +import 'package:otraku/modules/collection/collection_list.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/collection/collection_preview_provider.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; class CollectionPreviewView extends StatefulWidget { const CollectionPreviewView({ diff --git a/lib/collection/collection_providers.dart b/lib/modules/collection/collection_providers.dart similarity index 96% rename from lib/collection/collection_providers.dart rename to lib/modules/collection/collection_providers.dart index 544e79a3..da9913da 100644 --- a/lib/collection/collection_providers.dart +++ b/lib/modules/collection/collection_providers.dart @@ -1,11 +1,11 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/edit/edit_model.dart'; -import 'package:otraku/filter/filter_providers.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/edit/edit_model.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; final collectionProvider = ChangeNotifierProvider.autoDispose.family( (ref, CollectionTag tag) => CollectionNotifier(tag), diff --git a/lib/collection/collection_view.dart b/lib/modules/collection/collection_view.dart similarity index 89% rename from lib/collection/collection_view.dart rename to lib/modules/collection/collection_view.dart index cfaa928e..06ecd466 100644 --- a/lib/collection/collection_view.dart +++ b/lib/modules/collection/collection_view.dart @@ -3,23 +3,23 @@ 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'; -import 'package:otraku/filter/filter_providers.dart'; -import 'package:otraku/filter/filter_view.dart'; -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/scaffolds.dart'; -import 'package:otraku/widgets/layouts/top_bar.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.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'; +import 'package:otraku/modules/collection/collection_grid.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/collection/collection_providers.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/modules/filter/filter_view.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/modules/collection/collection_list.dart'; +import 'package:otraku/modules/filter/filter_search_field.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class CollectionView extends StatefulWidget { const CollectionView(this.userId, this.ofAnime); diff --git a/lib/composition/composition_model.dart b/lib/modules/composition/composition_model.dart similarity index 94% rename from lib/composition/composition_model.dart rename to lib/modules/composition/composition_model.dart index 345851c4..aacb7460 100644 --- a/lib/composition/composition_model.dart +++ b/lib/modules/composition/composition_model.dart @@ -1,6 +1,6 @@ -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/graphql.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/graphql.dart'; /// Can throw. Creates/updates an activity/reply /// and returns it as a map for deserialization. diff --git a/lib/composition/composition_view.dart b/lib/modules/composition/composition_view.dart similarity index 92% rename from lib/composition/composition_view.dart rename to lib/modules/composition/composition_view.dart index a44c55c9..f2412107 100644 --- a/lib/composition/composition_view.dart +++ b/lib/modules/composition/composition_view.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -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/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'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/modules/composition/composition_model.dart'; +import 'package:otraku/common/widgets/html_content.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/layouts/segment_switcher.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class CompositionView extends StatefulWidget { const CompositionView({required this.composition, required this.onDone}); diff --git a/lib/discover/discover_media_grid.dart b/lib/modules/discover/discover_media_grid.dart similarity index 92% rename from lib/discover/discover_media_grid.dart rename to lib/modules/discover/discover_media_grid.dart index bb2236f4..c41ff893 100644 --- a/lib/discover/discover_media_grid.dart +++ b/lib/modules/discover/discover_media_grid.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/discover/discover_models.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'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/text_rail.dart'; class DiscoverMediaGrid extends StatelessWidget { const DiscoverMediaGrid(this.items); diff --git a/lib/discover/discover_models.dart b/lib/modules/discover/discover_models.dart similarity index 92% rename from lib/discover/discover_models.dart rename to lib/modules/discover/discover_models.dart index 10b4c8a7..59717f16 100644 --- a/lib/discover/discover_models.dart +++ b/lib/modules/discover/discover_models.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/options.dart'; enum DiscoverType { anime, diff --git a/lib/discover/discover_providers.dart b/lib/modules/discover/discover_providers.dart similarity index 91% rename from lib/discover/discover_providers.dart rename to lib/modules/discover/discover_providers.dart index 7423b7c9..3262a940 100644 --- a/lib/discover/discover_providers.dart +++ b/lib/modules/discover/discover_providers.dart @@ -1,18 +1,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/character/character_models.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/filter/filter_models.dart'; -import 'package:otraku/filter/filter_providers.dart'; -import 'package:otraku/home/home_provider.dart'; -import 'package:otraku/review/review_models.dart'; -import 'package:otraku/review/review_providers.dart'; -import 'package:otraku/staff/staff_models.dart'; -import 'package:otraku/studio/studio_models.dart'; -import 'package:otraku/user/user_models.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; +import 'package:otraku/modules/character/character_models.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/filter/filter_models.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/modules/review/review_models.dart'; +import 'package:otraku/modules/review/review_providers.dart'; +import 'package:otraku/modules/staff/staff_models.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; /// Fetches another page on the discover tab, depending on the selected type. void discoverLoadMore(WidgetRef ref) { diff --git a/lib/discover/discover_view.dart b/lib/modules/discover/discover_view.dart similarity index 85% rename from lib/discover/discover_view.dart rename to lib/modules/discover/discover_view.dart index 96d97112..a2684810 100644 --- a/lib/discover/discover_view.dart +++ b/lib/modules/discover/discover_view.dart @@ -1,29 +1,29 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/discover/discover_providers.dart'; -import 'package:otraku/filter/filter_providers.dart'; -import 'package:otraku/filter/filter_view.dart'; -import 'package:otraku/review/review_models.dart'; -import 'package:otraku/review/review_providers.dart'; -import 'package:otraku/studio/studio_grid.dart'; -import 'package:otraku/studio/studio_models.dart'; -import 'package:otraku/user/user_grid.dart'; -import 'package:otraku/user/user_models.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/review/review_grid.dart'; -import 'package:otraku/utils/paged_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/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/paged_view.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/discover/discover_media_grid.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/discover/discover_providers.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/modules/filter/filter_view.dart'; +import 'package:otraku/modules/review/review_models.dart'; +import 'package:otraku/modules/review/review_providers.dart'; +import 'package:otraku/modules/studio/studio_grid.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; +import 'package:otraku/modules/user/user_grid.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/modules/review/review_grid.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/grids/tile_item_grid.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/modules/filter/filter_search_field.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class DiscoverView extends ConsumerWidget { const DiscoverView(this.scrollCtrl); diff --git a/lib/edit/edit_buttons.dart b/lib/modules/edit/edit_buttons.dart similarity index 87% rename from lib/edit/edit_buttons.dart rename to lib/modules/edit/edit_buttons.dart index bfd2b702..1f53f5d5 100644 --- a/lib/edit/edit_buttons.dart +++ b/lib/modules/edit/edit_buttons.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/collection/collection_preview_provider.dart'; -import 'package:otraku/collection/collection_providers.dart'; -import 'package:otraku/edit/edit_model.dart'; -import 'package:otraku/edit/edit_providers.dart'; -import 'package:otraku/filter/filter_providers.dart'; -import 'package:otraku/home/home_provider.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/collection/collection_preview_provider.dart'; +import 'package:otraku/modules/collection/collection_providers.dart'; +import 'package:otraku/modules/edit/edit_model.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; class EditButtons extends StatefulWidget { const EditButtons(this.tag, this.oldEdit, this.callback); diff --git a/lib/edit/edit_model.dart b/lib/modules/edit/edit_model.dart similarity index 97% rename from lib/edit/edit_model.dart rename to lib/modules/edit/edit_model.dart index 7723430d..e2883121 100644 --- a/lib/edit/edit_model.dart +++ b/lib/modules/edit/edit_model.dart @@ -1,6 +1,6 @@ -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/settings/settings_provider.dart'; -import 'package:otraku/utils/convert.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/convert.dart'; class Edit { Edit._({ diff --git a/lib/edit/edit_providers.dart b/lib/modules/edit/edit_providers.dart similarity index 87% rename from lib/edit/edit_providers.dart rename to lib/modules/edit/edit_providers.dart index 68170828..b27c636f 100644 --- a/lib/edit/edit_providers.dart +++ b/lib/modules/edit/edit_providers.dart @@ -1,10 +1,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/edit/edit_model.dart'; -import 'package:otraku/media/media_providers.dart'; -import 'package:otraku/settings/settings_provider.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/edit/edit_model.dart'; +import 'package:otraku/modules/media/media_providers.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; /// Updates an entry with an edit and returns the entry, or an error /// if unsuccessful. There is an api bug in entry updating, which prevents diff --git a/lib/edit/edit_view.dart b/lib/modules/edit/edit_view.dart similarity index 92% rename from lib/edit/edit_view.dart rename to lib/modules/edit/edit_view.dart index 41477262..67199294 100644 --- a/lib/edit/edit_view.dart +++ b/lib/modules/edit/edit_view.dart @@ -1,25 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/edit/edit_buttons.dart'; -import 'package:otraku/edit/edit_model.dart'; -import 'package:otraku/edit/edit_providers.dart'; -import 'package:otraku/media/media_constants.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/fields/date_field.dart'; -import 'package:otraku/widgets/fields/drop_down_field.dart'; -import 'package:otraku/widgets/fields/growable_text_field.dart'; -import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/fields/labeled_field.dart'; -import 'package:otraku/widgets/fields/number_field.dart'; -import 'package:otraku/edit/score_field.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/modules/collection/collection_models.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/edit/edit_buttons.dart'; +import 'package:otraku/modules/edit/edit_model.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/widgets/fields/checkbox_field.dart'; +import 'package:otraku/common/widgets/fields/date_field.dart'; +import 'package:otraku/common/widgets/fields/drop_down_field.dart'; +import 'package:otraku/common/widgets/fields/growable_text_field.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/fields/labeled_field.dart'; +import 'package:otraku/common/widgets/fields/number_field.dart'; +import 'package:otraku/modules/edit/score_field.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; /// A sheet for entry editing. Should be opened with [showSheet]. class EditView extends StatelessWidget { diff --git a/lib/edit/score_field.dart b/lib/modules/edit/score_field.dart similarity index 96% rename from lib/edit/score_field.dart rename to lib/modules/edit/score_field.dart index cbe476ee..b9ae3a00 100644 --- a/lib/edit/score_field.dart +++ b/lib/modules/edit/score_field.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/edit/edit_providers.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/settings/settings_provider.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; /// Score picker. class ScoreField extends StatelessWidget { diff --git a/lib/favorites/favorites_model.dart b/lib/modules/favorites/favorites_model.dart similarity index 90% rename from lib/favorites/favorites_model.dart rename to lib/modules/favorites/favorites_model.dart index bef59465..c574b53f 100644 --- a/lib/favorites/favorites_model.dart +++ b/lib/modules/favorites/favorites_model.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/studio/studio_models.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; class Favorites { const Favorites({ diff --git a/lib/favorites/favorites_provider.dart b/lib/modules/favorites/favorites_provider.dart similarity index 90% rename from lib/favorites/favorites_provider.dart rename to lib/modules/favorites/favorites_provider.dart index f091b549..58dbdb83 100644 --- a/lib/favorites/favorites_provider.dart +++ b/lib/modules/favorites/favorites_provider.dart @@ -1,13 +1,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/character/character_models.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/favorites/favorites_model.dart'; -import 'package:otraku/media/media_models.dart'; -import 'package:otraku/staff/staff_models.dart'; -import 'package:otraku/studio/studio_models.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; +import 'package:otraku/modules/character/character_models.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/favorites/favorites_model.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/modules/staff/staff_models.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; final favoritesProvider = StateNotifierProvider.autoDispose.family( diff --git a/lib/favorites/favorites_view.dart b/lib/modules/favorites/favorites_view.dart similarity index 81% rename from lib/favorites/favorites_view.dart rename to lib/modules/favorites/favorites_view.dart index af355906..fabd42eb 100644 --- a/lib/favorites/favorites_view.dart +++ b/lib/modules/favorites/favorites_view.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/favorites/favorites_model.dart'; -import 'package:otraku/studio/studio_models.dart'; -import 'package:otraku/favorites/favorites_provider.dart'; -import 'package:otraku/studio/studio_grid.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/widgets/grids/tile_item_grid.dart'; -import 'package:otraku/widgets/layouts/bottom_bar.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/paged_view.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/favorites/favorites_model.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; +import 'package:otraku/modules/favorites/favorites_provider.dart'; +import 'package:otraku/modules/studio/studio_grid.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/widgets/grids/tile_item_grid.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class FavoritesView extends ConsumerStatefulWidget { const FavoritesView(this.id); diff --git a/lib/feed/feed_view.dart b/lib/modules/feed/feed_view.dart similarity index 76% rename from lib/feed/feed_view.dart rename to lib/modules/feed/feed_view.dart index c31bdea0..e188cbf0 100644 --- a/lib/feed/feed_view.dart +++ b/lib/modules/feed/feed_view.dart @@ -1,17 +1,17 @@ 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/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/scaffolds.dart'; -import 'package:otraku/widgets/layouts/top_bar.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/activity/activities_providers.dart'; +import 'package:otraku/modules/activity/activities_view.dart'; +import 'package:otraku/modules/composition/composition_model.dart'; +import 'package:otraku/modules/composition/composition_view.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class FeedView extends StatelessWidget { const FeedView(this.scrollCtrl); diff --git a/lib/filter/chip_selector.dart b/lib/modules/filter/chip_selector.dart similarity index 98% rename from lib/filter/chip_selector.dart rename to lib/modules/filter/chip_selector.dart index e3594260..896dbb6d 100644 --- a/lib/filter/chip_selector.dart +++ b/lib/modules/filter/chip_selector.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/convert.dart'; +import 'package:otraku/common/utils/convert.dart'; /// A horizontal list of chips, where only one can be selected at a time. class ChipSelector extends StatefulWidget { diff --git a/lib/filter/filter_models.dart b/lib/modules/filter/filter_models.dart similarity index 96% rename from lib/filter/filter_models.dart rename to lib/modules/filter/filter_models.dart index aca0d0d7..c0435693 100644 --- a/lib/filter/filter_models.dart +++ b/lib/modules/filter/filter_models.dart @@ -1,5 +1,5 @@ -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/options.dart'; abstract class ApplicableMediaFilter> { ApplicableMediaFilter(this._ofAnime); diff --git a/lib/filter/filter_providers.dart b/lib/modules/filter/filter_providers.dart similarity index 84% rename from lib/filter/filter_providers.dart rename to lib/modules/filter/filter_providers.dart index 69206f61..c98406b5 100644 --- a/lib/filter/filter_providers.dart +++ b/lib/modules/filter/filter_providers.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/filter/filter_models.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/filter/filter_models.dart'; +import 'package:otraku/common/utils/options.dart'; final collectionFilterProvider = StateProvider.autoDispose.family( (ref, CollectionTag tag) => CollectionFilter(tag.ofAnime), diff --git a/lib/filter/filter_search_field.dart b/lib/modules/filter/filter_search_field.dart similarity index 93% rename from lib/filter/filter_search_field.dart rename to lib/modules/filter/filter_search_field.dart index ea6f617d..a8d119e4 100644 --- a/lib/filter/filter_search_field.dart +++ b/lib/modules/filter/filter_search_field.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/top_bar.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/common/widgets/fields/search_field.dart'; +import 'package:otraku/common/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. diff --git a/lib/filter/filter_view.dart b/lib/modules/filter/filter_view.dart similarity index 95% rename from lib/filter/filter_view.dart rename to lib/modules/filter/filter_view.dart index 7e3e8cf8..3a6236c4 100644 --- a/lib/filter/filter_view.dart +++ b/lib/modules/filter/filter_view.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/filter/chip_selector.dart'; -import 'package:otraku/filter/filter_models.dart'; -import 'package:otraku/filter/year_range_picker.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/tag/tag_models.dart'; -import 'package:otraku/tag/tag_provider.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/widgets/fields/checkbox_field.dart'; -import 'package:otraku/widgets/fields/search_field.dart'; -import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/grids/chip_grids.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/filter/chip_selector.dart'; +import 'package:otraku/modules/filter/filter_models.dart'; +import 'package:otraku/modules/filter/year_range_picker.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/tag/tag_models.dart'; +import 'package:otraku/modules/tag/tag_provider.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/widgets/fields/checkbox_field.dart'; +import 'package:otraku/common/widgets/fields/search_field.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/grids/chip_grids.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class _FilterView> extends StatefulWidget { const _FilterView({ diff --git a/lib/filter/year_range_picker.dart b/lib/modules/filter/year_range_picker.dart similarity index 100% rename from lib/filter/year_range_picker.dart rename to lib/modules/filter/year_range_picker.dart diff --git a/lib/home/home_provider.dart b/lib/modules/home/home_provider.dart similarity index 92% rename from lib/home/home_provider.dart rename to lib/modules/home/home_provider.dart index f1f79922..5c2c41b5 100644 --- a/lib/home/home_provider.dart +++ b/lib/modules/home/home_provider.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/activity/activities_providers.dart'; -import 'package:otraku/discover/discover_providers.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/activity/activities_providers.dart'; +import 'package:otraku/modules/discover/discover_providers.dart'; +import 'package:otraku/common/utils/options.dart'; final homeProvider = ChangeNotifierProvider.autoDispose((ref) => HomeNotifier()); diff --git a/lib/home/home_view.dart b/lib/modules/home/home_view.dart similarity index 84% rename from lib/home/home_view.dart rename to lib/modules/home/home_view.dart index 0fbd32a3..3f596bd0 100644 --- a/lib/home/home_view.dart +++ b/lib/modules/home/home_view.dart @@ -1,29 +1,29 @@ 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/collection/collection_models.dart'; -import 'package:otraku/collection/collection_preview_provider.dart'; -import 'package:otraku/collection/collection_preview_view.dart'; -import 'package:otraku/collection/collection_providers.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/discover/discover_providers.dart'; -import 'package:otraku/filter/filter_providers.dart'; -import 'package:otraku/home/home_provider.dart'; -import 'package:otraku/settings/settings_provider.dart'; -import 'package:otraku/tag/tag_provider.dart'; -import 'package:otraku/user/user_providers.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/discover/discover_view.dart'; -import 'package:otraku/collection/collection_view.dart'; -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/scaffolds.dart'; -import 'package:otraku/widgets/layouts/direct_page_view.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; +import 'package:otraku/modules/activity/activities_providers.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/collection/collection_preview_provider.dart'; +import 'package:otraku/modules/collection/collection_preview_view.dart'; +import 'package:otraku/modules/collection/collection_providers.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/discover/discover_providers.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/modules/tag/tag_provider.dart'; +import 'package:otraku/modules/user/user_providers.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/modules/discover/discover_view.dart'; +import 'package:otraku/modules/collection/collection_view.dart'; +import 'package:otraku/modules/feed/feed_view.dart'; +import 'package:otraku/modules/user/user_view.dart'; +import 'package:otraku/common/utils/background_handler.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; class HomeView extends ConsumerStatefulWidget { const HomeView(this.id); diff --git a/lib/media/media_action_buttons.dart b/lib/modules/media/media_action_buttons.dart similarity index 89% rename from lib/media/media_action_buttons.dart rename to lib/modules/media/media_action_buttons.dart index 62c2fa61..2ee086b4 100644 --- a/lib/media/media_action_buttons.dart +++ b/lib/modules/media/media_action_buttons.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.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_models.dart'; -import 'package:otraku/media/media_providers.dart'; -import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/modules/edit/edit_view.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/modules/media/media_providers.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class MediaEditButton extends StatefulWidget { const MediaEditButton(this.media); diff --git a/lib/media/media_constants.dart b/lib/modules/media/media_constants.dart similarity index 100% rename from lib/media/media_constants.dart rename to lib/modules/media/media_constants.dart diff --git a/lib/media/media_grids.dart b/lib/modules/media/media_grids.dart similarity index 96% rename from lib/media/media_grids.dart rename to lib/modules/media/media_grids.dart index c69a8893..e00cb6ae 100644 --- a/lib/media/media_grids.dart +++ b/lib/modules/media/media_grids.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.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/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'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/modules/media/media_providers.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/text_rail.dart'; class MediaRelatedGrid extends StatelessWidget { const MediaRelatedGrid(this.items); diff --git a/lib/media/media_header.dart b/lib/modules/media/media_header.dart similarity index 94% rename from lib/media/media_header.dart rename to lib/modules/media/media_header.dart index e8348f51..52d37b70 100644 --- a/lib/media/media_header.dart +++ b/lib/modules/media/media_header.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.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/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'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/modules/media/media_providers.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; +import 'package:otraku/common/widgets/text_rail.dart'; class MediaHeader extends StatelessWidget { const MediaHeader(this.id, this.coverUrl, this.tabCtrl); diff --git a/lib/media/media_info_view.dart b/lib/modules/media/media_info_view.dart similarity index 94% rename from lib/media/media_info_view.dart rename to lib/modules/media/media_info_view.dart index 8f3a3ddf..414b8842 100644 --- a/lib/media/media_info_view.dart +++ b/lib/modules/media/media_info_view.dart @@ -1,18 +1,18 @@ 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/discover/discover_models.dart'; -import 'package:otraku/filter/filter_providers.dart'; -import 'package:otraku/home/home_provider.dart'; -import 'package:otraku/home/home_view.dart'; -import 'package:otraku/media/media_models.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/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/filter/filter_providers.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/modules/home/home_view.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class MediaInfoView extends StatelessWidget { const MediaInfoView(this.media, this.scrollCtrl); diff --git a/lib/media/media_models.dart b/lib/modules/media/media_models.dart similarity index 95% rename from lib/media/media_models.dart rename to lib/modules/media/media_models.dart index c85f8139..47afb5b1 100644 --- a/lib/media/media_models.dart +++ b/lib/modules/media/media_models.dart @@ -1,13 +1,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/edit/edit_model.dart'; -import 'package:otraku/tag/tag_models.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/edit/edit_model.dart'; +import 'package:otraku/modules/tag/tag_models.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/options.dart'; TileItem mediaItem(Map map) => TileItem( id: map['id'], diff --git a/lib/media/media_providers.dart b/lib/modules/media/media_providers.dart similarity index 93% rename from lib/media/media_providers.dart rename to lib/modules/media/media_providers.dart index f881f7e3..1fb9ce44 100644 --- a/lib/media/media_providers.dart +++ b/lib/modules/media/media_providers.dart @@ -1,13 +1,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/edit/edit_model.dart'; -import 'package:otraku/media/media_models.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/settings/settings_provider.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/edit/edit_model.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; Future toggleFavoriteMedia(int id, bool isAnime) async { try { diff --git a/lib/media/media_view.dart b/lib/modules/media/media_view.dart similarity index 89% rename from lib/media/media_view.dart rename to lib/modules/media/media_view.dart index 73f4739b..cdedad9e 100644 --- a/lib/media/media_view.dart +++ b/lib/modules/media/media_view.dart @@ -1,21 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/media/media_action_buttons.dart'; -import 'package:otraku/media/media_grids.dart'; -import 'package:otraku/media/media_models.dart'; -import 'package:otraku/media/media_providers.dart'; -import 'package:otraku/statistics/charts.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/media/media_info_view.dart'; -import 'package:otraku/widgets/grids/relation_grid.dart'; -import 'package:otraku/widgets/layouts/constrained_view.dart'; -import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/layouts/scaffolds.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/media/media_header.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/paged_view.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/modules/media/media_action_buttons.dart'; +import 'package:otraku/modules/media/media_grids.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/modules/media/media_providers.dart'; +import 'package:otraku/modules/statistics/charts.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/modules/media/media_info_view.dart'; +import 'package:otraku/common/widgets/grids/relation_grid.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/modules/media/media_header.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class MediaView extends StatefulWidget { const MediaView(this.id, this.coverUrl); diff --git a/lib/notifications/notification_model.dart b/lib/modules/notification/notification_model.dart similarity index 98% rename from lib/notifications/notification_model.dart rename to lib/modules/notification/notification_model.dart index 5d6f7818..e5316ea6 100644 --- a/lib/notifications/notification_model.dart +++ b/lib/modules/notification/notification_model.dart @@ -1,6 +1,6 @@ -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/options.dart'; enum NotificationFilterType { all('All'), diff --git a/lib/notifications/notification_provider.dart b/lib/modules/notification/notification_provider.dart similarity index 87% rename from lib/notifications/notification_provider.dart rename to lib/modules/notification/notification_provider.dart index 1ae30eef..30c0bce9 100644 --- a/lib/notifications/notification_provider.dart +++ b/lib/modules/notification/notification_provider.dart @@ -1,8 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/notifications/notification_model.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; +import 'package:otraku/modules/notification/notification_model.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; final notificationFilterProvider = StateProvider.autoDispose( (ref) => NotificationFilterType.all, diff --git a/lib/notifications/notifications_view.dart b/lib/modules/notification/notifications_view.dart similarity index 91% rename from lib/notifications/notifications_view.dart rename to lib/modules/notification/notifications_view.dart index 21a8d794..3b07c9fc 100644 --- a/lib/notifications/notifications_view.dart +++ b/lib/modules/notification/notifications_view.dart @@ -1,25 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/edit/edit_providers.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/notifications/notification_model.dart'; -import 'package:otraku/notifications/notification_provider.dart'; -import 'package:otraku/utils/background_handler.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/utils/route_arg.dart'; -import 'package:otraku/edit/edit_view.dart'; -import 'package:otraku/widgets/layouts/top_bar.dart'; -import 'package:otraku/widgets/link_tile.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/scaffolds.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/paged_view.dart'; +import 'package:otraku/modules/edit/edit_providers.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/notification/notification_model.dart'; +import 'package:otraku/modules/notification/notification_provider.dart'; +import 'package:otraku/common/utils/background_handler.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/modules/edit/edit_view.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/html_content.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class NotificationsView extends ConsumerStatefulWidget { const NotificationsView(); diff --git a/lib/review/review_grid.dart b/lib/modules/review/review_grid.dart similarity index 90% rename from lib/review/review_grid.dart rename to lib/modules/review/review_grid.dart index f778d747..048e728f 100644 --- a/lib/review/review_grid.dart +++ b/lib/modules/review/review_grid.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -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/cached_image.dart'; -import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/review/review_models.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; class ReviewGrid extends StatelessWidget { const ReviewGrid(this.items); diff --git a/lib/review/review_header.dart b/lib/modules/review/review_header.dart similarity index 95% rename from lib/review/review_header.dart rename to lib/modules/review/review_header.dart index 72d7fed0..a5bb5ee0 100644 --- a/lib/review/review_header.dart +++ b/lib/modules/review/review_header.dart @@ -1,11 +1,11 @@ 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/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/common/utils/consts.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class ReviewHeader extends StatelessWidget { const ReviewHeader({ diff --git a/lib/review/review_models.dart b/lib/modules/review/review_models.dart similarity index 97% rename from lib/review/review_models.dart rename to lib/modules/review/review_models.dart index f940fd6f..95023a5e 100644 --- a/lib/review/review_models.dart +++ b/lib/modules/review/review_models.dart @@ -1,5 +1,5 @@ -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/options.dart'; class ReviewItem { ReviewItem._({ diff --git a/lib/review/review_providers.dart b/lib/modules/review/review_providers.dart similarity index 92% rename from lib/review/review_providers.dart rename to lib/modules/review/review_providers.dart index 5753dcbe..a1068deb 100644 --- a/lib/review/review_providers.dart +++ b/lib/modules/review/review_providers.dart @@ -1,8 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/review/review_models.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; +import 'package:otraku/modules/review/review_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; final reviewProvider = StateNotifierProvider.autoDispose .family, int>( diff --git a/lib/review/review_view.dart b/lib/modules/review/review_view.dart similarity index 95% rename from lib/review/review_view.dart rename to lib/modules/review/review_view.dart index 5571e63f..d93b8fbe 100644 --- a/lib/review/review_view.dart +++ b/lib/modules/review/review_view.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/review/review_header.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/link_tile.dart'; -import 'package:otraku/widgets/html_content.dart'; +import 'package:otraku/modules/review/review_header.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/review/review_providers.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/html_content.dart'; class ReviewView extends StatelessWidget { const ReviewView(this.id, this.bannerUrl); diff --git a/lib/review/reviews_view.dart b/lib/modules/review/reviews_view.dart similarity index 81% rename from lib/review/reviews_view.dart rename to lib/modules/review/reviews_view.dart index 51e46725..cc66cf82 100644 --- a/lib/review/reviews_view.dart +++ b/lib/modules/review/reviews_view.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/review/review_models.dart'; -import 'package:otraku/review/review_providers.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/review/review_grid.dart'; -import 'package:otraku/widgets/layouts/floating_bar.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/paged_view.dart'; +import 'package:otraku/modules/review/review_models.dart'; +import 'package:otraku/modules/review/review_providers.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/modules/review/review_grid.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class ReviewsView extends ConsumerStatefulWidget { const ReviewsView(this.id); diff --git a/lib/settings/settings_about_tab.dart b/lib/modules/settings/settings_about_tab.dart similarity index 92% rename from lib/settings/settings_about_tab.dart rename to lib/modules/settings/settings_about_tab.dart index 7fe4c1e2..d4c0bf1d 100644 --- a/lib/settings/settings_about_tab.dart +++ b/lib/modules/settings/settings_about_tab.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/widgets/cached_image.dart'; -import 'package:otraku/widgets/layouts/scaffolds.dart'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class SettingsAboutTab extends StatelessWidget { const SettingsAboutTab(this.scrollCtrl); diff --git a/lib/settings/settings_app_tab.dart b/lib/modules/settings/settings_app_tab.dart similarity index 90% rename from lib/settings/settings_app_tab.dart rename to lib/modules/settings/settings_app_tab.dart index f84bdfae..e0d8ac4a 100644 --- a/lib/settings/settings_app_tab.dart +++ b/lib/modules/settings/settings_app_tab.dart @@ -1,19 +1,19 @@ 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'; -import 'package:otraku/utils/options.dart'; -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/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'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/filter/chip_selector.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/modules/home/home_view.dart'; +import 'package:otraku/common/widgets/fields/checkbox_field.dart'; +import 'package:otraku/common/widgets/fields/drop_down_field.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/segment_switcher.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/modules/settings/theme_preview.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class SettingsAppTab extends StatelessWidget { const SettingsAppTab(this.scrollCtrl); diff --git a/lib/settings/settings_content_tab.dart b/lib/modules/settings/settings_content_tab.dart similarity index 91% rename from lib/settings/settings_content_tab.dart rename to lib/modules/settings/settings_content_tab.dart index 3d966157..d905026e 100644 --- a/lib/settings/settings_content_tab.dart +++ b/lib/modules/settings/settings_content_tab.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/media/media_constants.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/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/scaffolds.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/widgets/fields/checkbox_field.dart'; +import 'package:otraku/common/widgets/fields/drop_down_field.dart'; +import 'package:otraku/common/widgets/grids/chip_grids.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; class SettingsContentTab extends StatelessWidget { const SettingsContentTab(this.scrollCtrl, this.settings, this.scheduleUpdate); diff --git a/lib/settings/settings_notifications_tab.dart b/lib/modules/settings/settings_notifications_tab.dart similarity index 80% rename from lib/settings/settings_notifications_tab.dart rename to lib/modules/settings/settings_notifications_tab.dart index 332884de..d9caf4f5 100644 --- a/lib/settings/settings_notifications_tab.dart +++ b/lib/modules/settings/settings_notifications_tab.dart @@ -1,9 +1,9 @@ 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/scaffolds.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/widgets/fields/checkbox_field.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; class SettingsNotificationsTab extends StatelessWidget { const SettingsNotificationsTab( diff --git a/lib/settings/settings_provider.dart b/lib/modules/settings/settings_provider.dart similarity index 96% rename from lib/settings/settings_provider.dart rename to lib/modules/settings/settings_provider.dart index dd00fedd..369f1507 100644 --- a/lib/settings/settings_provider.dart +++ b/lib/modules/settings/settings_provider.dart @@ -1,9 +1,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/notifications/notification_model.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; +import 'package:otraku/modules/collection/collection_models.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/notification/notification_model.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; final settingsProvider = StateNotifierProvider>( diff --git a/lib/settings/settings_view.dart b/lib/modules/settings/settings_view.dart similarity index 77% rename from lib/settings/settings_view.dart rename to lib/modules/settings/settings_view.dart index 9d01272e..ed156277 100644 --- a/lib/settings/settings_view.dart +++ b/lib/modules/settings/settings_view.dart @@ -1,21 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/collection/collection_models.dart'; -import 'package:otraku/collection/collection_providers.dart'; -import 'package:otraku/settings/settings_provider.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/settings/settings_app_tab.dart'; -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/constrained_view.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/modules/collection/collection_models.dart'; +import 'package:otraku/modules/collection/collection_providers.dart'; +import 'package:otraku/modules/settings/settings_provider.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/modules/settings/settings_app_tab.dart'; +import 'package:otraku/modules/settings/settings_content_tab.dart'; +import 'package:otraku/modules/settings/settings_notifications_tab.dart'; +import 'package:otraku/modules/settings/settings_about_tab.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; class SettingsView extends ConsumerStatefulWidget { const SettingsView(); diff --git a/lib/settings/theme_preview.dart b/lib/modules/settings/theme_preview.dart similarity index 97% rename from lib/settings/theme_preview.dart rename to lib/modules/settings/theme_preview.dart index 354d8f9c..c5011f22 100644 --- a/lib/settings/theme_preview.dart +++ b/lib/modules/settings/theme_preview.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/home/home_provider.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/utils/theming.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/home/home_provider.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/utils/theming.dart'; class ThemePreview extends StatefulWidget { const ThemePreview(); diff --git a/lib/social/social_model.dart b/lib/modules/social/social_model.dart similarity index 87% rename from lib/social/social_model.dart rename to lib/modules/social/social_model.dart index e9e656e2..ea8ff975 100644 --- a/lib/social/social_model.dart +++ b/lib/modules/social/social_model.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/user/user_models.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/modules/user/user_models.dart'; class Social { const Social({ diff --git a/lib/social/social_provider.dart b/lib/modules/social/social_provider.dart similarity index 90% rename from lib/social/social_provider.dart rename to lib/modules/social/social_provider.dart index 06597565..b982967d 100644 --- a/lib/social/social_provider.dart +++ b/lib/modules/social/social_provider.dart @@ -1,9 +1,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/social/social_model.dart'; -import 'package:otraku/user/user_models.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; +import 'package:otraku/modules/social/social_model.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; final socialProvider = StateNotifierProvider.autoDispose.family( diff --git a/lib/social/social_view.dart b/lib/modules/social/social_view.dart similarity index 80% rename from lib/social/social_view.dart rename to lib/modules/social/social_view.dart index 7df09dc9..0760e6d3 100644 --- a/lib/social/social_view.dart +++ b/lib/modules/social/social_view.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/social/social_model.dart'; -import 'package:otraku/user/user_models.dart'; -import 'package:otraku/social/social_provider.dart'; -import 'package:otraku/user/user_grid.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/widgets/layouts/bottom_bar.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/paged_view.dart'; +import 'package:otraku/modules/social/social_model.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/modules/social/social_provider.dart'; +import 'package:otraku/modules/user/user_grid.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class SocialView extends ConsumerStatefulWidget { const SocialView(this.id); diff --git a/lib/staff/staff_action_buttons.dart b/lib/modules/staff/staff_action_buttons.dart similarity index 89% rename from lib/staff/staff_action_buttons.dart rename to lib/modules/staff/staff_action_buttons.dart index fd7ab93a..9e7e6cfc 100644 --- a/lib/staff/staff_action_buttons.dart +++ b/lib/modules/staff/staff_action_buttons.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/filter/chip_selector.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/staff/staff_models.dart'; -import 'package:otraku/staff/staff_providers.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/widgets/layouts/floating_bar.dart'; -import 'package:otraku/widgets/overlays/sheets.dart'; +import 'package:otraku/modules/filter/chip_selector.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/staff/staff_models.dart'; +import 'package:otraku/modules/staff/staff_providers.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; class StaffFavoriteButton extends StatefulWidget { const StaffFavoriteButton(this.data); diff --git a/lib/staff/staff_info_tab.dart b/lib/modules/staff/staff_info_tab.dart similarity index 92% rename from lib/staff/staff_info_tab.dart rename to lib/modules/staff/staff_info_tab.dart index f0506a3c..850f7d99 100644 --- a/lib/staff/staff_info_tab.dart +++ b/lib/modules/staff/staff_info_tab.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/staff/staff_providers.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/constrained_view.dart'; -import 'package:otraku/widgets/loaders.dart/loaders.dart'; -import 'package:otraku/widgets/overlays/dialogs.dart'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/staff/staff_providers.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/html_content.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class StaffInfoTab extends StatelessWidget { const StaffInfoTab(this.id, this.imageUrl, this.scrollCtrl); diff --git a/lib/staff/staff_models.dart b/lib/modules/staff/staff_models.dart similarity index 89% rename from lib/staff/staff_models.dart rename to lib/modules/staff/staff_models.dart index 5c1085d6..f6e39a8c 100644 --- a/lib/staff/staff_models.dart +++ b/lib/modules/staff/staff_models.dart @@ -1,10 +1,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/utils/convert.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/utils/convert.dart'; TileItem staffItem(Map map) => TileItem( id: map['id'], diff --git a/lib/staff/staff_providers.dart b/lib/modules/staff/staff_providers.dart similarity index 91% rename from lib/staff/staff_providers.dart rename to lib/modules/staff/staff_providers.dart index b65d2d58..9ec8521f 100644 --- a/lib/staff/staff_providers.dart +++ b/lib/modules/staff/staff_providers.dart @@ -1,12 +1,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/staff/staff_models.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; -import 'package:otraku/utils/options.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/modules/staff/staff_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; +import 'package:otraku/common/utils/options.dart'; /// Favorite/Unfavorite staff. Returns `true` if successful. Future toggleFavoriteStaff(int staffId) async { diff --git a/lib/staff/staff_view.dart b/lib/modules/staff/staff_view.dart similarity index 78% rename from lib/staff/staff_view.dart rename to lib/modules/staff/staff_view.dart index 142f3824..3b129b6d 100644 --- a/lib/staff/staff_view.dart +++ b/lib/modules/staff/staff_view.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/common/relation.dart'; -import 'package:otraku/staff/staff_action_buttons.dart'; -import 'package:otraku/staff/staff_info_tab.dart'; -import 'package:otraku/staff/staff_providers.dart'; -import 'package:otraku/utils/paged_controller.dart'; -import 'package:otraku/widgets/grids/relation_grid.dart'; -import 'package:otraku/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/widgets/layouts/floating_bar.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'; -import 'package:otraku/widgets/paged_view.dart'; +import 'package:otraku/common/models/relation.dart'; +import 'package:otraku/modules/staff/staff_action_buttons.dart'; +import 'package:otraku/modules/staff/staff_info_tab.dart'; +import 'package:otraku/modules/staff/staff_providers.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/widgets/grids/relation_grid.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/paged_view.dart'; class StaffView extends ConsumerStatefulWidget { const StaffView(this.id, this.imageUrl); diff --git a/lib/statistics/charts.dart b/lib/modules/statistics/charts.dart similarity index 99% rename from lib/statistics/charts.dart rename to lib/modules/statistics/charts.dart index e07ff14a..5bcffba9 100644 --- a/lib/statistics/charts.dart +++ b/lib/modules/statistics/charts.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:otraku/utils/consts.dart'; +import 'package:otraku/common/utils/consts.dart'; class BarChart extends StatelessWidget { const BarChart({ diff --git a/lib/statistics/statistics_view.dart b/lib/modules/statistics/statistics_view.dart similarity index 91% rename from lib/statistics/statistics_view.dart rename to lib/modules/statistics/statistics_view.dart index fdbce616..8dedc804 100644 --- a/lib/statistics/statistics_view.dart +++ b/lib/modules/statistics/statistics_view.dart @@ -1,20 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/statistics/user_statistics.dart'; -import 'package:otraku/user/user_models.dart'; -import 'package:otraku/user/user_providers.dart'; -import 'package:otraku/utils/paged_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/constrained_view.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'; +import 'package:otraku/modules/statistics/user_statistics.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/modules/user/user_providers.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/modules/statistics/charts.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/layouts/segment_switcher.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; class StatisticsView extends StatefulWidget { const StatisticsView(this.id); diff --git a/lib/statistics/user_statistics.dart b/lib/modules/statistics/user_statistics.dart similarity index 98% rename from lib/statistics/user_statistics.dart rename to lib/modules/statistics/user_statistics.dart index 8769a490..2bcfb9d5 100644 --- a/lib/statistics/user_statistics.dart +++ b/lib/modules/statistics/user_statistics.dart @@ -1,4 +1,4 @@ -import 'package:otraku/utils/convert.dart'; +import 'package:otraku/common/utils/convert.dart'; class UserStatistics { UserStatistics._({ diff --git a/lib/studio/studio_grid.dart b/lib/modules/studio/studio_grid.dart similarity index 77% rename from lib/studio/studio_grid.dart rename to lib/modules/studio/studio_grid.dart index bb255347..fccea429 100644 --- a/lib/studio/studio_grid.dart +++ b/lib/modules/studio/studio_grid.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:otraku/discover/discover_models.dart'; -import 'package:otraku/studio/studio_models.dart'; -import 'package:otraku/widgets/link_tile.dart'; -import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; class StudioGrid extends StatelessWidget { const StudioGrid(this.items); diff --git a/lib/studio/studio_models.dart b/lib/modules/studio/studio_models.dart similarity index 91% rename from lib/studio/studio_models.dart rename to lib/modules/studio/studio_models.dart index 4ef0286f..483194dd 100644 --- a/lib/studio/studio_models.dart +++ b/lib/modules/studio/studio_models.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/common/paged.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/common/models/paged.dart'; class StudioItem { StudioItem._({required this.id, required this.name}); diff --git a/lib/studio/studio_providers.dart b/lib/modules/studio/studio_providers.dart similarity index 87% rename from lib/studio/studio_providers.dart rename to lib/modules/studio/studio_providers.dart index d4038361..dfbe5462 100644 --- a/lib/studio/studio_providers.dart +++ b/lib/modules/studio/studio_providers.dart @@ -1,11 +1,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/common/tile_item.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/media/media_models.dart'; -import 'package:otraku/studio/studio_models.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; -import 'package:otraku/common/paged.dart'; +import 'package:otraku/common/models/tile_item.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/media/media_models.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; +import 'package:otraku/common/models/paged.dart'; /// Favorite/Unfavorite studio. Returns `true` if successful. Future toggleFavoriteStudio(int studioId) async { diff --git a/lib/studio/studio_view.dart b/lib/modules/studio/studio_view.dart similarity index 90% rename from lib/studio/studio_view.dart rename to lib/modules/studio/studio_view.dart index 4e1b9362..4124d64b 100644 --- a/lib/studio/studio_view.dart +++ b/lib/modules/studio/studio_view.dart @@ -1,22 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/filter/chip_selector.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/media/media_constants.dart'; -import 'package:otraku/studio/studio_models.dart'; -import 'package:otraku/studio/studio_providers.dart'; -import 'package:otraku/utils/convert.dart'; -import 'package:otraku/utils/paged_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/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'; -import 'package:otraku/widgets/overlays/toast.dart'; +import 'package:otraku/modules/filter/chip_selector.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/media/media_constants.dart'; +import 'package:otraku/modules/studio/studio_models.dart'; +import 'package:otraku/modules/studio/studio_providers.dart'; +import 'package:otraku/common/utils/convert.dart'; +import 'package:otraku/common/utils/paged_controller.dart'; +import 'package:otraku/common/widgets/grids/tile_item_grid.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/layouts/floating_bar.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; class StudioView extends ConsumerStatefulWidget { const StudioView(this.id, this.name); diff --git a/lib/tag/tag_models.dart b/lib/modules/tag/tag_models.dart similarity index 100% rename from lib/tag/tag_models.dart rename to lib/modules/tag/tag_models.dart diff --git a/lib/tag/tag_provider.dart b/lib/modules/tag/tag_provider.dart similarity index 78% rename from lib/tag/tag_provider.dart rename to lib/modules/tag/tag_provider.dart index 2b60f3f6..83161b23 100644 --- a/lib/tag/tag_provider.dart +++ b/lib/modules/tag/tag_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/tag/tag_models.dart'; -import 'package:otraku/utils/api.dart'; +import 'package:otraku/modules/tag/tag_models.dart'; +import 'package:otraku/common/utils/api.dart'; final tagsProvider = FutureProvider( (ref) async { diff --git a/lib/user/user_grid.dart b/lib/modules/user/user_grid.dart similarity index 79% rename from lib/user/user_grid.dart rename to lib/modules/user/user_grid.dart index 86aa28ed..d9d41b5d 100644 --- a/lib/user/user_grid.dart +++ b/lib/modules/user/user_grid.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -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/cached_image.dart'; -import 'package:otraku/widgets/grids/sliver_grid_delegates.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/common/widgets/link_tile.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; class UserGrid extends StatelessWidget { const UserGrid(this.items); diff --git a/lib/user/user_header.dart b/lib/modules/user/user_header.dart similarity index 94% rename from lib/user/user_header.dart rename to lib/modules/user/user_header.dart index c22b358d..37f60b0b 100644 --- a/lib/user/user_header.dart +++ b/lib/modules/user/user_header.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.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/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'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/modules/user/user_providers.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/widgets/cached_image.dart'; +import 'package:otraku/common/widgets/layouts/top_bar.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/common/widgets/overlays/sheets.dart'; +import 'package:otraku/common/widgets/overlays/toast.dart'; +import 'package:otraku/common/widgets/text_rail.dart'; class UserHeader extends StatelessWidget { const UserHeader({ diff --git a/lib/user/user_models.dart b/lib/modules/user/user_models.dart similarity index 94% rename from lib/user/user_models.dart rename to lib/modules/user/user_models.dart index 0273f686..48e2a051 100644 --- a/lib/user/user_models.dart +++ b/lib/modules/user/user_models.dart @@ -1,5 +1,5 @@ -import 'package:otraku/statistics/user_statistics.dart'; -import 'package:otraku/utils/convert.dart'; +import 'package:otraku/modules/statistics/user_statistics.dart'; +import 'package:otraku/common/utils/convert.dart'; class UserItem { UserItem._({required this.id, required this.name, required this.imageUrl}); diff --git a/lib/user/user_providers.dart b/lib/modules/user/user_providers.dart similarity index 76% rename from lib/user/user_providers.dart rename to lib/modules/user/user_providers.dart index 7100398e..0b9a1bf6 100644 --- a/lib/user/user_providers.dart +++ b/lib/modules/user/user_providers.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:otraku/user/user_models.dart'; -import 'package:otraku/utils/api.dart'; -import 'package:otraku/utils/graphql.dart'; +import 'package:otraku/modules/user/user_models.dart'; +import 'package:otraku/common/utils/api.dart'; +import 'package:otraku/common/utils/graphql.dart'; /// Follow/Unfollow user. Returns `true` if successful. Future toggleFollow(int userId) async { diff --git a/lib/user/user_view.dart b/lib/modules/user/user_view.dart similarity index 89% rename from lib/user/user_view.dart rename to lib/modules/user/user_view.dart index a71cde5d..3bd857e2 100644 --- a/lib/user/user_view.dart +++ b/lib/modules/user/user_view.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/user/user_models.dart'; -import 'package:otraku/user/user_providers.dart'; -import 'package:otraku/user/user_header.dart'; -import 'package:otraku/utils/options.dart'; -import 'package:otraku/utils/route_arg.dart'; -import 'package:otraku/widgets/html_content.dart'; -import 'package:otraku/utils/consts.dart'; -import 'package:otraku/widgets/layouts/constrained_view.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/modules/user/user_models.dart'; +import 'package:otraku/modules/user/user_providers.dart'; +import 'package:otraku/modules/user/user_header.dart'; +import 'package:otraku/common/utils/options.dart'; +import 'package:otraku/common/utils/route_arg.dart'; +import 'package:otraku/common/widgets/html_content.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/layouts/constrained_view.dart'; +import 'package:otraku/common/widgets/layouts/scaffolds.dart'; +import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; class UserView extends StatelessWidget { const UserView(this.id, this.avatarUrl); From 8ba943625506415c88741d9ff600e3f7b3c690bf Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sun, 7 May 2023 23:41:27 +0300 Subject: [PATCH 29/55] Improved review header --- lib/modules/review/review_header.dart | 263 +++++++++++++------------- lib/modules/review/review_view.dart | 186 +++++++++--------- 2 files changed, 219 insertions(+), 230 deletions(-) diff --git a/lib/modules/review/review_header.dart b/lib/modules/review/review_header.dart index a5bb5ee0..9ce6d5ee 100644 --- a/lib/modules/review/review_header.dart +++ b/lib/modules/review/review_header.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/common/utils/consts.dart'; import 'package:otraku/common/widgets/cached_image.dart'; @@ -24,18 +23,31 @@ class ReviewHeader extends StatelessWidget { Widget build(BuildContext context) { return SliverPersistentHeader( pinned: true, - delegate: _HeaderDelegate(id, bannerUrl, mediaTitle, siteUrl), + delegate: _Delegate( + id, + bannerUrl, + mediaTitle, + siteUrl, + MediaQuery.of(context).padding.top, + ), ); } } -class _HeaderDelegate extends SliverPersistentHeaderDelegate { - _HeaderDelegate(this.id, this.bannerUrl, this.title, this.siteUrl); +class _Delegate extends SliverPersistentHeaderDelegate { + _Delegate( + this.id, + this.bannerUrl, + this.title, + this.siteUrl, + this.topOffset, + ); final int id; final String? bannerUrl; final String? title; final String? siteUrl; + final double topOffset; @override Widget build( @@ -43,164 +55,145 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate { double shrinkOffset, bool overlapsContent, ) { - final extent = maxExtent - shrinkOffset; - final opacity = shrinkOffset < (maxExtent - minExtent) - ? shrinkOffset / (maxExtent - minExtent) - : 1.0; + final theme = Theme.of(context); + var transition = shrinkOffset / _bannerBaseHeight; + if (transition > 1) transition = 1; - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - ), - 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: [ - if (bannerUrl != null) - Expanded( - child: GestureDetector( - child: Hero(tag: id, child: CachedImage(bannerUrl!)), - onTap: () => - showPopUp(context, ImageDialog(bannerUrl!)), - ), + final body = Stack( + fit: StackFit.expand, + children: [ + if (transition < 1) ...[ + Positioned.fill( + child: bannerUrl != null + ? GestureDetector( + child: Hero(tag: id, child: CachedImage(bannerUrl!)), + onTap: () => showPopUp(context, ImageDialog(bannerUrl!)), + ) + : DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant, ), - - /// An annoying workaround for a bug in the - /// anti-aliasing of the overlaying [DecoratedBox]. - Container( - color: Theme.of(context).colorScheme.background, - height: 1, ), - ], + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + height: 15, + child: Container( + 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: 0, - right: 0, - height: maxExtent * 0.4, - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(0), - ], - ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: topOffset + Consts.tapTargetSize, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.background, + theme.colorScheme.background.withAlpha(200), + theme.colorScheme.background.withAlpha(0), + ], ), ), ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: topOffset + Consts.tapTargetSize, + child: Opacity( + opacity: transition, child: DecoratedBox( decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withAlpha(0), - ], - ), + color: theme.colorScheme.background, ), ), ), - 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( + left: 0, + right: 0, + top: topOffset, + height: Consts.tapTargetSize, + child: Row( + children: [ + TopBarIcon( + tooltip: 'Close', + icon: Ionicons.chevron_back_outline, + onTap: Navigator.of(context).pop, ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - height: minExtent, - child: Row( - children: [ - TopBarIcon( - tooltip: 'Close', - icon: Ionicons.chevron_back_outline, - onTap: Navigator.of(context).pop, - ), - if (title != null) - Expanded( - child: Opacity( - opacity: opacity, + Expanded( + child: title != null + ? Opacity( + opacity: transition, child: Text( title!, - style: Theme.of(context).textTheme.titleMedium, + style: theme.textTheme.titleMedium, overflow: TextOverflow.ellipsis, ), - ), - ), - if (siteUrl != null) - TopBarIcon( - tooltip: 'More', - icon: Ionicons.ellipsis_horizontal, - onTap: () => showSheet( - context, - GradientSheet.link(context, siteUrl!), - ), - ), - ], + ) + : const SizedBox(), ), - ), - ], + if (siteUrl != null) + TopBarIcon( + tooltip: 'More', + icon: Ionicons.ellipsis_horizontal, + onTap: () => showSheet( + context, + GradientSheet.link(context, siteUrl!), + ), + ), + ], + ), ), - ), + ], ); - } - - @override - double get maxExtent => 150; - - @override - double get minExtent => Consts.tapTargetSize; - @override - OverScrollHeaderStretchConfiguration? get stretchConfiguration => - OverScrollHeaderStretchConfiguration(stretchTriggerOffset: 100); + return transition < 1 + ? body + : ClipRect( + child: BackdropFilter( + filter: Consts.blurFilter, + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.bottomAppBarTheme.color, + ), + child: body, + ), + ), + ); + } - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => - true; + static const _bannerBaseHeight = 80.0; @override - PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => - null; + double get minExtent => topOffset + Consts.tapTargetSize; @override - FloatingHeaderSnapConfiguration? get snapConfiguration => null; + double get maxExtent => topOffset + Consts.tapTargetSize + _bannerBaseHeight; @override - TickerProvider? get vsync => null; + bool shouldRebuild(covariant _Delegate oldDelegate) => + title != oldDelegate.title; } diff --git a/lib/modules/review/review_view.dart b/lib/modules/review/review_view.dart index d93b8fbe..279e1cc5 100644 --- a/lib/modules/review/review_view.dart +++ b/lib/modules/review/review_view.dart @@ -16,111 +16,107 @@ class ReviewView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea( - bottom: false, - child: Consumer(builder: (context, ref, _) { - final data = ref.watch(reviewProvider(id).select((s) => s.value)); + body: Consumer(builder: (context, ref, _) { + final data = ref.watch(reviewProvider(id).select((s) => s.value)); - return CustomScrollView( - slivers: [ - ReviewHeader( - id: id, - bannerUrl: bannerUrl, - mediaTitle: data?.mediaTitle, - siteUrl: data?.siteUrl, - ), - if (data != null) - SliverPadding( - padding: EdgeInsets.only( - top: 15, - left: 10, - right: 10, - bottom: MediaQuery.of(context).viewPadding.bottom + 10, - ), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed([ - GestureDetector( - onTap: () => LinkTile.openView( - context: context, - id: data.mediaId, - imageUrl: data.mediaCover, - discoverType: DiscoverType.anime, - ), - child: Text( - data.mediaTitle, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), + return CustomScrollView( + slivers: [ + ReviewHeader( + id: id, + bannerUrl: bannerUrl, + mediaTitle: data?.mediaTitle, + siteUrl: data?.siteUrl, + ), + if (data != null) + SliverPadding( + padding: EdgeInsets.only( + top: 15, + left: 10, + right: 10, + bottom: MediaQuery.of(context).viewPadding.bottom + 10, + ), + sliver: SliverList( + delegate: SliverChildListDelegate.fixed([ + GestureDetector( + onTap: () => LinkTile.openView( + context: context, + id: data.mediaId, + imageUrl: data.mediaCover, + discoverType: DiscoverType.anime, ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => LinkTile.openView( - context: context, - id: data.userId, - imageUrl: data.userAvatar, - discoverType: DiscoverType.user, - ), - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: Theme.of(context).textTheme.titleMedium, - children: [ - TextSpan( - text: 'review by ', - style: Theme.of(context).textTheme.labelMedium, - ), - TextSpan(text: data.userName), - ], - ), - ), + child: Text( + data.mediaTitle, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Text( - data.summary, - style: Theme.of(context).textTheme.labelMedium, - textAlign: TextAlign.center, - ), + ), + const SizedBox(height: 5), + GestureDetector( + onTap: () => LinkTile.openView( + context: context, + id: data.userId, + imageUrl: data.userAvatar, + discoverType: DiscoverType.user, ), - HtmlContent(data.text), - Center( - child: Container( - margin: Consts.padding, - padding: Consts.padding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: Consts.borderRadiusMax, - ), - child: Text( - '${data.score}/100', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: - Theme.of(context).colorScheme.onPrimary, - fontSize: Consts.fontBig, - fontWeight: FontWeight.w500, - ), - ), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.titleMedium, + children: [ + TextSpan( + text: 'review by ', + style: Theme.of(context).textTheme.labelMedium, + ), + TextSpan(text: data.userName), + ], ), ), - _RateButtons(id), - Padding( - padding: const EdgeInsets.only(bottom: 10, top: 20), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + data.summary, + style: Theme.of(context).textTheme.labelMedium, + textAlign: TextAlign.center, + ), + ), + HtmlContent(data.text), + Center( + child: Container( + margin: Consts.padding, + padding: Consts.padding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: Consts.borderRadiusMax, + ), child: Text( - data.createdAt, - style: Theme.of(context).textTheme.labelMedium, - textAlign: TextAlign.center, + '${data.score}/100', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: Consts.fontBig, + fontWeight: FontWeight.w500, + ), ), ), - ]), - ), + ), + _RateButtons(id), + Padding( + padding: const EdgeInsets.only(bottom: 10, top: 20), + child: Text( + data.createdAt, + style: Theme.of(context).textTheme.labelMedium, + textAlign: TextAlign.center, + ), + ), + ]), ), - ], - ); - }), - ), + ), + ], + ); + }), ); } } From b5d7351f62621e6f9dcc83e468b2a2373e99115e Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sun, 7 May 2023 23:43:36 +0300 Subject: [PATCH 30/55] Cleanup --- lib/modules/character/character_action_buttons.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/modules/character/character_action_buttons.dart b/lib/modules/character/character_action_buttons.dart index e7683edc..efaa76cb 100644 --- a/lib/modules/character/character_action_buttons.dart +++ b/lib/modules/character/character_action_buttons.dart @@ -132,7 +132,7 @@ class CharacterLanguageSelectionButton extends StatelessWidget { showSheet( context, GradientSheet([ - for (int i = 0; i < languages.length; i++) ...[ + for (int i = 0; i < languages.length; i++) GradientSheetButton( text: languages.elementAt(i), selected: languages.elementAt(i) == language, @@ -140,7 +140,6 @@ class CharacterLanguageSelectionButton extends StatelessWidget { .read(characterMediaProvider(id).notifier) .changeLanguage(languages.elementAt(i)), ), - ] ]), ); }, From 1e3a97516eab4d1a25b853d0ea9df0c2f29b896a Mon Sep 17 00:00:00 2001 From: lotusgate Date: Thu, 11 May 2023 22:58:00 +0300 Subject: [PATCH 31/55] Upgraded to new flutter and dart --- android/build.gradle | 2 +- lib/common/utils/background_handler.dart | 110 ++--- lib/common/widgets/grids/relation_grid.dart | 66 ++- lib/common/widgets/link_tile.dart | 37 +- lib/common/widgets/loaders.dart/loaders.dart | 15 +- lib/main.dart | 7 +- lib/modules/character/character_models.dart | 30 +- lib/modules/character/character_view.dart | 14 +- lib/modules/collection/collection_grid.dart | 2 +- lib/modules/collection/collection_list.dart | 2 +- lib/modules/collection/collection_models.dart | 436 ++++++++---------- lib/modules/collection/collection_view.dart | 2 +- lib/modules/discover/discover_providers.dart | 36 +- lib/modules/discover/discover_view.dart | 3 +- lib/modules/edit/edit_buttons.dart | 4 +- lib/modules/edit/edit_model.dart | 2 + lib/modules/edit/edit_providers.dart | 13 - lib/modules/edit/score_field.dart | 21 +- lib/modules/favorites/favorites_model.dart | 42 +- lib/modules/home/home_view.dart | 43 +- lib/modules/media/media_action_buttons.dart | 3 +- lib/modules/media/media_constants.dart | 42 +- lib/modules/media/media_models.dart | 24 +- lib/modules/media/media_view.dart | 13 +- .../notification/notification_model.dart | 79 ++-- .../notification/notifications_view.dart | 130 +++--- lib/modules/review/review_models.dart | 18 +- lib/modules/settings/settings_view.dart | 9 +- lib/modules/social/social_model.dart | 24 +- lib/modules/staff/staff_models.dart | 6 +- lib/modules/staff/staff_providers.dart | 33 +- lib/modules/staff/staff_view.dart | 14 +- pubspec.lock | 154 +++---- pubspec.yaml | 2 +- 34 files changed, 622 insertions(+), 816 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index dc79e569..4b70de16 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -29,6 +29,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/lib/common/utils/background_handler.dart b/lib/common/utils/background_handler.dart index f32b3f8c..bd2db438 100644 --- a/lib/common/utils/background_handler.dart +++ b/lib/common/utils/background_handler.dart @@ -139,125 +139,96 @@ void _fetch() => Workmanager().executeTask((_, __) async { final notification = SiteNotification.maybe(ns[i]); if (notification == null) continue; - switch (notification.type) { - case NotificationType.FOLLOWING: - _show( + (switch (notification.type) { + NotificationType.FOLLOWING => _show( notification, 'New Follow', '${RouteArg.user}/${notification.bodyId}', - ); - break; - case NotificationType.ACTIVITY_MESSAGE: - _show( + ), + NotificationType.ACTIVITY_MESSAGE => _show( notification, 'New Message', '${RouteArg.activity}/${notification.bodyId}', - ); - break; - case NotificationType.ACTIVITY_REPLY: - case NotificationType.ACTIVITY_REPLY_SUBSCRIBED: - _show( + ), + NotificationType.ACTIVITY_REPLY => _show( notification, 'New Reply', '${RouteArg.activity}/${notification.bodyId}', - ); - break; - case NotificationType.ACTIVITY_MENTION: - _show( + ), + NotificationType.ACTIVITY_REPLY_SUBSCRIBED => _show( + notification, + 'New Reply To Subscription', + '${RouteArg.activity}/${notification.bodyId}', + ), + NotificationType.ACTIVITY_MENTION => _show( notification, 'New Mention', '${RouteArg.activity}/${notification.bodyId}', - ); - break; - case NotificationType.ACTIVITY_LIKE: - _show( + ), + NotificationType.ACTIVITY_LIKE => _show( notification, 'New Activity Like', '${RouteArg.activity}/${notification.bodyId}', - ); - break; - case NotificationType.ACTIVITY_REPLY_LIKE: - _show( + ), + NotificationType.ACTIVITY_REPLY_LIKE => _show( notification, 'New Reply Like', '${RouteArg.activity}/${notification.bodyId}', - ); - break; - case NotificationType.THREAD_COMMENT_REPLY: - _show( + ), + NotificationType.THREAD_COMMENT_REPLY => _show( notification, 'New Forum Reply', '${RouteArg.thread}/${notification.bodyId}', - ); - break; - case NotificationType.THREAD_COMMENT_MENTION: - _show( + ), + NotificationType.THREAD_COMMENT_MENTION => _show( notification, 'New Forum Mention', '${RouteArg.thread}/${notification.bodyId}', - ); - break; - case NotificationType.THREAD_SUBSCRIBED: - _show( + ), + NotificationType.THREAD_SUBSCRIBED => _show( notification, 'New Forum Comment', '${RouteArg.thread}/${notification.bodyId}', - ); - break; - case NotificationType.THREAD_LIKE: - _show( + ), + NotificationType.THREAD_LIKE => _show( notification, 'New Forum Like', '${RouteArg.thread}/${notification.bodyId}', - ); - break; - case NotificationType.THREAD_COMMENT_LIKE: - _show( + ), + NotificationType.THREAD_COMMENT_LIKE => _show( notification, 'New Forum Comment Like', '${RouteArg.thread}/${notification.bodyId}', - ); - break; - case NotificationType.AIRING: - _show( + ), + NotificationType.AIRING => _show( notification, 'New Episode', '${RouteArg.media}/${notification.bodyId}', - ); - break; - case NotificationType.RELATED_MEDIA_ADDITION: - _show( + ), + NotificationType.RELATED_MEDIA_ADDITION => _show( notification, 'New Addition', '${RouteArg.media}/${notification.bodyId}', - ); - break; - case NotificationType.MEDIA_DATA_CHANGE: - _show( + ), + NotificationType.MEDIA_DATA_CHANGE => _show( notification, 'Modified Media', '${RouteArg.media}/${notification.bodyId}', - ); - break; - case NotificationType.MEDIA_MERGE: - _show( + ), + NotificationType.MEDIA_MERGE => _show( notification, 'Merged Media', '${RouteArg.media}/${notification.bodyId}', - ); - break; - case NotificationType.MEDIA_DELETION: - _show(notification, 'Deleted Media', ''); - break; - default: - break; - } + ), + NotificationType.MEDIA_DELETION => + _show(notification, 'Deleted Media', ''), + }); } return true; }); -void _show(SiteNotification notification, String title, String payload) { +() _show(SiteNotification notification, String title, String payload) { _notificationPlugin.show( notification.id, title, @@ -272,4 +243,5 @@ void _show(SiteNotification notification, String title, String payload) { ), payload: payload, ); + return (); } diff --git a/lib/common/widgets/grids/relation_grid.dart b/lib/common/widgets/grids/relation_grid.dart index c5389ac0..3b946a75 100644 --- a/lib/common/widgets/grids/relation_grid.dart +++ b/lib/common/widgets/grids/relation_grid.dart @@ -6,43 +6,59 @@ import 'package:otraku/common/widgets/cached_image.dart'; import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; class RelationGrid extends StatelessWidget { - RelationGrid({ - required this.items, - this.connections = const [], - }) : assert(connections.isEmpty || items.length == connections.length); + const RelationGrid(this.items); + + final List<(Relation, Relation?)> items; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) return const SliverToBoxAdapter(); + + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 300, + height: 115, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) => _RelationTile(items[i].$1, items[i].$2), + ), + ); + } +} + +class SingleRelationGrid extends StatelessWidget { + const SingleRelationGrid(this.items); final List items; - final List connections; @override Widget build(BuildContext context) { if (items.isEmpty) return const SliverToBoxAdapter(); return SliverGrid( - gridDelegate: SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: connections.isEmpty ? 240 : 300, + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 240, height: 115, ), delegate: SliverChildBuilderDelegate( childCount: items.length, - connections.isNotEmpty - ? (context, i) => _RelationTile(items[i], connections[i]) - : (context, i) => _RelationTile(items[i], null), + (context, i) => _RelationTile(items[i], null), ), ); } } class _RelationTile extends StatelessWidget { - const _RelationTile(this.item, this.connection); + const _RelationTile(this.item, this.secondary); final Relation item; - final Relation? connection; + final Relation? secondary; @override Widget build(BuildContext context) { late final Widget centerContent; - if (connection != null) { + if (secondary != null) { centerContent = Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -76,24 +92,24 @@ class _RelationTile extends StatelessWidget { ), const SizedBox(height: 3), LinkTile( - id: connection!.id, - discoverType: connection!.type, - info: connection!.imageUrl, + id: secondary!.id, + discoverType: secondary!.type, + info: secondary!.imageUrl, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Flexible( child: Text( - connection!.title, + secondary!.title, maxLines: 2, textAlign: TextAlign.end, overflow: TextOverflow.fade, ), ), - if (connection!.subtitle != null) + if (secondary!.subtitle != null) Text( - connection!.subtitle!, + secondary!.subtitle!, maxLines: 2, overflow: TextOverflow.fade, style: Theme.of(context).textTheme.labelSmall, @@ -140,15 +156,15 @@ class _RelationTile extends StatelessWidget { Expanded( child: Padding(padding: Consts.padding, child: centerContent), ), - if (connection != null) + if (secondary != null) LinkTile( - key: ValueKey(connection!.id), - id: connection!.id, - discoverType: connection!.type, - info: connection!.imageUrl, + key: ValueKey(secondary!.id), + id: secondary!.id, + discoverType: secondary!.type, + info: secondary!.imageUrl, child: ClipRRect( borderRadius: Consts.borderRadiusMin, - child: CachedImage(connection!.imageUrl, width: 80), + child: CachedImage(secondary!.imageUrl, width: 80), ), ), ], diff --git a/lib/common/widgets/link_tile.dart b/lib/common/widgets/link_tile.dart index 1ea8814b..24706d54 100644 --- a/lib/common/widgets/link_tile.dart +++ b/lib/common/widgets/link_tile.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:otraku/modules/discover/discover_models.dart'; -import 'package:otraku/modules/edit/edit_providers.dart'; import 'package:otraku/common/utils/route_arg.dart'; import 'package:otraku/modules/edit/edit_view.dart'; import 'package:otraku/common/widgets/overlays/sheets.dart'; @@ -19,36 +18,22 @@ class LinkTile extends StatelessWidget { final String? info; final Widget child; - static void openView({ + static Future openView({ required BuildContext context, required DiscoverType discoverType, required int id, required String? imageUrl, }) { - String route = ''; - switch (discoverType) { - case DiscoverType.anime: - case DiscoverType.manga: - route = RouteArg.media; - break; - case DiscoverType.character: - route = RouteArg.character; - break; - case DiscoverType.staff: - route = RouteArg.staff; - break; - case DiscoverType.studio: - route = RouteArg.studio; - break; - case DiscoverType.user: - route = RouteArg.user; - break; - case DiscoverType.review: - route = RouteArg.review; - break; - } + final route = switch (discoverType) { + DiscoverType.anime || DiscoverType.manga => RouteArg.media, + DiscoverType.character => RouteArg.character, + DiscoverType.staff => RouteArg.staff, + DiscoverType.studio => RouteArg.studio, + DiscoverType.user => RouteArg.user, + DiscoverType.review => RouteArg.review, + }; - Navigator.pushNamed( + return Navigator.pushNamed( context, route, arguments: RouteArg(id: id, info: imageUrl), @@ -68,7 +53,7 @@ class LinkTile extends StatelessWidget { onLongPress: () { if (discoverType == DiscoverType.anime || discoverType == DiscoverType.manga) { - showSheet(context, EditView(EditTag(id))); + showSheet(context, EditView((id: id, setComplete: false))); } }, child: child, diff --git a/lib/common/widgets/loaders.dart/loaders.dart b/lib/common/widgets/loaders.dart/loaders.dart index c91c2eb1..69667a6d 100644 --- a/lib/common/widgets/loaders.dart/loaders.dart +++ b/lib/common/widgets/loaders.dart/loaders.dart @@ -51,18 +51,13 @@ class SliverRefreshControl extends StatelessWidget { if (visibility > 1) visibility = 1; } - switch (refreshState) { - case RefreshIndicatorMode.drag: - case RefreshIndicatorMode.done: - case RefreshIndicatorMode.armed: - case RefreshIndicatorMode.refresh: - return Opacity( + return switch (refreshState) { + RefreshIndicatorMode.inactive => const SizedBox(), + _ => Opacity( opacity: visibility, child: const Center(child: Loader()), - ); - case RefreshIndicatorMode.inactive: - return const SizedBox(); - } + ), + }; }, ), ); diff --git a/lib/main.dart b/lib/main.dart index 2cab31e9..ffd67a66 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:otraku/modules/home/home_provider.dart'; @@ -81,9 +80,11 @@ class AppState extends State { } final mode = Options().themeMode; - final platform = SchedulerBinding.instance.window.platformBrightness; + final platformBrightness = + View.of(context).platformDispatcher.platformBrightness; + final isDark = mode == ThemeMode.system - ? platform == Brightness.dark + ? platformBrightness == Brightness.dark : mode == ThemeMode.dark; final ColorScheme scheme; diff --git a/lib/modules/character/character_models.dart b/lib/modules/character/character_models.dart index 5189f89c..624480fa 100644 --- a/lib/modules/character/character_models.dart +++ b/lib/modules/character/character_models.dart @@ -103,37 +103,29 @@ class CharacterMedia { Iterable get languages => languageToVoiceActors.keys; - /// Fill [resultingMedia] and [resultingVoiceActors] lists, based on the - /// currently selected [language]. The lists must end up with equal length - /// or if an incorrect [language] is selected, [resultingVoiceActors] should - /// be empty. If there are multiple VAs for a media, add the corresponding - /// media item in [resultingMedia] enough times to compensate. If there are no - /// VAs to a media, compensate with one `null` item in [resultingVoiceActors]. - void getAnimeAndVoiceActors( - List resultingMedia, - List resultingVoiceActors, - ) { + /// Returns the media, in which the character has participated, + /// along with the voice actors, corresponding to the current [language]. + /// If there are multiple actors, the given media is repeated for each actor. + List<(Relation, Relation?)> getAnimeAndVoiceActors() { final anime = this.anime.valueOrNull?.items; - if (anime == null || anime.isEmpty) return; + if (anime == null || anime.isEmpty) return []; final actorsPerMedia = languageToVoiceActors[language]; - if (actorsPerMedia == null) { - resultingMedia.addAll(anime); - return; - } + if (actorsPerMedia == null) return [for (final a in anime) (a, null)]; + final animeAndVoiceActors = <(Relation, Relation?)>[]; for (final a in anime) { final actors = actorsPerMedia[a.id]; if (actors == null || actors.isEmpty) { - resultingMedia.add(a); - resultingVoiceActors.add(null); + animeAndVoiceActors.add((a, null)); continue; } for (final va in actors) { - resultingMedia.add(a); - resultingVoiceActors.add(va); + animeAndVoiceActors.add((a, va)); } } + + return animeAndVoiceActors; } } diff --git a/lib/modules/character/character_view.dart b/lib/modules/character/character_view.dart index e40acb21..15b34e22 100644 --- a/lib/modules/character/character_view.dart +++ b/lib/modules/character/character_view.dart @@ -96,13 +96,11 @@ class _CharacterViewState extends ConsumerState { provider: characterMediaProvider(widget.id).select((s) => s.anime), onData: (data) { - final anime = []; - final voiceActors = []; - ref - .watch(characterMediaProvider(widget.id)) - .getAnimeAndVoiceActors(anime, voiceActors); - - return RelationGrid(items: anime, connections: voiceActors); + return RelationGrid( + ref + .watch(characterMediaProvider(widget.id)) + .getAnimeAndVoiceActors(), + ); }, scrollCtrl: _ctrl, onRefresh: onRefresh, @@ -110,7 +108,7 @@ class _CharacterViewState extends ConsumerState { PagedView( provider: characterMediaProvider(widget.id).select((s) => s.manga), - onData: (data) => RelationGrid(items: data.items), + onData: (data) => SingleRelationGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, ), diff --git a/lib/modules/collection/collection_grid.dart b/lib/modules/collection/collection_grid.dart index 8453a62d..cbf0c535 100644 --- a/lib/modules/collection/collection_grid.dart +++ b/lib/modules/collection/collection_grid.dart @@ -121,7 +121,7 @@ class _IncrementButtonState extends State<_IncrementButton> { onPressed: () async { if (item.progressMax != null && item.progress >= item.progressMax! - 1) { - showSheet(context, EditView(EditTag(item.mediaId, true))); + showSheet(context, EditView((id: item.mediaId, setComplete: true))); return; } diff --git a/lib/modules/collection/collection_list.dart b/lib/modules/collection/collection_list.dart index cb93d231..8327be1e 100644 --- a/lib/modules/collection/collection_list.dart +++ b/lib/modules/collection/collection_list.dart @@ -295,7 +295,7 @@ class __TileContentState extends State<_TileContent> { onPressed: () async { if (item.progressMax != null && item.progress >= item.progressMax! - 1) { - showSheet(context, EditView(EditTag(item.mediaId, true))); + showSheet(context, EditView((id: item.mediaId, setComplete: true))); return; } diff --git a/lib/modules/collection/collection_models.dart b/lib/modules/collection/collection_models.dart index aa32d167..ff12478e 100644 --- a/lib/modules/collection/collection_models.dart +++ b/lib/modules/collection/collection_models.dart @@ -2,19 +2,7 @@ import 'package:otraku/modules/media/media_constants.dart'; import 'package:otraku/common/utils/convert.dart'; import 'package:otraku/common/utils/options.dart'; -/// Used as an argument for a [collectionProvider] `family` instance. -class CollectionTag { - CollectionTag(this.userId, this.ofAnime); - - final int userId; - final bool ofAnime; - - @override - int get hashCode => '$userId$ofAnime'.hashCode; - - @override - bool operator ==(Object other) => hashCode == other.hashCode; -} +typedef CollectionTag = ({int userId, bool ofAnime}); class EntryList { EntryList._({ @@ -77,245 +65,221 @@ class EntryList { void sort(EntrySort s) => entries.sort(entryComparator(s)); } -int Function(Entry, Entry) entryComparator(EntrySort s) { - switch (s) { - case EntrySort.TITLE: - return (a, b) => - a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - case EntrySort.TITLE_DESC: - return (a, b) => b.titles[0].compareTo(a.titles[0]); - case EntrySort.SCORE: - return (a, b) { - final comparison = a.score.compareTo(b.score); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.SCORE_DESC: - return (a, b) { - final comparison = b.score.compareTo(a.score); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.UPDATED: - return (a, b) { - final comparison = a.updatedAt!.compareTo(b.updatedAt!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.UPDATED_DESC: - return (a, b) { - final comparison = b.updatedAt!.compareTo(a.updatedAt!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.ADDED: - return (a, b) { - final comparison = a.createdAt!.compareTo(b.createdAt!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.ADDED_DESC: - return (a, b) { - final comparison = b.createdAt!.compareTo(a.createdAt!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.PROGRESS: - return (a, b) { - final comparison = a.progress.compareTo(b.progress); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.PROGRESS_DESC: - return (a, b) { - final comparison = b.progress.compareTo(a.progress); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.REPEATED: - return (a, b) { - final comparison = a.repeat.compareTo(b.repeat); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.REPEATED_DESC: - return (a, b) { - final comparison = b.repeat.compareTo(a.repeat); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.AIRING: - return (a, b) { - if (a.airingAt == null) { - if (b.airingAt == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); - } - return 1; - } - - if (b.airingAt == null) return -1; - - final comparison = a.airingAt!.compareTo(b.airingAt!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.AIRING_DESC: - return (a, b) { - if (b.airingAt == null) { +int Function(Entry, Entry) entryComparator(EntrySort s) => switch (s) { + EntrySort.TITLE => (a, b) => + a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()), + EntrySort.TITLE_DESC => (a, b) => b.titles[0].compareTo(a.titles[0]), + EntrySort.SCORE => (a, b) { + final comparison = a.score.compareTo(b.score); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.SCORE_DESC => (a, b) { + final comparison = b.score.compareTo(a.score); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.UPDATED => (a, b) { + final comparison = a.updatedAt!.compareTo(b.updatedAt!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.UPDATED_DESC => (a, b) { + final comparison = b.updatedAt!.compareTo(a.updatedAt!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.ADDED => (a, b) { + final comparison = a.createdAt!.compareTo(b.createdAt!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.ADDED_DESC => (a, b) { + final comparison = b.createdAt!.compareTo(a.createdAt!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.PROGRESS => (a, b) { + final comparison = a.progress.compareTo(b.progress); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.PROGRESS_DESC => (a, b) { + final comparison = b.progress.compareTo(a.progress); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.REPEATED => (a, b) { + final comparison = a.repeat.compareTo(b.repeat); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.REPEATED_DESC => (a, b) { + final comparison = b.repeat.compareTo(a.repeat); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.AIRING => (a, b) { if (a.airingAt == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + if (b.airingAt == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return 1; } - return -1; - } - - if (a.airingAt == null) return 1; - - final comparison = b.airingAt!.compareTo(a.airingAt!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.RELEASED_ON: - return (a, b) { - if (a.releaseStart == null) { - if (b.releaseStart == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + + if (b.airingAt == null) return -1; + + final comparison = a.airingAt!.compareTo(b.airingAt!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.AIRING_DESC => (a, b) { + if (b.airingAt == null) { + if (a.airingAt == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return -1; } - return 1; - } - - if (b.releaseStart == null) return -1; - - final comparison = a.releaseStart!.compareTo(b.releaseStart!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.RELEASED_ON_DESC: - return (a, b) { - if (b.releaseStart == null) { + + if (a.airingAt == null) return 1; + + final comparison = b.airingAt!.compareTo(a.airingAt!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.RELEASED_ON => (a, b) { if (a.releaseStart == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + if (b.releaseStart == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return 1; } - return -1; - } - - if (a.releaseStart == null) return 1; - - final comparison = b.releaseStart!.compareTo(a.releaseStart!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.STARTED_ON: - return (a, b) { - if (a.watchStart == null) { - if (b.watchStart == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + + if (b.releaseStart == null) return -1; + + final comparison = a.releaseStart!.compareTo(b.releaseStart!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.RELEASED_ON_DESC => (a, b) { + if (b.releaseStart == null) { + if (a.releaseStart == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return -1; } - return 1; - } - - if (b.watchStart == null) return -1; - - final comparison = a.watchStart!.compareTo(b.watchStart!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.STARTED_ON_DESC: - return (a, b) { - if (b.watchStart == null) { + + if (a.releaseStart == null) return 1; + + final comparison = b.releaseStart!.compareTo(a.releaseStart!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.STARTED_ON => (a, b) { if (a.watchStart == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + if (b.watchStart == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return 1; } - return -1; - } - - if (a.watchStart == null) return 1; - - final comparison = b.watchStart!.compareTo(a.watchStart!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.COMPLETED_ON: - return (a, b) { - if (a.watchEnd == null) { - if (b.watchEnd == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + + if (b.watchStart == null) return -1; + + final comparison = a.watchStart!.compareTo(b.watchStart!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.STARTED_ON_DESC => (a, b) { + if (b.watchStart == null) { + if (a.watchStart == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return -1; } - return 1; - } - - if (b.watchEnd == null) return -1; - - final comparison = a.watchEnd!.compareTo(b.watchEnd!); - if (comparison != 0) return comparison; - return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); - }; - case EntrySort.COMPLETED_ON_DESC: - return (a, b) { - if (b.watchEnd == null) { + + if (a.watchStart == null) return 1; + + final comparison = b.watchStart!.compareTo(a.watchStart!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.COMPLETED_ON => (a, b) { if (a.watchEnd == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + if (b.watchEnd == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return 1; } - return -1; - } - - if (a.watchEnd == null) return 1; - - final comparison = b.watchEnd!.compareTo(a.watchEnd!); - 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()); + + if (b.watchEnd == null) return -1; + + final comparison = a.watchEnd!.compareTo(b.watchEnd!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.COMPLETED_ON_DESC => (a, b) { + if (b.watchEnd == null) { + if (a.watchEnd == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return -1; } - 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.watchEnd == null) return 1; + + final comparison = b.watchEnd!.compareTo(a.watchEnd!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.AVG_SCORE => (a, b) { if (a.avgScore == null) { - return a.titles[0] - .toUpperCase() - .compareTo(b.titles[0].toUpperCase()); + if (b.avgScore == null) { + return a.titles[0] + .toUpperCase() + .compareTo(b.titles[0].toUpperCase()); + } + return 1; } - return -1; - } - if (a.avgScore == null) return 1; + if (b.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()); - }; - } -} + final comparison = a.avgScore!.compareTo(b.avgScore!); + if (comparison != 0) return comparison; + return a.titles[0].toUpperCase().compareTo(b.titles[0].toUpperCase()); + }, + EntrySort.AVG_SCORE_DESC => (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()); + }, + }; class Entry { Entry._({ diff --git a/lib/modules/collection/collection_view.dart b/lib/modules/collection/collection_view.dart index 06ecd466..d406cbfd 100644 --- a/lib/modules/collection/collection_view.dart +++ b/lib/modules/collection/collection_view.dart @@ -42,7 +42,7 @@ class _CollectionViewState extends State { @override Widget build(BuildContext context) { - final tag = CollectionTag(widget.userId, widget.ofAnime); + final tag = (userId: widget.userId, ofAnime: widget.ofAnime); return PageScaffold( child: Consumer( diff --git a/lib/modules/discover/discover_providers.dart b/lib/modules/discover/discover_providers.dart index 3262a940..18c84a99 100644 --- a/lib/modules/discover/discover_providers.dart +++ b/lib/modules/discover/discover_providers.dart @@ -15,31 +15,17 @@ import 'package:otraku/common/utils/graphql.dart'; import 'package:otraku/common/models/paged.dart'; /// Fetches another page on the discover tab, depending on the selected type. -void discoverLoadMore(WidgetRef ref) { - switch (ref.read(discoverFilterProvider).type) { - case DiscoverType.anime: - ref.read(discoverAnimeProvider.notifier).fetch(); - return; - case DiscoverType.manga: - ref.read(discoverMangaProvider.notifier).fetch(); - return; - case DiscoverType.character: - ref.read(discoverCharacterProvider.notifier).fetch(); - return; - case DiscoverType.staff: - ref.read(discoverStaffProvider.notifier).fetch(); - return; - case DiscoverType.studio: - ref.read(discoverStudioProvider.notifier).fetch(); - return; - case DiscoverType.user: - ref.read(discoverUserProvider.notifier).fetch(); - return; - case DiscoverType.review: - ref.read(discoverReviewProvider.notifier).fetch(); - return; - } -} +void discoverLoadMore(WidgetRef ref) => + switch (ref.read(discoverFilterProvider).type) { + DiscoverType.anime => ref.read(discoverAnimeProvider.notifier).fetch(), + DiscoverType.manga => ref.read(discoverMangaProvider.notifier).fetch(), + DiscoverType.character => + ref.read(discoverCharacterProvider.notifier).fetch(), + DiscoverType.staff => ref.read(discoverStaffProvider.notifier).fetch(), + DiscoverType.studio => ref.read(discoverStudioProvider.notifier).fetch(), + DiscoverType.user => ref.read(discoverUserProvider.notifier).fetch(), + DiscoverType.review => ref.read(discoverReviewProvider.notifier).fetch(), + }; final _searchSelector = (String? s) => s == null || s.isEmpty ? null : s; diff --git a/lib/modules/discover/discover_view.dart b/lib/modules/discover/discover_view.dart index a2684810..f902cf83 100644 --- a/lib/modules/discover/discover_view.dart +++ b/lib/modules/discover/discover_view.dart @@ -33,8 +33,7 @@ class DiscoverView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final onRefresh = () { - final type = ref.read(discoverFilterProvider).type; - switch (type) { + switch (ref.read(discoverFilterProvider).type) { case DiscoverType.anime: ref.invalidate(discoverAnimeProvider); return; diff --git a/lib/modules/edit/edit_buttons.dart b/lib/modules/edit/edit_buttons.dart index 1f53f5d5..98774a24 100644 --- a/lib/modules/edit/edit_buttons.dart +++ b/lib/modules/edit/edit_buttons.dart @@ -57,7 +57,7 @@ class _EditButtonsState extends State { } final ofAnime = newEdit.type == 'ANIME'; - final tag = CollectionTag(Options().id!, ofAnime); + final tag = (userId: Options().id!, ofAnime: ofAnime); if (ref.read(homeProvider).didExpandCollection(ofAnime)) { await ref.read(collectionProvider(tag)).updateEntry( @@ -123,7 +123,7 @@ class _EditButtonsState extends State { } final ofAnime = oldEdit.type == 'ANIME'; - final tag = CollectionTag(Options().id!, ofAnime); + final tag = (userId: Options().id!, ofAnime: ofAnime); if (ref.read(homeProvider).didExpandCollection(ofAnime)) { ref.read(collectionProvider(tag)).removeEntry(oldEdit); diff --git a/lib/modules/edit/edit_model.dart b/lib/modules/edit/edit_model.dart index e2883121..386c0543 100644 --- a/lib/modules/edit/edit_model.dart +++ b/lib/modules/edit/edit_model.dart @@ -2,6 +2,8 @@ import 'package:otraku/modules/collection/collection_models.dart'; import 'package:otraku/modules/settings/settings_provider.dart'; import 'package:otraku/common/utils/convert.dart'; +typedef EditTag = ({int id, bool setComplete}); + class Edit { Edit._({ required this.mediaId, diff --git a/lib/modules/edit/edit_providers.dart b/lib/modules/edit/edit_providers.dart index b27c636f..00fa9806 100644 --- a/lib/modules/edit/edit_providers.dart +++ b/lib/modules/edit/edit_providers.dart @@ -56,19 +56,6 @@ Future removeEntry(int entryId) async { } } -class EditTag { - const EditTag(this.id, [this.setComplete = false]); - - final int id; - final bool setComplete; - - @override - int get hashCode => id.hashCode; - - @override - bool operator ==(Object other) => hashCode == other.hashCode; -} - final oldEditProvider = FutureProvider.autoDispose.family( (ref, EditTag tag) async { if (ref.exists(mediaProvider(tag.id))) { diff --git a/lib/modules/edit/score_field.dart b/lib/modules/edit/score_field.dart index b9ae3a00..36be2682 100644 --- a/lib/modules/edit/score_field.dart +++ b/lib/modules/edit/score_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/modules/edit/edit_model.dart'; import 'package:otraku/modules/edit/edit_providers.dart'; import 'package:otraku/modules/media/media_constants.dart'; import 'package:otraku/modules/settings/settings_provider.dart'; @@ -20,18 +21,14 @@ class ScoreField extends StatelessWidget { .read(newEditProvider(tag).notifier) .update((s) => s.copyWith(score: v)); - switch (ref.watch(settingsProvider.notifier).value.scoreFormat) { - case ScoreFormat.POINT_3: - return _SmileyScorePicker(score, onChanged); - case ScoreFormat.POINT_5: - return _StarScorePicker(score, onChanged); - case ScoreFormat.POINT_10: - return _TenScorePicker(score, onChanged); - case ScoreFormat.POINT_10_DECIMAL: - return _TenDecimalScorePicker(score, onChanged); - default: - return _HundredScorePicker(score, onChanged); - } + return switch (ref.watch(settingsProvider.notifier).value.scoreFormat) { + ScoreFormat.POINT_3 => _SmileyScorePicker(score, onChanged), + ScoreFormat.POINT_5 => _StarScorePicker(score, onChanged), + ScoreFormat.POINT_10 => _TenScorePicker(score, onChanged), + ScoreFormat.POINT_10_DECIMAL => + _TenDecimalScorePicker(score, onChanged), + ScoreFormat.POINT_100 => _HundredScorePicker(score, onChanged), + }; }, ); } diff --git a/lib/modules/favorites/favorites_model.dart b/lib/modules/favorites/favorites_model.dart index c574b53f..ac537dbe 100644 --- a/lib/modules/favorites/favorites_model.dart +++ b/lib/modules/favorites/favorites_model.dart @@ -18,20 +18,13 @@ class Favorites { final AsyncValue> staff; final AsyncValue> studios; - int getCount(FavoritesTab tab) { - switch (tab) { - case FavoritesTab.anime: - return anime.valueOrNull?.total ?? 0; - case FavoritesTab.manga: - return manga.valueOrNull?.total ?? 0; - case FavoritesTab.characters: - return characters.valueOrNull?.total ?? 0; - case FavoritesTab.staff: - return staff.valueOrNull?.total ?? 0; - case FavoritesTab.studios: - return studios.valueOrNull?.total ?? 0; - } - } + int getCount(FavoritesTab tab) => switch (tab) { + FavoritesTab.anime => anime.valueOrNull?.total ?? 0, + FavoritesTab.manga => manga.valueOrNull?.total ?? 0, + FavoritesTab.characters => characters.valueOrNull?.total ?? 0, + FavoritesTab.staff => staff.valueOrNull?.total ?? 0, + FavoritesTab.studios => studios.valueOrNull?.total ?? 0, + }; } enum FavoritesTab { @@ -41,18 +34,11 @@ enum FavoritesTab { staff, studios; - String get title { - switch (this) { - case FavoritesTab.anime: - return 'Favourite Anime'; - case FavoritesTab.manga: - return 'Favourite Manga'; - case FavoritesTab.characters: - return 'Favourite Characters'; - case FavoritesTab.staff: - return 'Favourite Staff'; - case FavoritesTab.studios: - return 'Favourite Studios'; - } - } + String get title => switch (this) { + FavoritesTab.anime => 'Favourite Anime', + FavoritesTab.manga => 'Favourite Manga', + FavoritesTab.characters => 'Favourite Characters', + FavoritesTab.staff => 'Favourite Staff', + FavoritesTab.studios => 'Favourite Studios', + }; } diff --git a/lib/modules/home/home_view.dart b/lib/modules/home/home_view.dart index 3f596bd0..73f22f98 100644 --- a/lib/modules/home/home_view.dart +++ b/lib/modules/home/home_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/modules/activity/activities_providers.dart'; -import 'package:otraku/modules/collection/collection_models.dart'; import 'package:otraku/modules/collection/collection_preview_provider.dart'; import 'package:otraku/modules/collection/collection_preview_view.dart'; import 'package:otraku/modules/collection/collection_providers.dart'; @@ -42,8 +41,8 @@ class HomeView extends ConsumerStatefulWidget { class _HomeViewState extends ConsumerState { late final _ctrl = PagedController(loadMore: _scrollListener); - late final animeCollectionTag = CollectionTag(widget.id, true); - late final mangaCollectionTag = CollectionTag(widget.id, false); + late final animeCollectionTag = (userId: widget.id, ofAnime: true); + late final mangaCollectionTag = (userId: widget.id, ofAnime: false); @override void dispose() { @@ -90,29 +89,21 @@ class _HomeViewState extends ConsumerState { ref.watch(userProvider(widget.id).select((_) => null)); final discoverType = ref.watch(discoverFilterProvider.select((s) => s.type)); - switch (discoverType) { - case DiscoverType.anime: - ref.watch(discoverAnimeProvider.select((_) => null)); - break; - case DiscoverType.manga: - ref.watch(discoverMangaProvider.select((_) => null)); - break; - case DiscoverType.character: - ref.watch(discoverCharacterProvider.select((_) => null)); - break; - case DiscoverType.staff: - ref.watch(discoverStaffProvider.select((_) => null)); - break; - case DiscoverType.studio: - ref.watch(discoverStudioProvider.select((_) => null)); - break; - case DiscoverType.user: - ref.watch(discoverUserProvider.select((_) => null)); - break; - case DiscoverType.review: - ref.watch(discoverReviewProvider.select((_) => null)); - break; - } + (switch (discoverType) { + DiscoverType.anime => + ref.watch(discoverAnimeProvider.select((_) => null)), + DiscoverType.manga => + ref.watch(discoverMangaProvider.select((_) => null)), + DiscoverType.character => + ref.watch(discoverCharacterProvider.select((_) => null)), + DiscoverType.staff => + ref.watch(discoverStaffProvider.select((_) => null)), + DiscoverType.studio => + ref.watch(discoverStudioProvider.select((_) => null)), + DiscoverType.user => ref.watch(discoverUserProvider.select((_) => null)), + DiscoverType.review => + ref.watch(discoverReviewProvider.select((_) => null)), + }); final notifier = ref.watch(homeProvider); diff --git a/lib/modules/media/media_action_buttons.dart b/lib/modules/media/media_action_buttons.dart index 2ee086b4..c2c492c5 100644 --- a/lib/modules/media/media_action_buttons.dart +++ b/lib/modules/media/media_action_buttons.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; import 'package:otraku/modules/discover/discover_models.dart'; -import 'package:otraku/modules/edit/edit_providers.dart'; import 'package:otraku/modules/edit/edit_view.dart'; import 'package:otraku/modules/media/media_models.dart'; import 'package:otraku/modules/media/media_providers.dart'; @@ -28,7 +27,7 @@ class _MediaEditButtonState extends State { onTap: () => showSheet( context, EditView( - EditTag(media.info.id), + (id: media.info.id, setComplete: false), callback: (edit) => setState(() => media.edit = edit), ), ), diff --git a/lib/modules/media/media_constants.dart b/lib/modules/media/media_constants.dart index 5bd842ce..0a3ef9a1 100644 --- a/lib/modules/media/media_constants.dart +++ b/lib/modules/media/media_constants.dart @@ -123,34 +123,20 @@ enum EntrySort { ]; /// Format as an API row order. - String toRowOrder() { - switch (this) { - case EntrySort.SCORE_DESC: - return 'score'; - case EntrySort.UPDATED_DESC: - return 'updatedAt'; - case EntrySort.ADDED_DESC: - return 'id'; - case EntrySort.TITLE: - return 'title'; - default: - return 'title'; - } - } + String toRowOrder() => switch (this) { + EntrySort.SCORE_DESC => 'score', + EntrySort.UPDATED_DESC => 'updatedAt', + EntrySort.ADDED_DESC => 'id', + EntrySort.TITLE => 'title', + _ => 'title', + }; /// Translate API row order to general sorting. - static EntrySort fromRowOrder(String key) { - switch (key) { - case 'score': - return EntrySort.SCORE_DESC; - case 'updatedAt': - return EntrySort.UPDATED_DESC; - case 'id': - return EntrySort.ADDED_DESC; - case 'title': - return EntrySort.TITLE; - default: - return EntrySort.TITLE; - } - } + static EntrySort fromRowOrder(String key) => switch (key) { + 'score' => EntrySort.SCORE_DESC, + 'updatedAt' => EntrySort.UPDATED_DESC, + 'id' => EntrySort.ADDED_DESC, + 'title' => EntrySort.TITLE, + _ => EntrySort.TITLE, + }; } diff --git a/lib/modules/media/media_models.dart b/lib/modules/media/media_models.dart index 47afb5b1..f06f00a0 100644 --- a/lib/modules/media/media_models.dart +++ b/lib/modules/media/media_models.dart @@ -59,32 +59,30 @@ class MediaRelations { Iterable get languages => languageToVoiceActors.keys; - void getCharactersAndVoiceActors( - List resultingCharacters, - List resultingVoiceActors, - ) { + /// Returns the characters, along with their voice actors, + /// corresponding to the current [language]. If there are + /// multiple actors, the given character is repeated for each actor. + List<(Relation, Relation?)> getCharactersAndVoiceActors() { final chars = characters.valueOrNull?.items; - if (chars == null) return; + if (chars == null) return []; final actorsPerMedia = languageToVoiceActors[language]; - if (actorsPerMedia == null) { - resultingCharacters.addAll(chars); - return; - } + if (actorsPerMedia == null) return [for (final c in chars) (c, null)]; + final charactersAndVoiceActors = <(Relation, Relation?)>[]; for (final c in chars) { final actors = actorsPerMedia[c.id]; if (actors == null || actors.isEmpty) { - resultingCharacters.add(c); - resultingVoiceActors.add(null); + charactersAndVoiceActors.add((c, null)); continue; } for (final va in actors) { - resultingCharacters.add(c); - resultingVoiceActors.add(va); + charactersAndVoiceActors.add((c, va)); } } + + return charactersAndVoiceActors; } } diff --git a/lib/modules/media/media_view.dart b/lib/modules/media/media_view.dart index cdedad9e..ca2b2004 100644 --- a/lib/modules/media/media_view.dart +++ b/lib/modules/media/media_view.dart @@ -196,24 +196,19 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { ); if (mediaRelations.languages.isEmpty) { - return RelationGrid(items: data.items); + return SingleRelationGrid(data.items); } - final characters = []; - final voiceActors = []; - mediaRelations.getCharactersAndVoiceActors( - characters, - voiceActors, + return RelationGrid( + mediaRelations.getCharactersAndVoiceActors(), ); - - return RelationGrid(items: characters, connections: voiceActors); }, ), ), Consumer( builder: (context, ref, _) => PagedView( provider: mediaRelationsProvider(widget.id).select((s) => s.staff), - onData: (data) => RelationGrid(items: data.items), + onData: (data) => SingleRelationGrid(data.items), scrollCtrl: _scrollCtrl, onRefresh: () => _refresh(ref), ), diff --git a/lib/modules/notification/notification_model.dart b/lib/modules/notification/notification_model.dart index e5316ea6..823a6f48 100644 --- a/lib/modules/notification/notification_model.dart +++ b/lib/modules/notification/notification_model.dart @@ -15,50 +15,41 @@ enum NotificationFilterType { final String text; - List? get vars { - switch (this) { - case NotificationFilterType.all: - return null; - 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', - 'ACTIVITY_REPLY', - 'ACTIVITY_REPLY_SUBSCRIBED', - 'ACTIVITY_MENTION', - 'ACTIVITY_LIKE', - 'ACTIVITY_REPLY_LIKE', - ]; - case NotificationFilterType.forum: - return const [ - 'THREAD_COMMENT_REPLY', - 'THREAD_COMMENT_MENTION', - 'THREAD_SUBSCRIBED', - 'THREAD_LIKE', - 'THREAD_COMMENT_LIKE', - ]; - case NotificationFilterType.airing: - return const ['AIRING']; - case NotificationFilterType.follows: - return const ['FOLLOWING']; - case NotificationFilterType.media: - return const [ - 'RELATED_MEDIA_ADDITION', - 'MEDIA_DATA_CHANGE', - 'MEDIA_MERGE', - 'MEDIA_DELETION', - ]; - } - } + List? get vars => switch (this) { + NotificationFilterType.all => null, + NotificationFilterType.replies => const [ + 'ACTIVITY_MESSAGE', + 'ACTIVITY_REPLY', + 'ACTIVITY_REPLY_SUBSCRIBED', + 'ACTIVITY_MENTION', + 'THREAD_COMMENT_REPLY', + 'THREAD_COMMENT_MENTION', + 'THREAD_SUBSCRIBED', + ], + NotificationFilterType.activity => const [ + 'ACTIVITY_MESSAGE', + 'ACTIVITY_REPLY', + 'ACTIVITY_REPLY_SUBSCRIBED', + 'ACTIVITY_MENTION', + 'ACTIVITY_LIKE', + 'ACTIVITY_REPLY_LIKE', + ], + NotificationFilterType.forum => const [ + 'THREAD_COMMENT_REPLY', + 'THREAD_COMMENT_MENTION', + 'THREAD_SUBSCRIBED', + 'THREAD_LIKE', + 'THREAD_COMMENT_LIKE', + ], + NotificationFilterType.airing => const ['AIRING'], + NotificationFilterType.follows => const ['FOLLOWING'], + NotificationFilterType.media => const [ + 'RELATED_MEDIA_ADDITION', + 'MEDIA_DATA_CHANGE', + 'MEDIA_MERGE', + 'MEDIA_DELETION', + ], + }; } class SiteNotification { diff --git a/lib/modules/notification/notifications_view.dart b/lib/modules/notification/notifications_view.dart index 3b07c9fc..5170a403 100644 --- a/lib/modules/notification/notifications_view.dart +++ b/lib/modules/notification/notifications_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/modules/edit/edit_providers.dart'; import 'package:otraku/common/utils/consts.dart'; import 'package:otraku/modules/discover/discover_models.dart'; import 'package:otraku/modules/notification/notification_model.dart'; @@ -142,7 +141,10 @@ class _NotificationItem extends StatelessWidget { onLongPress: () { if (item.discoverType == DiscoverType.anime || item.discoverType == DiscoverType.manga) { - showSheet(context, EditView(EditTag(item.headId!))); + showSheet( + context, + EditView((id: item.headId!, setComplete: false)), + ); } }, child: ClipRRect( @@ -155,79 +157,65 @@ class _NotificationItem extends StatelessWidget { Flexible( child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () { - switch (item.type) { - case NotificationType.ACTIVITY_LIKE: - case NotificationType.ACTIVITY_MENTION: - case NotificationType.ACTIVITY_MESSAGE: - case NotificationType.ACTIVITY_REPLY: - case NotificationType.ACTIVITY_REPLY_LIKE: - case NotificationType.ACTIVITY_REPLY_SUBSCRIBED: - Navigator.pushNamed( - context, - RouteArg.activity, - arguments: RouteArg(id: item.bodyId), - ); - return; - case NotificationType.FOLLOWING: - LinkTile.openView( - context: context, - id: item.headId!, - imageUrl: item.imageUrl, - discoverType: DiscoverType.user, - ); - return; - case NotificationType.AIRING: - case NotificationType.RELATED_MEDIA_ADDITION: - LinkTile.openView( - context: context, - id: item.bodyId!, - imageUrl: item.imageUrl, - discoverType: item.discoverType!, - ); - return; - case NotificationType.MEDIA_DATA_CHANGE: - case NotificationType.MEDIA_MERGE: - case NotificationType.MEDIA_DELETION: - showPopUp(context, _NotificationDialog(item)); - return; - case NotificationType.THREAD_LIKE: - case NotificationType.THREAD_SUBSCRIBED: - case NotificationType.THREAD_COMMENT_LIKE: - case NotificationType.THREAD_COMMENT_REPLY: - case NotificationType.THREAD_COMMENT_MENTION: - showPopUp( - context, - ConfirmationDialog( - title: 'Forum is not yet supported', - content: 'Open in browser?', - mainAction: 'Open', - secondaryAction: 'Cancel', - onConfirm: () { - if (item.details == null) { - Toast.show(context, 'Invalid Link'); - return; - } - Toast.launch(context, item.details!); - }, - ), - ); - return; - default: - showPopUp( - context, - ConfirmationDialog( - title: 'Unknown action', - content: item.type.name, - ), - ); - return; - } + onTap: () => switch (item.type) { + NotificationType.ACTIVITY_LIKE || + NotificationType.ACTIVITY_MENTION || + NotificationType.ACTIVITY_MESSAGE || + NotificationType.ACTIVITY_REPLY || + NotificationType.ACTIVITY_REPLY_LIKE || + NotificationType.ACTIVITY_REPLY_SUBSCRIBED => + Navigator.pushNamed( + context, + RouteArg.activity, + arguments: RouteArg(id: item.bodyId), + ), + NotificationType.FOLLOWING => LinkTile.openView( + context: context, + id: item.headId!, + imageUrl: item.imageUrl, + discoverType: DiscoverType.user, + ), + NotificationType.AIRING || + NotificationType.RELATED_MEDIA_ADDITION => + LinkTile.openView( + context: context, + id: item.bodyId!, + imageUrl: item.imageUrl, + discoverType: item.discoverType!, + ), + NotificationType.MEDIA_DATA_CHANGE || + NotificationType.MEDIA_MERGE || + NotificationType.MEDIA_DELETION => + showPopUp(context, _NotificationDialog(item)), + NotificationType.THREAD_LIKE || + NotificationType.THREAD_SUBSCRIBED || + NotificationType.THREAD_COMMENT_LIKE || + NotificationType.THREAD_COMMENT_REPLY || + NotificationType.THREAD_COMMENT_MENTION => + showPopUp( + context, + ConfirmationDialog( + title: 'Forum is not yet supported', + content: 'Open in browser?', + mainAction: 'Open', + secondaryAction: 'Cancel', + onConfirm: () { + if (item.details == null) { + Toast.show(context, 'Invalid Link'); + return; + } + Toast.launch(context, item.details!); + }, + ), + ), }, onLongPress: () { if (item.discoverType == DiscoverType.anime || item.discoverType == DiscoverType.manga) { - showSheet(context, EditView(EditTag(item.headId!))); + showSheet( + context, + EditView((id: item.headId!, setComplete: false)), + ); } }, child: Padding( diff --git a/lib/modules/review/review_models.dart b/lib/modules/review/review_models.dart index 95023a5e..a6171bdf 100644 --- a/lib/modules/review/review_models.dart +++ b/lib/modules/review/review_models.dart @@ -118,16 +118,10 @@ enum ReviewSort { RATING_DESC, RATING; - String get text { - switch (this) { - case ReviewSort.CREATED_AT: - return 'Oldest'; - case ReviewSort.CREATED_AT_DESC: - return 'Newest'; - case ReviewSort.RATING: - return 'Lowest Rated'; - case ReviewSort.RATING_DESC: - return 'Highest Rated'; - } - } + String get text => switch (this) { + ReviewSort.CREATED_AT => 'Oldest', + ReviewSort.CREATED_AT_DESC => 'Newest', + ReviewSort.RATING => 'Lowest Rated', + ReviewSort.RATING_DESC => 'Highest Rated', + }; } diff --git a/lib/modules/settings/settings_view.dart b/lib/modules/settings/settings_view.dart index ed156277..feef119e 100644 --- a/lib/modules/settings/settings_view.dart +++ b/lib/modules/settings/settings_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:otraku/modules/collection/collection_models.dart'; import 'package:otraku/modules/collection/collection_providers.dart'; import 'package:otraku/modules/settings/settings_provider.dart'; import 'package:otraku/common/utils/paged_controller.dart'; @@ -50,12 +49,12 @@ class _SettingsViewState extends ConsumerState { if (prev?.scoreFormat != next.scoreFormat || prev?.titleLanguage != next.titleLanguage) { - ref.invalidate(collectionProvider(CollectionTag(id, true))); - ref.invalidate(collectionProvider(CollectionTag(id, false))); + ref.invalidate(collectionProvider((userId: id, ofAnime: true))); + ref.invalidate(collectionProvider((userId: id, ofAnime: false))); } else if (prev?.splitCompletedAnime != next.splitCompletedAnime) { - ref.invalidate(collectionProvider(CollectionTag(id, true))); + ref.invalidate(collectionProvider((userId: id, ofAnime: true))); } else if (prev?.splitCompletedManga != next.splitCompletedManga) { - ref.invalidate(collectionProvider(CollectionTag(id, false))); + ref.invalidate(collectionProvider((userId: id, ofAnime: false))); } }, ); diff --git a/lib/modules/social/social_model.dart b/lib/modules/social/social_model.dart index ea8ff975..87de85b9 100644 --- a/lib/modules/social/social_model.dart +++ b/lib/modules/social/social_model.dart @@ -11,26 +11,18 @@ class Social { final AsyncValue> following; final AsyncValue> followers; - int getCount(SocialTab tab) { - switch (tab) { - case SocialTab.following: - return following.valueOrNull?.total ?? 0; - case SocialTab.followers: - return followers.valueOrNull?.total ?? 0; - } - } + int getCount(SocialTab tab) => switch (tab) { + SocialTab.following => following.valueOrNull?.total ?? 0, + SocialTab.followers => followers.valueOrNull?.total ?? 0, + }; } enum SocialTab { following, followers; - String get title { - switch (this) { - case SocialTab.following: - return 'Following'; - case SocialTab.followers: - return 'Followers'; - } - } + String get title => switch (this) { + SocialTab.following => 'Following', + SocialTab.followers => 'Followers', + }; } diff --git a/lib/modules/staff/staff_models.dart b/lib/modules/staff/staff_models.dart index f6e39a8c..f88092b3 100644 --- a/lib/modules/staff/staff_models.dart +++ b/lib/modules/staff/staff_models.dart @@ -105,12 +105,10 @@ class StaffFilter { class StaffRelations { const StaffRelations({ - this.characters = const AsyncValue.loading(), + this.charactersAndMedia = const AsyncValue.loading(), this.roles = const AsyncValue.loading(), - this.characterMedia = const [], }); - final AsyncValue> characters; + final AsyncValue> charactersAndMedia; final AsyncValue> roles; - final List characterMedia; } diff --git a/lib/modules/staff/staff_providers.dart b/lib/modules/staff/staff_providers.dart index 9ec8521f..8710c875 100644 --- a/lib/modules/staff/staff_providers.dart +++ b/lib/modules/staff/staff_providers.dart @@ -59,9 +59,9 @@ class StaffRelationNotifier extends StateNotifier { variables['withCharacters'] = true; variables['withRoles'] = true; } else if (onCharacters) { - if (!(state.characters.valueOrNull?.hasNext ?? true)) return; + if (!(state.charactersAndMedia.valueOrNull?.hasNext ?? true)) return; variables['withCharacters'] = true; - variables['page'] = state.characters.valueOrNull?.next ?? 1; + variables['page'] = state.charactersAndMedia.valueOrNull?.next ?? 1; } else { if (!(state.roles.valueOrNull?.hasNext ?? true)) return; variables['withRoles'] = true; @@ -73,17 +73,16 @@ class StaffRelationNotifier extends StateNotifier { return data['Staff']; }); - var characters = state.characters; + var charactersAndMedia = state.charactersAndMedia; var roles = state.roles; - var characterMedia = [...state.characterMedia]; if (onCharacters == null || onCharacters) { - characters = await AsyncValue.guard(() { + charactersAndMedia = await AsyncValue.guard(() { if (data.hasError) throw data.error!; final map = data.value!['characterMedia']; - final value = characters.valueOrNull ?? const Paged(); + final value = charactersAndMedia.valueOrNull ?? const Paged(); - final items = []; + final items = <(Relation, Relation)>[]; for (final m in map['edges']) { final media = Relation( id: m['node']['id'], @@ -98,14 +97,15 @@ class StaffRelationNotifier extends StateNotifier { for (final c in m['characters']) { if (c == null) continue; - characterMedia.add(media); - - items.add(Relation( - id: c['id'], - title: c['name']['userPreferred'], - imageUrl: c['image']['large'], - type: DiscoverType.character, - subtitle: Convert.clarifyEnum(m['characterRole']), + items.add(( + Relation( + id: c['id'], + title: c['name']['userPreferred'], + imageUrl: c['image']['large'], + type: DiscoverType.character, + subtitle: Convert.clarifyEnum(m['characterRole']), + ), + media, )); } } @@ -144,9 +144,8 @@ class StaffRelationNotifier extends StateNotifier { } state = StaffRelations( - characters: characters, + charactersAndMedia: charactersAndMedia, roles: roles, - characterMedia: characterMedia, ); } } diff --git a/lib/modules/staff/staff_view.dart b/lib/modules/staff/staff_view.dart index 3b129b6d..2138d7cf 100644 --- a/lib/modules/staff/staff_view.dart +++ b/lib/modules/staff/staff_view.dart @@ -91,21 +91,17 @@ class _StaffViewState extends ConsumerState { onChanged: (i) => setState(() => _tab = i), children: [ StaffInfoTab(widget.id, widget.imageUrl, _ctrl), - PagedView( - provider: - staffRelationsProvider(widget.id).select((s) => s.characters), - onData: (data) => RelationGrid( - items: data.items, - connections: - ref.read(staffRelationsProvider(widget.id)).characterMedia, - ), + PagedView<(Relation, Relation)>( + provider: staffRelationsProvider(widget.id) + .select((s) => s.charactersAndMedia), + onData: (data) => RelationGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, ), PagedView( provider: staffRelationsProvider(widget.id).select((s) => s.roles), - onData: (data) => RelationGrid(items: data.items), + onData: (data) => SingleRelationGrid(data.items), scrollCtrl: _ctrl, onRefresh: onRefresh, ), diff --git a/pubspec.lock b/pubspec.lock index 8114ae0d..0a259953 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,26 +13,26 @@ packages: dependency: transitive description: name: archive - sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" url: "https://pub.dev" source: hosted - version: "3.3.6" + version: "3.3.7" args: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -69,18 +69,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" cli_util: dependency: transitive description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" csslib: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" file: dependency: transitive description: @@ -324,10 +324,10 @@ packages: dependency: transitive description: name: html - sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" + sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" url: "https://pub.dev" source: hosted - version: "0.15.2" + version: "0.15.3" http: dependency: "direct main" description: @@ -348,10 +348,10 @@ packages: dependency: transitive description: name: image - sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227" + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf url: "https://pub.dev" source: hosted - version: "4.0.15" + version: "4.0.17" ionicons: dependency: "direct main" description: @@ -364,34 +364,34 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -404,10 +404,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" octo_image: dependency: transitive description: @@ -420,34 +420,34 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: name: path_provider - sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.27" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "026b97a6c29da75181a37aae2eba9227f5fe13cb2838c6b975ce209328b8ab4e" + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.3" path_provider_linux: dependency: transitive description: @@ -468,10 +468,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" pedantic: dependency: transitive description: @@ -484,10 +484,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.4.0" platform: dependency: transitive description: @@ -508,10 +508,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.3" process: dependency: transitive description: @@ -553,18 +553,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "851d5040552cf911f4cabda08d003eca76b27da3ed0002978272e27c8fbf8ecc" + sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.8+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555 url: "https://pub.dev" source: hosted - version: "2.4.2+2" + version: "2.4.5" stack_trace: dependency: transitive description: @@ -601,10 +601,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" term_glyph: dependency: transitive description: @@ -617,66 +617,66 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timezone: dependency: transitive description: name: timezone - sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964" + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" url: "https://pub.dev" source: hosted - version: "0.9.1" + version: "0.9.2" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 url: "https://pub.dev" source: hosted - version: "6.1.10" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1" + sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" url: "https://pub.dev" source: hosted - version: "6.0.25" + version: "6.0.31" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7ab1e5b646623d6a2537aa59d5d039f90eebef75a7c25e105f6f75de1f7750c3" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: @@ -697,10 +697,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" uuid: dependency: transitive description: @@ -721,10 +721,10 @@ packages: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "4.1.4" workmanager: dependency: "direct main" description: @@ -737,26 +737,26 @@ packages: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.0" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.7.0-0" diff --git a/pubspec.yaml b/pubspec.yaml index 9d69b7d9..d224e9cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: 'none' version: 1.2.3+55 environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: flutter: From f410f0dd3aa4eb739443f3a8654095a5a0707ebe Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 13 May 2023 23:27:24 +0300 Subject: [PATCH 32/55] Removed DirectPageView and simplified home page code --- lib/common/utils/options.dart | 14 +- .../widgets/layouts/direct_page_view.dart | 113 ----------- lib/modules/character/character_view.dart | 47 +++-- lib/modules/composition/composition_view.dart | 186 +++++++++--------- lib/modules/favorites/favorites_view.dart | 56 +++--- lib/modules/feed/feed_view.dart | 2 +- lib/modules/home/home_provider.dart | 55 ++++-- lib/modules/home/home_view.dart | 135 ++++++------- lib/modules/media/media_info_view.dart | 5 +- lib/modules/settings/settings_app_tab.dart | 15 +- lib/modules/settings/settings_view.dart | 39 ++-- lib/modules/social/social_view.dart | 48 ++--- lib/modules/staff/staff_view.dart | 44 +++-- lib/modules/statistics/statistics_view.dart | 36 ++-- pubspec.lock | 4 +- pubspec.yaml | 2 +- 16 files changed, 370 insertions(+), 431 deletions(-) delete mode 100644 lib/common/widgets/layouts/direct_page_view.dart diff --git a/lib/common/utils/options.dart b/lib/common/utils/options.dart index d41e9596..60c4db8d 100644 --- a/lib/common/utils/options.dart +++ b/lib/common/utils/options.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:otraku/modules/discover/discover_models.dart'; +import 'package:otraku/modules/home/home_provider.dart'; import 'package:otraku/modules/media/media_constants.dart'; import 'package:otraku/common/utils/theming.dart'; import 'package:path_provider/path_provider.dart'; @@ -98,7 +99,7 @@ class Options extends ChangeNotifier { if (themeMode < 0 || themeMode >= ThemeMode.values.length) themeMode = 0; int homeTab = _optionBox.get(_OptionKey.defaultHomeTab.name) ?? 0; - if (homeTab < 0 || homeTab >= 5) homeTab = 0; + if (homeTab < 0 || homeTab >= HomeTab.values.length) homeTab = 0; int discoverType = _optionBox.get(_OptionKey.defaultDiscoverType.name) ?? 0; if (discoverType < 0 || discoverType >= DiscoverType.values.length) { @@ -145,7 +146,7 @@ class Options extends ChangeNotifier { ThemeMode.values[themeMode], _optionBox.get(_OptionKey.themeIndex.name), _optionBox.get(_OptionKey.pureBlackDarkTheme.name) ?? false, - homeTab, + HomeTab.values[homeTab], DiscoverType.values[discoverType], EntrySort.values[animeSort], EntrySort.values[mangaSort], @@ -217,7 +218,7 @@ class Options extends ChangeNotifier { ThemeMode _themeMode; int? _theme; bool _pureBlackDarkTheme; - int _defaultHomeTab; + HomeTab _defaultHomeTab; DiscoverType _defaultDiscoverType; EntrySort _defaultAnimeSort; EntrySort _defaultMangaSort; @@ -244,7 +245,7 @@ class Options extends ChangeNotifier { ThemeMode get themeMode => _themeMode; int? get theme => _theme; bool get pureBlackDarkTheme => _pureBlackDarkTheme; - int get defaultHomeTab => _defaultHomeTab; + HomeTab get defaultHomeTab => _defaultHomeTab; DiscoverType get defaultDiscoverType => _defaultDiscoverType; EntrySort get defaultAnimeSort => _defaultAnimeSort; EntrySort get defaultMangaSort => _defaultMangaSort; @@ -336,10 +337,9 @@ class Options extends ChangeNotifier { notifyListeners(); } - set defaultHomeTab(int v) { - if (v < 0 || v > 4) return; + set defaultHomeTab(HomeTab v) { _defaultHomeTab = v; - _optionBox.put(_OptionKey.defaultHomeTab.name, v); + _optionBox.put(_OptionKey.defaultHomeTab.name, v.index); } set defaultDiscoverType(DiscoverType v) { diff --git a/lib/common/widgets/layouts/direct_page_view.dart b/lib/common/widgets/layouts/direct_page_view.dart deleted file mode 100644 index 4302c681..00000000 --- a/lib/common/widgets/layouts/direct_page_view.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; - -/// A wrapper around [PageView] that skips over unnecessary -/// tabs when animating through multiple of them. -class DirectPageView extends StatefulWidget { - const DirectPageView({ - required this.children, - required this.current, - required this.onChanged, - }); - - final List children; - final int current; - - /// If `null` the tabs can't be swiped, but they will - /// still animate, when [current] is changed externally. - final void Function(int)? onChanged; - - @override - State createState() => _DirectPageViewState(); -} - -class _DirectPageViewState extends State { - late final PageController _ctrl; - - /// While [DirectPageView] is performing a switch triggered from the outside, - /// [_busy] is set to `true` to signal that [widget.onChanged] shouldn't be - /// called, as the outer environment already knows about the tab change. - bool _busy = false; - - @override - void initState() { - super.initState(); - _ctrl = PageController(initialPage: widget.current); - } - - @override - void didUpdateWidget(covariant DirectPageView oldWidget) { - super.didUpdateWidget(oldWidget); - _animateToCurrent(); - } - - /// Animate to any other tab as if it was a neighbouring one. - Future _animateToCurrent() async { - final page = _ctrl.page?.round() ?? 0; - _busy = true; - - if (widget.current == page + 1) { - await _ctrl.animateToPage( - page + 1, - curve: Curves.easeOutExpo, - duration: const Duration(milliseconds: 200), - ); - } else if (widget.current == page - 1) { - await _ctrl.animateToPage( - page - 1, - curve: Curves.easeOutExpo, - duration: const Duration(milliseconds: 200), - ); - } else if (widget.current > page) { - final temp = widget.children[page + 1]; - widget.children[page + 1] = widget.children[widget.current]; - - await _ctrl.animateToPage( - page + 1, - curve: Curves.easeOutExpo, - duration: const Duration(milliseconds: 200), - ); - - setState(() { - widget.children[page + 1] = temp; - _ctrl.jumpToPage(widget.current); - }); - } else if (widget.current < page) { - final temp = widget.children[page - 1]; - widget.children[page - 1] = widget.children[widget.current]; - - await _ctrl.animateToPage( - page - 1, - curve: Curves.easeOutExpo, - duration: const Duration(milliseconds: 200), - ); - - setState(() { - widget.children[page - 1] = temp; - _ctrl.jumpToPage(widget.current); - }); - } - - _busy = false; - } - - @override - void dispose() { - _ctrl.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return PageView( - controller: _ctrl, - physics: widget.onChanged == null - ? const NeverScrollableScrollPhysics() - : null, - onPageChanged: (int i) { - if (_busy) return; - widget.onChanged?.call(i); - }, - children: widget.children, - ); - } -} diff --git a/lib/modules/character/character_view.dart b/lib/modules/character/character_view.dart index 15b34e22..54618e2b 100644 --- a/lib/modules/character/character_view.dart +++ b/lib/modules/character/character_view.dart @@ -10,7 +10,6 @@ import 'package:otraku/common/widgets/grids/relation_grid.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; import 'package:otraku/common/widgets/layouts/floating_bar.dart'; import 'package:otraku/common/widgets/layouts/scaffolds.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/overlays/dialogs.dart'; import 'package:otraku/common/widgets/paged_view.dart'; @@ -25,18 +24,26 @@ class CharacterView extends ConsumerStatefulWidget { ConsumerState createState() => _CharacterViewState(); } -class _CharacterViewState extends ConsumerState { - int _tab = 0; - late final _ctrl = PagedController(loadMore: () { - if (_tab == 0) return; - _tab == 1 +class _CharacterViewState extends ConsumerState + with SingleTickerProviderStateMixin { + late final _tabCtrl = TabController(length: 3, vsync: this); + late final _scrollCtrl = PagedController(loadMore: () { + if (_tabCtrl.index == 0) return; + _tabCtrl.index == 1 ? ref.read(characterMediaProvider(widget.id).notifier).fetch(true) : ref.read(characterMediaProvider(widget.id).notifier).fetch(false); }); + @override + void initState() { + super.initState(); + _tabCtrl.addListener(() => setState(() {})); + } + @override void dispose() { - _ctrl.dispose(); + _tabCtrl.dispose(); + _scrollCtrl.dispose(); super.dispose(); } @@ -65,9 +72,9 @@ class _CharacterViewState extends ConsumerState { return PageScaffold( bottomBar: BottomNavBar( - current: _tab, - onChanged: (i) => setState(() => _tab = i), - onSame: (_) => _ctrl.scrollToTop(), + current: _tabCtrl.index, + onChanged: (i) => _tabCtrl.index = i, + onSame: (_) => _scrollCtrl.scrollToTop(), items: const { 'Bio': Ionicons.book_outline, 'Anime': Ionicons.film_outline, @@ -79,19 +86,19 @@ class _CharacterViewState extends ConsumerState { title: character.valueOrNull?.name, ), floatingBar: FloatingBar( - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, children: [ - if (_tab == 0 && character.hasValue) + if (_tabCtrl.index == 0 && character.hasValue) CharacterFavoriteButton(character.valueOrNull!), - if (_tab > 0) CharacterMediaFilterButton(widget.id), - if (_tab == 1) CharacterLanguageSelectionButton(widget.id), + if (_tabCtrl.index > 0) CharacterMediaFilterButton(widget.id), + if (_tabCtrl.index == 1) + CharacterLanguageSelectionButton(widget.id), ], ), - child: DirectPageView( - current: _tab, - onChanged: (i) => setState(() => _tab = i), + child: TabBarView( + controller: _tabCtrl, children: [ - CharacterInfoTab(widget.id, widget.imageUrl, _ctrl), + CharacterInfoTab(widget.id, widget.imageUrl, _scrollCtrl), PagedView( provider: characterMediaProvider(widget.id).select((s) => s.anime), @@ -102,14 +109,14 @@ class _CharacterViewState extends ConsumerState { .getAnimeAndVoiceActors(), ); }, - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), PagedView( provider: characterMediaProvider(widget.id).select((s) => s.manga), onData: (data) => SingleRelationGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), ], diff --git a/lib/modules/composition/composition_view.dart b/lib/modules/composition/composition_view.dart index f2412107..3cc53e6f 100644 --- a/lib/modules/composition/composition_view.dart +++ b/lib/modules/composition/composition_view.dart @@ -3,7 +3,6 @@ import 'package:ionicons/ionicons.dart'; import 'package:otraku/modules/composition/composition_model.dart'; import 'package:otraku/common/widgets/html_content.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/layouts/segment_switcher.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; @@ -24,24 +23,28 @@ class CompositionView extends StatefulWidget { State createState() => _CompositionViewState(); } -class _CompositionViewState extends State { - late final _ctrl = TextEditingController(text: widget.composition.text); - final _tab = ValueNotifier(0); +class _CompositionViewState extends State + with SingleTickerProviderStateMixin { + late final _textCtrl = TextEditingController(text: widget.composition.text); + late final _tabCtrl = TabController(length: 2, vsync: this); final _focus = FocusNode(); @override void initState() { super.initState(); - _ctrl.addListener(() => widget.composition.text = _ctrl.text); - _tab.addListener( - () => _tab.value == 0 ? _focus.requestFocus() : _focus.unfocus(), + _textCtrl.addListener(() => widget.composition.text = _textCtrl.text); + _tabCtrl.addListener( + () { + setState(() {}); + _tabCtrl.index == 0 ? _focus.requestFocus() : _focus.unfocus(); + }, ); } @override void dispose() { - _tab.dispose(); - _ctrl.dispose(); + _tabCtrl.dispose(); + _textCtrl.dispose(); _focus.dispose(); super.dispose(); } @@ -50,14 +53,14 @@ class _CompositionViewState extends State { Widget build(BuildContext context) { return OpaqueSheetView( builder: (context, scrollCtrl) => _CompositionView( - tab: _tab, focus: _focus, - textCtrl: _ctrl, + tabCtrl: _tabCtrl, + textCtrl: _textCtrl, scrollCtrl: scrollCtrl, ), buttons: _BottomBar( - tab: _tab, - textCtrl: _ctrl, + textCtrl: _textCtrl, + isEditing: _tabCtrl.index == 0, composition: widget.composition, onSave: () async { try { @@ -82,17 +85,16 @@ class _CompositionViewState extends State { } } -/// A view with 2 tabs - one for editing and one for an html preview. class _CompositionView extends StatelessWidget { const _CompositionView({ - required this.tab, required this.focus, + required this.tabCtrl, required this.textCtrl, required this.scrollCtrl, }); - final ValueNotifier tab; final FocusNode focus; + final TabController tabCtrl; final TextEditingController textCtrl; final ScrollController scrollCtrl; @@ -103,49 +105,45 @@ class _CompositionView extends StatelessWidget { vertical: 60, ); - return ValueListenableBuilder( - valueListenable: tab, - builder: (context, int i, _) => Stack( - children: [ - DirectPageView( - current: i, - onChanged: (val) => tab.value = val, - children: [ - SingleChildScrollView( - controller: scrollCtrl, - child: TextField( - autofocus: true, - focusNode: focus, - controller: textCtrl, - style: Theme.of(context).textTheme.bodyMedium, - maxLines: null, - decoration: InputDecoration( - fillColor: Theme.of(context).colorScheme.background, - contentPadding: padding, - ), + return Stack( + children: [ + TabBarView( + controller: tabCtrl, + children: [ + SingleChildScrollView( + controller: scrollCtrl, + child: TextField( + autofocus: true, + focusNode: focus, + controller: textCtrl, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: null, + decoration: InputDecoration( + fillColor: Theme.of(context).colorScheme.background, + contentPadding: padding, ), ), - SingleChildScrollView( - controller: scrollCtrl, - child: Padding( - padding: padding, - child: HtmlContent('

${textCtrl.text}

'), - ), + ), + SingleChildScrollView( + controller: scrollCtrl, + child: Padding( + padding: padding, + child: HtmlContent('

${textCtrl.text}

'), ), - ], - ), - Positioned( - top: 10, - left: 10, - right: 10, - child: SegmentSwitcher( - current: i, - items: const ['Compose', 'Preview'], - onChanged: (val) => tab.value = val, ), + ], + ), + Positioned( + top: 10, + left: 10, + right: 10, + child: SegmentSwitcher( + current: tabCtrl.index, + items: const ['Compose', 'Preview'], + onChanged: (i) => tabCtrl.index = i, ), - ], - ), + ), + ], ); } } @@ -154,13 +152,13 @@ class _CompositionView extends StatelessWidget { /// when the user isn't on the editing tab. class _BottomBar extends StatefulWidget { const _BottomBar({ - required this.tab, + required this.isEditing, required this.composition, required this.textCtrl, required this.onSave, }); - final ValueNotifier tab; + final bool isEditing; final Composition composition; final TextEditingController textCtrl; final void Function() onSave; @@ -177,45 +175,43 @@ class _BottomBarState extends State<_BottomBar> { if (_loading) return const Center(child: Loader()); return BottomBar([ - ValueListenableBuilder( - valueListenable: widget.tab, - builder: (context, i, child) => i == 0 ? child! : const SizedBox(), - child: Expanded( - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - _FormatButton( - tag: 'b', - name: 'Bold', - icon: Icons.format_bold_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'i', - name: 'Italic', - icon: Icons.format_italic_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'del', - name: 'Strikethrough', - icon: Icons.format_strikethrough_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'center', - name: 'Center', - icon: Icons.align_horizontal_center_outlined, - textCtrl: widget.textCtrl, - ), - _FormatButton( - tag: 'code', - name: 'Code', - icon: Icons.code_outlined, - textCtrl: widget.textCtrl, - ), - ], - ), + Expanded( + child: ListView( + scrollDirection: Axis.horizontal, + children: widget.isEditing + ? [ + _FormatButton( + tag: 'b', + name: 'Bold', + icon: Icons.format_bold_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'i', + name: 'Italic', + icon: Icons.format_italic_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'del', + name: 'Strikethrough', + icon: Icons.format_strikethrough_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'center', + name: 'Center', + icon: Icons.align_horizontal_center_outlined, + textCtrl: widget.textCtrl, + ), + _FormatButton( + tag: 'code', + name: 'Code', + icon: Icons.code_outlined, + textCtrl: widget.textCtrl, + ), + ] + : const [], ), ), Row( diff --git a/lib/modules/favorites/favorites_view.dart b/lib/modules/favorites/favorites_view.dart index fabd42eb..747622e7 100644 --- a/lib/modules/favorites/favorites_view.dart +++ b/lib/modules/favorites/favorites_view.dart @@ -10,7 +10,6 @@ import 'package:otraku/common/utils/paged_controller.dart'; import 'package:otraku/common/widgets/grids/tile_item_grid.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; import 'package:otraku/common/widgets/layouts/scaffolds.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/paged_view.dart'; @@ -23,34 +22,46 @@ class FavoritesView extends ConsumerStatefulWidget { ConsumerState createState() => _FavoritesViewState(); } -class _FavoritesViewState extends ConsumerState { - FavoritesTab _tab = FavoritesTab.anime; - late final _ctrl = PagedController( - loadMore: () => ref.read(favoritesProvider(widget.id).notifier).fetch(_tab), +class _FavoritesViewState extends ConsumerState + with SingleTickerProviderStateMixin { + late final _tabCtrl = TabController( + length: FavoritesTab.values.length, + vsync: this, ); + late final _scrollCtrl = PagedController( + loadMore: () => ref + .read(favoritesProvider(widget.id).notifier) + .fetch(FavoritesTab.values[_tabCtrl.index]), + ); + + @override + void initState() { + super.initState(); + _tabCtrl.addListener(() => setState(() {})); + } @override void dispose() { - _ctrl.dispose(); + _tabCtrl.dispose(); + _scrollCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final tab = FavoritesTab.values[_tabCtrl.index]; + final count = ref.watch( - favoritesProvider(widget.id).select((s) => s.getCount(_tab)), + favoritesProvider(widget.id).select((s) => s.getCount(tab)), ); final onRefresh = () => ref.invalidate(favoritesProvider(widget.id)); return PageScaffold( bottomBar: BottomNavBar( - current: _tab.index, - onChanged: (page) { - setState(() => _tab = FavoritesTab.values.elementAt(page)); - _ctrl.scrollToTop(); - }, - onSame: (_) => _ctrl.scrollToTop(), + current: _tabCtrl.index, + onChanged: (i) => _tabCtrl.index = i, + onSame: (_) => _scrollCtrl.scrollToTop(), items: const { 'Anime': Ionicons.film_outline, 'Manga': Ionicons.bookmark_outline, @@ -61,7 +72,7 @@ class _FavoritesViewState extends ConsumerState { ), child: TabScaffold( topBar: TopBar( - title: _tab.title, + title: tab.title, trailing: [ if (count > 0) Padding( @@ -73,22 +84,19 @@ class _FavoritesViewState extends ConsumerState { ), ], ), - child: DirectPageView( - current: _tab.index, - onChanged: (page) => setState( - () => _tab = FavoritesTab.values.elementAt(page), - ), + child: TabBarView( + controller: _tabCtrl, children: [ PagedView( provider: favoritesProvider(widget.id).select((s) => s.anime), onData: (data) => TileItemGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), PagedView( provider: favoritesProvider(widget.id).select((s) => s.manga), onData: (data) => TileItemGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), PagedView( @@ -96,19 +104,19 @@ class _FavoritesViewState extends ConsumerState { (s) => s.characters, ), onData: (data) => TileItemGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), PagedView( provider: favoritesProvider(widget.id).select((s) => s.staff), onData: (data) => TileItemGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), PagedView( provider: favoritesProvider(widget.id).select((s) => s.studios), onData: (data) => StudioGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), ], diff --git a/lib/modules/feed/feed_view.dart b/lib/modules/feed/feed_view.dart index e188cbf0..566d4b4a 100644 --- a/lib/modules/feed/feed_view.dart +++ b/lib/modules/feed/feed_view.dart @@ -42,7 +42,7 @@ class FeedView extends StatelessWidget { if (count > 0) { result = Badge.count( count: count, - alignment: AlignmentDirectional.bottomEnd, + alignment: AlignmentDirectional.centerStart, child: result, ); } diff --git a/lib/modules/home/home_provider.dart b/lib/modules/home/home_provider.dart index 5c2c41b5..f32eed7d 100644 --- a/lib/modules/home/home_provider.dart +++ b/lib/modules/home/home_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ionicons/ionicons.dart'; import 'package:otraku/modules/activity/activities_providers.dart'; import 'package:otraku/modules/discover/discover_providers.dart'; import 'package:otraku/common/utils/options.dart'; @@ -8,11 +9,11 @@ final homeProvider = ChangeNotifierProvider.autoDispose((ref) => HomeNotifier()); class HomeNotifier extends ChangeNotifier { - int _homeTab = Options().defaultHomeTab; + HomeTab _homeTab = Options().defaultHomeTab; - int get homeTab => _homeTab; + HomeTab get homeTab => _homeTab; - set homeTab(int val) { + set homeTab(HomeTab val) { if (_homeTab == val) return; _homeTab = val; notifyListeners(); @@ -31,23 +32,23 @@ class HomeNotifier extends ChangeNotifier { _systemDarkScheme = d; } - /// The discover and feed tab are loaded lazily, when they are first opened. - var _didLoadDiscover = false; + /// The discover and feed tab are loaded lazily, when first opened. var _didLoadFeed = false; + var _didLoadDiscover = false; - bool get didLoadDiscover => _didLoadDiscover; bool get didLoadFeed => _didLoadFeed; + bool get didLoadDiscover => _didLoadDiscover; - void lazyLoadDiscover(WidgetRef ref) { - if (_didLoadDiscover) return; - _didLoadDiscover = true; - discoverLoadMore(ref); - } + void lazyLoadTabs(WidgetRef ref) { + if (_homeTab == HomeTab.feed && !_didLoadFeed) { + _didLoadFeed = true; + ref.read(activitiesProvider(null).notifier).fetch(); + } - void lazyLoadFeed(WidgetRef ref) { - if (_didLoadFeed) return; - _didLoadFeed = true; - ref.read(activitiesProvider(null).notifier).fetch(); + if (_homeTab == HomeTab.discover && !_didLoadDiscover) { + _didLoadDiscover = true; + discoverLoadMore(ref); + } } /// In preview mode, user's collections first load only current media. @@ -73,3 +74,27 @@ class HomeNotifier extends ChangeNotifier { } } } + +enum HomeTab { + feed, + anime, + manga, + discover, + profile; + + String get title => switch (this) { + feed => 'Feed', + anime => 'Anime', + manga => 'Manga', + discover => 'Discover', + profile => 'Profile', + }; + + IconData get iconData => switch (this) { + feed => Ionicons.file_tray_outline, + anime => Ionicons.film_outline, + manga => Ionicons.bookmark_outline, + discover => Ionicons.compass_outline, + profile => Ionicons.person_outline, + }; +} diff --git a/lib/modules/home/home_view.dart b/lib/modules/home/home_view.dart index 73f22f98..89833856 100644 --- a/lib/modules/home/home_view.dart +++ b/lib/modules/home/home_view.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:ionicons/ionicons.dart'; import 'package:otraku/modules/activity/activities_providers.dart'; import 'package:otraku/modules/collection/collection_preview_provider.dart'; import 'package:otraku/modules/collection/collection_preview_view.dart'; @@ -21,7 +20,6 @@ import 'package:otraku/modules/user/user_view.dart'; import 'package:otraku/common/utils/background_handler.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; import 'package:otraku/common/widgets/layouts/scaffolds.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/overlays/dialogs.dart'; class HomeView extends ConsumerStatefulWidget { @@ -29,25 +27,34 @@ class HomeView extends ConsumerStatefulWidget { final int id; - static const INBOX = 0; - static const ANIME_LIST = 1; - static const MANGA_LIST = 2; - static const DISCOVER = 3; - static const USER = 4; - @override ConsumerState createState() => _HomeViewState(); } -class _HomeViewState extends ConsumerState { - late final _ctrl = PagedController(loadMore: _scrollListener); - late final animeCollectionTag = (userId: widget.id, ofAnime: true); - late final mangaCollectionTag = (userId: widget.id, ofAnime: false); +class _HomeViewState extends ConsumerState + with SingleTickerProviderStateMixin { + late final _animeCollectionTag = (userId: widget.id, ofAnime: true); + late final _mangaCollectionTag = (userId: widget.id, ofAnime: false); + late final _scrollCtrl = PagedController(loadMore: _scrollListener); + late final _tabCtrl = TabController( + length: HomeTab.values.length, + vsync: this, + ); + + @override + void initState() { + super.initState(); + _tabCtrl.index = ref.read(homeProvider.notifier).homeTab.index; + _tabCtrl.addListener( + () => ref.read(homeProvider).homeTab = HomeTab.values[_tabCtrl.index], + ); + } @override void dispose() { BackgroundHandler.clearNotifications(); - _ctrl.dispose(); + _scrollCtrl.dispose(); + _tabCtrl.dispose(); super.dispose(); } @@ -105,63 +112,60 @@ class _HomeViewState extends ConsumerState { ref.watch(discoverReviewProvider.select((_) => null)), }); - final notifier = ref.watch(homeProvider); + ref.listen( + homeProvider.select((s) => s.homeTab), + (_, tab) => _tabCtrl.index = tab.index, + ); - // Lazy-load current tab if necessary. - if (notifier.homeTab == HomeView.INBOX) { - notifier.lazyLoadFeed(ref); - } else if (notifier.homeTab == HomeView.DISCOVER) { - notifier.lazyLoadDiscover(ref); - } + final notifier = ref.watch(homeProvider); + notifier.lazyLoadTabs(ref); notifier.didExpandCollection(true) - ? ref.watch(entriesProvider(animeCollectionTag).select((_) => null)) + ? ref.watch(entriesProvider(_animeCollectionTag).select((_) => null)) : ref.watch( - collectionPreviewProvider(animeCollectionTag).select((_) => null), + collectionPreviewProvider(_animeCollectionTag).select((_) => null), ); notifier.didExpandCollection(false) - ? ref.watch(entriesProvider(mangaCollectionTag).select((_) => null)) + ? ref.watch(entriesProvider(_mangaCollectionTag).select((_) => null)) : ref.watch( - collectionPreviewProvider(mangaCollectionTag).select((_) => null), + collectionPreviewProvider(_mangaCollectionTag).select((_) => null), ); return WillPopScope( onWillPop: () => _onWillPop(context), child: PageScaffold( bottomBar: BottomNavBar( - current: notifier.homeTab, - onChanged: (i) => ref.read(homeProvider).homeTab = i, - items: const { - 'Feed': Ionicons.file_tray_outline, - 'Anime': Ionicons.film_outline, - 'Manga': Ionicons.bookmark_outline, - 'Discover': Ionicons.compass_outline, - 'Profile': Ionicons.person_outline, + current: notifier.homeTab.index, + onChanged: (i) => ref.read(homeProvider).homeTab = HomeTab.values[i], + items: { + for (final t in HomeTab.values) t.title: t.iconData, }, onSame: (i) { - switch (i) { - case HomeView.ANIME_LIST: - if (_ctrl.position.pixels > 0) { - _ctrl.scrollToTop(); + final tab = HomeTab.values[i]; + + switch (tab) { + case HomeTab.anime: + if (_scrollCtrl.position.pixels > 0) { + _scrollCtrl.scrollToTop(); } else if (ref.read(homeProvider).didExpandCollection(true)) { ref - .read(searchProvider(animeCollectionTag).notifier) + .read(searchProvider(_animeCollectionTag).notifier) .update((s) => s == null ? '' : null); } return; - case HomeView.MANGA_LIST: - if (_ctrl.position.pixels > 0) { - _ctrl.scrollToTop(); + case HomeTab.manga: + if (_scrollCtrl.position.pixels > 0) { + _scrollCtrl.scrollToTop(); } else if (ref.read(homeProvider).didExpandCollection(false)) { ref - .read(searchProvider(mangaCollectionTag).notifier) + .read(searchProvider(_mangaCollectionTag).notifier) .update((s) => s == null ? '' : null); } return; - case HomeView.DISCOVER: - if (_ctrl.position.pixels > 0) { - _ctrl.scrollToTop(); + case HomeTab.discover: + if (_scrollCtrl.position.pixels > 0) { + _scrollCtrl.scrollToTop(); } else { ref .read(searchProvider(null).notifier) @@ -169,42 +173,41 @@ class _HomeViewState extends ConsumerState { } return; default: - _ctrl.scrollToTop(); + _scrollCtrl.scrollToTop(); return; } }, ), - child: DirectPageView( - current: notifier.homeTab, - onChanged: (i) => ref.read(homeProvider).homeTab = i, + child: TabBarView( + controller: _tabCtrl, children: [ - FeedView(_ctrl), + FeedView(_scrollCtrl), if (notifier.didExpandCollection(true)) CollectionSubView( - scrollCtrl: _ctrl, - tag: animeCollectionTag, + scrollCtrl: _scrollCtrl, + tag: _animeCollectionTag, key: Key(true.toString()), ) else CollectionPreviewView( - scrollCtrl: _ctrl, - tag: animeCollectionTag, + scrollCtrl: _scrollCtrl, + tag: _animeCollectionTag, key: Key(true.toString()), ), if (notifier.didExpandCollection(false)) CollectionSubView( - scrollCtrl: _ctrl, - tag: mangaCollectionTag, + scrollCtrl: _scrollCtrl, + tag: _mangaCollectionTag, key: Key(false.toString()), ) else CollectionPreviewView( - scrollCtrl: _ctrl, - tag: mangaCollectionTag, + scrollCtrl: _scrollCtrl, + tag: _mangaCollectionTag, key: Key(false.toString()), ), - DiscoverView(_ctrl), - UserSubView(widget.id, null, _ctrl), + DiscoverView(_scrollCtrl), + UserSubView(widget.id, null, _scrollCtrl), ], ), ), @@ -213,16 +216,16 @@ class _HomeViewState extends ConsumerState { void _scrollListener() { final notifier = ref.read(homeProvider); - if (notifier.homeTab == HomeView.INBOX) { + if (notifier.homeTab == HomeTab.feed) { ref.read(activitiesProvider(null).notifier).fetch(); - } else if (notifier.homeTab == HomeView.DISCOVER) { + } else if (notifier.homeTab == HomeTab.discover) { discoverLoadMore(ref); } } Future _onWillPop(BuildContext context) async { final notifier = ref.read(homeProvider); - if (notifier.homeTab == HomeView.DISCOVER) { + if (notifier.homeTab == HomeTab.discover) { final notifier = ref.read(searchProvider(null).notifier); if (notifier.state != null) { notifier.state = null; @@ -230,18 +233,18 @@ class _HomeViewState extends ConsumerState { } } - if (notifier.homeTab == HomeView.ANIME_LIST && + if (notifier.homeTab == HomeTab.anime && notifier.didExpandCollection(true)) { - final notifier = ref.read(searchProvider(animeCollectionTag).notifier); + final notifier = ref.read(searchProvider(_animeCollectionTag).notifier); if (notifier.state != null) { notifier.state = null; return Future.value(false); } } - if (notifier.homeTab == HomeView.MANGA_LIST && + if (notifier.homeTab == HomeTab.manga && notifier.didExpandCollection(false)) { - final notifier = ref.read(searchProvider(mangaCollectionTag).notifier); + final notifier = ref.read(searchProvider(_mangaCollectionTag).notifier); if (notifier.state != null) { notifier.state = null; return Future.value(false); diff --git a/lib/modules/media/media_info_view.dart b/lib/modules/media/media_info_view.dart index 414b8842..1cfe7031 100644 --- a/lib/modules/media/media_info_view.dart +++ b/lib/modules/media/media_info_view.dart @@ -5,7 +5,6 @@ import 'package:otraku/common/utils/consts.dart'; import 'package:otraku/modules/discover/discover_models.dart'; import 'package:otraku/modules/filter/filter_providers.dart'; import 'package:otraku/modules/home/home_provider.dart'; -import 'package:otraku/modules/home/home_view.dart'; import 'package:otraku/modules/media/media_models.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/link_tile.dart'; @@ -138,7 +137,7 @@ class MediaInfoView extends StatelessWidget { filter.genreIn.add(info.genres[i]); notifier.filter = filter; - ref.read(homeProvider).homeTab = HomeView.DISCOVER; + ref.read(homeProvider).homeTab = HomeTab.discover; Navigator.popUntil(context, (r) => r.isFirst); }, ), @@ -311,7 +310,7 @@ class _TagScrollCardsState extends State<_TagScrollCards> { filter.tagIn.add(tags[i].name); notifier.filter = filter; - widget.ref.read(homeProvider).homeTab = HomeView.DISCOVER; + widget.ref.read(homeProvider).homeTab = HomeTab.discover; Navigator.popUntil(context, (r) => r.isFirst); }, onLongPress: (i) => showPopUp( diff --git a/lib/modules/settings/settings_app_tab.dart b/lib/modules/settings/settings_app_tab.dart index e0d8ac4a..c13ee9f3 100644 --- a/lib/modules/settings/settings_app_tab.dart +++ b/lib/modules/settings/settings_app_tab.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:otraku/modules/discover/discover_models.dart'; import 'package:otraku/modules/filter/chip_selector.dart'; import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/modules/home/home_provider.dart'; import 'package:otraku/modules/media/media_constants.dart'; import 'package:otraku/common/utils/convert.dart'; import 'package:otraku/common/utils/options.dart'; -import 'package:otraku/modules/home/home_view.dart'; import 'package:otraku/common/widgets/fields/checkbox_field.dart'; import 'package:otraku/common/widgets/fields/drop_down_field.dart'; import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; @@ -69,15 +69,12 @@ class SettingsAppTab extends StatelessWidget { delegate: SliverChildListDelegate.fixed([ DropDownField( title: 'Startup Page', - value: Options().defaultHomeTab, - items: const { - 'Feed': HomeView.INBOX, - 'Anime': HomeView.ANIME_LIST, - 'Manga': HomeView.MANGA_LIST, - 'Discover': HomeView.DISCOVER, - 'Profile': HomeView.USER, + value: Options().defaultHomeTab.index, + items: { + for (final t in HomeTab.values) t.title: t.index, }, - onChanged: (val) => Options().defaultHomeTab = val, + onChanged: (val) => + Options().defaultHomeTab = HomeTab.values[val], ), DropDownField( title: 'Default Anime Sort', diff --git a/lib/modules/settings/settings_view.dart b/lib/modules/settings/settings_view.dart index feef119e..65e0e919 100644 --- a/lib/modules/settings/settings_view.dart +++ b/lib/modules/settings/settings_view.dart @@ -12,7 +12,6 @@ import 'package:otraku/modules/settings/settings_about_tab.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; import 'package:otraku/common/widgets/layouts/constrained_view.dart'; import 'package:otraku/common/widgets/layouts/scaffolds.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; @@ -23,15 +22,23 @@ class SettingsView extends ConsumerStatefulWidget { ConsumerState createState() => _SettingsViewState(); } -class _SettingsViewState extends ConsumerState { +class _SettingsViewState extends ConsumerState + with SingleTickerProviderStateMixin { late var _settings = ref.read(settingsProvider).whenData((v) => v.copy()); - final _ctrl = ScrollController(); + late final _tabCtrl = TabController(length: 4, vsync: this); + final _scrollCtrl = ScrollController(); bool _shouldUpdate = false; - int _tabIndex = 0; + + @override + void initState() { + super.initState(); + _tabCtrl.addListener(() => setState(() {})); + } @override void dispose() { - _ctrl.dispose(); + _tabCtrl.dispose(); + _scrollCtrl.dispose(); super.dispose(); } @@ -63,21 +70,21 @@ class _SettingsViewState extends ConsumerState { const errorWidget = Center(child: Text('Failed to load settings')); final tabs = [ - ConstrainedView(child: SettingsAppTab(_ctrl)), + ConstrainedView(child: SettingsAppTab(_scrollCtrl)), if (_settings.hasError) ...[ errorWidget, errorWidget, ] else if (_settings.hasValue) ...[ ConstrainedView( child: SettingsContentTab( - _ctrl, + _scrollCtrl, _settings.value!, () => _shouldUpdate = true, ), ), ConstrainedView( child: SettingsNotificationsTab( - _ctrl, + _scrollCtrl, _settings.value!, () => _shouldUpdate = true, ), @@ -86,7 +93,7 @@ class _SettingsViewState extends ConsumerState { loadWidget, loadWidget, ], - ConstrainedView(child: SettingsAboutTab(_ctrl)), + ConstrainedView(child: SettingsAboutTab(_scrollCtrl)), ]; return WillPopScope( @@ -98,9 +105,9 @@ class _SettingsViewState extends ConsumerState { }, child: PageScaffold( bottomBar: BottomNavBar( - current: _tabIndex, - onSame: (_) => _ctrl.scrollToTop(), - onChanged: (i) => setState(() => _tabIndex = i), + current: _tabCtrl.index, + onSame: (_) => _scrollCtrl.scrollToTop(), + onChanged: (i) => _tabCtrl.index = i, items: const { 'App': Ionicons.color_palette_outline, 'Content': Ionicons.tv_outline, @@ -109,12 +116,8 @@ class _SettingsViewState extends ConsumerState { }, ), child: TabScaffold( - topBar: TopBar(title: pageNames[_tabIndex]), - child: DirectPageView( - current: _tabIndex, - onChanged: (i) => setState(() => _tabIndex = i), - children: tabs, - ), + topBar: TopBar(title: pageNames[_tabCtrl.index]), + child: TabBarView(controller: _tabCtrl, children: tabs), ), ), ); diff --git a/lib/modules/social/social_view.dart b/lib/modules/social/social_view.dart index 0760e6d3..8108fe43 100644 --- a/lib/modules/social/social_view.dart +++ b/lib/modules/social/social_view.dart @@ -8,7 +8,6 @@ import 'package:otraku/modules/user/user_grid.dart'; import 'package:otraku/common/utils/paged_controller.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; import 'package:otraku/common/widgets/layouts/scaffolds.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/paged_view.dart'; @@ -21,34 +20,43 @@ class SocialView extends ConsumerStatefulWidget { ConsumerState createState() => _SocialViewState(); } -class _SocialViewState extends ConsumerState { - late SocialTab _tab = SocialTab.following; - late final _ctrl = PagedController( - loadMore: () => ref.read(socialProvider(widget.id).notifier).fetch(_tab), +class _SocialViewState extends ConsumerState + with SingleTickerProviderStateMixin { + late final _tabCtrl = TabController(length: 2, vsync: this); + late final _scrollCtrl = PagedController( + loadMore: () => ref + .read(socialProvider(widget.id).notifier) + .fetch(SocialTab.values[_tabCtrl.index]), ); + @override + void initState() { + super.initState(); + _tabCtrl.addListener(() => setState(() {})); + } + @override void dispose() { - _ctrl.dispose(); + _tabCtrl.dispose(); + _scrollCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final tab = SocialTab.values[_tabCtrl.index]; + final count = ref.watch( - socialProvider(widget.id).select((s) => s.getCount(_tab)), + socialProvider(widget.id).select((s) => s.getCount(tab)), ); final onRefresh = () => ref.invalidate(socialProvider(widget.id)); return PageScaffold( bottomBar: BottomNavBar( - current: _tab.index, - onChanged: (page) { - setState(() => _tab = SocialTab.values.elementAt(page)); - _ctrl.scrollToTop(); - }, - onSame: (_) => _ctrl.scrollToTop(), + current: _tabCtrl.index, + onChanged: (i) => _tabCtrl.index = i, + onSame: (_) => _scrollCtrl.scrollToTop(), items: const { 'Following': Ionicons.people_circle, 'Followers': Ionicons.person_circle, @@ -56,7 +64,7 @@ class _SocialViewState extends ConsumerState { ), child: TabScaffold( topBar: TopBar( - title: _tab.title, + title: tab.title, trailing: [ if (count > 0) Padding( @@ -68,23 +76,19 @@ class _SocialViewState extends ConsumerState { ), ], ), - child: DirectPageView( - current: _tab.index, - onChanged: (page) { - setState(() => _tab = SocialTab.values.elementAt(page)); - _ctrl.scrollToTop(); - }, + child: TabBarView( + controller: _tabCtrl, children: [ PagedView( provider: socialProvider(widget.id).select((s) => s.following), onData: (data) => UserGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), PagedView( provider: socialProvider(widget.id).select((s) => s.followers), onData: (data) => UserGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), ], diff --git a/lib/modules/staff/staff_view.dart b/lib/modules/staff/staff_view.dart index 2138d7cf..2a9731fd 100644 --- a/lib/modules/staff/staff_view.dart +++ b/lib/modules/staff/staff_view.dart @@ -10,7 +10,6 @@ import 'package:otraku/common/widgets/grids/relation_grid.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; import 'package:otraku/common/widgets/layouts/floating_bar.dart'; import 'package:otraku/common/widgets/layouts/scaffolds.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/overlays/dialogs.dart'; import 'package:otraku/common/widgets/paged_view.dart'; @@ -25,18 +24,26 @@ class StaffView extends ConsumerStatefulWidget { ConsumerState createState() => _StaffViewState(); } -class _StaffViewState extends ConsumerState { - int _tab = 0; - late final _ctrl = PagedController(loadMore: () { - if (_tab == 0) return; - _tab == 1 +class _StaffViewState extends ConsumerState + with SingleTickerProviderStateMixin { + late final _tabCtrl = TabController(length: 3, vsync: this); + late final _scrollCtrl = PagedController(loadMore: () { + if (_tabCtrl.index == 0) return; + _tabCtrl.index == 1 ? ref.read(staffRelationsProvider(widget.id).notifier).fetch(true) : ref.read(staffRelationsProvider(widget.id).notifier).fetch(false); }); + @override + void initState() { + super.initState(); + _tabCtrl.addListener(() => setState(() {})); + } + @override void dispose() { - _ctrl.dispose(); + _tabCtrl.dispose(); + _scrollCtrl.dispose(); super.dispose(); } @@ -65,9 +72,9 @@ class _StaffViewState extends ConsumerState { return PageScaffold( bottomBar: BottomNavBar( - current: _tab, - onChanged: (i) => setState(() => _tab = i), - onSame: (_) => _ctrl.scrollToTop(), + current: _tabCtrl.index, + onChanged: (i) => _tabCtrl.index = i, + onSame: (_) => _scrollCtrl.scrollToTop(), items: const { 'Bio': Ionicons.book_outline, 'Characters': Ionicons.mic_outline, @@ -79,30 +86,29 @@ class _StaffViewState extends ConsumerState { title: staff.valueOrNull?.name, ), floatingBar: FloatingBar( - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, children: [ - if (_tab == 0 && staff.hasValue) + if (_tabCtrl.index == 0 && staff.hasValue) StaffFavoriteButton(staff.valueOrNull!), - if (_tab > 0) StaffFilterButton(widget.id, true), + if (_tabCtrl.index > 0) StaffFilterButton(widget.id, true), ], ), - child: DirectPageView( - current: _tab, - onChanged: (i) => setState(() => _tab = i), + child: TabBarView( + controller: _tabCtrl, children: [ - StaffInfoTab(widget.id, widget.imageUrl, _ctrl), + StaffInfoTab(widget.id, widget.imageUrl, _scrollCtrl), PagedView<(Relation, Relation)>( provider: staffRelationsProvider(widget.id) .select((s) => s.charactersAndMedia), onData: (data) => RelationGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), PagedView( provider: staffRelationsProvider(widget.id).select((s) => s.roles), onData: (data) => SingleRelationGrid(data.items), - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, onRefresh: onRefresh, ), ], diff --git a/lib/modules/statistics/statistics_view.dart b/lib/modules/statistics/statistics_view.dart index 8dedc804..a5673309 100644 --- a/lib/modules/statistics/statistics_view.dart +++ b/lib/modules/statistics/statistics_view.dart @@ -10,7 +10,6 @@ import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; import 'package:otraku/common/widgets/layouts/bottom_bar.dart'; import 'package:otraku/common/widgets/layouts/constrained_view.dart'; import 'package:otraku/common/widgets/layouts/scaffolds.dart'; -import 'package:otraku/common/widgets/layouts/direct_page_view.dart'; import 'package:otraku/common/widgets/layouts/top_bar.dart'; import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; import 'package:otraku/common/widgets/layouts/segment_switcher.dart'; @@ -25,16 +24,24 @@ class StatisticsView extends StatefulWidget { State createState() => _StatisticsViewState(); } -class _StatisticsViewState extends State { - final _ctrl = ScrollController(); - bool _onAnime = true; +class _StatisticsViewState extends State + with SingleTickerProviderStateMixin { + late final _tabCtrl = TabController(length: 2, vsync: this); + final _scrollCtrl = ScrollController(); int _primaryBarChartTab = 0; // 0-1 int _secondaryBarChartTab = 0; // 0-2 + @override + void initState() { + super.initState(); + _tabCtrl.addListener(() => setState(() {})); + } + @override void dispose() { - _ctrl.dispose(); + _tabCtrl.dispose(); + _scrollCtrl.dispose(); super.dispose(); } @@ -61,15 +68,13 @@ class _StatisticsViewState extends State { child: Text('Failed to load statistics'), ), data: (data) { - return DirectPageView( - current: _onAnime ? 0 : 1, - onChanged: (i) => - setState(() => _onAnime = i > 0 ? false : true), + return TabBarView( + controller: _tabCtrl, children: [ _StatisticsView( statistics: data.animeStats, ofAnime: true, - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, primaryBarChartTab: () => _primaryBarChartTab, secondaryBarChartTab: () => _secondaryBarChartTab, onPrimaryTabChanged: (i) => _primaryBarChartTab = i, @@ -78,7 +83,7 @@ class _StatisticsViewState extends State { _StatisticsView( statistics: data.mangaStats, ofAnime: false, - scrollCtrl: _ctrl, + scrollCtrl: _scrollCtrl, primaryBarChartTab: () => _primaryBarChartTab, secondaryBarChartTab: () => _secondaryBarChartTab, onPrimaryTabChanged: (i) => _primaryBarChartTab = i, @@ -93,10 +98,9 @@ class _StatisticsViewState extends State { return PageScaffold( bottomBar: BottomNavBar( - current: _onAnime ? 0 : 1, - onChanged: (page) => - setState(() => _onAnime = page == 0 ? true : false), - onSame: (_) => _ctrl.scrollToTop(), + current: _tabCtrl.index, + onChanged: (i) => _tabCtrl.index = i, + onSame: (_) => _scrollCtrl.scrollToTop(), items: const { 'Anime': Ionicons.film_outline, 'Manga': Ionicons.bookmark_outline, @@ -104,7 +108,7 @@ class _StatisticsViewState extends State { ), child: TabScaffold( topBar: TopBar( - title: _onAnime ? 'Anime Statistics' : 'Manga Statistics', + title: _tabCtrl.index == 0 ? 'Anime Statistics' : 'Manga Statistics', ), child: ConstrainedView(child: content), ), diff --git a/pubspec.lock b/pubspec.lock index 0a259953..3f044bdb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -210,10 +210,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "2876372952b65ca7f684e698eba22bda1cf581fa071dd30ba2f01900f507d0d1" + sha256: ee6ee56855aa920899b68586b538474d086c149932220b47b92502cbfb5ba5e5 url: "https://pub.dev" source: hosted - version: "14.0.0+1" + version: "14.0.0+2" flutter_local_notifications_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d224e9cd..05bbd7a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: workmanager: ^0.5.1 # Sending device notifications. - flutter_local_notifications: ^14.0.0+1 + flutter_local_notifications: ^14.0.0+2 # Translating html into flutter widgets. flutter_widget_from_html_core: ^0.10.0 From 8adae7206d43d73932320e1b893f4e4f6cde6176 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 12:50:58 +0300 Subject: [PATCH 33/55] changed the icons of android to support the new themed icons --- .../dev/res/mipmap-anydpi-v26/ic_launcher.xml | 7 ++++--- .../src/dev/res/mipmap-hdpi/ic_launcher.png | Bin 1083 -> 5243 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 0 -> 855 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3683 bytes .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 0 -> 3683 bytes .../src/dev/res/mipmap-mdpi/ic_launcher.png | Bin 810 -> 2904 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 0 -> 463 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2309 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 0 -> 2309 bytes .../src/dev/res/mipmap-xhdpi/ic_launcher.png | Bin 1465 -> 7051 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 0 -> 1320 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5342 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 0 -> 5342 bytes .../src/dev/res/mipmap-xxhdpi/ic_launcher.png | Bin 2045 -> 12304 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 0 -> 2952 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 8250 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 0 -> 8250 bytes .../dev/res/mipmap-xxxhdpi/ic_launcher.png | Bin 2664 -> 16770 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 0 -> 4236 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 12652 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 0 -> 12652 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 ++++--- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1083 -> 5243 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 0 -> 855 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3683 bytes .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 0 -> 3683 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 810 -> 2904 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 0 -> 463 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2309 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 0 -> 2309 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 1465 -> 7051 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 0 -> 1320 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5342 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 0 -> 5342 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 2045 -> 12304 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 0 -> 2952 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 8250 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 0 -> 8250 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 2664 -> 16770 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 0 -> 4236 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 12652 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 0 -> 12652 bytes 42 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png create mode 100644 android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 android/app/src/dev/res/mipmap-hdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/dev/res/mipmap-mdpi/ic_launcher_background.png create mode 100644 android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 android/app/src/dev/res/mipmap-mdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/dev/res/mipmap-xhdpi/ic_launcher_background.png create mode 100644 android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/dev/res/mipmap-xhdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_background.png create mode 100644 android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_background.png create mode 100644 android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png diff --git a/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml index 5f349f7f..345888d2 100644 --- a/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png index 22107cfaef4c4f2b592939bbb47c50abb485fb56..b48b9af2f8ef8459bd5d90089f77eb6bfd6e8b7e 100644 GIT binary patch literal 5243 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 B215V< delta 1073 zcmV-11kU^WD7y%d8Gix*007#LBoF`q1OiD!K~#7F?O9t$6j2zSJu^GA?&i9dmb*$M zq}D?a=tIyAk|+qGg5H8|1VRweQ!mj=LA`b%>M5d!Aj2|xke~;B2~mpZ!3Qxlt<+t- zu6vtH|1|FGsA=Or$2=qRad6KZ{{Q%Y=f9kPc7>8HTe#`U~=fI9eLZtqBYg0 zZ~J^cRnF#S5^{1}o}G|qCzaHcoSFuBJyJhkdP;6%RekvZ?=^X6eL$&G54VQx)Z;o_}3>RtO?O-9A*v(rgm`9YPmF z?8k@4Fkqq=04WHSDj3^Qd+q_OOb>h>>wINOLZJX5_>3L8B$dWsB|9|+aRn7cQx(0a z7(e(OU|drsTI`soWQcsS0w7d|t72kD0{g<^4)vx|&sw zp?^a$?`pO@MhYIohT^*S7|)c)oaY$p#lx)HD}|}(BZT-RHHWzjt)M}S@V@-3w1%#V z7DaFx6T~3h^*3b$*c`Je>di-T+@V!8h*{n;(HFE;kxKobK#W)mG`^*p?=i=q;;{lT zbdGV1d(1%-%%I04%QrdrCb*zTXfz+RLw^z9c@z|RMm#p}fE{=D9Hr)iy2mJlB8wiw zcOHy;4BvS$SnjP=5#M=OJti>{iZnd7pb2(p5=W~EHs|=xLt`%#=XW0HCfIV1F>*nV zbUaIZdS}ejqkXtdK(X)T+!w&I<98O03bBcvcITVqW)gq>#SjhVew3gEkC@xw5h`;V rJYsHxN6c;Th`9|OF}J}Z<~IBWIoznw6Bn6Q00000NkvXXu0mjf^+@`F diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 0000000000000000000000000000000000000000..c416cf3175b2f827143a5a9f8ab1d0ee47a17354 GIT binary patch literal 855 zcmeAS@N?(olHy`uVBq!ia0vp^i$Iuz4M-mPBqj}{7>k44ofy`glX=O&z%1zL;uum9 z_x6$_FN1;ri=jb-ODkaM;8Edfm@(s^pn}l^4Thv7Hur`T4x@@jgJ7tpf|xmTPb+gBI_w6_DGZ*j KelF{r5}E)!^7pU+ literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..ed078697a20f3c6bbc27c042ba722e32bd6f4b35 GIT binary patch literal 3683 zcmeH~=T{R-w}%5LO++G9BpeJ%F``lw0TIMdB1M`gf`;BSbfiQfpkgTM;SG@*2)&0Q zp?6R^p(k+YT_A)K!9*aJ^Zo_*^IiAD%=2x}%(M2MwSG~iCi-W2#d!e$z*(39)ck~* z|1NIMlf7+M<^8O5SE#evfa)2IkKkX#54&I{Gek}m zoP%1v*mYB2?{`#v`)X@u)@Ms*Vk#TUm+)ufuEF}eg27JeN2^`H~B_IGSLEn!Q0DfnKaHG*y z9a0a_=mnJUDJT@;@b@Js)Tf1)9h@nw*d+kYykmEo4LB4n=EMy&lPHToq1|-(l-{G| z`2X*fH&X^*w^5}$o5gd{Sd=Lp2?sgQk3(i1|xReMKj)j;%)=C zbpvOq08zC+Tyb(FNrSbcC}@c*JDb5$lg$r~DE(xbh57f792<$m06!nH4M4 zMeRcV&5tiMF0$zgNFJcJjtO)O(U;`$5ULd~Ji8K7dLe3v)6|zV*e$gh2uMD+(UN$e zw8s0$@nW92#_D|35Yj4bMXQ3`XXOXx#8@|Z{&vFQ^#PR<15t|G9uXK}wyJ8B&yFSl7 z=hdy!AjKjFe z^6ktKwp4ZMs-)~fXN#P&glKWElkzjFC z>-%$`sVGzwsDM-MnOAc0p3 zLKlACCw8P;?9B}*Bw=K@2xb=@FFEKFy!laZj?L!u4cd+p<)F~_;OHPsc6P)$Ui(d1 zP4>FzZG-6KzNKXTT1G8o@aR7SAnRxQVNG zH|~WSbCS#L-GG!}K;O3?Wcj7~5n~y0Wn(ou5yHn|@Ff_XslV*^r+($Gi&D+N)L@dh z2ZT)!{B>_yp4GMKj$Dz+D=P}@({)Tc z>SIeXRFwWS5%&&~ZOJP$EA1mOH|MCx;!z?Wf&r2~Q#szul~vA!%nfTW zuNxkX_)J7EelZH;Kc&(U{dK=Y2?Dhtb=3$8p9&C*|5f>Ovzs+twrqy3;^{Pk3Q>B*B-{`2|fF%6B#LfZ> z4BAb!Pc++LjuTEUO+MRz5sX3zA~%kob$IyMN7zwBP_@Z@%H$e-UnUs+!h_-jhkSO@;~Vn__f za!kVz8=Oz{_u<`L9ZVE#*P9_rX2w|F@v`W`dO}sEp^{KnBV|uOJm4l z`D5#0Zi_nUcDy;Q&P(C@u4x%1y5`OFwgj#P4p6@*)gBhVcunc;9joP@D#8Z5+Q%H) zwnx}Enf;IZ1to@|-AfuJn(~_5W#(bY;%T9{vtvKKmPryh8sA=?okVh)NSXWwOFn*{ zP+_^-t(M&Yg<9s0^Z~z zO(7>B$QV`lqyznykxhSW3Hta?qiN9JYC&%ip+3!4uAa9JSADEWMV;!lt6Jh*x_2*g z*`m>3fnpkW#{SX6ueTNe8u0}__FNKaq*TLR>1&4(ckAV7U| z+hchgUx-wY42YXlWZmFB(v!=E*$9);@U1jFlxyj<3baCHf&;dG5cIP0Dv}%EUfF@K zSSr=79}@)P!lD-D9@nN78Loe5uB{2){-lj7sG{!2Wm$r>cvlwLH6Tcy^|y`F->ONL z@R;gOtB&P~HYBml#d~|$_b|1W$v0BE9N;O}6m{!Q+iBfv?q6pAA`9l9Sk?E;K0C;W zin;t*tLHGqa9No-KERW@A#t=wivIOI*g2>AQLrK|-^kW;gX$S9#T2H}#B6{X-#(mC z^LA$K`B^irw*m3m9fYK)a?WP5Y17R+#WdJ)eckh zg?Bgtw_bu2HrB2En*=u3%vs+*vGU&>erWAydG|ayAlwwXV)DBQHsTBQs=f#R&=krS-3# zZ9mBj?WUOIE*>;GP8u6&f07&Q^I^vSfBgSSemILwK2vQr_!Fb-BsBtHx+YM9j$Oq6 E0KV<{r2qf` literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..ed078697a20f3c6bbc27c042ba722e32bd6f4b35 GIT binary patch literal 3683 zcmeH~=T{R-w}%5LO++G9BpeJ%F``lw0TIMdB1M`gf`;BSbfiQfpkgTM;SG@*2)&0Q zp?6R^p(k+YT_A)K!9*aJ^Zo_*^IiAD%=2x}%(M2MwSG~iCi-W2#d!e$z*(39)ck~* z|1NIMlf7+M<^8O5SE#evfa)2IkKkX#54&I{Gek}m zoP%1v*mYB2?{`#v`)X@u)@Ms*Vk#TUm+)ufuEF}eg27JeN2^`H~B_IGSLEn!Q0DfnKaHG*y z9a0a_=mnJUDJT@;@b@Js)Tf1)9h@nw*d+kYykmEo4LB4n=EMy&lPHToq1|-(l-{G| z`2X*fH&X^*w^5}$o5gd{Sd=Lp2?sgQk3(i1|xReMKj)j;%)=C zbpvOq08zC+Tyb(FNrSbcC}@c*JDb5$lg$r~DE(xbh57f792<$m06!nH4M4 zMeRcV&5tiMF0$zgNFJcJjtO)O(U;`$5ULd~Ji8K7dLe3v)6|zV*e$gh2uMD+(UN$e zw8s0$@nW92#_D|35Yj4bMXQ3`XXOXx#8@|Z{&vFQ^#PR<15t|G9uXK}wyJ8B&yFSl7 z=hdy!AjKjFe z^6ktKwp4ZMs-)~fXN#P&glKWElkzjFC z>-%$`sVGzwsDM-MnOAc0p3 zLKlACCw8P;?9B}*Bw=K@2xb=@FFEKFy!laZj?L!u4cd+p<)F~_;OHPsc6P)$Ui(d1 zP4>FzZG-6KzNKXTT1G8o@aR7SAnRxQVNG zH|~WSbCS#L-GG!}K;O3?Wcj7~5n~y0Wn(ou5yHn|@Ff_XslV*^r+($Gi&D+N)L@dh z2ZT)!{B>_yp4GMKj$Dz+D=P}@({)Tc z>SIeXRFwWS5%&&~ZOJP$EA1mOH|MCx;!z?Wf&r2~Q#szul~vA!%nfTW zuNxkX_)J7EelZH;Kc&(U{dK=Y2?Dhtb=3$8p9&C*|5f>Ovzs+twrqy3;^{Pk3Q>B*B-{`2|fF%6B#LfZ> z4BAb!Pc++LjuTEUO+MRz5sX3zA~%kob$IyMN7zwBP_@Z@%H$e-UnUs+!h_-jhkSO@;~Vn__f za!kVz8=Oz{_u<`L9ZVE#*P9_rX2w|F@v`W`dO}sEp^{KnBV|uOJm4l z`D5#0Zi_nUcDy;Q&P(C@u4x%1y5`OFwgj#P4p6@*)gBhVcunc;9joP@D#8Z5+Q%H) zwnx}Enf;IZ1to@|-AfuJn(~_5W#(bY;%T9{vtvKKmPryh8sA=?okVh)NSXWwOFn*{ zP+_^-t(M&Yg<9s0^Z~z zO(7>B$QV`lqyznykxhSW3Hta?qiN9JYC&%ip+3!4uAa9JSADEWMV;!lt6Jh*x_2*g z*`m>3fnpkW#{SX6ueTNe8u0}__FNKaq*TLR>1&4(ckAV7U| z+hchgUx-wY42YXlWZmFB(v!=E*$9);@U1jFlxyj<3baCHf&;dG5cIP0Dv}%EUfF@K zSSr=79}@)P!lD-D9@nN78Loe5uB{2){-lj7sG{!2Wm$r>cvlwLH6Tcy^|y`F->ONL z@R;gOtB&P~HYBml#d~|$_b|1W$v0BE9N;O}6m{!Q+iBfv?q6pAA`9l9Sk?E;K0C;W zin;t*tLHGqa9No-KERW@A#t=wivIOI*g2>AQLrK|-^kW;gX$S9#T2H}#B6{X-#(mC z^LA$K`B^irw*m3m9fYK)a?WP5Y17R+#WdJ)eckh zg?Bgtw_bu2HrB2En*=u3%vs+*vGU&>erWAydG|ayAlwwXV)DBQHsTBQs=f#R&=krS-3# zZ9mBj?WUOIE*>;GP8u6&f07&Q^I^vSfBgSSemILwK2vQr_!Fb-BsBtHx+YM9j$Oq6 E0KV<{r2qf` literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png index ed4ca1ebf44401a72b9457f74217a0da159d6d05..b6ebe7d08c3f7cb741536bc89c6704bb4a4c6ec0 100644 GIT binary patch literal 2904 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 diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 0000000000000000000000000000000000000000..98f2af83652dab969cf01c41d5d90f98677cb8ec GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^IUvlz1|<8_!p{OJ#^NA%Cx&(BWL^Tzopr0E*(M5C8xG literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2388d70528f616fea263897e4082e106aa3de813 GIT binary patch literal 2309 zcmc&$c{J1w7yiwdVa7y8uRR%KX*IS`w2UP?1__j`uE$=LSLXXvbVTz0`iAe?!{g;(-rsi_*=}(LeqI4bRGbDMT_^C+07!64! zS28Lv(Jxc?@u+Oy+=u6iHCyqgdBf5p?4JQm%Z<3jm!~Py^GmZE-(x>)?EA3+ZRMOr z<}+$e9ck1$j{|FVBmQHK#k+H~G;QTT=fmQeJ7q|A+ltsyYc|qBdf4E9Lb!VbF(^;t4i|k^G2V>J9xff|yfk*BobaXqQ>#)ej#gzxr$o@`p-W zh--#y8ezjm7YqqfVuW|`T=lU`5z8i#eCSh&UE8w4Z}v-nvdojQeo$~QBd1DAPOHWg zEN9}@s4cOpR=OlQB8+vQ5m;XHmonAU2Q^ZfhE8;;^eW8buWo>@Ck8l@53J{ z6V@-bo-Mj0SCl5HMYNq3+RRr8?FieaD2h3wFOwfHlip=U|q-<0)5R3nK$2abTfbf zwT(`25{(-$j?BPQn0`MRp({m2GgE8vWVF2E1fQ35v7POyWJk0!fW**hA*6@u@?U}8 zBA$139uxnLWmXv94|{Z`S62xWmgk zWvZoaUQgopE3jQR9p531VtvfkQnkgrzrP&{t`CY5K@l~?8!0M zQ@5KQd%a<4u2KrKaY(P!-1QJ)Tyt8Inhn2nxcDO{jGFyAM;~4xhZVXY{>PX?IXO8L zf1>SoyOIY&bwXGx)V-6lER2kPXK;Yft|pMN3e{tJKoA-;$XSMU-4st+J4QhtARy+_ z7wX^GMdc2qv$p^R&^}DD4}!Sr6zF`pVklT}ppyVBr{>4$q<6TZ)FOm}pwWk^vZ$Ry zWu?0RVr6QnSGVP2n{*owh|o2XBd0_#bjNzC9{4>RWxrTLj{ElE_W~v(iM=1A`s;f% zuje5wAGnT=5A-kBA&PyYCojFZBt!@J3rjLS28zT^WbVD3TTaSPFXAdk%>^zL1N4b_ z+XW&m3BsFxidude!QDyz1O_vD_Fa2KB;f*w!=s9>3FGDw+4|<3YC3uUww)$IQX+wM>U3Wee|bOK0H|u z5&BkEeaXhB!#&#B+c~wO?Co3@KLG0oK4zD>vVMj10+D~VtkDFuTc}Qa@*tD_=zd7~ zc2QTabvHxeX17=|wm~WJrUZsncGy?)bTlY?oZxl^7M-=gl2d>@SRm?u9R5ABsjCae zUm~5hU-Pc=u`{p7$cB|1s6$X~c~Ta6Ps<8dL{1j(=6Z+Jd){Bo&k1HTli?w$)rfj@ zkld_1Jz-)P)&h0VAy_BDaBeBBKP=1GFFO9_RaHw?AEIfzUMbho;?8FC4cmq<%-tbd z4B&{sb6$oGJjICly{!=2G<3t>y{U|O{`=WphJ=3fUli^t-{31?k0s%h_ZWQ!tB(>l zb!gm~vFdjF(KNgVn{_|*mvm~a)(TdmkWfQ2`<76ZC9H^X)0?sifBo^l129Y#{1@9JhS&_F5)};u!aO;fA~wT-A@p)R*15*jL5l)c+!j vdgTBGGyWtmH(R^u<<|epPyVmIU`t85@LOhl3|xB0*8o;$?9IwdJQM!~anC!N literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..2388d70528f616fea263897e4082e106aa3de813 GIT binary patch literal 2309 zcmc&$c{J1w7yiwdVa7y8uRR%KX*IS`w2UP?1__j`uE$=LSLXXvbVTz0`iAe?!{g;(-rsi_*=}(LeqI4bRGbDMT_^C+07!64! zS28Lv(Jxc?@u+Oy+=u6iHCyqgdBf5p?4JQm%Z<3jm!~Py^GmZE-(x>)?EA3+ZRMOr z<}+$e9ck1$j{|FVBmQHK#k+H~G;QTT=fmQeJ7q|A+ltsyYc|qBdf4E9Lb!VbF(^;t4i|k^G2V>J9xff|yfk*BobaXqQ>#)ej#gzxr$o@`p-W zh--#y8ezjm7YqqfVuW|`T=lU`5z8i#eCSh&UE8w4Z}v-nvdojQeo$~QBd1DAPOHWg zEN9}@s4cOpR=OlQB8+vQ5m;XHmonAU2Q^ZfhE8;;^eW8buWo>@Ck8l@53J{ z6V@-bo-Mj0SCl5HMYNq3+RRr8?FieaD2h3wFOwfHlip=U|q-<0)5R3nK$2abTfbf zwT(`25{(-$j?BPQn0`MRp({m2GgE8vWVF2E1fQ35v7POyWJk0!fW**hA*6@u@?U}8 zBA$139uxnLWmXv94|{Z`S62xWmgk zWvZoaUQgopE3jQR9p531VtvfkQnkgrzrP&{t`CY5K@l~?8!0M zQ@5KQd%a<4u2KrKaY(P!-1QJ)Tyt8Inhn2nxcDO{jGFyAM;~4xhZVXY{>PX?IXO8L zf1>SoyOIY&bwXGx)V-6lER2kPXK;Yft|pMN3e{tJKoA-;$XSMU-4st+J4QhtARy+_ z7wX^GMdc2qv$p^R&^}DD4}!Sr6zF`pVklT}ppyVBr{>4$q<6TZ)FOm}pwWk^vZ$Ry zWu?0RVr6QnSGVP2n{*owh|o2XBd0_#bjNzC9{4>RWxrTLj{ElE_W~v(iM=1A`s;f% zuje5wAGnT=5A-kBA&PyYCojFZBt!@J3rjLS28zT^WbVD3TTaSPFXAdk%>^zL1N4b_ z+XW&m3BsFxidude!QDyz1O_vD_Fa2KB;f*w!=s9>3FGDw+4|<3YC3uUww)$IQX+wM>U3Wee|bOK0H|u z5&BkEeaXhB!#&#B+c~wO?Co3@KLG0oK4zD>vVMj10+D~VtkDFuTc}Qa@*tD_=zd7~ zc2QTabvHxeX17=|wm~WJrUZsncGy?)bTlY?oZxl^7M-=gl2d>@SRm?u9R5ABsjCae zUm~5hU-Pc=u`{p7$cB|1s6$X~c~Ta6Ps<8dL{1j(=6Z+Jd){Bo&k1HTli?w$)rfj@ zkld_1Jz-)P)&h0VAy_BDaBeBBKP=1GFFO9_RaHw?AEIfzUMbho;?8FC4cmq<%-tbd z4B&{sb6$oGJjICly{!=2G<3t>y{U|O{`=WphJ=3fUli^t-{31?k0s%h_ZWQ!tB(>l zb!gm~vFdjF(KNgVn{_|*mvm~a)(TdmkWfQ2`<76ZC9H^X)0?sifBo^l129Y#{1@9JhS&_F5)};u!aO;fA~wT-A@p)R*15*jL5l)c+!j vdgTBGGyWtmH(R^u<<|epPyVmIU`t85@LOhl3|xB0*8o;$?9IwdJQM!~anC!N literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png index be832d148099c1928b28382eb0fe7f1de420252b..9172cd990e803be1ff9523b52d699fe2a39b8bbb 100644 GIT binary patch literal 7051 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 literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba273a3ee667f828ad9f5a3850f2423f1a0cd92 GIT binary patch literal 5342 zcmeI0XH%0=w}wMPKoWWvX;P#Y1q@v}gepkyhzQb=-Z4s-P?V-1RXQj|5CuXM1R*pb z1SAwGK}skB(gaSN^Nw?V!#n50v-hkId-ht_y7xV6p5)ucx-<|D2mk<}f$M3Rk@l&- z-z5stn#?OfByD7YX1W@HhGEV%0Dw6UuBC1s=144r263*0C(QeW1ka}mcmtU|&HCZM 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 WWdf6;pOAi70^r)lS`8Y`i2nhRu8fZW literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba273a3ee667f828ad9f5a3850f2423f1a0cd92 GIT binary patch literal 5342 zcmeI0XH%0=w}wMPKoWWvX;P#Y1q@v}gepkyhzQb=-Z4s-P?V-1RXQj|5CuXM1R*pb z1SAwGK}skB(gaSN^Nw?V!#n50v-hkId-ht_y7xV6p5)ucx-<|D2mk<}f$M3Rk@l&- z-z5stn#?OfByD7YX1W@HhGEV%0Dw6UuBC1s=144r263*0C(QeW1ka}mcmtU|&HCZM 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 WWdf6;pOAi70^r)lS`8Y`i2nhRu8fZW literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png index 5a95c61b10183b2ec08b23102da89f81982b4a7f..947df615f3c39d7f5aea8988fa9f84a1a29063a7 100644 GIT binary patch literal 12304 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 Hz@7gAk?hbD diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000000000000000000000000000000000000..63ec3d37f1d8cdebb1be7c368d9f518a96472348 GIT binary patch literal 2952 zcmeAS@N?(olHy`uVBq!ia0y~yU~~at4mO}j{44ikK#H+A$lZxy-8q?;3=G^(o-U3d z6>)EGZtP`p6mUIwlf|R$Pcn0J=>&nQf9p5je);O6aKZJ*Hu1;h8JSo(=6DP2WdhPQ zEG!qj6&xB4=qhx~V*<*sT$BcJw>S8A#IXSRHBMjHI0O_P2rn{sXkciZ&$Q2|T7rc` z;KS6LAcYO<&)j1GDNFqZQue|BKz_$PcA&tYwimnt3JxGHNFI&b)KI^QMMar~BZq_I z(XqL?o(&EH0!JEBS{NF686EHJ*sRMbz`@icF`-95VS}PVhjjTiRYoR92La|uf=n!C zEG&hO&uvq2P~hN5axijgIMCLhkay?iG!_mPMn@hGHx7XtK;wF?Z%<-qR8Z((Jjubx zB+b;cqcA;MNI`&w#ZW;~(ILUvLBRZ7jyDraQ-cEAsAEP$W;D%==BCkdX0!wztqw*j i%F!mo2y0w5ntz!+E#!aH?Koi5n!(f6&t;ucLK6TQ$zyE* literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..080af9f07dd1d88ee73ccffeb499ee2169cc9018 GIT binary patch literal 8250 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 literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f7c8744ea00a776189d8d96e991d39118647cb GIT binary patch literal 12652 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 acil57bEATQ1K{5$u;iqaz@=}E1OFG90Nv*R literal 0 HcmV?d00001 diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f7c8744ea00a776189d8d96e991d39118647cb GIT binary patch literal 12652 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 acil57bEATQ1K{5$u;iqaz@=}E1OFG90Nv*R literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 5f349f7f..345888d2 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 22107cfaef4c4f2b592939bbb47c50abb485fb56..b48b9af2f8ef8459bd5d90089f77eb6bfd6e8b7e 100644 GIT binary patch literal 5243 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 B215V< delta 1073 zcmV-11kU^WD7y%d8Gix*007#LBoF`q1OiD!K~#7F?O9t$6j2zSJu^GA?&i9dmb*$M zq}D?a=tIyAk|+qGg5H8|1VRweQ!mj=LA`b%>M5d!Aj2|xke~;B2~mpZ!3Qxlt<+t- zu6vtH|1|FGsA=Or$2=qRad6KZ{{Q%Y=f9kPc7>8HTe#`U~=fI9eLZtqBYg0 zZ~J^cRnF#S5^{1}o}G|qCzaHcoSFuBJyJhkdP;6%RekvZ?=^X6eL$&G54VQx)Z;o_}3>RtO?O-9A*v(rgm`9YPmF z?8k@4Fkqq=04WHSDj3^Qd+q_OOb>h>>wINOLZJX5_>3L8B$dWsB|9|+aRn7cQx(0a z7(e(OU|drsTI`soWQcsS0w7d|t72kD0{g<^4)vx|&sw zp?^a$?`pO@MhYIohT^*S7|)c)oaY$p#lx)HD}|}(BZT-RHHWzjt)M}S@V@-3w1%#V z7DaFx6T~3h^*3b$*c`Je>di-T+@V!8h*{n;(HFE;kxKobK#W)mG`^*p?=i=q;;{lT zbdGV1d(1%-%%I04%QrdrCb*zTXfz+RLw^z9c@z|RMm#p}fE{=D9Hr)iy2mJlB8wiw zcOHy;4BvS$SnjP=5#M=OJti>{iZnd7pb2(p5=W~EHs|=xLt`%#=XW0HCfIV1F>*nV zbUaIZdS}ejqkXtdK(X)T+!w&I<98O03bBcvcITVqW)gq>#SjhVew3gEkC@xw5h`;V rJYsHxN6c;Th`9|OF}J}Z<~IBWIoznw6Bn6Q00000NkvXXu0mjf^+@`F diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 0000000000000000000000000000000000000000..c416cf3175b2f827143a5a9f8ab1d0ee47a17354 GIT binary patch literal 855 zcmeAS@N?(olHy`uVBq!ia0vp^i$Iuz4M-mPBqj}{7>k44ofy`glX=O&z%1zL;uum9 z_x6$_FN1;ri=jb-ODkaM;8Edfm@(s^pn}l^4Thv7Hur`T4x@@jgJ7tpf|xmTPb+gBI_w6_DGZ*j KelF{r5}E)!^7pU+ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..ed078697a20f3c6bbc27c042ba722e32bd6f4b35 GIT binary patch literal 3683 zcmeH~=T{R-w}%5LO++G9BpeJ%F``lw0TIMdB1M`gf`;BSbfiQfpkgTM;SG@*2)&0Q zp?6R^p(k+YT_A)K!9*aJ^Zo_*^IiAD%=2x}%(M2MwSG~iCi-W2#d!e$z*(39)ck~* z|1NIMlf7+M<^8O5SE#evfa)2IkKkX#54&I{Gek}m zoP%1v*mYB2?{`#v`)X@u)@Ms*Vk#TUm+)ufuEF}eg27JeN2^`H~B_IGSLEn!Q0DfnKaHG*y z9a0a_=mnJUDJT@;@b@Js)Tf1)9h@nw*d+kYykmEo4LB4n=EMy&lPHToq1|-(l-{G| z`2X*fH&X^*w^5}$o5gd{Sd=Lp2?sgQk3(i1|xReMKj)j;%)=C zbpvOq08zC+Tyb(FNrSbcC}@c*JDb5$lg$r~DE(xbh57f792<$m06!nH4M4 zMeRcV&5tiMF0$zgNFJcJjtO)O(U;`$5ULd~Ji8K7dLe3v)6|zV*e$gh2uMD+(UN$e zw8s0$@nW92#_D|35Yj4bMXQ3`XXOXx#8@|Z{&vFQ^#PR<15t|G9uXK}wyJ8B&yFSl7 z=hdy!AjKjFe z^6ktKwp4ZMs-)~fXN#P&glKWElkzjFC z>-%$`sVGzwsDM-MnOAc0p3 zLKlACCw8P;?9B}*Bw=K@2xb=@FFEKFy!laZj?L!u4cd+p<)F~_;OHPsc6P)$Ui(d1 zP4>FzZG-6KzNKXTT1G8o@aR7SAnRxQVNG zH|~WSbCS#L-GG!}K;O3?Wcj7~5n~y0Wn(ou5yHn|@Ff_XslV*^r+($Gi&D+N)L@dh z2ZT)!{B>_yp4GMKj$Dz+D=P}@({)Tc z>SIeXRFwWS5%&&~ZOJP$EA1mOH|MCx;!z?Wf&r2~Q#szul~vA!%nfTW zuNxkX_)J7EelZH;Kc&(U{dK=Y2?Dhtb=3$8p9&C*|5f>Ovzs+twrqy3;^{Pk3Q>B*B-{`2|fF%6B#LfZ> z4BAb!Pc++LjuTEUO+MRz5sX3zA~%kob$IyMN7zwBP_@Z@%H$e-UnUs+!h_-jhkSO@;~Vn__f za!kVz8=Oz{_u<`L9ZVE#*P9_rX2w|F@v`W`dO}sEp^{KnBV|uOJm4l z`D5#0Zi_nUcDy;Q&P(C@u4x%1y5`OFwgj#P4p6@*)gBhVcunc;9joP@D#8Z5+Q%H) zwnx}Enf;IZ1to@|-AfuJn(~_5W#(bY;%T9{vtvKKmPryh8sA=?okVh)NSXWwOFn*{ zP+_^-t(M&Yg<9s0^Z~z zO(7>B$QV`lqyznykxhSW3Hta?qiN9JYC&%ip+3!4uAa9JSADEWMV;!lt6Jh*x_2*g z*`m>3fnpkW#{SX6ueTNe8u0}__FNKaq*TLR>1&4(ckAV7U| z+hchgUx-wY42YXlWZmFB(v!=E*$9);@U1jFlxyj<3baCHf&;dG5cIP0Dv}%EUfF@K zSSr=79}@)P!lD-D9@nN78Loe5uB{2){-lj7sG{!2Wm$r>cvlwLH6Tcy^|y`F->ONL z@R;gOtB&P~HYBml#d~|$_b|1W$v0BE9N;O}6m{!Q+iBfv?q6pAA`9l9Sk?E;K0C;W zin;t*tLHGqa9No-KERW@A#t=wivIOI*g2>AQLrK|-^kW;gX$S9#T2H}#B6{X-#(mC z^LA$K`B^irw*m3m9fYK)a?WP5Y17R+#WdJ)eckh zg?Bgtw_bu2HrB2En*=u3%vs+*vGU&>erWAydG|ayAlwwXV)DBQHsTBQs=f#R&=krS-3# zZ9mBj?WUOIE*>;GP8u6&f07&Q^I^vSfBgSSemILwK2vQr_!Fb-BsBtHx+YM9j$Oq6 E0KV<{r2qf` literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..ed078697a20f3c6bbc27c042ba722e32bd6f4b35 GIT binary patch literal 3683 zcmeH~=T{R-w}%5LO++G9BpeJ%F``lw0TIMdB1M`gf`;BSbfiQfpkgTM;SG@*2)&0Q zp?6R^p(k+YT_A)K!9*aJ^Zo_*^IiAD%=2x}%(M2MwSG~iCi-W2#d!e$z*(39)ck~* z|1NIMlf7+M<^8O5SE#evfa)2IkKkX#54&I{Gek}m zoP%1v*mYB2?{`#v`)X@u)@Ms*Vk#TUm+)ufuEF}eg27JeN2^`H~B_IGSLEn!Q0DfnKaHG*y z9a0a_=mnJUDJT@;@b@Js)Tf1)9h@nw*d+kYykmEo4LB4n=EMy&lPHToq1|-(l-{G| z`2X*fH&X^*w^5}$o5gd{Sd=Lp2?sgQk3(i1|xReMKj)j;%)=C zbpvOq08zC+Tyb(FNrSbcC}@c*JDb5$lg$r~DE(xbh57f792<$m06!nH4M4 zMeRcV&5tiMF0$zgNFJcJjtO)O(U;`$5ULd~Ji8K7dLe3v)6|zV*e$gh2uMD+(UN$e zw8s0$@nW92#_D|35Yj4bMXQ3`XXOXx#8@|Z{&vFQ^#PR<15t|G9uXK}wyJ8B&yFSl7 z=hdy!AjKjFe z^6ktKwp4ZMs-)~fXN#P&glKWElkzjFC z>-%$`sVGzwsDM-MnOAc0p3 zLKlACCw8P;?9B}*Bw=K@2xb=@FFEKFy!laZj?L!u4cd+p<)F~_;OHPsc6P)$Ui(d1 zP4>FzZG-6KzNKXTT1G8o@aR7SAnRxQVNG zH|~WSbCS#L-GG!}K;O3?Wcj7~5n~y0Wn(ou5yHn|@Ff_XslV*^r+($Gi&D+N)L@dh z2ZT)!{B>_yp4GMKj$Dz+D=P}@({)Tc z>SIeXRFwWS5%&&~ZOJP$EA1mOH|MCx;!z?Wf&r2~Q#szul~vA!%nfTW zuNxkX_)J7EelZH;Kc&(U{dK=Y2?Dhtb=3$8p9&C*|5f>Ovzs+twrqy3;^{Pk3Q>B*B-{`2|fF%6B#LfZ> z4BAb!Pc++LjuTEUO+MRz5sX3zA~%kob$IyMN7zwBP_@Z@%H$e-UnUs+!h_-jhkSO@;~Vn__f za!kVz8=Oz{_u<`L9ZVE#*P9_rX2w|F@v`W`dO}sEp^{KnBV|uOJm4l z`D5#0Zi_nUcDy;Q&P(C@u4x%1y5`OFwgj#P4p6@*)gBhVcunc;9joP@D#8Z5+Q%H) zwnx}Enf;IZ1to@|-AfuJn(~_5W#(bY;%T9{vtvKKmPryh8sA=?okVh)NSXWwOFn*{ zP+_^-t(M&Yg<9s0^Z~z zO(7>B$QV`lqyznykxhSW3Hta?qiN9JYC&%ip+3!4uAa9JSADEWMV;!lt6Jh*x_2*g z*`m>3fnpkW#{SX6ueTNe8u0}__FNKaq*TLR>1&4(ckAV7U| z+hchgUx-wY42YXlWZmFB(v!=E*$9);@U1jFlxyj<3baCHf&;dG5cIP0Dv}%EUfF@K zSSr=79}@)P!lD-D9@nN78Loe5uB{2){-lj7sG{!2Wm$r>cvlwLH6Tcy^|y`F->ONL z@R;gOtB&P~HYBml#d~|$_b|1W$v0BE9N;O}6m{!Q+iBfv?q6pAA`9l9Sk?E;K0C;W zin;t*tLHGqa9No-KERW@A#t=wivIOI*g2>AQLrK|-^kW;gX$S9#T2H}#B6{X-#(mC z^LA$K`B^irw*m3m9fYK)a?WP5Y17R+#WdJ)eckh zg?Bgtw_bu2HrB2En*=u3%vs+*vGU&>erWAydG|ayAlwwXV)DBQHsTBQs=f#R&=krS-3# zZ9mBj?WUOIE*>;GP8u6&f07&Q^I^vSfBgSSemILwK2vQr_!Fb-BsBtHx+YM9j$Oq6 E0KV<{r2qf` literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index ed4ca1ebf44401a72b9457f74217a0da159d6d05..b6ebe7d08c3f7cb741536bc89c6704bb4a4c6ec0 100644 GIT binary patch literal 2904 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 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 0000000000000000000000000000000000000000..98f2af83652dab969cf01c41d5d90f98677cb8ec GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^IUvlz1|<8_!p{OJ#^NA%Cx&(BWL^Tzopr0E*(M5C8xG literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2388d70528f616fea263897e4082e106aa3de813 GIT binary patch literal 2309 zcmc&$c{J1w7yiwdVa7y8uRR%KX*IS`w2UP?1__j`uE$=LSLXXvbVTz0`iAe?!{g;(-rsi_*=}(LeqI4bRGbDMT_^C+07!64! zS28Lv(Jxc?@u+Oy+=u6iHCyqgdBf5p?4JQm%Z<3jm!~Py^GmZE-(x>)?EA3+ZRMOr z<}+$e9ck1$j{|FVBmQHK#k+H~G;QTT=fmQeJ7q|A+ltsyYc|qBdf4E9Lb!VbF(^;t4i|k^G2V>J9xff|yfk*BobaXqQ>#)ej#gzxr$o@`p-W zh--#y8ezjm7YqqfVuW|`T=lU`5z8i#eCSh&UE8w4Z}v-nvdojQeo$~QBd1DAPOHWg zEN9}@s4cOpR=OlQB8+vQ5m;XHmonAU2Q^ZfhE8;;^eW8buWo>@Ck8l@53J{ z6V@-bo-Mj0SCl5HMYNq3+RRr8?FieaD2h3wFOwfHlip=U|q-<0)5R3nK$2abTfbf zwT(`25{(-$j?BPQn0`MRp({m2GgE8vWVF2E1fQ35v7POyWJk0!fW**hA*6@u@?U}8 zBA$139uxnLWmXv94|{Z`S62xWmgk zWvZoaUQgopE3jQR9p531VtvfkQnkgrzrP&{t`CY5K@l~?8!0M zQ@5KQd%a<4u2KrKaY(P!-1QJ)Tyt8Inhn2nxcDO{jGFyAM;~4xhZVXY{>PX?IXO8L zf1>SoyOIY&bwXGx)V-6lER2kPXK;Yft|pMN3e{tJKoA-;$XSMU-4st+J4QhtARy+_ z7wX^GMdc2qv$p^R&^}DD4}!Sr6zF`pVklT}ppyVBr{>4$q<6TZ)FOm}pwWk^vZ$Ry zWu?0RVr6QnSGVP2n{*owh|o2XBd0_#bjNzC9{4>RWxrTLj{ElE_W~v(iM=1A`s;f% zuje5wAGnT=5A-kBA&PyYCojFZBt!@J3rjLS28zT^WbVD3TTaSPFXAdk%>^zL1N4b_ z+XW&m3BsFxidude!QDyz1O_vD_Fa2KB;f*w!=s9>3FGDw+4|<3YC3uUww)$IQX+wM>U3Wee|bOK0H|u z5&BkEeaXhB!#&#B+c~wO?Co3@KLG0oK4zD>vVMj10+D~VtkDFuTc}Qa@*tD_=zd7~ zc2QTabvHxeX17=|wm~WJrUZsncGy?)bTlY?oZxl^7M-=gl2d>@SRm?u9R5ABsjCae zUm~5hU-Pc=u`{p7$cB|1s6$X~c~Ta6Ps<8dL{1j(=6Z+Jd){Bo&k1HTli?w$)rfj@ zkld_1Jz-)P)&h0VAy_BDaBeBBKP=1GFFO9_RaHw?AEIfzUMbho;?8FC4cmq<%-tbd z4B&{sb6$oGJjICly{!=2G<3t>y{U|O{`=WphJ=3fUli^t-{31?k0s%h_ZWQ!tB(>l zb!gm~vFdjF(KNgVn{_|*mvm~a)(TdmkWfQ2`<76ZC9H^X)0?sifBo^l129Y#{1@9JhS&_F5)};u!aO;fA~wT-A@p)R*15*jL5l)c+!j vdgTBGGyWtmH(R^u<<|epPyVmIU`t85@LOhl3|xB0*8o;$?9IwdJQM!~anC!N literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..2388d70528f616fea263897e4082e106aa3de813 GIT binary patch literal 2309 zcmc&$c{J1w7yiwdVa7y8uRR%KX*IS`w2UP?1__j`uE$=LSLXXvbVTz0`iAe?!{g;(-rsi_*=}(LeqI4bRGbDMT_^C+07!64! zS28Lv(Jxc?@u+Oy+=u6iHCyqgdBf5p?4JQm%Z<3jm!~Py^GmZE-(x>)?EA3+ZRMOr z<}+$e9ck1$j{|FVBmQHK#k+H~G;QTT=fmQeJ7q|A+ltsyYc|qBdf4E9Lb!VbF(^;t4i|k^G2V>J9xff|yfk*BobaXqQ>#)ej#gzxr$o@`p-W zh--#y8ezjm7YqqfVuW|`T=lU`5z8i#eCSh&UE8w4Z}v-nvdojQeo$~QBd1DAPOHWg zEN9}@s4cOpR=OlQB8+vQ5m;XHmonAU2Q^ZfhE8;;^eW8buWo>@Ck8l@53J{ z6V@-bo-Mj0SCl5HMYNq3+RRr8?FieaD2h3wFOwfHlip=U|q-<0)5R3nK$2abTfbf zwT(`25{(-$j?BPQn0`MRp({m2GgE8vWVF2E1fQ35v7POyWJk0!fW**hA*6@u@?U}8 zBA$139uxnLWmXv94|{Z`S62xWmgk zWvZoaUQgopE3jQR9p531VtvfkQnkgrzrP&{t`CY5K@l~?8!0M zQ@5KQd%a<4u2KrKaY(P!-1QJ)Tyt8Inhn2nxcDO{jGFyAM;~4xhZVXY{>PX?IXO8L zf1>SoyOIY&bwXGx)V-6lER2kPXK;Yft|pMN3e{tJKoA-;$XSMU-4st+J4QhtARy+_ z7wX^GMdc2qv$p^R&^}DD4}!Sr6zF`pVklT}ppyVBr{>4$q<6TZ)FOm}pwWk^vZ$Ry zWu?0RVr6QnSGVP2n{*owh|o2XBd0_#bjNzC9{4>RWxrTLj{ElE_W~v(iM=1A`s;f% zuje5wAGnT=5A-kBA&PyYCojFZBt!@J3rjLS28zT^WbVD3TTaSPFXAdk%>^zL1N4b_ z+XW&m3BsFxidude!QDyz1O_vD_Fa2KB;f*w!=s9>3FGDw+4|<3YC3uUww)$IQX+wM>U3Wee|bOK0H|u z5&BkEeaXhB!#&#B+c~wO?Co3@KLG0oK4zD>vVMj10+D~VtkDFuTc}Qa@*tD_=zd7~ zc2QTabvHxeX17=|wm~WJrUZsncGy?)bTlY?oZxl^7M-=gl2d>@SRm?u9R5ABsjCae zUm~5hU-Pc=u`{p7$cB|1s6$X~c~Ta6Ps<8dL{1j(=6Z+Jd){Bo&k1HTli?w$)rfj@ zkld_1Jz-)P)&h0VAy_BDaBeBBKP=1GFFO9_RaHw?AEIfzUMbho;?8FC4cmq<%-tbd z4B&{sb6$oGJjICly{!=2G<3t>y{U|O{`=WphJ=3fUli^t-{31?k0s%h_ZWQ!tB(>l zb!gm~vFdjF(KNgVn{_|*mvm~a)(TdmkWfQ2`<76ZC9H^X)0?sifBo^l129Y#{1@9JhS&_F5)};u!aO;fA~wT-A@p)R*15*jL5l)c+!j vdgTBGGyWtmH(R^u<<|epPyVmIU`t85@LOhl3|xB0*8o;$?9IwdJQM!~anC!N literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index be832d148099c1928b28382eb0fe7f1de420252b..9172cd990e803be1ff9523b52d699fe2a39b8bbb 100644 GIT binary patch literal 7051 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba273a3ee667f828ad9f5a3850f2423f1a0cd92 GIT binary patch literal 5342 zcmeI0XH%0=w}wMPKoWWvX;P#Y1q@v}gepkyhzQb=-Z4s-P?V-1RXQj|5CuXM1R*pb z1SAwGK}skB(gaSN^Nw?V!#n50v-hkId-ht_y7xV6p5)ucx-<|D2mk<}f$M3Rk@l&- z-z5stn#?OfByD7YX1W@HhGEV%0Dw6UuBC1s=144r263*0C(QeW1ka}mcmtU|&HCZM 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 WWdf6;pOAi70^r)lS`8Y`i2nhRu8fZW literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba273a3ee667f828ad9f5a3850f2423f1a0cd92 GIT binary patch literal 5342 zcmeI0XH%0=w}wMPKoWWvX;P#Y1q@v}gepkyhzQb=-Z4s-P?V-1RXQj|5CuXM1R*pb z1SAwGK}skB(gaSN^Nw?V!#n50v-hkId-ht_y7xV6p5)ucx-<|D2mk<}f$M3Rk@l&- z-z5stn#?OfByD7YX1W@HhGEV%0Dw6UuBC1s=144r263*0C(QeW1ka}mcmtU|&HCZM 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 WWdf6;pOAi70^r)lS`8Y`i2nhRu8fZW literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 5a95c61b10183b2ec08b23102da89f81982b4a7f..947df615f3c39d7f5aea8988fa9f84a1a29063a7 100644 GIT binary patch literal 12304 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 Hz@7gAk?hbD diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000000000000000000000000000000000000..63ec3d37f1d8cdebb1be7c368d9f518a96472348 GIT binary patch literal 2952 zcmeAS@N?(olHy`uVBq!ia0y~yU~~at4mO}j{44ikK#H+A$lZxy-8q?;3=G^(o-U3d z6>)EGZtP`p6mUIwlf|R$Pcn0J=>&nQf9p5je);O6aKZJ*Hu1;h8JSo(=6DP2WdhPQ zEG!qj6&xB4=qhx~V*<*sT$BcJw>S8A#IXSRHBMjHI0O_P2rn{sXkciZ&$Q2|T7rc` z;KS6LAcYO<&)j1GDNFqZQue|BKz_$PcA&tYwimnt3JxGHNFI&b)KI^QMMar~BZq_I z(XqL?o(&EH0!JEBS{NF686EHJ*sRMbz`@icF`-95VS}PVhjjTiRYoR92La|uf=n!C zEG&hO&uvq2P~hN5axijgIMCLhkay?iG!_mPMn@hGHx7XtK;wF?Z%<-qR8Z((Jjubx zB+b;cqcA;MNI`&w#ZW;~(ILUvLBRZ7jyDraQ-cEAsAEP$W;D%==BCkdX0!wztqw*j i%F!mo2y0w5ntz!+E#!aH?Koi5n!(f6&t;ucLK6TQ$zyE* literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..080af9f07dd1d88ee73ccffeb499ee2169cc9018 GIT binary patch literal 8250 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f7c8744ea00a776189d8d96e991d39118647cb GIT binary patch literal 12652 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 acil57bEATQ1K{5$u;iqaz@=}E1OFG90Nv*R literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f7c8744ea00a776189d8d96e991d39118647cb GIT binary patch literal 12652 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 acil57bEATQ1K{5$u;iqaz@=}E1OFG90Nv*R literal 0 HcmV?d00001 From 920cfb7742660195777c95ffca3ffb84164266f2 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 12:51:19 +0300 Subject: [PATCH 34/55] Changed home provider to support the navigation bar to be fully transparent --- lib/home/home_provider.dart | 9 +++------ lib/main.dart | 21 ++++++++------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/home/home_provider.dart b/lib/home/home_provider.dart index f1f79922..40109147 100644 --- a/lib/home/home_provider.dart +++ b/lib/home/home_provider.dart @@ -4,8 +4,7 @@ import 'package:otraku/activity/activities_providers.dart'; import 'package:otraku/discover/discover_providers.dart'; import 'package:otraku/utils/options.dart'; -final homeProvider = - ChangeNotifierProvider.autoDispose((ref) => HomeNotifier()); +final homeProvider = ChangeNotifierProvider.autoDispose((ref) => HomeNotifier()); class HomeNotifier extends ChangeNotifier { int _homeTab = Options().defaultHomeTab; @@ -23,8 +22,7 @@ class HomeNotifier extends ChangeNotifier { ColorScheme? _systemLightScheme; ColorScheme? _systemDarkScheme; - ColorScheme? getSystemScheme(bool isDark) => - isDark ? _systemDarkScheme : _systemLightScheme; + ColorScheme? getSystemScheme(bool isDark) => isDark ? _systemDarkScheme : _systemLightScheme; void setSystemSchemes(ColorScheme? l, ColorScheme? d) { _systemLightScheme = l; @@ -58,8 +56,7 @@ class HomeNotifier extends ChangeNotifier { var _didExpandAnimeCollection = !Options().animeCollectionPreview; var _didExpandMangaCollection = !Options().mangaCollectionPreview; - bool didExpandCollection(bool ofAnime) => - ofAnime ? _didExpandAnimeCollection : _didExpandMangaCollection; + bool didExpandCollection(bool ofAnime) => ofAnime ? _didExpandAnimeCollection : _didExpandMangaCollection; void expandCollection(bool ofAnime) { if (ofAnime) { diff --git a/lib/main.dart b/lib/main.dart index 8dff602a..a8813129 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,8 +48,7 @@ class AppState extends State { final notifier = ref.watch(homeProvider.notifier); final hasDynamic = lightDynamic != null && darkDynamic != null; - final darkBackground = - Options().pureBlackDarkTheme ? Colors.black : null; + final darkBackground = Options().pureBlackDarkTheme ? Colors.black : null; if (hasDynamic) { lightDynamic = lightDynamic.harmonized(); @@ -72,17 +71,12 @@ class AppState extends State { final seed = colorSeeds.values.elementAt(theme); lightScheme = seed.scheme(Brightness.light); - darkScheme = seed - .scheme(Brightness.dark) - .copyWith(background: darkBackground); + darkScheme = seed.scheme(Brightness.dark).copyWith(background: darkBackground); } final mode = Options().themeMode; - final platform = - SchedulerBinding.instance.window.platformBrightness; - final isDark = mode == ThemeMode.system - ? platform == Brightness.dark - : mode == ThemeMode.dark; + final platform = SchedulerBinding.instance.window.platformBrightness; + final isDark = mode == ThemeMode.system ? platform == Brightness.dark : mode == ThemeMode.dark; final ColorScheme scheme; final Brightness overlayBrightness; @@ -94,11 +88,13 @@ class AppState extends State { overlayBrightness = Brightness.dark; } + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarBrightness: scheme.brightness, statusBarIconBrightness: overlayBrightness, - systemNavigationBarColor: scheme.background, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarContrastEnforced: false, systemNavigationBarIconBrightness: overlayBrightness, )); final data = themeDataFrom(scheme); @@ -115,8 +111,7 @@ class AppState extends State { /// too high of a factor and it breaks the app visually. /// [child] can't be null, because [onGenerateRoute] is provided. final mediaQuery = MediaQuery.of(context); - final scale = - mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); + final scale = mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); return MediaQuery( data: mediaQuery.copyWith(textScaleFactor: scale), From d7f10f774cfd61ce792e1e8d06a0d1b2f48bb985 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 21:41:16 +0300 Subject: [PATCH 35/55] Changed the navigation bar to be transparent and to not be contrasted --- lib/main.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 8dff602a..b07524c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -94,11 +94,13 @@ class AppState extends State { overlayBrightness = Brightness.dark; } + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarBrightness: scheme.brightness, statusBarIconBrightness: overlayBrightness, - systemNavigationBarColor: scheme.background, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarContrastEnforced: false, systemNavigationBarIconBrightness: overlayBrightness, )); final data = themeDataFrom(scheme); From af57a5b1f751b0cc334be2d646a12cb2b158c3fa Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 21:42:15 +0300 Subject: [PATCH 36/55] Formatted everything to be at 80 --- lib/home/home_provider.dart | 9 ++++++--- lib/main.dart | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/home/home_provider.dart b/lib/home/home_provider.dart index 40109147..f1f79922 100644 --- a/lib/home/home_provider.dart +++ b/lib/home/home_provider.dart @@ -4,7 +4,8 @@ import 'package:otraku/activity/activities_providers.dart'; import 'package:otraku/discover/discover_providers.dart'; import 'package:otraku/utils/options.dart'; -final homeProvider = ChangeNotifierProvider.autoDispose((ref) => HomeNotifier()); +final homeProvider = + ChangeNotifierProvider.autoDispose((ref) => HomeNotifier()); class HomeNotifier extends ChangeNotifier { int _homeTab = Options().defaultHomeTab; @@ -22,7 +23,8 @@ class HomeNotifier extends ChangeNotifier { ColorScheme? _systemLightScheme; ColorScheme? _systemDarkScheme; - ColorScheme? getSystemScheme(bool isDark) => isDark ? _systemDarkScheme : _systemLightScheme; + ColorScheme? getSystemScheme(bool isDark) => + isDark ? _systemDarkScheme : _systemLightScheme; void setSystemSchemes(ColorScheme? l, ColorScheme? d) { _systemLightScheme = l; @@ -56,7 +58,8 @@ class HomeNotifier extends ChangeNotifier { var _didExpandAnimeCollection = !Options().animeCollectionPreview; var _didExpandMangaCollection = !Options().mangaCollectionPreview; - bool didExpandCollection(bool ofAnime) => ofAnime ? _didExpandAnimeCollection : _didExpandMangaCollection; + bool didExpandCollection(bool ofAnime) => + ofAnime ? _didExpandAnimeCollection : _didExpandMangaCollection; void expandCollection(bool ofAnime) { if (ofAnime) { diff --git a/lib/main.dart b/lib/main.dart index a8813129..b07524c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,7 +48,8 @@ class AppState extends State { final notifier = ref.watch(homeProvider.notifier); final hasDynamic = lightDynamic != null && darkDynamic != null; - final darkBackground = Options().pureBlackDarkTheme ? Colors.black : null; + final darkBackground = + Options().pureBlackDarkTheme ? Colors.black : null; if (hasDynamic) { lightDynamic = lightDynamic.harmonized(); @@ -71,12 +72,17 @@ class AppState extends State { final seed = colorSeeds.values.elementAt(theme); lightScheme = seed.scheme(Brightness.light); - darkScheme = seed.scheme(Brightness.dark).copyWith(background: darkBackground); + darkScheme = seed + .scheme(Brightness.dark) + .copyWith(background: darkBackground); } final mode = Options().themeMode; - final platform = SchedulerBinding.instance.window.platformBrightness; - final isDark = mode == ThemeMode.system ? platform == Brightness.dark : mode == ThemeMode.dark; + final platform = + SchedulerBinding.instance.window.platformBrightness; + final isDark = mode == ThemeMode.system + ? platform == Brightness.dark + : mode == ThemeMode.dark; final ColorScheme scheme; final Brightness overlayBrightness; @@ -111,7 +117,8 @@ class AppState extends State { /// too high of a factor and it breaks the app visually. /// [child] can't be null, because [onGenerateRoute] is provided. final mediaQuery = MediaQuery.of(context); - final scale = mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); + final scale = + mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); return MediaQuery( data: mediaQuery.copyWith(textScaleFactor: scale), From d588fe9b460b309c94dc533b6586e7e7a7aae8b4 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 21:45:40 +0300 Subject: [PATCH 37/55] Fixed android icons to be the correct color and to be the correct shape --- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 5243 -> 3223 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 855 -> 858 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2904 -> 1984 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 463 -> 464 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 7051 -> 4660 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 1320 -> 1321 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 12304 -> 7362 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 2952 -> 2953 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 16770 -> 11106 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 4236 -> 4237 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index b48b9af2f8ef8459bd5d90089f77eb6bfd6e8b7e..b3cd40cf42d3a7296c389993072d778926f5889c 100644 GIT binary patch delta 3206 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 Ang9R* diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png index c416cf3175b2f827143a5a9f8ab1d0ee47a17354..453315ada47ec4facc381987310d4058dd14e494 100644 GIT binary patch delta 94 zcmV-k0HOca2HFOYF)|cML_t(|UhUGc4Zt7}1VN~P(3$)1(w1oAHy9e=wKw}b@3%Y! zh6`*EhZpeuEdU}2vjGA@0khBnasiXj0WKEt1N^ql36mkqd;kCd07*qoM6N<$f@!=Z A-T(jq delta 94 zcmcb`cAag4p@E>Mi(^Pd+}lfzybKBgEQSURl7F(BS%l9lmUv)vd*?B`!@&=V8#y06 tXRZ0q)xhDj`6FX6p#^^4<1~^181JN-_ z3^B%_Q3RvhM+Aw6kob!jVXt3Y@mpd7!|`G_@Nsh0d+*PA6tOI7;I~IsoPkY zUAOie^PQe^`kh;Qwqk(9Cd+BR^IhKOy*%&xecu>7;veP_mwzAs19;FD)UyC-BY&*; ze9$tI^?3lC_$1%uj~W2^ZLO)P-7?8CAMiZy7Dn^p81WhV8Exl<`D6V^e`$=|SEr9S zNHaDnFPTr4(`U0-KWuOP<0ycz2t;_P20*m#ShRFSM=&_<;y8{HL@T6`@JcXIA!2}( z^AIUD?NRaHOX6-|lED2QVgDLl=MrKxX-ZC4+jx>k49^S&ao`>rnvo+7uRz zfhQpRJvVaz3aTTQo)xzppKo!8W zg^~s!PbMxBFE}+!rSf-)V&Vm`dVbRYO6D#ywtx{GYTPWb_9X!oAPoGU^i3Lo#P@od zpDJ}G@_oJcQvx6-FDVuzw_#YMIE%n@5loB?ArcxBGL9+Q6s;utKEr^uJ79HW39E#L zy1_*z4S!>2r0=#l7MfkLFy=y(ze`%leQF(CE7rr3ngKo%MmRW%$oM@0Oe7QpAD%!Y zG6|j}LhxW%77SwpW3z*`rNENn0BcW!HFXwXu^LUu24E}z!-C^TjuCVW#(!cHo?Mt@|0pFgtVMY8d_=HZXvB-Q zgRhC2kChL4`N2GZiUkR2ln69)-m|bdGGR~8Mn+{VNcIkN{S{3x41=sEUIKUygI7)i zd>DL$Lz-s^z*;fV-h|MoACvcnaJRjgs;9Ktm%y6_P*h$e1JKI{l6Ht|ur>#5GjmY* z{C{qY5BI|R^WkVQ!1Jhj_XihS!G-cNPmmj(wuNTvl%vf8q&&^VKOj?d);laze|~z zgh*5o3>-xZVNK6LpyLuE;{kY^8w3D}5+50VxBreg7NP;DEkJ8Za3Om-Qk(@SdhvY( zIrElySm9g%26)1UOsU%eCA)oXD5_yO21@$6>vkY_>3_?(`sF(a_Fa!{SLv;*LHTal0%`y;pHOnN)Sl|D zK*grdF>vXm5R_`_Rk%=HJzUG*z}3d>_{V!qH(vCBla8hVxaZ6>!9uYex1*pEb2lG? z@4_*mB+wH;+QGS3ZAae9O}N%jkFnmignMpUdD8-*&qCD62E!n&c(LF@-hXqA@SQ)V zA1h)_<*wZZckNbOKe`K}xBt`xx?V6#6PgB)S29;oN*Kj}z)1{?jPe@HS+^g)a}DUb z@RimAB6$(v2!Q-mThVc1FGf2qi?x$>Z>)CPDY=jufMzn9npg{FRo0}KTGZpW?f_kUryy%}62R_Li?N4>6T3%F)i=*9qjgdx{M7Au_9FQ916 z9(bFNp#S36(ZE#f++rj1p56#|T|K%^e~h8lGeS9T6tu|$&`Sa;x??R?WG{OK?$tZt zZ90sBi{Hq?(^*@hUQ#>d*VUu@>;Vk6{0J^gHZc0^Hhloa<%Z?7dVew2VnfdI^>D3t z6TapK4E%nQqDF20$gSA`cilF4n;J3r+eu*`phpx*#eho$U^Fq^&M+)&nFX+AT(rTc43Afy zCxS2~0P)O9pH9SFN`JY$QDB-4*rZ%2r?6BT1FBQhm>}^ru8tYA{+<+o>_U%*M(GI? zRj6tgN=EDSx~5%-2B6k7tAAcge)oK~^_jFc?H#E@# zO6QJ@Trs5=mz2Fd!C?>2Bij&!>d5hax{4YI%dmbM%YWK?r}GN{fp}L;gcI!mGRVOj z*)59Kt;r}b*|Fr1DMUg*{3GAxAk@1jCO4JD66Ct+VYn-n?ud!okn@-5shNQbskQa6 s{40W%_Dlju)*t-;`S^!L!_x-(FN82*i`uEr!2kdN07*qoM6N<$g6|Wbga7~l delta 2885 zcmV-L3%c~c57-uvFn2D*`9ZHZ}!gY%w_@qbPjuVGjr$8^Stl#e%w0<_&+~{uYUhBi*vt0=b}bsM5zyw!ZPIRrrE zNS!p9Qz1b2hybL9(U)G8Ql+_a0^gC73X(|~vZ5fFNM^w6Nl20nqD_Qkv%zkcV6&_L zRBfr>?QebS6@LISN2XvRCrf~U+b4~_Y>t|g?&ymvXT{@jBoi{S7H6ROZz1P8xy0_U z!{LzNblO`*yRy9b@V{RGK_ z&^gDX`b!r@da;sOp82nE?Wc8Z79)BO8_rX#$dL%b2lwLB)%PK#rVYR=fc`G_dk(h+|2qLd z1b{-<83E1u6GqLL-P^5fP?T)#U=#5~9Dji`Fz(^!vc-(g(2PLlT$We{kl{Qt)JIFM zLA>iLgXOaxWEDG5WuM#n`PM%$FE9dReb3mMnXmMA$1m35F}Q+RRCKr?+H8=D1E^iR z95zoej&3AUDL@pE5=0EX@_MZlxBYDc1UU&g5l2dvG3@%EBN6OI%c|Rv40S`56@Mu4 zm^L;y9`wneB;ek3q)u2n*oEDTXZJtKI*|1QC>GSB&xdw*w|_|)iS_i3u_G?L zw)c2yy-_l}bok%S@-yKXR*RIXVt@FYpTg$ypmqIYnWw3myoTSfM7w_Ixs^I%XU$)# zY5wrSEMAALQ&BugunGSE6g{zXp^KBT_oZx$d*h_im+>$`&WSQGZnz|05bn zo?nOu2$1OV!0tK;Bzi<4iKZ26L z&V{o~WMf>48rpD5mv#wV$$#zT0D?q70#I7zyP!iWHpT*=3)tL>tOe_ne?>CXgFSc8 z)9z=ZjJccS`4VF^%~l&rC@%V@|!miKGcH!%YViJvexmKd!K=4 zXbnD?cgcWDR5R;~qJMy(+SDHdl!XFAOQ*e-hzsZD#()VaE-)yaHaxWvp#vYIWyRwD z{>W%O-T?J?uZ4HmDD1eg5o&@hRjt#e5tGv`Z2KzKL@7+mLPVgrrsBMnk+5)s837gp zeCx^pIV%QZ+_LrP-M?F_1Nnfnd+P664R6h8?7sb4#5<`YFn?cTn^~XE6_*Il9t}1v zW_3VoJA?l?^LW>-Jm4fmsZYqz#F;AW6bC~BQ(sQ>iQnWO zW;PY?l!}XHVaV)-=-Kri4zB%WVIElgD2m5S#^DXC(D}~mIi#)O18%plxg*x}Jpetl zCg9ftz9EAqU7H9y*BT2|dP4p3nT;8I^;}doUW4u(n}2cenPmlep!SwWP(1D&9NoAc z?SFsCvici|P%L^&6ARnB_E0Ap=GOy+&I#a*D*w6f%5e-$2cx+yc!I-!xESTrW}<8R z+h|?)aE=FZgBlol^8*M>oQ7kYHsZ*OYYHY{cj5D{zMazm94Gk@MBc1gHX2;k@Z)6E z^(c!4t$(*M>5&+T$}4_=qMA_%?c0T8Z*8=c1HYeC**F`%QDYGPWDnZk_)|W`^j{REdYA2psl=&yAmz{<<BNPJsPk@B8MqfR>ip}nI?ZFV32=Dh%Q`}P*Jd&g?N;}LGJ1gS zp~9AyQeg6fiKy*5#e#%+Z0K!kRSydB0D-rAmFGJ{yQsi0>FC}Um=Vy3Q*|a1Re>pw zCV!*icX<-#F<=gyowZshSrJKFUj1`N=tJsH2s~}0Xq#F5VMTzE@B%2SENQqu8I$Hv zOymO6cb%sSmWVx?!C3;~8odrP0`ohVcJ+;(@V z*EM{)O>j(9WpSh`+oqTjkdz&wq<*TT61yUOhquI%F$z$!Hf0#>7v#6YdUr$77eQo$|;autMx<>uPwj*c-f y3&yniparvf6%6ZDu!3m@Fy4NV;MtL|coh%6_lU8H>7v#Za$+W$d3#!4?m83C70fX0LNs#~7G` zF|9sm!K_{d!+I60V44Apw;z)+0g91*#un}a29bwtLI=D400000NkvXXu0mjf8;vEU diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 9172cd990e803be1ff9523b52d699fe2a39b8bbb..80cea64fa15d890d5996141cd16375202090558f 100644 GIT binary patch delta 4655 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 delta 92 zcmZ37srr_xVM)bc^MRVSRAt$jDK8b(h!s8KND wQPz|L;sP_=Hw!W|F>NkjTFf-LfQd&`j&B9ui(1E2IR+r`boFyt=akR{0IU5SivR!s diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 947df615f3c39d7f5aea8988fa9f84a1a29063a7..e986aeda1e1cec42b382abfc4d14eea4dc2fb548 100644 GIT binary patch literal 7362 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 literal 12304 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 delta 220 zcmeAa?+~A0SnuTN;uum9_x9$-UM5EY*Mm1%Jlg&wGdGt`5UBdMe)H{@uPzD~Tz_m6 ze_WoCiG^d1x4>Q|AYH@4a?xACq2Yk8LdQHNpbX1JX%KgNgMUXH3y@#q^o5N>K;eP# zB6EiZhQ|3!`;4k3SU3beOuY$G*s%W0JqDmMlbY0TAax)759D{;V+V@-X?wvdpx^-F uf)t=}n;Pm@v1~46X5!dvz}&+m)@c4^_Oy`yQMcn5fWXt$&t;ucLK6UicTcYX diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index dc2362f9aae58b35e7695143e3674c05b2395e91..67b7fade22a6d669f9676688a5b609ecfb28c40a 100644 GIT binary patch literal 11106 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 From f94ead1fee34f82d0031cd26c33a84beba4669b4 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 22:03:59 +0300 Subject: [PATCH 38/55] Fixed an error made during formatting --- lib/main.dart | 97 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 88554520..8f33dd56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,8 +36,6 @@ class AppState extends State { @override Widget build(BuildContext context) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - return Consumer( builder: (context, ref, _) => DynamicColorBuilder( builder: (lightDynamic, darkDynamic) { @@ -72,39 +70,64 @@ class AppState extends State { theme = colorSeeds.length - 1; } - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: scheme.brightness, - statusBarIconBrightness: overlayBrightness, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarContrastEnforced: false, - systemNavigationBarIconBrightness: overlayBrightness, - )); - final data = themeDataFrom(scheme); - - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Otraku', - theme: data, - darkTheme: data, - navigatorKey: RouteArg.navKey, - onGenerateRoute: RouteArg.generateRoute, - builder: (context, child) { - /// Override the [textScaleFactor], because some devices apply - /// too high of a factor and it breaks the app visually. - /// [child] can't be null, because [onGenerateRoute] is provided. - final mediaQuery = MediaQuery.of(context); - final scale = - mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); - - return MediaQuery( - data: mediaQuery.copyWith(textScaleFactor: scale), - child: child!, - ); - }, - ); - }, - ), - ); + final seed = colorSeeds.values.elementAt(theme); + lightScheme = seed.scheme(Brightness.light); + darkScheme = seed + .scheme(Brightness.dark) + .copyWith(background: darkBackground); + } + + final mode = Options().themeMode; + final platformBrightness = + View.of(context).platformDispatcher.platformBrightness; + + final isDark = mode == ThemeMode.system + ? platformBrightness == Brightness.dark + : mode == ThemeMode.dark; + + final ColorScheme scheme; + final Brightness overlayBrightness; + if (isDark) { + scheme = darkScheme; + overlayBrightness = Brightness.light; + } else { + scheme = lightScheme; + overlayBrightness = Brightness.dark; + } + + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: scheme.brightness, + statusBarIconBrightness: overlayBrightness, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarContrastEnforced: false, + systemNavigationBarIconBrightness: overlayBrightness, + )); + final data = themeDataFrom(scheme); + + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Otraku', + theme: data, + darkTheme: data, + navigatorKey: RouteArg.navKey, + onGenerateRoute: RouteArg.generateRoute, + builder: (context, child) { + /// Override the [textScaleFactor], because some devices apply + /// too high of a factor and it breaks the app visually. + /// [child] can't be null, because [onGenerateRoute] is provided. + final mediaQuery = MediaQuery.of(context); + final scale = mediaQuery.textScaleFactor.clamp(0.8, 1).toDouble(); + + return MediaQuery( + data: mediaQuery.copyWith(textScaleFactor: scale), + child: child!, + ); + }, + ); + }, + ), + ); + } } From 02a57950cbe01738a27dee072ec267f6ba3a59ca Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 22:24:48 +0300 Subject: [PATCH 39/55] Revert to old ic_launcher.png --- .../dev/res/mipmap-anydpi-v26/ic_launcher.xml | 7 +++---- .../src/dev/res/mipmap-hdpi/ic_launcher.png | Bin 5243 -> 1083 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 855 -> 858 bytes .../src/dev/res/mipmap-mdpi/ic_launcher.png | Bin 2904 -> 810 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 463 -> 464 bytes .../src/dev/res/mipmap-xhdpi/ic_launcher.png | Bin 7051 -> 1465 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 1320 -> 1321 bytes .../src/dev/res/mipmap-xxhdpi/ic_launcher.png | Bin 12304 -> 2045 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 2952 -> 2953 bytes .../dev/res/mipmap-xxxhdpi/ic_launcher.png | Bin 16770 -> 2664 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 4236 -> 4237 bytes android/app/src/dev/res/values/colors.xml | 2 +- android/app/src/dev/res/values/strings.xml | 2 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 +++---- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3223 -> 1083 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 858 -> 858 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1984 -> 810 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 464 -> 464 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4660 -> 1465 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 1321 -> 1321 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7362 -> 2045 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 2953 -> 2953 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 11106 -> 2664 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 4237 -> 4237 bytes lib/main.dart | 14 ++++---------- 25 files changed, 12 insertions(+), 20 deletions(-) diff --git a/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml index 345888d2..5f349f7f 100644 --- a/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - - \ No newline at end of file + + + diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png index b48b9af2f8ef8459bd5d90089f77eb6bfd6e8b7e..22107cfaef4c4f2b592939bbb47c50abb485fb56 100644 GIT binary patch delta 1073 zcmV-11kU^WD7y%d8Gix*007#LBoF`q1OiD!K~#7F?O9t$6j2zSJu^GA?&i9dmb*$M zq}D?a=tIyAk|+qGg5H8|1VRweQ!mj=LA`b%>M5d!Aj2|xke~;B2~mpZ!3Qxlt<+t- zu6vtH|1|FGsA=Or$2=qRad6KZ{{Q%Y=f9kPc7>8HTe#`U~=fI9eLZtqBYg0 zZ~J^cRnF#S5^{1}o}G|qCzaHcoSFuBJyJhkdP;6%RekvZ?=^X6eL$&G54VQx)Z;o_}3>RtO?O-9A*v(rgm`9YPmF z?8k@4Fkqq=04WHSDj3^Qd+q_OOb>h>>wINOLZJX5_>3L8B$dWsB|9|+aRn7cQx(0a z7(e(OU|drsTI`soWQcsS0w7d|t72kD0{g<^4)vx|&sw zp?^a$?`pO@MhYIohT^*S7|)c)oaY$p#lx)HD}|}(BZT-RHHWzjt)M}S@V@-3w1%#V z7DaFx6T~3h^*3b$*c`Je>di-T+@V!8h*{n;(HFE;kxKobK#W)mG`^*p?=i=q;;{lT zbdGV1d(1%-%%I04%QrdrCb*zTXfz+RLw^z9c@z|RMm#p}fE{=D9Hr)iy2mJlB8wiw zcOHy;4BvS$SnjP=5#M=OJti>{iZnd7pb2(p5=W~EHs|=xLt`%#=XW0HCfIV1F>*nV zbUaIZdS}ejqkXtdK(X)T+!w&I<98O03bBcvcITVqW)gq>#SjhVew3gEkC@xw5h`;V rJYsHxN6c;Th`9|OF}J}Z<~IBWIoznw6Bn6Q00000NkvXXu0mjf^+@`F literal 5243 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 B215V< diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png index c416cf3175b2f827143a5a9f8ab1d0ee47a17354..064b4a4bdc08b9947b97475e4d6850a556f13786 100644 GIT binary patch delta 94 zcmV-k0HOca2HFOYF)|cML_t(|UhUGc4Zt7}1VJc()S>(D(w1oAHy9e=wKw}b@3%Y! zh6`*EhZpeuEdU}2vjGA@0khBnasiXj0WKEt1F1#JBM`V1O#lD@07*qoM6N<$f-rd_ A?EnA( delta 94 zcmcb`cAag4p@E>Mi(^Pd+}lfzybKBgEQSURl7F(BS%l9lmUv)vd*?B`!@&=V8#y06 tXRZ0q)xhDj`6FX6Go#s9VZRK^Z1y+%`|S5OGrQq~n>v{FB>rCm{C_A12qy;!CkF^82MDMC z2Z)pW+lH>|NKpzIrSwHD=TJGXR`aM@(&VxxS2U%jAsGU6O=EP;7~^7HEbHO~mJ?Y) zVnq)tdO5eBcLzA{2HqPId=bGPaq;3r@}gEN(*dDcasKhG_^~to&K^&6%U?gaAvwQP zNlz|38TlQ}T7N*G!qjVMZ3j+n9lX{Qpt*-Pmfzf4I|e_8DvJ}JUyT~(i(cLr3Uuw) zR2eE*zYIm<1l}8ju3BEvl&Y>Fv#C-j&I<0r^q5-8BBRP>xD_@X9;h^zf@bxlNMi5h z2gRAU?{A)M1bAXQx-Z>@&WDk6jY!b0;bfp|-_+=pT7PB+n}q`Cdx1|>@rDZEP8P87eDq6Se;pC?wP!jh7N22v70>tah$IUI zP(z&C2S;OTCXt31V;y*?Qcwa(zD6djbn|_}*$Tv(KvpI^kZC0CPVpxW>0}NuI%)xs zd3PFz>@C@tWT=gYG<&b?PV?{T7uQKF2wHe0pqKo5oq&6R@K7lCrA;vTbRr1XT`er^&$r$rV z>;V!arIdvZ{DmNiM{>!Ehk@i-D1hLTZ|rIxdHmwj+tA44sexJ}2M8wz2qy;!CkF_p cbr%Qp6^CXwg%!-rV*mgE07*qoM6N<$f~dP|!2kdN literal 2904 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 delta 93 zcmV-j0HXiU1J47HF*u1yL_t(|UhUNZ2>>7v#Za$+W$d3#!4?m83C70fX0LNs#~7G` zF|9sm!K_{d!+I60V44Apw;z)+0g91*#un}a29bwtLI=D400000NkvXXu0mjf8;vEU diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png index 9172cd990e803be1ff9523b52d699fe2a39b8bbb..be832d148099c1928b28382eb0fe7f1de420252b 100644 GIT binary patch literal 1465 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 diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png index 947df615f3c39d7f5aea8988fa9f84a1a29063a7..5a95c61b10183b2ec08b23102da89f81982b4a7f 100644 GIT binary patch literal 2045 zcmcJQ`#%#31I8W1#-h3GtYVmiFjKUQgeI-I<+_;5lvl{5Tq3+whPf4SCU>RA+@{)) zxedqVY#ppgF_Tn8XgeixdA)zc`Fy_5^ZfYy@ci(QU7YOq%N>%FkdWAqJ&$qS%lrQq zKyuHo9*JF*kdW=iVr<;wfBRa7yEcu1rfz$$8k9PgrdwSFgR`pk0m;<+S;uD6Pie{x z%8{jl`>S;i%=Q14rl;%I3bQ${mgVi&siM$m{f`~r)}tgdjfrKJa=W$zSRp*$U+Zx^ z#^A2Tj`Kul*hEEdW$>rn;AQ9lcU=#po>ODc)36_M4)m{)lS#_`dh2Lw5Y40O9gPrf z%xyJz)9@wM?)qb+eJ9RE73L|P4>yEg?#YcXD~l8Snu$Pr@)-TWe=)rC1aX~Qk(K%sBm50Rbr4JifBq6a zN`uoLg=Rgiqcw%Tq-FnISX*>J6VVh}Op6HBcRMleeeChWw?)prN#Cr*x6srl;#>d0 zEclPOv6!MVOIq7{|Y9H_;~MgNdrjWqc&+SfJa;%+1q2InA0Nj`}tsN2nxM z@JB|YcxWcC5xa3;hvWO1JqK;FJ_1xGz9_q+bk;LoXA%7__oJw?yZCXb*$FDuF6#jg z^B|jwI$UL%P}5$tc^>olvd;0^*hl` zoh*q89xlVHAaHdB0(@2=WV&5x>#8J&d6%0nq5oK5unaq%>!Z4Vb_M@=Gw16xFZm}! z+26PV9e7hI0sJerD-+X&2wc}ga7wTZR3$GLQ;U|^)z{A-cU5wCeugYAD@s*F4~ior z1mcp&&vSLFh~$ll)L`h;_Xg)l5$BbXiUo%C;h*bkeZGlEw85+IPSQ6OfErU(IPr z0TmNLL`KO!Kp0LwZ722iEaq(?Opu{B@x;+WH8H3zcamKBN1&J5CSmlw)5YqmDbEP7Jf64oYMr$24Si3*6vFAQsNlw3$B( zmN7(W5BI4!+>5OO%NU__>C8tx)%q1wX*%F8rBpeD7(h8SpXf4n+YUc;BU#3fn}`ll@1gD6X{fz2*j3wxf6di0|5}?8V!;69XWt2GrzFBZ}@Weh4ai; zmy0z(kzzq=-=>X(@CHU@LQHt1Xc0 z(WhdX;ILaZz`Q{7NK(Gt}%*Zz}J3&fWFD zL3gSS#C@E!Y0>e`KFrzO%$+P~veVN`Gr8Nx&g}iy_@?m_ur_NAJkqy!>k?QyCrp!V Hz@7gAk?hbD literal 12304 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 diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png index dc2362f9aae58b35e7695143e3674c05b2395e91..ea8c5aa9c4f130c17918bda2c7c37cc253d60e6a 100644 GIT binary patch literal 2664 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 A3;+NC delta 95 zcmeBG>`|OxXzAwZ;uum9_x5TdCxZeHv*YfD(>wm>aOHh0n5z)OHFNK|*gghECKiq( xEXNv-N(oN~arQMZe&*Qtv5kLn1ACj;1I8(DWzUN@IlpB90#8>zmvv4FO#r-4BVhml diff --git a/android/app/src/dev/res/values/colors.xml b/android/app/src/dev/res/values/colors.xml index 24ca654e..26d42416 100644 --- a/android/app/src/dev/res/values/colors.xml +++ b/android/app/src/dev/res/values/colors.xml @@ -1,4 +1,4 @@ - #E3F2FF + #0D161E \ No newline at end of file diff --git a/android/app/src/dev/res/values/strings.xml b/android/app/src/dev/res/values/strings.xml index d8d80c9f..ade8cb3d 100644 --- a/android/app/src/dev/res/values/strings.xml +++ b/android/app/src/dev/res/values/strings.xml @@ -1,4 +1,4 @@ - Otraku-dev + Otraku \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 345888d2..5f349f7f 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - - \ No newline at end of file + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index b3cd40cf42d3a7296c389993072d778926f5889c..22107cfaef4c4f2b592939bbb47c50abb485fb56 100644 GIT binary patch delta 1073 zcmV-11kU@H8M_FO8Gix*007#LBoF`q1OiD!K~#7F?O9t$6j2zSJu^GA?&i9dmb*$M zq}D?a=tIyAk|+qGg5H8|1VRweQ!mj=LA`b%>M5d!Aj2|xke~;B2~mpZ!3Qxlt<+t- zu6vtH|1|FGsA=Or$2=qRad6KZ{{Q%Y=f9kPc7>8HTe#`U~=fI9eLZtqBYg0 zZ~J^cRnF#S5^{1}o}G|qCzaHcoSFuBJyJhkdP;6%RekvZ?=^X6eL$&G54VQx)Z;o_}3>RtO?O-9A*v(rgm`9YPmF z?8k@4Fkqq=04WHSDj3^Qd+q_OOb>h>>wINOLZJX5_>3L8B$dWsB|9|+aRn7cQx(0a z7(e(OU|drsTI`soWQcsS0w7d|t72kD0{g<^4)vx|&sw zp?^a$?`pO@MhYIohT^*S7|)c)oaY$p#lx)HD}|}(BZT-RHHWzjt)M}S@V@-3w1%#V z7DaFx6T~3h^*3b$*c`Je>di-T+@V!8h*{n;(HFE;kxKobK#W)mG`^*p?=i=q;;{lT zbdGV1d(1%-%%I04%QrdrCb*zTXfz+RLw^z9c@z|RMm#p}fE{=D9Hr)iy2mJlB8wiw zcOHy;4BvS$SnjP=5#M=OJti>{iZnd7pb2(p5=W~EHs|=xLt`%#=XW0HCfIV1F>*nV zbUaIZdS}ejqkXtdK(X)T+!w&I<98O03bBcvcITVqW)gq>#SjhVew3gEkC@xw5h`;V rJYsHxN6c;Th`9|OF}J}Z<~IBWIoznw6Bn6Q00000NkvXXu0mjf6Gi$B literal 3223 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 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index caa82c4a42c30f890563edf3589d578a55c2bd3f..ed4ca1ebf44401a72b9457f74217a0da159d6d05 100644 GIT binary patch delta 798 zcmV+(1L6F@52^-`8Gix*007uvZqNV#0`W;iK~#7F?U&1I6G0ruH~Y-fJg70Lp{?Qr zk!lsG_^2oAT}lrgDteNlAf7yU(?3A)rcgZOB0cCyP!LfOR1{hvpwd!nTiV7pCe1s$ zJG=gNH;v+>Go#s9VZRK^Z1y+%`|S5OGrQq~n>v{FB>rCm{C_A12qy;!CkF^82MDMC z2Z)pW+lH>|NKpzIrSwHD=TJGXR`aM@(&VxxS2U%jAsGU6O=EP;7~^7HEbHO~mJ?Y) zVnq)tdO5eBcLzA{2HqPId=bGPaq;3r@}gEN(*dDcasKhG_^~to&K^&6%U?gaAvwQP zNlz|38TlQ}T7N*G!qjVMZ3j+n9lX{Qpt*-Pmfzf4I|e_8DvJ}JUyT~(i(cLr3Uuw) zR2eE*zYIm<1l}8ju3BEvl&Y>Fv#C-j&I<0r^q5-8BBRP>xD_@X9;h^zf@bxlNMi5h z2gRAU?{A)M1bAXQx-Z>@&WDk6jY!b0;bfp|-_+=pT7PB+n}q`Cdx1|>@rDZEP8P87eDq6Se;pC?wP!jh7N22v70>tah$IUI zP(z&C2S;OTCXt31V;y*?Qcwa(zD6djbn|_}*$Tv(KvpI^kZC0CPVpxW>0}NuI%)xs zd3PFz>@C@tWT=gYG<&b?PV?{T7uQKF2wHe0pqKo5oq&6R@K7lCrA;vTbRr1XT`er^&$r$rV z>;V!arIdvZ{DmNiM{>!Ehk@i-D1hLTZ|rIxdHmwj+tA44sexJ}2M8wz2qy;!CkF_p cbr%Qp6^CXwg%!-rV*mgE07*qoM6N<$f=D21q5uE@ delta 1981 zcmV;u2SWI&2EY%H8Gi-<00374`G)`i00DDSM?wIu&K&6g00(+WL_t(&L+x5?Y*S?z ze$Hu2H@h*`UApeZ=q|tpI7Bc5(J@I3F~*=#1f$$X1c`=__=^~0B0o$F7!x6Epooze z6~iC+p&K9pbwsltTY$kBY-@L^+gO=hxAq+Kot|_0om+dhVt;_dCd+BR^IhKOy*%&x zecu>7;veP_mmmKFc+eKqvjAx$f2{a?&@z(sc>tXFB;Vza8UXoit*NQqGRZO@@I3Dp zM)Tqr@frIWZRdsgWBo{fX^h-gr;j*DGd3zOnNOC}XR}y8Y;XPJD1fjCM0lwNK(y{y zv~)#BFgWhwIDd{3L@T6`@JcXIA!2}(^AIUD?>T#rt7jmkYp<=tjC0AONyF^M9-R6uz&s(EW0s_*PW{G!0@#64E&$;O&Wm2_j;P2Ds?9EeZBWn0w5nq9Fl=0cReOIpc&Y8_lF z*29vT0X`B&I5>*P_&otkBoqW6o?wsYfpnUbrxW; zAQB9~cm62)FMlf%hc@{U1(^m=T)v=B%F!B2P=EQoYWH`Tk>LUu24E}z!-C^TjuCVW z#$pqmT$p74C?ztiMR@XjM6g_F#EZ6ruZfzEl@EIP!90M91qo@C2sCrvv#>cbVNcIS zMrAEX_6~IY6-_V#5GjmY*{BDd7_rm-0;b<|y^Qe0F2O(D4K6^FFnhcBl z=XPMmtUPp|J&5qAAK~!;cwVZxV$7u%#M1&0&xLFbC-PQq1Z#f`X`W>W2S+g0)hY-? zKG{px3O+H|@>8^cWEeP#7sHa8j?tSf;D08@;B9WihS!G-cNQ>Fh zoNzs}88&+w(#n=$GBAjH-EFeJOPQF2NK_FF97PLZP0vA~;}Rm{0eG7m1OSN=9~pnQ z|Bg8pq5-HaKx;~HA$vMfoCPR)@qGk3ub}VO;}igOu?M(ow;p|IuPw;o^8T2+Ck!VEIpY+XNy%X8hYjFMedk7BP zFm#=y0OWgO77)0bQL+O)Ir*OADt|@ATL<88Ifb6426>yO21@$6>vkY_>C3qK8-0l`EJ?*Y5*~xP;#`?p6aeZ#iq|OaOtEFlxpf#xKLd^T+83U)yD1k$9qjT zUi5&Ij-~;)=gc#~La`mUqo5LVHy?uU!ZD#F&=Ww~!MRs$N8ZX!xYkgQv47sSgnMpU zdD8-*&qCD62E!n&c(LF@-gAxcoj;}@D`HLMuH6QA?N(erx(lPX|I`G!UNB1&ng)

}KTGZpW?f_hGob8C)b*=&55z zy{>5sxMo-A#sGbUA=g6|E1cCYplHn=c$<%)|Kiutz*OwqVk7gO-UxSHJ-SYRjG@*u zLOE^}w8;a|O9CpoV=Y!>FM9><)jQ#BI*fsf-^jw#SzDrBQak0>)qkV=>;Vk6{0J^g zHZc0^Hhloa<%Z?7dNI~wL(cN`aIJU~zUBrD{C<+6Ms5Dct=Rx~-8Oie8Zr3WNnszL zM-)lLfJ+2mG%?-IFf44D1+Zo0Vq&NV6LJlL}e5E}AgV%SHO(t08* z+f2tmZc&*j780hX7=I?ZT(rTc43AfyCxS2~0P)O9pH9SFO1ZpIV44lsq+BScuv8lZ zs#DaMAn`S>jv2H5o)m!WLXU<<=?N26sA?BVM(gytrd@~zpw=|4(L_}$4I~-OOKX}2 zkX4Y#0*YInxIt@{;}Zoid2begGrO>~pB$0sY1l}hWZ1=w@P9-Wvzz)%*X+`^aG1|A zlLyQKG9#o>A0oWfY8mwRbXJKsG|>V|=Z=hAF{Kxml)XK{VGqwE+Yp56$nk!6YT&p$iW-gEsEEz$tW<{vE+~`L_$FPBj4p9)Vn7pHe1Um_Ba7~V_5K@O%Na%Z8QXI&87ci?00K`}KbLh*2~7a$stf@D delta 36 rcmcb>e1Um_Bg@(Y%k4M1mNSa%``)p!^QqH)1|aZs^>bP0l+XkKAbAeJ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 80cea64fa15d890d5996141cd16375202090558f..be832d148099c1928b28382eb0fe7f1de420252b 100644 GIT binary patch literal 1465 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 Hz@7gAk?hbD literal 7362 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 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png index 0d7644751ce8bdd06ae1a02c8c7c09f8e5fa4e7f..de61260a799b1bf1a6b35db9bea9ee903158c011 100644 GIT binary patch delta 36 qcmeAa?-ZZl#KN$IZ|6ogeQpunU(-TZt|uH}00K`}KbLh*2~7aj+6$=w delta 36 qcmeAa?-ZZl#KO{YZpTJ9eQuF;pJplCXuROW00f?{elF{r5}E+`fDIu4 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 67b7fade22a6d669f9676688a5b609ecfb28c40a..ea8c5aa9c4f130c17918bda2c7c37cc253d60e6a 100644 GIT binary patch literal 2664 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 delta 36 qcmeBG>{XoL$g=jpa{G<0CITY&Pb%HdO;j;w00K`}KbLh*2~7a_xeS*8 diff --git a/lib/main.dart b/lib/main.dart index 8f33dd56..7624b13e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,8 +48,7 @@ class AppState extends State { final notifier = ref.watch(homeProvider.notifier); final hasDynamic = lightDynamic != null && darkDynamic != null; - final darkBackground = - Options().pureBlackDarkTheme ? Colors.black : null; + final darkBackground = Options().pureBlackDarkTheme ? Colors.black : null; if (hasDynamic) { lightDynamic = lightDynamic.harmonized(); @@ -72,18 +71,13 @@ class AppState extends State { final seed = colorSeeds.values.elementAt(theme); lightScheme = seed.scheme(Brightness.light); - darkScheme = seed - .scheme(Brightness.dark) - .copyWith(background: darkBackground); + darkScheme = seed.scheme(Brightness.dark).copyWith(background: darkBackground); } final mode = Options().themeMode; - final platformBrightness = - View.of(context).platformDispatcher.platformBrightness; + final platformBrightness = View.of(context).platformDispatcher.platformBrightness; - final isDark = mode == ThemeMode.system - ? platformBrightness == Brightness.dark - : mode == ThemeMode.dark; + final isDark = mode == ThemeMode.system ? platformBrightness == Brightness.dark : mode == ThemeMode.dark; final ColorScheme scheme; final Brightness overlayBrightness; From dea88ff1762712e3dded934cec023f23c5faf393 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 23:11:50 +0300 Subject: [PATCH 40/55] Forgot to add monochrome tag --- android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 5f349f7f..cdefbc77 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - - + + + From 3b20b34e99af28481730c8be548822debe633eb2 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 23:14:07 +0300 Subject: [PATCH 41/55] Formatted again ;-; --- lib/main.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7624b13e..8f33dd56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,7 +48,8 @@ class AppState extends State { final notifier = ref.watch(homeProvider.notifier); final hasDynamic = lightDynamic != null && darkDynamic != null; - final darkBackground = Options().pureBlackDarkTheme ? Colors.black : null; + final darkBackground = + Options().pureBlackDarkTheme ? Colors.black : null; if (hasDynamic) { lightDynamic = lightDynamic.harmonized(); @@ -71,13 +72,18 @@ class AppState extends State { final seed = colorSeeds.values.elementAt(theme); lightScheme = seed.scheme(Brightness.light); - darkScheme = seed.scheme(Brightness.dark).copyWith(background: darkBackground); + darkScheme = seed + .scheme(Brightness.dark) + .copyWith(background: darkBackground); } final mode = Options().themeMode; - final platformBrightness = View.of(context).platformDispatcher.platformBrightness; + final platformBrightness = + View.of(context).platformDispatcher.platformBrightness; - final isDark = mode == ThemeMode.system ? platformBrightness == Brightness.dark : mode == ThemeMode.dark; + final isDark = mode == ThemeMode.system + ? platformBrightness == Brightness.dark + : mode == ThemeMode.dark; final ColorScheme scheme; final Brightness overlayBrightness; From 9932a6f49d1d4b7d6a2450b332ed4b886afbf17b Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Sun, 21 May 2023 23:15:22 +0300 Subject: [PATCH 42/55] Change the dev icon --- android/app/src/dev/res/values/colors.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/dev/res/values/colors.xml b/android/app/src/dev/res/values/colors.xml index 26d42416..978fbfb4 100644 --- a/android/app/src/dev/res/values/colors.xml +++ b/android/app/src/dev/res/values/colors.xml @@ -1,4 +1,4 @@ - #0D161E - \ No newline at end of file + #E3F2FF + From 6f96345ee2b7483796c862530339fe480a9a9888 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Wed, 24 May 2023 14:48:23 +0300 Subject: [PATCH 43/55] Fixed search toggling on the home page --- lib/common/utils/paged_controller.dart | 8 +-- lib/common/widgets/fields/search_field.dart | 17 ++++-- lib/modules/discover/discover_view.dart | 3 +- lib/modules/home/home_view.dart | 57 +++++++++++---------- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/lib/common/utils/paged_controller.dart b/lib/common/utils/paged_controller.dart index cbeab691..81f881c5 100644 --- a/lib/common/utils/paged_controller.dart +++ b/lib/common/utils/paged_controller.dart @@ -18,11 +18,11 @@ class PagedController extends ScrollController { /// When the user reaches the bottom, try loading more data. void _listener() { - final pos = positions.last; - if (pos.pixels < pos.maxScrollExtent - 100) return; - if (_lastMaxExtent == pos.maxScrollExtent) return; + if (!hasClients) return; + if (positions.last.pixels < positions.last.maxScrollExtent - 100) return; + if (_lastMaxExtent == positions.last.maxScrollExtent) return; - _lastMaxExtent = pos.maxScrollExtent; + _lastMaxExtent = positions.last.maxScrollExtent; loadMore(); } diff --git a/lib/common/widgets/fields/search_field.dart b/lib/common/widgets/fields/search_field.dart index 8cbd99dc..55651fe9 100644 --- a/lib/common/widgets/fields/search_field.dart +++ b/lib/common/widgets/fields/search_field.dart @@ -20,14 +20,21 @@ class SearchField extends StatefulWidget { } class _SearchFieldState extends State { - late final TextEditingController _ctrl; - late bool _empty; + late final TextEditingController _ctrl = TextEditingController( + text: widget.value, + ); + late bool _empty = _ctrl.text.isEmpty; + FocusNode? _focus; @override void initState() { super.initState(); - _ctrl = TextEditingController(text: widget.value); - _empty = _ctrl.text.isEmpty; + if (widget.onHide != null && _empty) { + _focus = FocusNode(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _focus!.requestFocus(), + ); + } } @override @@ -46,7 +53,7 @@ class _SearchFieldState extends State { Widget build(BuildContext context) { return TextField( controller: _ctrl, - autofocus: widget.onHide != null, + focusNode: _focus, style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( isDense: false, diff --git a/lib/modules/discover/discover_view.dart b/lib/modules/discover/discover_view.dart index f902cf83..aac39d46 100644 --- a/lib/modules/discover/discover_view.dart +++ b/lib/modules/discover/discover_view.dart @@ -15,7 +15,6 @@ import 'package:otraku/modules/user/user_grid.dart'; import 'package:otraku/modules/user/user_models.dart'; import 'package:otraku/common/utils/convert.dart'; import 'package:otraku/modules/review/review_grid.dart'; -import 'package:otraku/common/utils/paged_controller.dart'; import 'package:otraku/common/utils/options.dart'; import 'package:otraku/common/widgets/grids/tile_item_grid.dart'; import 'package:otraku/common/widgets/layouts/floating_bar.dart'; @@ -28,7 +27,7 @@ import 'package:otraku/common/widgets/paged_view.dart'; class DiscoverView extends ConsumerWidget { const DiscoverView(this.scrollCtrl); - final PagedController scrollCtrl; + final ScrollController scrollCtrl; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/modules/home/home_view.dart b/lib/modules/home/home_view.dart index 89833856..bb0806cc 100644 --- a/lib/modules/home/home_view.dart +++ b/lib/modules/home/home_view.dart @@ -35,7 +35,14 @@ class _HomeViewState extends ConsumerState with SingleTickerProviderStateMixin { late final _animeCollectionTag = (userId: widget.id, ofAnime: true); late final _mangaCollectionTag = (userId: widget.id, ofAnime: false); - late final _scrollCtrl = PagedController(loadMore: _scrollListener); + final _animeScrollCtrl = ScrollController(); + final _mangaScrollCtrl = ScrollController(); + late final _feedScrollCtrl = PagedController( + loadMore: () => ref.read(activitiesProvider(null).notifier).fetch(), + ); + late final _discoverScrollCtrl = PagedController( + loadMore: () => discoverLoadMore(ref), + ); late final _tabCtrl = TabController( length: HomeTab.values.length, vsync: this, @@ -53,7 +60,10 @@ class _HomeViewState extends ConsumerState @override void dispose() { BackgroundHandler.clearNotifications(); - _scrollCtrl.dispose(); + _animeScrollCtrl.dispose(); + _mangaScrollCtrl.dispose(); + _feedScrollCtrl.dispose(); + _discoverScrollCtrl.dispose(); _tabCtrl.dispose(); super.dispose(); } @@ -132,6 +142,8 @@ class _HomeViewState extends ConsumerState collectionPreviewProvider(_mangaCollectionTag).select((_) => null), ); + final primaryScrollCtrl = PrimaryScrollController.of(context); + return WillPopScope( onWillPop: () => _onWillPop(context), child: PageScaffold( @@ -146,8 +158,8 @@ class _HomeViewState extends ConsumerState switch (tab) { case HomeTab.anime: - if (_scrollCtrl.position.pixels > 0) { - _scrollCtrl.scrollToTop(); + if (_animeScrollCtrl.position.pixels > 0) { + _animeScrollCtrl.scrollToTop(); } else if (ref.read(homeProvider).didExpandCollection(true)) { ref .read(searchProvider(_animeCollectionTag).notifier) @@ -155,8 +167,8 @@ class _HomeViewState extends ConsumerState } return; case HomeTab.manga: - if (_scrollCtrl.position.pixels > 0) { - _scrollCtrl.scrollToTop(); + if (_mangaScrollCtrl.position.pixels > 0) { + _mangaScrollCtrl.scrollToTop(); } else if (ref.read(homeProvider).didExpandCollection(false)) { ref .read(searchProvider(_mangaCollectionTag).notifier) @@ -164,16 +176,18 @@ class _HomeViewState extends ConsumerState } return; case HomeTab.discover: - if (_scrollCtrl.position.pixels > 0) { - _scrollCtrl.scrollToTop(); + if (_discoverScrollCtrl.position.pixels > 0) { + _discoverScrollCtrl.scrollToTop(); } else { ref .read(searchProvider(null).notifier) .update((s) => s == null ? '' : null); } return; - default: - _scrollCtrl.scrollToTop(); + case HomeTab.feed: + _feedScrollCtrl.scrollToTop(); + case HomeTab.profile: + primaryScrollCtrl.scrollToTop(); return; } }, @@ -181,48 +195,39 @@ class _HomeViewState extends ConsumerState child: TabBarView( controller: _tabCtrl, children: [ - FeedView(_scrollCtrl), + FeedView(_feedScrollCtrl), if (notifier.didExpandCollection(true)) CollectionSubView( - scrollCtrl: _scrollCtrl, + scrollCtrl: _animeScrollCtrl, tag: _animeCollectionTag, key: Key(true.toString()), ) else CollectionPreviewView( - scrollCtrl: _scrollCtrl, + scrollCtrl: _animeScrollCtrl, tag: _animeCollectionTag, key: Key(true.toString()), ), if (notifier.didExpandCollection(false)) CollectionSubView( - scrollCtrl: _scrollCtrl, + scrollCtrl: _mangaScrollCtrl, tag: _mangaCollectionTag, key: Key(false.toString()), ) else CollectionPreviewView( - scrollCtrl: _scrollCtrl, + scrollCtrl: _mangaScrollCtrl, tag: _mangaCollectionTag, key: Key(false.toString()), ), - DiscoverView(_scrollCtrl), - UserSubView(widget.id, null, _scrollCtrl), + DiscoverView(_discoverScrollCtrl), + UserSubView(widget.id, null, primaryScrollCtrl), ], ), ), ); } - void _scrollListener() { - final notifier = ref.read(homeProvider); - if (notifier.homeTab == HomeTab.feed) { - ref.read(activitiesProvider(null).notifier).fetch(); - } else if (notifier.homeTab == HomeTab.discover) { - discoverLoadMore(ref); - } - } - Future _onWillPop(BuildContext context) async { final notifier = ref.read(homeProvider); if (notifier.homeTab == HomeTab.discover) { From 64aed984ccca40cd420d01513990abd5c3d2c89e Mon Sep 17 00:00:00 2001 From: lotusgate Date: Wed, 24 May 2023 15:03:16 +0300 Subject: [PATCH 44/55] Cleanup --- android/app/build.gradle | 2 +- android/gradle.properties | 5 +- .../collection/collection_providers.dart | 60 ++++++------- lib/modules/edit/edit_providers.dart | 2 +- pubspec.lock | 88 ++----------------- pubspec.yaml | 9 +- 6 files changed, 42 insertions(+), 124 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7201a53b..c1797e52 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { applicationId "com.otraku.app" - minSdkVersion 21 + minSdkVersion 26 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/gradle.properties b/android/gradle.properties index 7614ad11..4d3226ab 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,6 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true -android.enableJetifier=true -# Without this line there may be crashes on Android 6.0 devices apparently. -# This makes the app bundle larger, but it is the only solution currently. -android.bundle.enableUncompressedNativeLibs=false \ No newline at end of file +android.enableJetifier=true \ No newline at end of file diff --git a/lib/modules/collection/collection_providers.dart b/lib/modules/collection/collection_providers.dart index da9913da..d2e5e0c3 100644 --- a/lib/modules/collection/collection_providers.dart +++ b/lib/modules/collection/collection_providers.dart @@ -186,7 +186,6 @@ class CollectionNotifier extends ChangeNotifier { notifyListeners(); } - /// Update an existing entry, taking into account status and custom lists. Future updateEntry( Entry entry, Edit oldEdit, @@ -217,69 +216,67 @@ class CollectionNotifier extends ChangeNotifier { break; } } + if (!added) { _fetch(); return entry; } } - // Find from which custom lists to remove. + // Remove from old custom lists. final oldCustomLists = oldEdit.customLists.entries .where((e) => e.value) .map((e) => e.key.toLowerCase()) .toList(); - // Remove from old custom lists. if (oldCustomLists.isNotEmpty) { for (final list in lists) { + if (list.status != null) continue; + for (int i = 0; i < oldCustomLists.length; i++) { if (oldCustomLists[i] == list.name.toLowerCase()) { list.removeByMediaId(entry.mediaId); - oldCustomLists.removeAt(i); + oldCustomLists[i] = oldCustomLists.last; + oldCustomLists.removeLast(); break; } } } } - // Find in which custom lists to add. + // Add to new custom lists. final newCustomLists = newEdit.customLists.entries .where((e) => e.value) .map((e) => e.key.toLowerCase()) .toList(); - // Add to new custom lists. if (newCustomLists.isNotEmpty) { for (final list in lists) { + if (list.status != null) continue; + for (int i = 0; i < newCustomLists.length; i++) { if (newCustomLists[i] == list.name.toLowerCase()) { list.insertSorted(entry, sort); - newCustomLists.removeAt(i); + newCustomLists[i] = newCustomLists.last; + newCustomLists.removeLast(); break; } } } + if (newCustomLists.isNotEmpty) { _fetch(); return entry; } } - // Remove empty lists. - for (int i = 0; i < lists.length; i++) { - if (lists[i].entries.isEmpty) { - if (i <= _index && _index != 0) _index--; - lists.removeAt(i--); - } - } - + _removeEmptyLists(); notifyListeners(); return entry; } - /// Faster alternative to [updateEntry]. Should be used only when - /// the progress was incremented. When reaching the last episode, - /// [updateEntry] should be called instead. + /// An alternative to [updateEntry], that only updates the progress. + /// When incrementing to last episode, [updateEntry] should be called instead. Future updateProgress({ required int mediaId, required int progress, @@ -288,11 +285,6 @@ class CollectionNotifier extends ChangeNotifier { required String? format, required EntrySort sort, }) async { - final mustSort = sort == EntrySort.PROGRESS || - sort == EntrySort.PROGRESS_DESC || - sort == EntrySort.UPDATED || - sort == EntrySort.UPDATED_DESC; - // Update status list. for (final list in lists) { if (list.status == null || @@ -307,16 +299,16 @@ class CollectionNotifier extends ChangeNotifier { } } - if (mustSort) list.sort(sort); break; } // Update custom lists. if (customLists.isNotEmpty) { for (final list in lists) { + if (list.status != null) continue; + for (int i = 0; i < customLists.length; i++) { - if (list.status == null && - customLists[i] == list.name.toLowerCase()) { + if (customLists[i] == list.name.toLowerCase()) { for (final entry in list.entries) { if (entry.mediaId == mediaId) { entry.progress = progress; @@ -324,8 +316,8 @@ class CollectionNotifier extends ChangeNotifier { } } - if (mustSort) list.sort(sort); - customLists.removeAt(i); + customLists[i] = customLists.last; + customLists.removeLast(); break; } } @@ -334,7 +326,6 @@ class CollectionNotifier extends ChangeNotifier { } Future removeEntry(Edit edit) async { - final lists = this.lists; final customLists = edit.customLists.entries .where((e) => e.value) .map((e) => e.key.toLowerCase()) @@ -355,21 +346,24 @@ class CollectionNotifier extends ChangeNotifier { for (int i = 0; i < customLists.length; i++) { if (customLists[i] == list.name.toLowerCase()) { list.removeByMediaId(edit.mediaId); - customLists.removeAt(i); + customLists[i] = customLists.last; + customLists.removeLast(); break; } } } } - // Remove empty lists. + _removeEmptyLists(); + notifyListeners(); + } + + void _removeEmptyLists() { for (int i = 0; i < lists.length; i++) { if (lists[i].entries.isEmpty) { if (i <= _index && _index != 0) _index--; lists.removeAt(i--); } } - - notifyListeners(); } } diff --git a/lib/modules/edit/edit_providers.dart b/lib/modules/edit/edit_providers.dart index 00fa9806..9758e94a 100644 --- a/lib/modules/edit/edit_providers.dart +++ b/lib/modules/edit/edit_providers.dart @@ -8,7 +8,7 @@ import 'package:otraku/common/utils/graphql.dart'; /// Updates an entry with an edit and returns the entry, or an error /// if unsuccessful. There is an api bug in entry updating, which prevents -/// certain data from being returned. This is why 2 requests are needed. +/// tag data from being returned. This is why 2 requests are needed. Future updateEntry(Edit edit, int userId) async { try { await Api.get(GqlMutation.updateEntry, edit.toMap()); diff --git a/pubspec.lock b/pubspec.lock index 3f044bdb..9f33c6d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: d572dcdff49c4cfcfa6f315e2683e518ec6eb54e084d01e51d9631a4dcc1b5e8 + sha256: "16725e716afd0634a5441654b1dda2b6c5557aa230884b5e1f41a5aa546a4cb6" url: "https://pub.dev" source: hosted - version: "3.4.2" - archive: - dependency: transitive - description: - name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" - url: "https://pub.dev" - source: hosted - version: "3.3.7" + version: "3.4.3" args: dependency: transitive description: @@ -73,22 +65,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 - url: "https://pub.dev" - source: hosted - version: "0.4.0" clock: dependency: transitive description: @@ -105,14 +81,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.1" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" crypto: dependency: transitive description: @@ -141,10 +109,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad + sha256: "74dff1435a695887ca64899b8990004f8d1232b0e84bfc4faa1fdda7c6f57cc1" url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.6.5" fake_async: dependency: transitive description: @@ -190,14 +158,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.0" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" - url: "https://pub.dev" - source: hosted - version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -210,10 +170,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: ee6ee56855aa920899b68586b538474d086c149932220b47b92502cbfb5ba5e5 + sha256: "12f8abacca8bf29c042ec50c554f967da4c6f88ec99fc215e0325e5b43a25188" url: "https://pub.dev" source: hosted - version: "14.0.0+2" + version: "14.1.0" flutter_local_notifications_linux: dependency: transitive description: @@ -300,10 +260,10 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html_core - sha256: "2ee1f47662f5ba34fe535915b034fae0450bc0a15ae6935ca4abcc7fa987e948" + sha256: "77f05cd7a738078dcdbe07741140d58b2fe7509197f3855a91269fb5a90f4bee" url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.1" fwfh_text_style: dependency: transitive description: @@ -344,14 +304,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - image: - dependency: transitive - description: - name: image - sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf - url: "https://pub.dev" - source: hosted - version: "4.0.17" ionicons: dependency: "direct main" description: @@ -368,14 +320,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" lints: dependency: transitive description: @@ -504,14 +448,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" - url: "https://pub.dev" - source: hosted - version: "3.7.3" process: dependency: transitive description: @@ -749,14 +685,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.3.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" sdks: dart: ">=3.0.0 <4.0.0" flutter: ">=3.7.0-0" diff --git a/pubspec.yaml b/pubspec.yaml index 05bbd7a0..d42f59ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,19 +37,19 @@ dependencies: 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.4.2 + app_links: ^3.4.3 # Access to platform theme and easy theme interpolation. - dynamic_color: ^1.6.3 + dynamic_color: ^1.6.5 # Background tasks for notification fetching. workmanager: ^0.5.1 # Sending device notifications. - flutter_local_notifications: ^14.0.0+2 + flutter_local_notifications: ^14.1.0 # Translating html into flutter widgets. - flutter_widget_from_html_core: ^0.10.0 + flutter_widget_from_html_core: ^0.10.1 # An addition to the material icons. ionicons: ^0.2.1 @@ -58,7 +58,6 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_launcher_icons: ^0.13.1 flutter_lints: ^2.0.1 flutter_icons: From cb93548ed2b8314f1307994c2035d3bcf39d7064 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Mon, 29 May 2023 15:48:57 +0300 Subject: [PATCH 45/55] Fixed repeating activities and cleanup --- .../activity/activities_providers.dart | 85 +++++-------------- lib/modules/activity/activities_view.dart | 35 +++++--- lib/modules/activity/activity_models.dart | 23 +++++ lib/modules/discover/discover_providers.dart | 2 +- lib/modules/filter/filter_models.dart | 33 ++++--- lib/modules/filter/filter_providers.dart | 12 +-- lib/modules/filter/filter_view.dart | 16 ++-- 7 files changed, 97 insertions(+), 109 deletions(-) diff --git a/lib/modules/activity/activities_providers.dart b/lib/modules/activity/activities_providers.dart index 1eb075d4..03d33645 100644 --- a/lib/modules/activity/activities_providers.dart +++ b/lib/modules/activity/activities_providers.dart @@ -9,7 +9,6 @@ import 'package:otraku/common/utils/options.dart'; final activitiesProvider = StateNotifierProvider.autoDispose .family>, int?>( (ref, userId) => ActivitiesNotifier( - userId: userId, viewerId: Options().id!, filter: ref.watch(activityFilterProvider(userId)), shouldLoad: @@ -17,30 +16,19 @@ final activitiesProvider = StateNotifierProvider.autoDispose ), ); -final activityFilterProvider = StateNotifierProvider.autoDispose - .family( - (ref, userId) { - var typeIn = ActivityType.values; - FeedFilter? feedFilter; - - if (userId == null) { - feedFilter = FeedFilter( - Options().feedOnFollowing, - Options().viewerActivitiesInFeed, - ); - typeIn = Options() - .feedActivityFilters - .map((e) => ActivityType.values.elementAt(e)) - .toList(); - } - - return ActivityFilterNotifier(typeIn, feedFilter); - }, +final activityFilterProvider = + StateProvider.autoDispose.family( + (ref, userId) => userId == null + ? HomeActivitiesFilter( + ActivityType.values, + Options().feedOnFollowing, + Options().viewerActivitiesInFeed, + ) + : UserActivitiesFilter(ActivityType.values, userId), ); class ActivitiesNotifier extends StateNotifier>> { ActivitiesNotifier({ - required this.userId, required this.viewerId, required this.filter, required bool shouldLoad, @@ -48,15 +36,9 @@ class ActivitiesNotifier extends StateNotifier>> { if (shouldLoad) fetch(); } - /// [userId] being `null` means that this notifier handles the home feed. - final int? userId; 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; + final ActivitiesFilter filter; + int _lastCreatedAt = DateTime.now().millisecondsSinceEpoch; Future fetch() async { state = await AsyncValue.guard(() async { @@ -64,13 +46,18 @@ class ActivitiesNotifier extends StateNotifier>> { final data = await Api.get(GqlQuery.activities, { 'typeIn': filter.typeIn.map((t) => t.name).toList(), - if (userId != null) 'userId': userId, - if (filter.feedFilter != null) ...{ - 'isFollowing': filter.feedFilter!.onFollowing, - if (!filter.feedFilter!.withViewerActivities) 'userIdNot': viewerId, - if (!filter.feedFilter!.onFollowing) 'hasRepliesOrText': true, + ...switch (filter) { + HomeActivitiesFilter filter => { + 'isFollowing': filter.onFollowing, + if (!filter.withViewerActivities) 'userIdNot': viewerId, + if (!filter.onFollowing) 'hasRepliesOrText': true, + if (value.items.isNotEmpty) 'createdBefore': _lastCreatedAt, + }, + UserActivitiesFilter filter => { + 'userId': filter.userId, + 'page': value.next, + }, }, - if (_lastCreatedAt != null) 'createdBefore': _lastCreatedAt! }); final items = []; @@ -79,7 +66,7 @@ class ActivitiesNotifier extends StateNotifier>> { if (item != null) items.add(item); } - if (data['Page']['activities']?.isNotEmpty ?? false) { + if (data['Page']['activities'].isNotEmpty) { _lastCreatedAt = data['Page']['activities'].last['createdAt']; } @@ -185,29 +172,3 @@ class ActivitiesNotifier extends StateNotifier>> { } } } - -class ActivityFilterNotifier extends StateNotifier { - ActivityFilterNotifier(List typeIn, FeedFilter? feedFilter) - : super(ActivityFilter(typeIn, feedFilter)); - - void update( - List typeIn, - bool? onFollowing, - bool? withViewerActivities, - ) { - state = state.feedFilter == null - ? ActivityFilter(typeIn, null) - : ActivityFilter( - typeIn, - FeedFilter( - onFollowing ?? state.feedFilter!.onFollowing, - withViewerActivities ?? state.feedFilter!.withViewerActivities, - ), - ); - - if (state.feedFilter == null) return; - Options().feedActivityFilters = typeIn.map((e) => e.index).toList(); - Options().feedOnFollowing = state.feedFilter!.onFollowing; - Options().viewerActivitiesInFeed = state.feedFilter!.withViewerActivities; - } -} diff --git a/lib/modules/activity/activities_view.dart b/lib/modules/activity/activities_view.dart index e566dae5..4a4bd40c 100644 --- a/lib/modules/activity/activities_view.dart +++ b/lib/modules/activity/activities_view.dart @@ -24,17 +24,19 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { final typeIn = [...filter.typeIn]; bool changed = false; - bool? onFollowing; - bool? withViewerActivities; - if (filter.feedFilter != null) { - onFollowing = filter.feedFilter!.onFollowing; - withViewerActivities = filter.feedFilter!.withViewerActivities; - } - + bool onFollowing = false; + bool withViewerActivities = false; double initialHeight = MediaQuery.of(context).padding.bottom + Consts.tapTargetSize * ActivityType.values.length + 20; - if (onFollowing != null) initialHeight += Consts.tapTargetSize * 1.5; + + switch (filter) { + case HomeActivitiesFilter filter: + onFollowing = filter.onFollowing; + withViewerActivities = filter.withViewerActivities; + initialHeight += Consts.tapTargetSize * 1.5; + default: + } showSheet( context, @@ -54,11 +56,11 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { changed = true; }, ), - if (onFollowing != null) ...[ + if (filter is HomeActivitiesFilter) ...[ const Divider(), CheckBoxField( title: 'Your Activities', - initial: withViewerActivities!, + initial: withViewerActivities, onChanged: (val) { withViewerActivities = val; changed = true; @@ -66,7 +68,7 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { ), SegmentSwitcher( items: const ['Following', 'Global'], - current: onFollowing! ? 0 : 1, + current: onFollowing ? 0 : 1, onChanged: (val) { onFollowing = val == 0; changed = true; @@ -79,9 +81,14 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { ).then((_) { if (changed) { ref.read(activityFilterProvider(id).notifier).update( - typeIn, - onFollowing, - withViewerActivities, + (s) => switch (s) { + UserActivitiesFilter _ => UserActivitiesFilter(typeIn, s.userId), + HomeActivitiesFilter _ => HomeActivitiesFilter( + typeIn, + onFollowing, + withViewerActivities, + ), + }, ); } }); diff --git a/lib/modules/activity/activity_models.dart b/lib/modules/activity/activity_models.dart index c628e64a..504a1e0a 100644 --- a/lib/modules/activity/activity_models.dart +++ b/lib/modules/activity/activity_models.dart @@ -219,3 +219,26 @@ class FeedFilter { final bool onFollowing; final bool withViewerActivities; } + +sealed class ActivitiesFilter { + const ActivitiesFilter(this.typeIn); + + final List typeIn; +} + +class UserActivitiesFilter extends ActivitiesFilter { + const UserActivitiesFilter(super.typeIn, this.userId); + + final int userId; +} + +class HomeActivitiesFilter extends ActivitiesFilter { + const HomeActivitiesFilter( + super.typeIn, + this.onFollowing, + this.withViewerActivities, + ); + + final bool onFollowing; + final bool withViewerActivities; +} diff --git a/lib/modules/discover/discover_providers.dart b/lib/modules/discover/discover_providers.dart index 18c84a99..45626dc1 100644 --- a/lib/modules/discover/discover_providers.dart +++ b/lib/modules/discover/discover_providers.dart @@ -104,7 +104,7 @@ class DiscoverMediaNotifier if (shouldLoad) fetch(); } - final DiscoverFilter filter; + final DiscoverMediaFilter filter; final String? search; Future fetch() async { diff --git a/lib/modules/filter/filter_models.dart b/lib/modules/filter/filter_models.dart index c0435693..05eb88b1 100644 --- a/lib/modules/filter/filter_models.dart +++ b/lib/modules/filter/filter_models.dart @@ -1,21 +1,18 @@ import 'package:otraku/modules/media/media_constants.dart'; import 'package:otraku/common/utils/options.dart'; -abstract class ApplicableMediaFilter> { - ApplicableMediaFilter(this._ofAnime); +sealed class MediaFilter> { + MediaFilter(this._ofAnime); bool _ofAnime; bool get ofAnime => _ofAnime; - /// Creates a copy. - T copy(); - - /// Creates an unconfigured instance. T clear(); + T copy(); } -class CollectionFilter extends ApplicableMediaFilter { - CollectionFilter(super.ofAnime); +class CollectionMediaFilter extends MediaFilter { + CollectionMediaFilter(super.ofAnime); final statuses = []; final formats = []; @@ -32,7 +29,10 @@ class CollectionFilter extends ApplicableMediaFilter { OriginCountry? country; @override - CollectionFilter copy() => CollectionFilter(_ofAnime) + CollectionMediaFilter clear() => CollectionMediaFilter(_ofAnime); + + @override + CollectionMediaFilter copy() => CollectionMediaFilter(_ofAnime) ..statuses.addAll(statuses) ..formats.addAll(formats) ..genreIn.addAll(genreIn) @@ -45,13 +45,10 @@ class CollectionFilter extends ApplicableMediaFilter { ..startYearFrom = startYearFrom ..startYearTo = startYearTo ..country = country; - - @override - CollectionFilter clear() => CollectionFilter(_ofAnime); } -class DiscoverFilter extends ApplicableMediaFilter { - DiscoverFilter(super.ofAnime); +class DiscoverMediaFilter extends MediaFilter { + DiscoverMediaFilter(super.ofAnime); final statuses = []; final formats = []; @@ -74,7 +71,10 @@ class DiscoverFilter extends ApplicableMediaFilter { } @override - DiscoverFilter copy() => DiscoverFilter(_ofAnime) + DiscoverMediaFilter clear() => DiscoverMediaFilter(_ofAnime); + + @override + DiscoverMediaFilter copy() => DiscoverMediaFilter(_ofAnime) ..statuses.addAll(statuses) ..formats.addAll(formats) ..genreIn.addAll(genreIn) @@ -90,9 +90,6 @@ class DiscoverFilter extends ApplicableMediaFilter { ..onList = onList ..isAdult = isAdult; - @override - DiscoverFilter clear() => DiscoverFilter(_ofAnime); - Map toMap() => { 'sort': sort.name, if (statuses.isNotEmpty) 'status_in': statuses, diff --git a/lib/modules/filter/filter_providers.dart b/lib/modules/filter/filter_providers.dart index c98406b5..317a75a1 100644 --- a/lib/modules/filter/filter_providers.dart +++ b/lib/modules/filter/filter_providers.dart @@ -6,7 +6,7 @@ import 'package:otraku/modules/filter/filter_models.dart'; import 'package:otraku/common/utils/options.dart'; final collectionFilterProvider = StateProvider.autoDispose.family( - (ref, CollectionTag tag) => CollectionFilter(tag.ofAnime), + (ref, CollectionTag tag) => CollectionMediaFilter(tag.ofAnime), ); /// If the [CollectionTag] is `null`, this is related to the discover tab. @@ -22,25 +22,25 @@ class DiscoverFilterNotifier extends ChangeNotifier { DiscoverFilterNotifier(this._type); DiscoverType _type; - late var _filter = DiscoverFilter(_type == DiscoverType.anime); + late var _filter = DiscoverMediaFilter(_type == DiscoverType.anime); bool _birthday = false; DiscoverType get type => _type; - DiscoverFilter get filter => _filter; + DiscoverMediaFilter get filter => _filter; bool get birthday => _birthday; set type(DiscoverType val) { if (_type == val) return; if (val == DiscoverType.anime) { - _filter = DiscoverFilter(true); + _filter = DiscoverMediaFilter(true); } else { - _filter = DiscoverFilter(false); + _filter = DiscoverMediaFilter(false); } _type = val; notifyListeners(); } - set filter(DiscoverFilter val) { + set filter(DiscoverMediaFilter val) { _filter = val; notifyListeners(); } diff --git a/lib/modules/filter/filter_view.dart b/lib/modules/filter/filter_view.dart index 3a6236c4..20b52c29 100644 --- a/lib/modules/filter/filter_view.dart +++ b/lib/modules/filter/filter_view.dart @@ -15,7 +15,7 @@ import 'package:otraku/common/widgets/loaders.dart/loaders.dart'; import 'package:otraku/common/widgets/grids/chip_grids.dart'; import 'package:otraku/common/widgets/overlays/sheets.dart'; -class _FilterView> extends StatefulWidget { +class _FilterView> extends StatefulWidget { const _FilterView({ required this.filter, required this.onChanged, @@ -30,7 +30,7 @@ class _FilterView> extends StatefulWidget { State<_FilterView> createState() => __FilterViewState(); } -class __FilterViewState> +class __FilterViewState> extends State<_FilterView> { late final T _filter = widget.filter.copy(); @@ -65,12 +65,12 @@ class __FilterViewState> class CollectionFilterView extends StatelessWidget { const CollectionFilterView({required this.filter, required this.onChanged}); - final CollectionFilter filter; - final void Function(CollectionFilter) onChanged; + final CollectionMediaFilter filter; + final void Function(CollectionMediaFilter) onChanged; @override Widget build(BuildContext context) { - return _FilterView( + return _FilterView( filter: filter, onChanged: onChanged, builder: (context, scrollCtrl, filter) => ListView( @@ -135,12 +135,12 @@ class CollectionFilterView extends StatelessWidget { class DiscoverFilterView extends StatelessWidget { const DiscoverFilterView({required this.filter, required this.onChanged}); - final DiscoverFilter filter; - final void Function(DiscoverFilter) onChanged; + final DiscoverMediaFilter filter; + final void Function(DiscoverMediaFilter) onChanged; @override Widget build(BuildContext context) { - return _FilterView( + return _FilterView( filter: filter, onChanged: onChanged, builder: (context, scrollCtrl, filter) => ListView( From 91371656049a4a286696e632e998af5688a6d9a2 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 3 Jun 2023 23:15:48 +0300 Subject: [PATCH 46/55] Tapping the same media tab will scroll to top --- lib/modules/media/media_header.dart | 16 +++++++++++- lib/modules/media/media_providers.dart | 36 +++++++++++++------------- lib/modules/media/media_view.dart | 7 ++++- pubspec.lock | 4 +-- pubspec.yaml | 2 +- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/lib/modules/media/media_header.dart b/lib/modules/media/media_header.dart index 52d37b70..7273c35d 100644 --- a/lib/modules/media/media_header.dart +++ b/lib/modules/media/media_header.dart @@ -14,11 +14,17 @@ import 'package:otraku/common/widgets/overlays/toast.dart'; import 'package:otraku/common/widgets/text_rail.dart'; class MediaHeader extends StatelessWidget { - const MediaHeader(this.id, this.coverUrl, this.tabCtrl); + const MediaHeader({ + required this.id, + required this.coverUrl, + required this.tabCtrl, + required this.scrollToTop, + }); final int id; final String? coverUrl; final TabController tabCtrl; + final void Function() scrollToTop; @override Widget build(BuildContext context) { @@ -67,6 +73,7 @@ class MediaHeader extends StatelessWidget { info: media?.info, coverUrl: coverUrl, topOffset: topOffset, + scrollToTop: scrollToTop, textRailItems: textRailItems, imageWidth: MediaQuery.of(context).size.width < 430.0 ? MediaQuery.of(context).size.width * 0.30 @@ -86,6 +93,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { required this.coverUrl, required this.topOffset, required this.textRailItems, + required this.scrollToTop, required this.tabCtrl, }); @@ -96,6 +104,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { final double topOffset; final Map textRailItems; final TabController tabCtrl; + final void Function() scrollToTop; @override Widget build( @@ -321,6 +330,11 @@ class _Delegate extends SliverPersistentHeaderDelegate { Tab(text: 'Recommendations'), Tab(text: 'Statistics'), ], + onTap: (i) { + if (i == tabCtrl.index) { + scrollToTop(); + } + }, ), ), ], diff --git a/lib/modules/media/media_providers.dart b/lib/modules/media/media_providers.dart index 1fb9ce44..6426f503 100644 --- a/lib/modules/media/media_providers.dart +++ b/lib/modules/media/media_providers.dart @@ -107,30 +107,13 @@ class MediaRelationsNotifier extends StateNotifier { return data['Media']; }); - var recommended = state.recommendations; var characters = state.characters; var staff = state.staff; var reviews = state.reviews; + var recommended = state.recommendations; var languageToVoiceActors = state.languageToVoiceActors; var language = state.language; - if (tab == null || tab == MediaTab.recommendations) { - recommended = await AsyncValue.guard(() { - if (data.hasError) throw data.error!; - final map = data.value!['recommendations']; - final value = recommended.valueOrNull ?? const Paged(); - - final items = []; - for (final r in map['nodes']) { - if (r['mediaRecommendation'] != null) items.add(Recommendation(r)); - } - - return Future.value( - value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), - ); - }); - } - if (tab == null || tab == MediaTab.characters) { characters = await AsyncValue.guard(() { if (data.hasError) throw data.error!; @@ -227,6 +210,23 @@ class MediaRelationsNotifier extends StateNotifier { }); } + if (tab == null || tab == MediaTab.recommendations) { + recommended = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!['recommendations']; + final value = recommended.valueOrNull ?? const Paged(); + + final items = []; + for (final r in map['nodes']) { + if (r['mediaRecommendation'] != null) items.add(Recommendation(r)); + } + + return Future.value( + value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), + ); + }); + } + state = MediaRelations( recommendations: recommended, characters: characters, diff --git a/lib/modules/media/media_view.dart b/lib/modules/media/media_view.dart index ca2b2004..4b5eebb9 100644 --- a/lib/modules/media/media_view.dart +++ b/lib/modules/media/media_view.dart @@ -48,7 +48,12 @@ class _MediaViewState extends State child: NestedScrollView( controller: _scrollCtrl, headerSliverBuilder: (context, _) => [ - MediaHeader(widget.id, widget.coverUrl, _tabCtrl), + MediaHeader( + id: widget.id, + coverUrl: widget.coverUrl, + tabCtrl: _tabCtrl, + scrollToTop: _scrollCtrl.scrollToTop, + ), ], body: Consumer( builder: (context, ref, _) { diff --git a/pubspec.lock b/pubspec.lock index 9f33c6d7..be2559c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -170,10 +170,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "12f8abacca8bf29c042ec50c554f967da4c6f88ec99fc215e0325e5b43a25188" + sha256: "812791d43ccfc1b443a0d39fa02a206fc228c597e28ff9337e09e3ca8d370391" url: "https://pub.dev" source: hosted - version: "14.1.0" + version: "14.1.1" flutter_local_notifications_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d42f59ae..069b4c87 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: workmanager: ^0.5.1 # Sending device notifications. - flutter_local_notifications: ^14.1.0 + flutter_local_notifications: ^14.1.1 # Translating html into flutter widgets. flutter_widget_from_html_core: ^0.10.1 From 2c400677631af28d3f32892e1f0062ab27fecb57 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 10 Jun 2023 00:50:16 +0300 Subject: [PATCH 47/55] Added followed people on the media page --- lib/common/utils/graphql.dart | 62 +++-- lib/common/widgets/entry_labels.dart | 107 ++++++++ lib/modules/collection/collection_grid.dart | 3 - lib/modules/collection/collection_list.dart | 85 +----- .../collection/collection_preview_view.dart | 1 - lib/modules/collection/collection_view.dart | 1 - lib/modules/media/media_grids.dart | 249 ++++++++++++------ lib/modules/media/media_header.dart | 1 + lib/modules/media/media_models.dart | 118 ++++++--- lib/modules/media/media_providers.dart | 63 ++++- lib/modules/media/media_view.dart | 14 + 11 files changed, 474 insertions(+), 230 deletions(-) create mode 100644 lib/common/widgets/entry_labels.dart diff --git a/lib/common/utils/graphql.dart b/lib/common/utils/graphql.dart index 0ad71a4c..e00fc64a 100644 --- a/lib/common/utils/graphql.dart +++ b/lib/common/utils/graphql.dart @@ -77,7 +77,7 @@ abstract class GqlQuery { popularity studios {edges {isMain node {id name}}} tags {name description rank isMediaSpoiler isGeneralSpoiler} - source + source(version: 3) hashtag siteUrl rankings {rank type year season allTime} @@ -113,33 +113,23 @@ abstract class GqlQuery { updatedAt createdAt } - fragment recommendations on Media { - recommendations(page: $page, sort: [RATING_DESC]) { - pageInfo {hasNextPage} - nodes { - rating - userRating - mediaRecommendation { - id - type - title {userPreferred} - coverImage {extraLarge large medium} - } - } - } - } fragment characters on Media { - characters(page: $page, sort: [ROLE, ID]) { + characters(page: $page, sort: [ROLE, RELEVANCE]) { pageInfo {hasNextPage} edges { role - voiceActors {id name {userPreferred} languageV2 image {large}} node {id name {userPreferred} image {large}} + voiceActors(sort: RELEVANCE) { + id + name {userPreferred} + image {large} + languageV2 + } } } } fragment staff on Media { - staff(page: $page) { + staff(page: $page, sort: RELEVANCE) { pageInfo {hasNextPage} edges {role node {id name {userPreferred} image {large}}} } @@ -156,6 +146,40 @@ abstract class GqlQuery { } } } + fragment recommendations on Media { + recommendations(page: $page, sort: [RATING_DESC]) { + pageInfo {hasNextPage} + nodes { + rating + userRating + mediaRecommendation { + id + type + title {userPreferred} + coverImage {extraLarge large medium} + } + } + } + } + '''; + + static const mediaFollowing = r''' + query MediaFollowing($mediaId: Int, $page: Int) { + Page(page: $page) { + pageInfo {hasNextPage} + mediaList(mediaId: $mediaId, isFollowing: true, sort: UPDATED_TIME_DESC) { + status + score + notes + user { + id + name + avatar {large} + mediaListOptions {scoreFormat} + } + } + } + } '''; static const entry = r''' diff --git a/lib/common/widgets/entry_labels.dart b/lib/common/widgets/entry_labels.dart new file mode 100644 index 00000000..73778b5d --- /dev/null +++ b/lib/common/widgets/entry_labels.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:ionicons/ionicons.dart'; +import 'package:otraku/common/utils/consts.dart'; +import 'package:otraku/common/widgets/overlays/dialogs.dart'; +import 'package:otraku/modules/media/media_constants.dart'; + +class ScoreLabel extends StatelessWidget { + const ScoreLabel(this.score, this.scoreFormat); + + final double score; + final ScoreFormat scoreFormat; + + @override + Widget build(BuildContext context) { + if (score == 0) return const SizedBox(); + + Widget content; + switch (scoreFormat) { + case ScoreFormat.POINT_3: + if (score == 3) { + content = const Icon( + Icons.sentiment_very_satisfied, + size: Consts.iconSmall, + ); + } else if (score == 2) { + content = const Icon( + Icons.sentiment_neutral, + size: Consts.iconSmall, + ); + } else { + content = const Icon( + Icons.sentiment_very_dissatisfied, + size: Consts.iconSmall, + ); + } + case ScoreFormat.POINT_5: + content = Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_rounded, size: Consts.iconSmall), + const SizedBox(width: 3), + Text( + score.toStringAsFixed(0), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + case ScoreFormat.POINT_10_DECIMAL: + content = Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_half_rounded, size: Consts.iconSmall), + const SizedBox(width: 3), + Text( + score.toStringAsFixed( + score.truncate() == score ? 0 : 1, + ), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + default: + content = Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_half_rounded, size: Consts.iconSmall), + const SizedBox(width: 3), + Text( + score.toStringAsFixed(0), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + } + + return Tooltip(message: 'Score', child: content); + } +} + +class NotesLabel extends StatelessWidget { + const NotesLabel(this.notes); + + final String? notes; + + @override + Widget build(BuildContext context) { + if (notes == null) return const SizedBox(); + + return SizedBox( + height: 35, + child: Tooltip( + message: 'Comment', + child: InkResponse( + radius: 10, + child: const Icon(Ionicons.chatbox, size: Consts.iconSmall), + onTap: () => showPopUp( + context, + TextDialog( + title: 'Comment', + text: notes!, + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/collection/collection_grid.dart b/lib/modules/collection/collection_grid.dart index cbf0c535..0f85cbe6 100644 --- a/lib/modules/collection/collection_grid.dart +++ b/lib/modules/collection/collection_grid.dart @@ -4,7 +4,6 @@ import 'package:otraku/modules/collection/collection_models.dart'; import 'package:otraku/modules/discover/discover_models.dart'; import 'package:otraku/modules/edit/edit_providers.dart'; import 'package:otraku/modules/edit/edit_view.dart'; -import 'package:otraku/modules/media/media_constants.dart'; import 'package:otraku/common/utils/consts.dart'; import 'package:otraku/common/widgets/cached_image.dart'; import 'package:otraku/common/widgets/grids/sliver_grid_delegates.dart'; @@ -15,12 +14,10 @@ import 'package:otraku/common/widgets/overlays/sheets.dart'; class CollectionGrid extends StatelessWidget { const CollectionGrid({ 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 diff --git a/lib/modules/collection/collection_list.dart b/lib/modules/collection/collection_list.dart index 8327be1e..172a098f 100644 --- a/lib/modules/collection/collection_list.dart +++ b/lib/modules/collection/collection_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; +import 'package:otraku/common/widgets/entry_labels.dart'; import 'package:otraku/modules/collection/collection_models.dart'; import 'package:otraku/modules/discover/discover_models.dart'; import 'package:otraku/modules/edit/edit_providers.dart'; @@ -166,7 +167,7 @@ class __TileContentState extends State<_TileContent> { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Tooltip(message: 'Score', child: _buildScore(context)), + ScoreLabel(widget.item.score, widget.scoreFormat), if (widget.item.repeat > 0) Tooltip( message: 'Repeats', @@ -184,26 +185,7 @@ class __TileContentState extends State<_TileContent> { ) 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(), + NotesLabel(item.notes), _buildProgressButton(), ], ), @@ -211,67 +193,6 @@ class __TileContentState extends State<_TileContent> { ); } - 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( diff --git a/lib/modules/collection/collection_preview_view.dart b/lib/modules/collection/collection_preview_view.dart index 5d8dda8b..8c07a123 100644 --- a/lib/modules/collection/collection_preview_view.dart +++ b/lib/modules/collection/collection_preview_view.dart @@ -72,7 +72,6 @@ class _CollectionPreviewViewState extends State { ) : CollectionGrid( items: entries, - scoreFormat: notifier.scoreFormat, onProgressUpdate: (_, __) {}, ); } diff --git a/lib/modules/collection/collection_view.dart b/lib/modules/collection/collection_view.dart index d406cbfd..2dc4037e 100644 --- a/lib/modules/collection/collection_view.dart +++ b/lib/modules/collection/collection_view.dart @@ -308,7 +308,6 @@ class _ContentState extends State<_Content> { ) : CollectionGrid( items: entries, - scoreFormat: notifier.scoreFormat, onProgressUpdate: update, ); }, diff --git a/lib/modules/media/media_grids.dart b/lib/modules/media/media_grids.dart index e00cb6ae..46b0e1f3 100644 --- a/lib/modules/media/media_grids.dart +++ b/lib/modules/media/media_grids.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; +import 'package:otraku/common/widgets/entry_labels.dart'; import 'package:otraku/modules/discover/discover_models.dart'; import 'package:otraku/modules/media/media_models.dart'; import 'package:otraku/modules/media/media_providers.dart'; @@ -18,7 +19,7 @@ class MediaRelatedGrid extends StatelessWidget { Widget build(BuildContext context) { if (items.isEmpty) { return const SliverFillRemaining( - child: Center(child: Text('No Relations')), + child: Center(child: Text('No results')), ); } @@ -88,6 +89,169 @@ class MediaRelatedGrid extends StatelessWidget { } } +class MediaReviewGrid extends StatelessWidget { + const MediaReviewGrid(this.items, this.bannerUrl); + + final List items; + final String? bannerUrl; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) { + return const SliverFillRemaining( + child: Center(child: Text('No results')), + ); + } + + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 300, + height: 140, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LinkTile( + id: items[i].userId, + info: items[i].avatar, + discoverType: DiscoverType.user, + child: Row( + children: [ + Hero( + tag: items[i].userId, + child: ClipRRect( + borderRadius: Consts.borderRadiusMin, + child: CachedImage( + items[i].avatar, + height: 50, + width: 50, + ), + ), + ), + 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, + ), + ], + ), + ), + const SizedBox(height: 5), + Expanded( + child: LinkTile( + id: items[i].reviewId, + info: bannerUrl, + discoverType: DiscoverType.review, + child: Card( + child: SizedBox( + width: double.infinity, + child: Padding( + padding: Consts.padding, + child: Text( + items[i].summary, + style: Theme.of(context).textTheme.labelMedium, + overflow: TextOverflow.fade, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class MediaFollowingGrid extends StatelessWidget { + const MediaFollowingGrid(this.items); + + final List items; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) { + return const SliverFillRemaining( + child: Center(child: Text('No results')), + ); + } + + return SliverGrid( + gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( + minWidth: 300, + height: 70, + ), + delegate: SliverChildBuilderDelegate( + childCount: items.length, + (context, i) => LinkTile( + id: items[i].userId, + info: items[i].userAvatar, + discoverType: DiscoverType.user, + child: Card( + child: Row( + children: [ + Hero( + tag: items[i].userId, + child: ClipRRect( + borderRadius: const BorderRadius.horizontal( + left: Consts.radiusMin, + ), + child: CachedImage(items[i].userAvatar, width: 70), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 10, + left: 10, + right: 10, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(items[i].userName), + SizedBox( + height: 35, + child: Row( + children: [ + Expanded(child: Text(items[i].status)), + Expanded( + child: Center( + child: NotesLabel(items[i].notes), + ), + ), + Expanded( + child: Center( + child: ScoreLabel( + items[i].score, + items[i].scoreFormat, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + class MediaRecommendationGrid extends StatelessWidget { const MediaRecommendationGrid(this.mediaId, this.items); @@ -98,7 +262,7 @@ class MediaRecommendationGrid extends StatelessWidget { Widget build(BuildContext context) { if (items.isEmpty) { return const SliverFillRemaining( - child: Center(child: Text('No Recommendations')), + child: Center(child: Text('No results')), ); } @@ -281,87 +445,6 @@ class _RecommendationRatingState extends State<_RecommendationRating> { } } -class MediaReviewGrid extends StatelessWidget { - const MediaReviewGrid(this.items, this.bannerUrl); - - final List items; - final String? bannerUrl; - - @override - Widget build(BuildContext context) { - if (items.isEmpty) { - return const SliverFillRemaining( - child: Center(child: Text('No reviews')), - ); - } - - return SliverGrid( - gridDelegate: const SliverGridDelegateWithMinWidthAndFixedHeight( - minWidth: 300, - height: 140, - ), - delegate: SliverChildBuilderDelegate( - childCount: items.length, - (context, i) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LinkTile( - id: items[i].userId, - info: items[i].avatar, - discoverType: DiscoverType.user, - child: Row( - children: [ - Hero( - tag: items[i].userId, - child: ClipRRect( - borderRadius: Consts.borderRadiusMin, - child: CachedImage( - items[i].avatar, - height: 50, - width: 50, - ), - ), - ), - 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, - ), - ], - ), - ), - const SizedBox(height: 5), - Expanded( - child: LinkTile( - id: items[i].reviewId, - info: bannerUrl, - discoverType: DiscoverType.review, - child: Card( - child: SizedBox( - width: double.infinity, - child: Padding( - padding: Consts.padding, - child: Text( - items[i].summary, - style: Theme.of(context).textTheme.labelMedium, - overflow: TextOverflow.fade, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} - class MediaRankGrid extends StatelessWidget { const MediaRankGrid(this.rankTexts, this.rankTypes); diff --git a/lib/modules/media/media_header.dart b/lib/modules/media/media_header.dart index 7273c35d..75d87c45 100644 --- a/lib/modules/media/media_header.dart +++ b/lib/modules/media/media_header.dart @@ -327,6 +327,7 @@ class _Delegate extends SliverPersistentHeaderDelegate { Tab(text: 'Characters'), Tab(text: 'Staff'), Tab(text: 'Reviews'), + Tab(text: 'Following'), Tab(text: 'Recommendations'), Tab(text: 'Statistics'), ], diff --git a/lib/modules/media/media_models.dart b/lib/modules/media/media_models.dart index f06f00a0..9fabc876 100644 --- a/lib/modules/media/media_models.dart +++ b/lib/modules/media/media_models.dart @@ -5,6 +5,7 @@ import 'package:otraku/common/models/relation.dart'; import 'package:otraku/common/models/tile_item.dart'; import 'package:otraku/modules/discover/discover_models.dart'; import 'package:otraku/modules/edit/edit_model.dart'; +import 'package:otraku/modules/media/media_constants.dart'; import 'package:otraku/modules/tag/tag_models.dart'; import 'package:otraku/common/utils/convert.dart'; import 'package:otraku/common/utils/options.dart'; @@ -31,6 +32,7 @@ enum MediaTab { characters, staff, reviews, + following, recommendations, statistics, } @@ -40,6 +42,7 @@ class MediaRelations { this.characters = const AsyncValue.loading(), this.staff = const AsyncValue.loading(), this.reviews = const AsyncValue.loading(), + this.following = const AsyncValue.loading(), this.recommendations = const AsyncValue.loading(), this.languageToVoiceActors = const {}, this.language = '', @@ -48,6 +51,7 @@ class MediaRelations { final AsyncValue> characters; final AsyncValue> staff; final AsyncValue> reviews; + final AsyncValue> following; final AsyncValue> recommendations; /// For each language, a list of voice actors @@ -84,6 +88,26 @@ class MediaRelations { return charactersAndVoiceActors; } + + MediaRelations copyWith({ + AsyncValue>? characters, + AsyncValue>? staff, + AsyncValue>? reviews, + AsyncValue>? following, + AsyncValue>? recommendations, + Map>>? languageToVoiceActors, + String? language, + }) => + MediaRelations( + characters: characters ?? this.characters, + staff: staff ?? this.staff, + reviews: reviews ?? this.reviews, + following: following ?? this.following, + recommendations: recommendations ?? this.recommendations, + languageToVoiceActors: + languageToVoiceActors ?? this.languageToVoiceActors, + language: language ?? this.language, + ); } class RelatedMedia { @@ -118,6 +142,69 @@ class RelatedMedia { final String? status; } +class RelatedReview { + RelatedReview._({ + required this.reviewId, + required this.userId, + required this.avatar, + required this.username, + required this.summary, + required this.rating, + }); + + static RelatedReview? maybe(Map map) { + if (map['user'] == null) return null; + + return RelatedReview._( + reviewId: map['id'], + userId: map['user']['id'], + username: map['user']['name'] ?? '', + summary: map['summary'] ?? '', + avatar: map['user']['avatar']['large'], + rating: '${map['rating']}/${map['ratingAmount']}', + ); + } + + final int reviewId; + final int userId; + final String username; + final String avatar; + final String summary; + final String rating; +} + +class MediaFollowing { + MediaFollowing._({ + required this.status, + required this.score, + required this.notes, + required this.userId, + required this.userName, + required this.userAvatar, + required this.scoreFormat, + }); + + factory MediaFollowing(Map map) => MediaFollowing._( + status: Convert.clarifyEnum(map['status'])!, + score: (map['score'] ?? 0).toDouble(), + notes: map['notes'], + userId: map['user']['id'], + userName: map['user']['name'], + userAvatar: map['user']['avatar']['large'], + scoreFormat: ScoreFormat.values.byName( + map['user']['mediaListOptions']['scoreFormat'] ?? 'POINT_10_DECIMAL', + ), + ); + + final String status; + final double score; + final String? notes; + final int userId; + final String userName; + final String userAvatar; + final ScoreFormat scoreFormat; +} + class Recommendation { Recommendation._({ required this.id, @@ -152,37 +239,6 @@ class Recommendation { final DiscoverType type; } -class RelatedReview { - RelatedReview._({ - required this.reviewId, - required this.userId, - required this.avatar, - required this.username, - required this.summary, - required this.rating, - }); - - static RelatedReview? maybe(Map map) { - if (map['user'] == null) return null; - - return RelatedReview._( - reviewId: map['id'], - userId: map['user']['id'], - username: map['user']['name'] ?? '', - summary: map['summary'] ?? '', - avatar: map['user']['avatar']['large'], - rating: '${map['rating']}/${map['ratingAmount']}', - ); - } - - final int reviewId; - final int userId; - final String username; - final String avatar; - final String summary; - final String rating; -} - class MediaInfo { MediaInfo._({ required this.id, diff --git a/lib/modules/media/media_providers.dart b/lib/modules/media/media_providers.dart index 6426f503..e47f9779 100644 --- a/lib/modules/media/media_providers.dart +++ b/lib/modules/media/media_providers.dart @@ -64,20 +64,33 @@ final mediaRelationsProvider = StateNotifierProvider.autoDispose class MediaRelationsNotifier extends StateNotifier { MediaRelationsNotifier(this.mediaId) : super(const MediaRelations()) { - _fetch(null); + _fetchGroups(null); } final int mediaId; - - Future fetch(MediaTab tab) => _fetch(tab); - - Future _fetch(MediaTab? tab) async { - if (tab == MediaTab.info || - tab == MediaTab.relations || - tab == MediaTab.statistics) { - return; + bool _didLazyLoadFollowing = false; + + Future fetch(MediaTab tab) => switch (tab) { + MediaTab.info || + MediaTab.relations || + MediaTab.statistics => + Future.value(), + MediaTab.following => _fetchFollowing(), + _ => _fetchGroups(tab), + }; + + void lazyLoad(MediaTab tab) { + switch (tab) { + case MediaTab.following: + if (_didLazyLoadFollowing) return; + _didLazyLoadFollowing = true; + _fetchFollowing(); + default: + return; } + } + Future _fetchGroups(MediaTab? tab) async { final variables = {'id': mediaId}; if (tab == null) { variables['withRecommendations'] = true; @@ -227,7 +240,7 @@ class MediaRelationsNotifier extends StateNotifier { }); } - state = MediaRelations( + state = state.copyWith( recommendations: recommended, characters: characters, staff: staff, @@ -237,6 +250,36 @@ class MediaRelationsNotifier extends StateNotifier { ); } + Future _fetchFollowing() async { + if (!(state.following.valueOrNull?.hasNext ?? true)) return; + + final data = await AsyncValue.guard>(() async { + final data = await Api.get(GqlQuery.mediaFollowing, { + 'mediaId': mediaId, + 'page': state.following.valueOrNull?.next ?? 1, + }); + return data['Page']; + }); + + var following = state.following; + following = await AsyncValue.guard(() { + if (data.hasError) throw data.error!; + final map = data.value!; + final value = following.valueOrNull ?? const Paged(); + + final items = []; + for (final f in map['mediaList']) { + items.add(MediaFollowing(f)); + } + + return Future.value( + value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), + ); + }); + + state = state.copyWith(following: following); + } + void changeLanguage(String language) => state = MediaRelations( recommendations: state.recommendations, characters: state.characters, diff --git a/lib/modules/media/media_view.dart b/lib/modules/media/media_view.dart index 4b5eebb9..2653d1e4 100644 --- a/lib/modules/media/media_view.dart +++ b/lib/modules/media/media_view.dart @@ -139,6 +139,10 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { void _tabListener() { _lastMaxExtent = 0; + ref + .read(mediaRelationsProvider(widget.id).notifier) + .lazyLoad(MediaTab.values[widget.tabCtrl.index]); + // This is a workaround for an issue with [NestedScrollView]. // If you switch to a tab with pagination, where the content // doesn't fill the view, the scroll controller has it's maximum @@ -231,6 +235,16 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { onRefresh: () => _refresh(ref), ), ), + Consumer( + builder: (context, ref, _) => PagedView( + provider: mediaRelationsProvider(widget.id).select( + (s) => s.following, + ), + onData: (data) => MediaFollowingGrid(data.items), + scrollCtrl: _scrollCtrl, + onRefresh: () => _refresh(ref), + ), + ), Consumer( builder: (context, ref, _) => PagedView( provider: mediaRelationsProvider(widget.id).select( From 471d5b8c1ebc0007a1e9c9003d33972e6116c0ec Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sat, 10 Jun 2023 19:38:15 +0300 Subject: [PATCH 48/55] Visual fixes and providers cleanup --- lib/modules/media/media_header.dart | 4 +- lib/modules/media/media_models.dart | 4 -- lib/modules/media/media_providers.dart | 97 ++++++++++++-------------- lib/modules/media/media_view.dart | 29 +++++--- lib/modules/user/user_view.dart | 7 +- 5 files changed, 74 insertions(+), 67 deletions(-) diff --git a/lib/modules/media/media_header.dart b/lib/modules/media/media_header.dart index 75d87c45..fa0a1f51 100644 --- a/lib/modules/media/media_header.dart +++ b/lib/modules/media/media_header.dart @@ -348,7 +348,9 @@ class _Delegate extends SliverPersistentHeaderDelegate { child: BackdropFilter( filter: Consts.blurFilter, child: DecoratedBox( - decoration: BoxDecoration(color: theme.bottomAppBarTheme.color), + decoration: BoxDecoration( + color: Theme.of(context).navigationBarTheme.backgroundColor, + ), child: body, ), ), diff --git a/lib/modules/media/media_models.dart b/lib/modules/media/media_models.dart index 9fabc876..b1f262a4 100644 --- a/lib/modules/media/media_models.dart +++ b/lib/modules/media/media_models.dart @@ -42,7 +42,6 @@ class MediaRelations { this.characters = const AsyncValue.loading(), this.staff = const AsyncValue.loading(), this.reviews = const AsyncValue.loading(), - this.following = const AsyncValue.loading(), this.recommendations = const AsyncValue.loading(), this.languageToVoiceActors = const {}, this.language = '', @@ -51,7 +50,6 @@ class MediaRelations { final AsyncValue> characters; final AsyncValue> staff; final AsyncValue> reviews; - final AsyncValue> following; final AsyncValue> recommendations; /// For each language, a list of voice actors @@ -93,7 +91,6 @@ class MediaRelations { AsyncValue>? characters, AsyncValue>? staff, AsyncValue>? reviews, - AsyncValue>? following, AsyncValue>? recommendations, Map>>? languageToVoiceActors, String? language, @@ -102,7 +99,6 @@ class MediaRelations { characters: characters ?? this.characters, staff: staff ?? this.staff, reviews: reviews ?? this.reviews, - following: following ?? this.following, recommendations: recommendations ?? this.recommendations, languageToVoiceActors: languageToVoiceActors ?? this.languageToVoiceActors, diff --git a/lib/modules/media/media_providers.dart b/lib/modules/media/media_providers.dart index e47f9779..733e660d 100644 --- a/lib/modules/media/media_providers.dart +++ b/lib/modules/media/media_providers.dart @@ -62,35 +62,23 @@ final mediaRelationsProvider = StateNotifierProvider.autoDispose (ref, int mediaId) => MediaRelationsNotifier(mediaId), ); +final mediaFollowingProvider = StateNotifierProvider.autoDispose + .family>, int>( + (ref, int mediaId) => MediaFollowingNotifier(mediaId), +); + class MediaRelationsNotifier extends StateNotifier { MediaRelationsNotifier(this.mediaId) : super(const MediaRelations()) { - _fetchGroups(null); + _fetch(null); } final int mediaId; - bool _didLazyLoadFollowing = false; - - Future fetch(MediaTab tab) => switch (tab) { - MediaTab.info || - MediaTab.relations || - MediaTab.statistics => - Future.value(), - MediaTab.following => _fetchFollowing(), - _ => _fetchGroups(tab), - }; - - void lazyLoad(MediaTab tab) { - switch (tab) { - case MediaTab.following: - if (_didLazyLoadFollowing) return; - _didLazyLoadFollowing = true; - _fetchFollowing(); - default: - return; - } - } - Future _fetchGroups(MediaTab? tab) async { + Future fetch(MediaTab tab) => _fetch(tab); + + Future _fetch(MediaTab? tab) async { + if (tab == MediaTab.following) return; + final variables = {'id': mediaId}; if (tab == null) { variables['withRecommendations'] = true; @@ -250,42 +238,49 @@ class MediaRelationsNotifier extends StateNotifier { ); } - Future _fetchFollowing() async { - if (!(state.following.valueOrNull?.hasNext ?? true)) return; + void changeLanguage(String language) => state = MediaRelations( + recommendations: state.recommendations, + characters: state.characters, + staff: state.staff, + reviews: state.reviews, + languageToVoiceActors: state.languageToVoiceActors, + language: language, + ); +} - final data = await AsyncValue.guard>(() async { - final data = await Api.get(GqlQuery.mediaFollowing, { - 'mediaId': mediaId, - 'page': state.following.valueOrNull?.next ?? 1, - }); - return data['Page']; - }); +class MediaFollowingNotifier + extends StateNotifier>> { + MediaFollowingNotifier(this.mediaId) : super(const AsyncValue.loading()); - var following = state.following; - following = await AsyncValue.guard(() { - if (data.hasError) throw data.error!; - final map = data.value!; - final value = following.valueOrNull ?? const Paged(); + final int mediaId; + bool _didLazyLoad = false; + + void lazyLoad() { + if (_didLazyLoad) return; + _didLazyLoad = true; + fetch(); + } + + Future fetch() async { + if (!(state.valueOrNull?.hasNext ?? true)) return; + + state = await AsyncValue.guard(() async { + final value = state.valueOrNull ?? const Paged(); + + final data = await Api.get( + GqlQuery.mediaFollowing, + {'mediaId': mediaId, 'page': value.next}, + ); final items = []; - for (final f in map['mediaList']) { + for (final f in data['Page']['mediaList']) { items.add(MediaFollowing(f)); } - return Future.value( - value.withNext(items, map['pageInfo']['hasNextPage'] ?? false), + return value.withNext( + items, + data['Page']['pageInfo']['hasNextPage'] ?? false, ); }); - - state = state.copyWith(following: following); } - - void changeLanguage(String language) => state = MediaRelations( - recommendations: state.recommendations, - characters: state.characters, - staff: state.staff, - reviews: state.reviews, - languageToVoiceActors: state.languageToVoiceActors, - language: language, - ); } diff --git a/lib/modules/media/media_view.dart b/lib/modules/media/media_view.dart index 2653d1e4..b9ac21b1 100644 --- a/lib/modules/media/media_view.dart +++ b/lib/modules/media/media_view.dart @@ -139,9 +139,9 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { void _tabListener() { _lastMaxExtent = 0; - ref - .read(mediaRelationsProvider(widget.id).notifier) - .lazyLoad(MediaTab.values[widget.tabCtrl.index]); + if (widget.tabCtrl.index == MediaTab.following.index) { + ref.read(mediaFollowingProvider(widget.id).notifier).lazyLoad(); + } // This is a workaround for an issue with [NestedScrollView]. // If you switch to a tab with pagination, where the content @@ -163,18 +163,29 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { _loadNextPage(); } - void _loadNextPage() => ref - .read(mediaRelationsProvider(widget.id).notifier) - .fetch(MediaTab.values.elementAt(widget.tabCtrl.index)); + void _loadNextPage() { + if (widget.tabCtrl.index == MediaTab.following.index) { + ref.read(mediaFollowingProvider(widget.id).notifier).fetch(); + } else { + ref + .read(mediaRelationsProvider(widget.id).notifier) + .fetch(MediaTab.values.elementAt(widget.tabCtrl.index)); + } + } void _refresh(WidgetRef ref) { - ref.invalidate(mediaRelationsProvider(widget.id)); + if (widget.tabCtrl.index == MediaTab.following.index) { + ref.invalidate(mediaFollowingProvider(widget.id)); + } else { + ref.invalidate(mediaRelationsProvider(widget.id)); + } _lastMaxExtent = 0; } @override Widget build(BuildContext context) { ref.watch(mediaRelationsProvider(widget.id).select((_) => null)); + ref.watch(mediaFollowingProvider(widget.id).select((_) => null)); final stats = widget.media.stats; @@ -237,9 +248,7 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { ), Consumer( builder: (context, ref, _) => PagedView( - provider: mediaRelationsProvider(widget.id).select( - (s) => s.following, - ), + provider: mediaFollowingProvider(widget.id), onData: (data) => MediaFollowingGrid(data.items), scrollCtrl: _scrollCtrl, onRefresh: () => _refresh(ref), diff --git a/lib/modules/user/user_view.dart b/lib/modules/user/user_view.dart index 3bd857e2..3e370927 100644 --- a/lib/modules/user/user_view.dart +++ b/lib/modules/user/user_view.dart @@ -82,7 +82,12 @@ class UserSubView extends StatelessWidget { if (data.description.isNotEmpty) SliverToBoxAdapter( child: ConstrainedView( - child: Card(child: HtmlContent(data.description)), + child: Card( + child: Padding( + padding: Consts.padding, + child: HtmlContent(data.description), + ), + ), ), ), const SliverFooter(), From 9a7f00afffe08af593ea731a025813b474c3a004 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Sun, 11 Jun 2023 18:13:03 +0300 Subject: [PATCH 49/55] File cleanup and auth view fixes --- .../src/dev/res/mipmap-hdpi/ic_launcher.png | Bin 1083 -> 0 bytes .../src/dev/res/mipmap-mdpi/ic_launcher.png | Bin 810 -> 0 bytes .../src/dev/res/mipmap-xhdpi/ic_launcher.png | Bin 1465 -> 0 bytes .../src/dev/res/mipmap-xxhdpi/ic_launcher.png | Bin 2045 -> 0 bytes .../dev/res/mipmap-xxxhdpi/ic_launcher.png | Bin 2664 -> 0 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1083 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 810 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 1465 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 2045 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 2664 -> 0 bytes lib/modules/auth/auth_view.dart | 23 +++++++++++++++--- 11 files changed, 20 insertions(+), 3 deletions(-) delete mode 100644 android/app/src/dev/res/mipmap-hdpi/ic_launcher.png delete mode 100644 android/app/src/dev/res/mipmap-mdpi/ic_launcher.png delete mode 100644 android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 22107cfaef4c4f2b592939bbb47c50abb485fb56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1083 zcmV-B1jPG^P)dJG1WQx|Wu^N+hJ#LlEdg&<&C(2%>`Cf^Gyt5YbaF(Mv(Sb|LC1qK6>E zGJ24p2Ym@qis-=yF*U8!UA(S)n@j&R?(C>(<3GndBlB@^&m8{$_4IVMK!7j02Y^N$`a#LeUdU{n2S_*ui z*dH$E#b9#is~vgU#G*CTr*HdwK2^@BJyJhkdP;6%RekvZ?=^X6eL$&G54VQx)Z;o?UuY z2qHq=K2*rkY!dz*LKj2q$A`x-V4@cQDF~G+7~4^M?g6Y!4}2c$d}T^Pp#UNHj2*fp zmBwHtJ2eJz1rug>uuxa-(V*&!%$zM*aGC=2ohB9qa5YArz3_-z zlLV7i)BkD)^`?`pO@MhYIohT^*S7|)c)oaY$p#lx)HD}|}(BZT-RHHWzj zt)M}S@V@-3w1%#V7DaFx6T~3h^*3b$*c`Je>di-T+@V!8h*{n;(HFE;kxKobK#W)m zG`^*p?=i=q;;{lTbdGV1d(1%-%%I04%QrdrCb*zTXfz+RLlNJ36cl+zJT~uu9e4K} zrRIaW$0&p%iyp&w9*lbo-+3@t?yXf3-+5R)CNUC`o=lIS;V=olv zcOK{_*m93CazT%DJWG6fXUx;1eYj0PvG3*F7r?UPcNUHcv5B5`=bPka5`X>05Dn&j zl%NHVnA_kHa~nKjZi7e6ZSaV>4IVMK!6W82{02GPsL&G^nN|P*002ovPDHLkV1mGb B_e1~y diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index ed4ca1ebf44401a72b9457f74217a0da159d6d05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 810 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 Hz@7gAk?hbD diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index ea8c5aa9c4f130c17918bda2c7c37cc253d60e6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2664 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 B_e1~y diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index ed4ca1ebf44401a72b9457f74217a0da159d6d05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 810 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 Hz@7gAk?hbD diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index ea8c5aa9c4f130c17918bda2c7c37cc253d60e6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2664 zcmd6p`8O1d8pjzEW0ai+F%$F3KK6C4rjq3%GL|ecvNsvIAw@K{&aD=?$7xLNf~g;==4e$gcHb+gGWA7T_8T-s4Rgne88 z$7xf~5TyQRr0-J`T(n56C!9u@v5MT8WlVSX=lv!H?TJk zSI1cKI;?DH$6qL$Po57Jv&^8euCXD3U%ZR7XgJ{O7P6p8>$LA(G*U6UaQT<2ml*H0Un~4Bh_bW*L|J-} z{7mSlryawcr(4yz$rA@<%;0m4(~S*>>+RII`Q$W`tyL_fg{9$=_jB{Jsn*eE??C(! zZb!;Kli7$pJr%R^D5&*UXgi&A)Ye6fuiJi3m5mz)q-?vn=1b(>SQ$V>FQlvsJX^yt z428F(a^>5IuqQj1HK)73DI4+J@HOaCztM}-doT1Suif*WP*!N!vz>0-;|d4`XNK;2 zKG&jhTK+l}QlG-c7M^#*_!-;7({UO(_{~E$d+EqALf!n~QxwK9_=WNwf95hgm2&b;NE((;O>% z-v6i}b%tCD)krd+kNo)dzR_#Hy^#H7D32!$6ITCtJcf2Y0kTw zlCLzPKk0rwjYA2avCAGDfoFYK;qGt~XS0U!=MTb^SO^D7yifezIV|ExMPXlq(usi& z5wkjG-x>d#o!_`K5|*E8T3)pAqvBCMHg0FLq%?)~*^PSdNASDI#6D-dCB?op^KrH7 z3!Q3Ls!@n|iQ57cK2e#+F}J_LK#?uEW|7DR*~>SJzLK$wtx>pWu=qxSXN|&`ff){j zPxn($wQENzV@S;Oj(vKVjRH#upWgoM9d)2!JOShk$K`x4sJg$X3l@SohW9Hazsge@ znfrwWrJ|B&Tuw7-v%t(;hXj*d3l?4Tjq4%tDUOjp?@=SPYHPCj5ovd#aZb)bbBfDI z6A%d`>D$aRbh*0#ZV>c=d-!c@wS434Hmf>GKF!v$n`EEs4IDFiuVR`;Qo~$%ko@zK z35uNy zAH`?@ykNvXYA)hY**y5RD&T)#A4YBH-_(3u^EURk;a&}u@pDQ*_`G>jRKNzgcxhBi z+~@%pTdP+OjceLl3^STOZ00h0D~+g@SAEqSIn(qYoKkTzO;9q@aQbL%aA`16 z>6;bhRiTI5kK9ejVDS{7AxK}xpVTw~w{-q~kDKR_1mta?)HH6+6UEHo^j1{nLA z91ccD7V3c20Rhm|Fz_YdY|>50pmB1EqUa&&GGJ3n7A+R)AdwqS4}+OG<=>H%@Xe-i8YX2UhPQI4u8 zMw%sHq1Faq^)E=7&{EWndZ}>?Dcrz&QlPr-Okg=`Rh`d0m#Jp$8@S!KYHRx?hgn7; z)VaX|`s;O*3q<@Oa_F2o2_nc}S`H0K*(MA%&D~NW1uBXaX^#e{Xx`$W@|^e;Ng)xc zgi;iwOYh9S{>$Gw5piM0TC6Y!{k9q=s3U-)|Qd!|!L zo6s9V9N}JInWVKvOt%ZS0>&P_ud76=N|{e316JZ+lfrad*xG;l&zAWwsFVTmc7?6V zXXYenTYNIVH)@(HonQ|(^1{KRRxtH*MVeAQBa$iRq9VIX+T*4oNs@sC!c_=6H~2SF zRFR~kn=w5ndwl(DLQB-gI*D>;d>#czfy+SnpAJk@hp)91IpbDl*3h z@=qtES=1fJo+?=PnYsJwB*>pY@PJH9(x}!+C|XaR^;=!Q+2q{zL2F+xC6ZyiVsb7s zobsZ~eXveC+r_TaqEM`wJkVfnMQzmY?LBW&ba!tk2)d#;3 zMEdgX^fa-7WjLDCRO#@de?Zv>Uw!|;o571;yb|Su4+DSDW_Rh$Z%Fjaf}RD`5q}4B zfPJygWd<$Ih2-5#AsKov518Yt5sRIX${vadq#|UoVNqANI0smLSJG8BNT>Lv7)bRN zS0x^MaOnrFawJ;KdPT%^*s@2 { @override Widget build(BuildContext context) { - if (_loading) return const Scaffold(body: Center(child: Loader())); + if (_loading) { + return Scaffold( + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Center(child: Loader()), + const SizedBox(height: 10), + TextButton( + child: const Text('Cancel'), + onPressed: () { + _sub?.cancel(); + setState(() => _loading = false); + }, + ), + ], + ), + ); + } final available0 = Options().isAvailableAccount(0); final available1 = Options().isAvailableAccount(1); @@ -229,8 +247,7 @@ class AuthViewState extends State { ), ), Padding( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 20), + padding: const EdgeInsets.symmetric(vertical: 20), child: Text( 'Before connecting another account, you should log out from the first one in the browser.', style: Theme.of(context).textTheme.labelMedium, From 280850ee66da9904db071be3b585ed29eee1a1c0 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Mon, 12 Jun 2023 00:21:07 +0300 Subject: [PATCH 50/55] ios configuration and minor rounding improvements --- ios/Podfile | 2 +- ios/Podfile.lock | 14 ++--- ios/Runner.xcodeproj/project.pbxproj | 1 + lib/modules/collection/collection_list.dart | 4 +- lib/modules/discover/discover_media_grid.dart | 4 +- pubspec.lock | 60 +++++++++++-------- 6 files changed, 49 insertions(+), 36 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 9411102b..313ea4a1 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '10.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 600786bc..5ff5decf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -12,7 +12,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.2): + - sqflite (0.0.3): - Flutter - FMDB (>= 2.7.5) - url_launcher_ios (0.0.1): @@ -25,7 +25,7 @@ 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_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - workmanager (from `.symlinks/plugins/workmanager/ios`) @@ -44,7 +44,7 @@ EXTERNAL SOURCES: flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/ios" + :path: ".symlinks/plugins/path_provider_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: @@ -53,16 +53,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: - app_links: ab4ba54d10a13d45825336bc9707b5eadee81191 + app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 - sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 -PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea +PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 906691fc..7fc8c9a1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -212,6 +212,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( diff --git a/lib/modules/collection/collection_list.dart b/lib/modules/collection/collection_list.dart index 172a098f..0997b803 100644 --- a/lib/modules/collection/collection_list.dart +++ b/lib/modules/collection/collection_list.dart @@ -66,7 +66,9 @@ class _Tile extends StatelessWidget { Hero( tag: entry.mediaId, child: ClipRRect( - borderRadius: Consts.borderRadiusMin, + borderRadius: const BorderRadius.horizontal( + left: Consts.radiusMin, + ), child: Container( width: _TILE_HEIGHT / Consts.coverHtoWRatio, color: Theme.of(context).colorScheme.surfaceVariant, diff --git a/lib/modules/discover/discover_media_grid.dart b/lib/modules/discover/discover_media_grid.dart index c41ff893..01ce4638 100644 --- a/lib/modules/discover/discover_media_grid.dart +++ b/lib/modules/discover/discover_media_grid.dart @@ -58,7 +58,9 @@ class _Tile extends StatelessWidget { Hero( tag: item.id, child: ClipRRect( - borderRadius: Consts.borderRadiusMin, + borderRadius: const BorderRadius.horizontal( + left: Consts.radiusMin, + ), child: Container( width: 120 / Consts.coverHtoWRatio, color: Theme.of(context).colorScheme.surfaceVariant, diff --git a/pubspec.lock b/pubspec.lock index be2559c2..c87fdbab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: args - sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" async: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: csslib - sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" url: "https://pub.dev" source: hosted - version: "0.17.2" + version: "0.17.3" dbus: dependency: transitive description: @@ -178,18 +178,18 @@ packages: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "909bb95de05a2e793503a2437146285a2f600cd0b3f826e26b870a334d8586d7" + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "63235c42de5b6c99846969a27ad0209c401e6b77b0498939813725b5791c107c" + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.0+1" flutter_riverpod: dependency: "direct main" description: @@ -268,10 +268,10 @@ packages: dependency: transitive description: name: fwfh_text_style - sha256: "37806ee0222f79b6e8d4c698c322c897eae6a817258156f40aeece4e588fac60" + sha256: f0883ccb64b7bb3f2a7a091542c2e834fc3e2a6aa54158f46b3c43b55675d8f7 url: "https://pub.dev" source: hosted - version: "2.22.08+1" + version: "2.22.8+3" hive: dependency: "direct main" description: @@ -284,10 +284,10 @@ packages: dependency: transitive description: name: html - sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" url: "https://pub.dev" source: hosted - version: "0.15.3" + version: "0.15.4" http: dependency: "direct main" description: @@ -324,10 +324,18 @@ packages: dependency: transitive description: name: lints - sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" matcher: dependency: transitive description: @@ -396,10 +404,10 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: @@ -412,10 +420,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" pedantic: dependency: transitive description: @@ -489,10 +497,10 @@ packages: dependency: transitive description: name: sqflite - sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00" + sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 url: "https://pub.dev" source: hosted - version: "2.2.8+2" + version: "2.2.8+4" sqflite_common: dependency: transitive description: @@ -585,10 +593,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" + sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51 url: "https://pub.dev" source: hosted - version: "6.0.31" + version: "6.0.35" url_launcher_ios: dependency: transitive description: @@ -625,10 +633,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.17" url_launcher_windows: dependency: transitive description: @@ -657,10 +665,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: "7dacfda1edcca378031db9905ad7d7bd56b29fd1a90b0908b71a52a12c41e36b" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.0.3" workmanager: dependency: "direct main" description: From d7c5f493fd81f8db1664668e9eaf50bd8229d19d Mon Sep 17 00:00:00 2001 From: lotusgate Date: Mon, 12 Jun 2023 18:49:58 +0300 Subject: [PATCH 51/55] ios cleanup and version upgrade --- ios/Runner.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7fc8c9a1..7e670908 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -374,7 +374,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -514,7 +514,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -546,7 +546,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 9b3ce7dbf5a43e074b34a00e031980ef8b9325ed Mon Sep 17 00:00:00 2001 From: lotusgate Date: Mon, 12 Jun 2023 18:52:28 +0300 Subject: [PATCH 52/55] Version fix --- README.md | 12 +++++++++--- lib/common/utils/options.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7ad3cd4e..12fb31f5 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,16 @@ An unofficial AniList app. The iOS .ipa and the android .apk are bundled with each Github release.

-Note: The project is going through a structural overhaul. -

-

Screenshots +Screenshots (Old. New ones coming soon)

+
+
Building for ios + +1. Make an unsigned build by going into the `ios` directory and running `xcodebuild -scheme Runner -workspace Runner.xcworkspace -configuration Release clean archive -archivePath "build/Otraku.xcarchive" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO` +2. In the `build` directory, open package contents and go into `Products/Applications` +3. Copy the `.app` file into a `Payload` folder +4. Compress the `Payload` folder and change the extension to `.ipa` +
\ No newline at end of file diff --git a/lib/common/utils/options.dart b/lib/common/utils/options.dart index 60c4db8d..64a4484f 100644 --- a/lib/common/utils/options.dart +++ b/lib/common/utils/options.dart @@ -8,7 +8,7 @@ import 'package:otraku/common/utils/theming.dart'; import 'package:path_provider/path_provider.dart'; /// Current app version. -const versionCode = '1.2.3'; +const versionCode = '1.2.4'; /// General options keys. enum _OptionKey { diff --git a/pubspec.yaml b/pubspec.yaml index 069b4c87..c127656d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: An unofficial AniList app. publish_to: 'none' -version: 1.2.3+55 +version: 1.2.4+56 environment: sdk: '>=3.0.0 <4.0.0' From 2fa49e95132966442a02e2191a03bc2a001775c6 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Wed, 14 Jun 2023 11:47:19 +0300 Subject: [PATCH 53/55] Bug fixes --- lib/common/utils/background_handler.dart | 2 +- lib/common/widgets/paged_view.dart | 53 ++--- .../activity/activities_providers.dart | 47 +++- lib/modules/activity/activities_view.dart | 27 +-- lib/modules/activity/activity_models.dart | 28 +-- lib/modules/auth/auth_view.dart | 36 ++- lib/modules/edit/edit_buttons.dart | 224 +++++++++--------- lib/modules/feed/feed_view.dart | 2 +- lib/modules/filter/filter_view.dart | 44 ++-- lib/modules/media/media_view.dart | 3 +- 10 files changed, 246 insertions(+), 220 deletions(-) diff --git a/lib/common/utils/background_handler.dart b/lib/common/utils/background_handler.dart index bd2db438..da77b93e 100644 --- a/lib/common/utils/background_handler.dart +++ b/lib/common/utils/background_handler.dart @@ -157,7 +157,7 @@ void _fetch() => Workmanager().executeTask((_, __) async { ), NotificationType.ACTIVITY_REPLY_SUBSCRIBED => _show( notification, - 'New Reply To Subscription', + 'New Reply To Subscribed Activity', '${RouteArg.activity}/${notification.bodyId}', ), NotificationType.ACTIVITY_MENTION => _show( diff --git a/lib/common/widgets/paged_view.dart b/lib/common/widgets/paged_view.dart index 269d35f4..aed759f7 100644 --- a/lib/common/widgets/paged_view.dart +++ b/lib/common/widgets/paged_view.dart @@ -40,38 +40,33 @@ class PagedView extends StatelessWidget { ), ); - bool? hasNext; - final child = ref.watch(provider).unwrapPrevious().when( - loading: () => const SliverFillRemaining( - child: Center(child: Loader()), + return ref.watch(provider).unwrapPrevious().when( + loading: () => const Center(child: Loader()), + error: (_, __) => CustomScrollView( + physics: Consts.physics, + slivers: [ + SliverRefreshControl(onRefresh: onRefresh), + const SliverFillRemaining( + child: Center(child: Text('Failed to load')), + ), + ], ), - error: (_, __) => const SliverFillRemaining( - child: Center(child: Text('Failed to load')), + data: (data) => ConstrainedView( + child: CustomScrollView( + physics: Consts.physics, + controller: scrollCtrl, + slivers: [ + SliverRefreshControl(onRefresh: onRefresh), + data.items.isEmpty + ? const SliverFillRemaining( + child: Center(child: Text('No results')), + ) + : onData(data), + SliverFooter(loading: data.hasNext), + ], + ), ), - data: (data) { - hasNext = data.hasNext; - - if (data.items.isEmpty) { - return const SliverFillRemaining( - child: Center(child: Text('No results')), - ); - } - - return onData(data); - }, ); - - return ConstrainedView( - child: CustomScrollView( - physics: Consts.physics, - controller: hasNext != null ? scrollCtrl : null, - slivers: [ - SliverRefreshControl(onRefresh: onRefresh), - child, - SliverFooter(loading: hasNext ?? false), - ], - ), - ); }, ); } diff --git a/lib/modules/activity/activities_providers.dart b/lib/modules/activity/activities_providers.dart index 03d33645..5e940e69 100644 --- a/lib/modules/activity/activities_providers.dart +++ b/lib/modules/activity/activities_providers.dart @@ -16,15 +16,20 @@ final activitiesProvider = StateNotifierProvider.autoDispose ), ); -final activityFilterProvider = - StateProvider.autoDispose.family( - (ref, userId) => userId == null - ? HomeActivitiesFilter( - ActivityType.values, - Options().feedOnFollowing, - Options().viewerActivitiesInFeed, - ) - : UserActivitiesFilter(ActivityType.values, userId), +final activityFilterProvider = StateNotifierProvider.autoDispose + .family( + (ref, userId) => ActivityFilterNotifier( + userId == null + ? HomeActivityFilter( + Options() + .feedActivityFilters + .map((f) => ActivityType.values[f]) + .toList(), + Options().feedOnFollowing, + Options().viewerActivitiesInFeed, + ) + : UserActivityFilter(ActivityType.values, userId), + ), ); class ActivitiesNotifier extends StateNotifier>> { @@ -37,7 +42,7 @@ class ActivitiesNotifier extends StateNotifier>> { } final int viewerId; - final ActivitiesFilter filter; + final ActivityFilter filter; int _lastCreatedAt = DateTime.now().millisecondsSinceEpoch; Future fetch() async { @@ -47,13 +52,13 @@ class ActivitiesNotifier extends StateNotifier>> { final data = await Api.get(GqlQuery.activities, { 'typeIn': filter.typeIn.map((t) => t.name).toList(), ...switch (filter) { - HomeActivitiesFilter filter => { + HomeActivityFilter filter => { 'isFollowing': filter.onFollowing, if (!filter.withViewerActivities) 'userIdNot': viewerId, if (!filter.onFollowing) 'hasRepliesOrText': true, if (value.items.isNotEmpty) 'createdBefore': _lastCreatedAt, }, - UserActivitiesFilter filter => { + UserActivityFilter filter => { 'userId': filter.userId, 'page': value.next, }, @@ -172,3 +177,21 @@ class ActivitiesNotifier extends StateNotifier>> { } } } + +class ActivityFilterNotifier extends StateNotifier { + ActivityFilterNotifier(super.state) { + addListener((s) { + switch (state) { + case HomeActivityFilter f: + Options().feedActivityFilters = f.typeIn.map((t) => t.index).toList(); + Options().feedOnFollowing = f.onFollowing; + Options().viewerActivitiesInFeed = f.withViewerActivities; + case _: + return; + } + }); + } + + void update(ActivityFilter Function(ActivityFilter) callback) => + state = callback(state); +} diff --git a/lib/modules/activity/activities_view.dart b/lib/modules/activity/activities_view.dart index 4a4bd40c..8079422a 100644 --- a/lib/modules/activity/activities_view.dart +++ b/lib/modules/activity/activities_view.dart @@ -31,7 +31,7 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { 20; switch (filter) { - case HomeActivitiesFilter filter: + case HomeActivityFilter filter: onFollowing = filter.onFollowing; withViewerActivities = filter.withViewerActivities; initialHeight += Consts.tapTargetSize * 1.5; @@ -56,7 +56,7 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { changed = true; }, ), - if (filter is HomeActivitiesFilter) ...[ + if (filter is HomeActivityFilter) ...[ const Divider(), CheckBoxField( title: 'Your Activities', @@ -79,18 +79,17 @@ void showActivityFilterSheet(BuildContext context, WidgetRef ref, int? id) { ), ), ).then((_) { - if (changed) { - ref.read(activityFilterProvider(id).notifier).update( - (s) => switch (s) { - UserActivitiesFilter _ => UserActivitiesFilter(typeIn, s.userId), - HomeActivitiesFilter _ => HomeActivitiesFilter( - typeIn, - onFollowing, - withViewerActivities, - ), - }, - ); - } + if (!changed) return; + ref.read(activityFilterProvider(id).notifier).update( + (s) => switch (s) { + UserActivityFilter _ => UserActivityFilter(typeIn, s.userId), + HomeActivityFilter _ => HomeActivityFilter( + typeIn, + onFollowing, + withViewerActivities, + ), + }, + ); }); } diff --git a/lib/modules/activity/activity_models.dart b/lib/modules/activity/activity_models.dart index 504a1e0a..90f438b2 100644 --- a/lib/modules/activity/activity_models.dart +++ b/lib/modules/activity/activity_models.dart @@ -204,36 +204,20 @@ enum ActivityType { final String text; } -class ActivityFilter { - const ActivityFilter(this.typeIn, this.feedFilter); - - final List typeIn; - - /// Not `null` only for the main feed. - final FeedFilter? feedFilter; -} - -class FeedFilter { - const FeedFilter(this.onFollowing, this.withViewerActivities); - - final bool onFollowing; - final bool withViewerActivities; -} - -sealed class ActivitiesFilter { - const ActivitiesFilter(this.typeIn); +sealed class ActivityFilter { + const ActivityFilter(this.typeIn); final List typeIn; } -class UserActivitiesFilter extends ActivitiesFilter { - const UserActivitiesFilter(super.typeIn, this.userId); +class UserActivityFilter extends ActivityFilter { + const UserActivityFilter(super.typeIn, this.userId); final int userId; } -class HomeActivitiesFilter extends ActivitiesFilter { - const HomeActivitiesFilter( +class HomeActivityFilter extends ActivityFilter { + const HomeActivityFilter( super.typeIn, this.onFollowing, this.withViewerActivities, diff --git a/lib/modules/auth/auth_view.dart b/lib/modules/auth/auth_view.dart index 18a03de7..390c8fb4 100644 --- a/lib/modules/auth/auth_view.dart +++ b/lib/modules/auth/auth_view.dart @@ -24,7 +24,7 @@ class AuthViewState extends State { bool _loading = false; void _verify(int account) { - if (!_loading) setState(() => _loading = true); + setState(() => _loading = true); Api.logIn(account).then((loggedIn) { if (!loggedIn) { @@ -45,7 +45,7 @@ class AuthViewState extends State { setState(() => _loading = true); // Prepare to receive an authentication token. - _sub?.cancel(); + _clearStreamSubscription(); _sub = AppLinks().stringLinkStream.listen((link) async { final start = link.indexOf('=') + 1; final middle = link.indexOf('&'); @@ -79,6 +79,7 @@ class AuthViewState extends State { } await Api.register(account, token, expiration); + _clearStreamSubscription(); _verify(account); }); @@ -91,12 +92,19 @@ class AuthViewState extends State { if (!ok) setState(() => _loading = false); } + void _clearStreamSubscription() { + _sub?.cancel(); + _sub = null; + } + @override void initState() { super.initState(); - if (Options().account == null) return; - _loading = true; - _verify(Options().account!); + if (Options().account != null) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _verify(Options().account!), + ); + } } @override @@ -114,14 +122,16 @@ class AuthViewState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Center(child: Loader()), - const SizedBox(height: 10), - TextButton( - child: const Text('Cancel'), - onPressed: () { - _sub?.cancel(); - setState(() => _loading = false); - }, - ), + if (_sub != null) ...[ + const SizedBox(height: 10), + TextButton( + child: const Text('Cancel'), + onPressed: () { + _clearStreamSubscription(); + setState(() => _loading = false); + }, + ), + ], ], ), ); diff --git a/lib/modules/edit/edit_buttons.dart b/lib/modules/edit/edit_buttons.dart index 98774a24..e910c064 100644 --- a/lib/modules/edit/edit_buttons.dart +++ b/lib/modules/edit/edit_buttons.dart @@ -30,116 +30,124 @@ class _EditButtonsState extends State { @override Widget build(BuildContext context) { return Consumer( - builder: (context, ref, __) => BottomBar([ - _loading + builder: (context, ref, __) { + final saveButton = _loading ? const Expanded(child: Center(child: Loader())) - : BottomBarButton( - text: 'Save', - icon: Ionicons.save_outline, - onTap: () async { - final oldEdit = widget.oldEdit; - final newEdit = ref.read(newEditProvider(widget.tag)); - setState(() => _loading = true); - - final entry = await updateEntry(newEdit, Options().id!); - - if (entry is! Entry) { - if (mounted) { - showPopUp( - context, - ConfirmationDialog( - title: 'Could not update entry', - content: entry.toString(), - ), - ); - } - return; - } - - final ofAnime = newEdit.type == 'ANIME'; - final tag = (userId: Options().id!, ofAnime: ofAnime); - - if (ref.read(homeProvider).didExpandCollection(ofAnime)) { - await ref.read(collectionProvider(tag)).updateEntry( - entry, - oldEdit, - newEdit, - ref.read(collectionFilterProvider(tag)).sort, - ); - } else if (newEdit.status == EntryStatus.CURRENT || - newEdit.status == EntryStatus.REPEATING) { - if (oldEdit.status == EntryStatus.CURRENT || - oldEdit.status == EntryStatus.REPEATING) { - ref.read(collectionPreviewProvider(tag)).update(entry); - } else { - ref.read(collectionPreviewProvider(tag)).add(entry); - } - } else if (oldEdit.status == EntryStatus.CURRENT || - oldEdit.status == EntryStatus.REPEATING) { - ref - .read(collectionPreviewProvider(tag)) - .remove(entry.mediaId); - } - - widget.callback?.call(newEdit); - if (mounted) { - Navigator.pop(context); - } - }, - ), - widget.oldEdit.entryId == null + : _saveButton(context, ref); + final removeButton = widget.oldEdit.entryId == null ? const Spacer() - : BottomBarButton( - text: 'Remove', - icon: Ionicons.trash_bin_outline, - warning: true, - onTap: () => showPopUp( - context, - ConfirmationDialog( - title: 'Remove entry?', - mainAction: 'Yes', - secondaryAction: 'No', - onConfirm: () async { - setState(() => _loading = true); - - final oldEdit = widget.oldEdit; - final err = await removeEntry(oldEdit.entryId!); - - if (mounted) { - Navigator.pop(context); - - if (err != null) { - showPopUp( - context, - ConfirmationDialog( - title: 'Could not remove entry', - content: err.toString(), - ), - ); - return; - } - } else { - if (err != null) return; - } - - final ofAnime = oldEdit.type == 'ANIME'; - final tag = (userId: Options().id!, ofAnime: ofAnime); - - if (ref.read(homeProvider).didExpandCollection(ofAnime)) { - ref.read(collectionProvider(tag)).removeEntry(oldEdit); - } else if (oldEdit.status == EntryStatus.CURRENT || - oldEdit.status == EntryStatus.REPEATING) { - ref - .read(collectionPreviewProvider(tag)) - .remove(oldEdit.mediaId); - } - - widget.callback?.call(oldEdit.emptyCopy()); - }, - ), - ), - ), - ]), + : _removeButton(context, ref); + + return BottomBar( + Options().leftHanded + ? [saveButton, removeButton] + : [removeButton, saveButton], + ); + }, ); } + + Widget _saveButton(BuildContext context, WidgetRef ref) => BottomBarButton( + text: 'Save', + icon: Ionicons.save_outline, + onTap: () async { + final oldEdit = widget.oldEdit; + final newEdit = ref.read(newEditProvider(widget.tag)); + setState(() => _loading = true); + + final entry = await updateEntry(newEdit, Options().id!); + + if (entry is! Entry) { + if (mounted) { + showPopUp( + context, + ConfirmationDialog( + title: 'Could not update entry', + content: entry.toString(), + ), + ); + } + return; + } + + final ofAnime = newEdit.type == 'ANIME'; + final tag = (userId: Options().id!, ofAnime: ofAnime); + + if (ref.read(homeProvider).didExpandCollection(ofAnime)) { + await ref.read(collectionProvider(tag)).updateEntry( + entry, + oldEdit, + newEdit, + ref.read(collectionFilterProvider(tag)).sort, + ); + } else if (newEdit.status == EntryStatus.CURRENT || + newEdit.status == EntryStatus.REPEATING) { + if (oldEdit.status == EntryStatus.CURRENT || + oldEdit.status == EntryStatus.REPEATING) { + ref.read(collectionPreviewProvider(tag)).update(entry); + } else { + ref.read(collectionPreviewProvider(tag)).add(entry); + } + } else if (oldEdit.status == EntryStatus.CURRENT || + oldEdit.status == EntryStatus.REPEATING) { + ref.read(collectionPreviewProvider(tag)).remove(entry.mediaId); + } + + widget.callback?.call(newEdit); + if (mounted) { + Navigator.pop(context); + } + }, + ); + + Widget _removeButton(BuildContext context, WidgetRef ref) => BottomBarButton( + text: 'Remove', + icon: Ionicons.trash_bin_outline, + warning: true, + onTap: () => showPopUp( + context, + ConfirmationDialog( + title: 'Remove entry?', + mainAction: 'Yes', + secondaryAction: 'No', + onConfirm: () async { + setState(() => _loading = true); + + final oldEdit = widget.oldEdit; + final err = await removeEntry(oldEdit.entryId!); + + if (mounted) { + Navigator.pop(context); + + if (err != null) { + showPopUp( + context, + ConfirmationDialog( + title: 'Could not remove entry', + content: err.toString(), + ), + ); + return; + } + } else { + if (err != null) return; + } + + final ofAnime = oldEdit.type == 'ANIME'; + final tag = (userId: Options().id!, ofAnime: ofAnime); + + if (ref.read(homeProvider).didExpandCollection(ofAnime)) { + ref.read(collectionProvider(tag)).removeEntry(oldEdit); + } else if (oldEdit.status == EntryStatus.CURRENT || + oldEdit.status == EntryStatus.REPEATING) { + ref + .read(collectionPreviewProvider(tag)) + .remove(oldEdit.mediaId); + } + + widget.callback?.call(oldEdit.emptyCopy()); + }, + ), + ), + ); } diff --git a/lib/modules/feed/feed_view.dart b/lib/modules/feed/feed_view.dart index 566d4b4a..7fb4e55b 100644 --- a/lib/modules/feed/feed_view.dart +++ b/lib/modules/feed/feed_view.dart @@ -42,7 +42,7 @@ class FeedView extends StatelessWidget { if (count > 0) { result = Badge.count( count: count, - alignment: AlignmentDirectional.centerStart, + alignment: AlignmentDirectional.topStart, child: result, ); } diff --git a/lib/modules/filter/filter_view.dart b/lib/modules/filter/filter_view.dart index 20b52c29..2b5c1424 100644 --- a/lib/modules/filter/filter_view.dart +++ b/lib/modules/filter/filter_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:otraku/common/utils/options.dart'; import 'package:otraku/modules/filter/chip_selector.dart'; import 'package:otraku/modules/filter/filter_models.dart'; import 'package:otraku/modules/filter/year_range_picker.dart'; @@ -36,26 +37,31 @@ class __FilterViewState> @override Widget build(BuildContext context) { + final applyButton = BottomBarButton( + text: 'Apply', + icon: Icons.done_rounded, + onTap: () { + widget.onChanged(_filter); + Navigator.pop(context); + }, + ); + + final clearButton = BottomBarButton( + text: 'Clear', + icon: Icons.close, + warning: true, + onTap: () { + widget.onChanged(_filter.clear()); + Navigator.pop(context); + }, + ); + return OpaqueSheetView( - buttons: BottomBar([ - BottomBarButton( - text: 'Apply', - icon: Icons.done_rounded, - onTap: () { - widget.onChanged(_filter); - Navigator.pop(context); - }, - ), - BottomBarButton( - text: 'Clear', - icon: Icons.close, - warning: true, - onTap: () { - widget.onChanged(_filter.clear()); - Navigator.pop(context); - }, - ), - ]), + buttons: BottomBar( + Options().leftHanded + ? [applyButton, clearButton] + : [clearButton, applyButton], + ), builder: (context, scrollCtrl) => widget.builder(context, scrollCtrl, _filter), ); diff --git a/lib/modules/media/media_view.dart b/lib/modules/media/media_view.dart index b9ac21b1..fadba82b 100644 --- a/lib/modules/media/media_view.dart +++ b/lib/modules/media/media_view.dart @@ -148,7 +148,7 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { // doesn't fill the view, the scroll controller has it's maximum // extent set to 0 and the loading of a next page of items is not triggered. // This is why we need to manually load the second page. - if (!widget.tabCtrl.indexIsChanging) { + if (!widget.tabCtrl.indexIsChanging && _scrollCtrl.hasClients) { final pos = _scrollCtrl.positions.last; if (pos.minScrollExtent == pos.maxScrollExtent) _loadNextPage(); } @@ -176,6 +176,7 @@ class __MediaSubViewState extends ConsumerState<_MediaViewContent> { void _refresh(WidgetRef ref) { if (widget.tabCtrl.index == MediaTab.following.index) { ref.invalidate(mediaFollowingProvider(widget.id)); + ref.read(mediaFollowingProvider(widget.id).notifier).lazyLoad(); } else { ref.invalidate(mediaRelationsProvider(widget.id)); } From d4e858a1c2bc10cb26e9332efdbff67cdca1d962 Mon Sep 17 00:00:00 2001 From: lotusgate Date: Wed, 14 Jun 2023 11:49:52 +0300 Subject: [PATCH 54/55] Versioning --- android/app/build.gradle | 12 ++++++------ lib/common/utils/options.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c1797e52..7d41605f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -68,12 +68,12 @@ android { } } - flavorDimensions "default" - productFlavors { - dev { - applicationIdSuffix ".dev" - } - } + // flavorDimensions "default" + // productFlavors { + // dev { + // applicationIdSuffix ".dev" + // } + // } } flutter { diff --git a/lib/common/utils/options.dart b/lib/common/utils/options.dart index 64a4484f..231a68cd 100644 --- a/lib/common/utils/options.dart +++ b/lib/common/utils/options.dart @@ -8,7 +8,7 @@ import 'package:otraku/common/utils/theming.dart'; import 'package:path_provider/path_provider.dart'; /// Current app version. -const versionCode = '1.2.4'; +const versionCode = '1.2.4+1'; /// General options keys. enum _OptionKey { diff --git a/pubspec.yaml b/pubspec.yaml index c127656d..015be3b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: An unofficial AniList app. publish_to: 'none' -version: 1.2.4+56 +version: 1.2.4+57 environment: sdk: '>=3.0.0 <4.0.0' From df8993ed9808dbd74edc7d9d1e6872f19b8b693d Mon Sep 17 00:00:00 2001 From: lotusgate Date: Wed, 14 Jun 2023 11:58:23 +0300 Subject: [PATCH 55/55] Flavors fix --- android/app/build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7d41605f..c1797e52 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -68,12 +68,12 @@ android { } } - // flavorDimensions "default" - // productFlavors { - // dev { - // applicationIdSuffix ".dev" - // } - // } + flavorDimensions "default" + productFlavors { + dev { + applicationIdSuffix ".dev" + } + } } flutter {