From 1c091e144d6da25ef43b3a107c87adc4af56b03f Mon Sep 17 00:00:00 2001 From: iota9star Date: Sun, 17 Sep 2023 13:03:25 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=96=20v1.2.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-GP.yml | 2 +- lib/main.dart | 41 +- lib/mikan_route.dart | 48 --- lib/mikan_routes.dart | 84 ---- lib/model/record_details.dart | 12 +- lib/providers/record_detail_model.dart | 21 +- lib/ui/components/list_record_item.dart | 301 ++++++++------ lib/ui/components/rss_record_item.dart | 194 +++++---- lib/ui/components/simple_record_item.dart | 136 ++++--- lib/ui/fragments/index.dart | 92 +++-- lib/ui/fragments/list.dart | 22 +- lib/ui/fragments/sliver_bangumi_list.dart | 471 +++++++++++----------- lib/ui/fragments/subscribed.dart | 120 +++--- lib/ui/fragments/theme_color.dart | 13 +- lib/ui/pages/bangumi.dart | 79 ++-- lib/ui/pages/home.dart | 13 +- lib/ui/pages/recent_subscribed.dart | 9 - lib/ui/pages/record.dart | 261 ++++++------ lib/ui/pages/search.dart | 115 +++--- lib/widget/bottom_sheet.dart | 2 +- lib/widget/particle.dart | 128 +++--- lib/widget/scalable_tap.dart | 11 +- lib/widget/transition_container.dart | 41 ++ pubspec.yaml | 15 +- 24 files changed, 1109 insertions(+), 1122 deletions(-) create mode 100644 lib/widget/transition_container.dart diff --git a/.github/workflows/build-GP.yml b/.github/workflows/build-GP.yml index 26e3c7d..7f73396 100644 --- a/.github/workflows/build-GP.yml +++ b/.github/workflows/build-GP.yml @@ -43,7 +43,7 @@ jobs: keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} - name: Release to GP - uses: r0adkll/upload-google-play@v1.0.18 + uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} packageName: io.nichijou.flutter.mikan diff --git a/lib/main.dart b/lib/main.dart index 642dfb2..eebe13f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -178,13 +178,7 @@ class _MikanAppState extends State { ), ]; return MaterialApp( - scrollBehavior: const ScrollBehavior().copyWith( - dragDevices: PointerDeviceKind.values.toSet(), - overscroll: true, - platform: TargetPlatform.iOS, - physics: const BouncingScrollPhysics(), - scrollbars: false, - ), + scrollBehavior: const AlwaysStretchScrollBehavior(), themeMode: mode, theme: ThemeData( useMaterial3: true, @@ -303,3 +297,36 @@ class _ThemeProviderState extends LifecycleAppState { ); } } + +class AlwaysStretchScrollBehavior extends ScrollBehavior { + const AlwaysStretchScrollBehavior() + : super(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch); + + @override + Set get dragDevices => PointerDeviceKind.values.toSet(); + + @override + TargetPlatform getPlatform(BuildContext context) => TargetPlatform.android; + + @override + Widget buildScrollbar( + BuildContext context, + Widget child, + ScrollableDetails details, + ) { + return child; + } + + @override + Widget buildOverscrollIndicator( + BuildContext context, + Widget child, + ScrollableDetails details, + ) { + return StretchingOverscrollIndicator( + axisDirection: details.direction, + clipBehavior: details.decorationClipBehavior ?? Clip.hardEdge, + child: child, + ); + } +} diff --git a/lib/mikan_route.dart b/lib/mikan_route.dart index 9011fb1..0df4e84 100644 --- a/lib/mikan_route.dart +++ b/lib/mikan_route.dart @@ -16,7 +16,6 @@ import '../../model/season_gallery.dart'; import '../../model/subgroup.dart'; import '../../model/year_season.dart'; import 'ui/pages/announcement.dart'; -import 'ui/pages/bangumi.dart'; import 'ui/pages/fonts.dart'; import 'ui/pages/forgot_password.dart'; import 'ui/pages/home.dart'; @@ -24,9 +23,7 @@ import 'ui/pages/license.dart'; import 'ui/pages/license_detail.dart'; import 'ui/pages/login.dart'; import 'ui/pages/recent_subscribed.dart'; -import 'ui/pages/record.dart'; import 'ui/pages/register.dart'; -import 'ui/pages/search.dart'; import 'ui/pages/season_bangumi.dart'; import 'ui/pages/single_season.dart'; import 'ui/pages/splash.dart'; @@ -52,28 +49,6 @@ FFRouteSettings getRouteSettings({ ), ), ); - case '/bangumi': - return FFRouteSettings( - name: name, - arguments: arguments, - builder: () => BangumiPage( - key: asT( - safeArguments['key'], - ), - bangumiId: asT( - safeArguments['bangumiId'], - )!, - cover: asT( - safeArguments['cover'], - )!, - heroTag: asT( - safeArguments['heroTag'], - )!, - title: asT( - safeArguments['title'], - ), - ), - ); case '/bangumi/season': return FFRouteSettings( name: name, @@ -153,19 +128,6 @@ FFRouteSettings getRouteSettings({ ), ), ); - case '/record': - return FFRouteSettings( - name: name, - arguments: arguments, - builder: () => Record( - key: asT( - safeArguments['key'], - ), - url: asT( - safeArguments['url'], - )!, - ), - ); case '/register': return FFRouteSettings( name: name, @@ -176,16 +138,6 @@ FFRouteSettings getRouteSettings({ ), ), ); - case '/search': - return FFRouteSettings( - name: name, - arguments: arguments, - builder: () => Search( - key: asT( - safeArguments['key'], - ), - ), - ); case '/season': return FFRouteSettings( name: name, diff --git a/lib/mikan_routes.dart b/lib/mikan_routes.dart index 2ad5ce6..e2ffebb 100644 --- a/lib/mikan_routes.dart +++ b/lib/mikan_routes.dart @@ -17,7 +17,6 @@ import '../../model/year_season.dart'; /// The routeNames auto generated by https://github.com/fluttercandies/ff_annotation_route const List routeNames = [ '/announcements', - '/bangumi', '/bangumi/season', '/fonts', '/forget-password', @@ -25,9 +24,7 @@ const List routeNames = [ '/license', '/license/detail', '/login', - '/record', '/register', - '/search', '/season', '/splash', '/subgroup', @@ -44,15 +41,6 @@ class Routes { /// [name] : '/announcements' static const _Announcements announcements = _Announcements(); - /// '/bangumi' - /// - /// [name] : '/bangumi' - /// - /// [constructors] : - /// - /// BangumiPage : [String(required) bangumiId, String(required) cover, String(required) heroTag, String? title] - static const _Bangumi bangumi = _Bangumi(); - /// '/bangumi/season' /// /// [name] : '/bangumi/season' @@ -96,25 +84,11 @@ class Routes { /// [name] : '/login' static const _Login login = _Login(); - /// '/record' - /// - /// [name] : '/record' - /// - /// [constructors] : - /// - /// Record : [String(required) url] - static const _Record record = _Record(); - /// '/register' /// /// [name] : '/register' static const _Register register = _Register(); - /// '/search' - /// - /// [name] : '/search' - static const _Search search = _Search(); - /// '/season' /// /// [name] : '/season' @@ -173,30 +147,6 @@ class _Announcements { String toString() => name; } -class _Bangumi { - const _Bangumi(); - - String get name => '/bangumi'; - - Map d({ - Key? key, - required String bangumiId, - required String cover, - required String heroTag, - String? title, - }) => - { - 'key': key, - 'bangumiId': bangumiId, - 'cover': cover, - 'heroTag': heroTag, - 'title': title, - }; - - @override - String toString() => name; -} - class _BangumiSeason { const _BangumiSeason(); @@ -315,24 +265,6 @@ class _Login { String toString() => name; } -class _Record { - const _Record(); - - String get name => '/record'; - - Map d({ - Key? key, - required String url, - }) => - { - 'key': key, - 'url': url, - }; - - @override - String toString() => name; -} - class _Register { const _Register(); @@ -349,22 +281,6 @@ class _Register { String toString() => name; } -class _Search { - const _Search(); - - String get name => '/search'; - - Map d({ - Key? key, - }) => - { - 'key': key, - }; - - @override - String toString() => name; -} - class _Season { const _Season(); diff --git a/lib/model/record_details.dart b/lib/model/record_details.dart index 44e4010..6ba4c4a 100644 --- a/lib/model/record_details.dart +++ b/lib/model/record_details.dart @@ -3,13 +3,13 @@ import '../internal/extension.dart'; import 'subgroup.dart'; class RecordDetail { - late String id; - late String cover; + String? id; + String cover = ''; late String name; - late bool subscribed; - late Map more; - late String intro; - late List subgroups = []; + bool subscribed = false; + Map more = {}; + String intro = ''; + List subgroups = []; // 详情地址 late String url = ''; diff --git a/lib/providers/record_detail_model.dart b/lib/providers/record_detail_model.dart index 9831e2b..5379f21 100644 --- a/lib/providers/record_detail_model.dart +++ b/lib/providers/record_detail_model.dart @@ -3,19 +3,30 @@ import 'package:easy_refresh/easy_refresh.dart'; import '../internal/extension.dart'; import '../internal/repo.dart'; import '../model/record_details.dart'; +import '../model/record_item.dart'; import 'base_model.dart'; class RecordDetailModel extends BaseModel { - RecordDetailModel(this.url); + RecordDetailModel(this.record) + : _recordDetail = RecordDetail() + ..name = record.name + ..url = record.url + ..title = record.title + ..subgroups = record.groups + ..id = record.id + ..cover = record.cover + ..tags = record.tags + ..torrent = record.torrent + ..magnet = record.magnet; - final String url; + final RecordItem record; - RecordDetail? _recordDetail; + RecordDetail _recordDetail; - RecordDetail? get recordDetail => _recordDetail; + RecordDetail get recordDetail => _recordDetail; Future refresh() async { - final resp = await Repo.details(url); + final resp = await Repo.details(record.url); if (resp.success) { _recordDetail = resp.data; '加载完成'.toast(); diff --git a/lib/ui/components/list_record_item.dart b/lib/ui/components/list_record_item.dart index b1ed34b..fb6e4b6 100644 --- a/lib/ui/components/list_record_item.dart +++ b/lib/ui/components/list_record_item.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; import '../../internal/extension.dart'; import '../../model/record_item.dart'; import '../../topvars.dart'; +import '../../widget/bottom_sheet.dart'; import '../../widget/icon_button.dart'; import '../../widget/ripple_tap.dart'; -import '../../widget/scalable_tap.dart'; -import '../fragments/subgroup_bangumis.dart'; +import '../../widget/transition_container.dart'; +import '../fragments/select_subgroup.dart'; +import '../pages/record.dart'; +import '../pages/subgroup.dart'; @immutable class ListRecordItem extends StatelessWidget { @@ -15,155 +18,191 @@ class ListRecordItem extends StatelessWidget { super.key, required this.index, required this.record, - required this.onTap, }); final int index; final RecordItem record; - final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final subgroups = record.groups; final subgroupsName = subgroups.map((e) => e.name).join('/'); - return ScalableCard( - onTap: onTap, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 8.0, - right: 8.0, - top: 8.0, - bottom: 4.0, - ), - child: Row( - children: [ - Expanded( - child: Tooltip( - message: subgroupsName, - child: RippleTap( - borderRadius: borderRadiusCircle, - onTap: () { - showSelectSubgroupPanel(context, subgroups); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - shape: BoxShape.circle, - ), - alignment: AlignmentDirectional.center, - padding: const EdgeInsets.symmetric( - horizontal: 2.0, - ), - child: AutoSizeText( - subgroups - .map((e) => e.name[0].toUpperCase()) - .join(), - style: TextStyle( - fontWeight: FontWeight.w700, - color: theme.colorScheme.onPrimaryContainer, + final closedColor = ElevationOverlay.applySurfaceTint( + theme.cardColor, + theme.colorScheme.surfaceTint, + 1.0, + ); + return TransitionContainer( + closedColor: closedColor, + builder: (context, open) { + return RippleTap( + onTap: open, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + top: 8.0, + bottom: 4.0, + ), + child: Row( + children: [ + Expanded( + child: Tooltip( + message: subgroupsName, + child: TransitionContainer( + closedColor: closedColor, + builder: (context, open) { + return RippleTap( + borderRadius: borderRadius12, + onTap: () { + if (subgroups.length == 1) { + final subgroup = subgroups[0]; + if (subgroup.id == null) { + '无字幕组详情'.toast(); + return; + } + open(); + } else { + MBottomSheet.show( + context, + (context) => MBottomSheet( + child: SelectSubgroup( + subgroups: subgroups, + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + color: + theme.colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + alignment: AlignmentDirectional.center, + padding: const EdgeInsets.symmetric( + horizontal: 2.0, + ), + child: AutoSizeText( + subgroups + .map( + (e) => e.name[0].toUpperCase(), + ) + .join(), + style: TextStyle( + fontWeight: FontWeight.w700, + color: theme + .colorScheme.onPrimaryContainer, + ), + minFontSize: 8.0, + ), + ), + sizedBoxW8, + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + subgroupsName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall, + ), + Text( + record.publishAt, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ), + ], + ), + ), + ], ), - minFontSize: 8.0, ), - ), - sizedBoxW8, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - subgroupsName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleSmall, - ), - Text( - record.publishAt, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, - ), - ], - ), - ), - ], + ); + }, + next: SubgroupPage(subgroup: subgroups.first), ), ), ), - ), + sizedBoxW8, + TMSMenuButton( + torrent: record.torrent, + magnet: record.magnet, + share: record.share, + ), + ], ), - sizedBoxW8, - TMSMenuButton( - torrent: record.torrent, - magnet: record.magnet, - share: record.share, + ), + Padding( + padding: edgeH16, + child: Text( + record.title, + style: theme.textTheme.bodySmall, ), - ], - ), - ), - Padding( - padding: edgeH16, - child: Text( - record.title, - style: theme.textTheme.bodySmall, - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 16.0, - right: 16.0, - top: 8.0, - bottom: 20.0, - ), - child: Wrap( - runSpacing: 6.0, - spacing: 6.0, - children: [ - if (record.size.isNotBlank) - Container( - padding: edgeH6V4, - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, - borderRadius: borderRadius8, - ), - child: Text( - record.size, - style: theme.textTheme.labelSmall!.copyWith( - color: theme.colorScheme.onSecondaryContainer, - ), - ), - ), - if (!record.tags.isNullOrEmpty) - ...List.generate(record.tags.length, (index) { - return Container( - padding: edgeH6V4, - decoration: BoxDecoration( - color: theme.colorScheme.tertiaryContainer, - borderRadius: borderRadius8, - ), - child: Text( - record.tags[index], - style: theme.textTheme.labelSmall!.copyWith( - color: theme.colorScheme.onTertiaryContainer, + ), + Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 8.0, + bottom: 20.0, + ), + child: Wrap( + runSpacing: 6.0, + spacing: 6.0, + children: [ + if (record.size.isNotBlank) + Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: borderRadius8, + ), + child: Text( + record.size, + style: theme.textTheme.labelSmall!.copyWith( + color: theme.colorScheme.onSecondaryContainer, + ), ), ), - ); - }), - ], - ), + if (!record.tags.isNullOrEmpty) + ...List.generate(record.tags.length, (index) { + return Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer, + borderRadius: borderRadius8, + ), + child: Text( + record.tags[index], + style: theme.textTheme.labelSmall!.copyWith( + color: theme.colorScheme.onTertiaryContainer, + ), + ), + ); + }), + ], + ), + ), + ], ), - ], - ), + ); + }, + next: RecordPage(record: record), ); } } diff --git a/lib/ui/components/rss_record_item.dart b/lib/ui/components/rss_record_item.dart index 9b27a72..f80e2b5 100644 --- a/lib/ui/components/rss_record_item.dart +++ b/lib/ui/components/rss_record_item.dart @@ -2,12 +2,13 @@ import 'package:flutter/material.dart'; import '../../internal/extension.dart'; import '../../internal/image_provider.dart'; -import '../../mikan_routes.dart'; import '../../model/record_item.dart'; import '../../topvars.dart'; import '../../widget/icon_button.dart'; import '../../widget/ripple_tap.dart'; -import '../../widget/scalable_tap.dart'; +import '../../widget/transition_container.dart'; +import '../pages/bangumi.dart'; +import '../pages/record.dart'; @immutable class RssRecordItem extends StatelessWidget { @@ -15,20 +16,15 @@ class RssRecordItem extends StatelessWidget { super.key, required this.index, required this.record, - required this.onTap, - this.enableHero = true, }); final int index; final RecordItem record; - final VoidCallback onTap; - final bool enableHero; @override Widget build(BuildContext context) { final theme = Theme.of(context); final tags = record.tags; - final heroTag = 'rss:${record.id}:${record.cover}:${record.torrent}'; final cover = Container( decoration: BoxDecoration( image: DecorationImage( @@ -46,110 +42,106 @@ class RssRecordItem extends StatelessWidget { final sizeStyle = theme.textTheme.labelSmall!.copyWith( color: theme.colorScheme.onSecondaryContainer, ); - return ScalableCard( - onTap: onTap, - child: Stack( - children: [ - Positioned.fill( - child: enableHero - ? Hero( - tag: heroTag, - child: cover, - ) - : cover, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + return TransitionContainer( + builder: (context, open) { + return RippleTap( + onTap: open, + child: Stack( children: [ - RippleTap( - borderRadius: borderRadius12, - onTap: () { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( + Positioned.fill(child: cover), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + TransitionContainer( + builder: (context, open) { + return RippleTap( + borderRadius: borderRadius12, + onTap: open, + child: Padding( + padding: edgeH16V12, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Tooltip( + message: record.name, + child: Text( + record.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium, + ), + ), + Text( + record.publishAt, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ), + ], + ), + ), + TMSMenuButton( + torrent: record.torrent, + magnet: record.magnet, + share: record.share, + ), + ], + ), + ), + ); + }, + next: BangumiPage( bangumiId: record.id!, cover: record.cover, - heroTag: heroTag, - title: record.name, + name: record.name, ), - ); - }, - child: Padding( - padding: edgeH16V12, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Tooltip( - message: record.name, - child: Text( - record.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium, - ), + ), + Padding( + padding: edgeH16, + child: Text( + record.title, + style: theme.textTheme.bodySmall, + ), + ), + Container( + padding: edgeHB16T8, + child: Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: [ + if (record.size.isNotBlank) + Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: borderRadius8, ), - Text( - record.publishAt, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, + child: Text(record.size, style: sizeStyle), + ), + if (!tags.isNullOrEmpty) + for (final tag in tags) + Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer, + borderRadius: borderRadius8, + ), + child: Text(tag, style: tagStyle), ), - ], - ), - ), - TMSMenuButton( - torrent: record.torrent, - magnet: record.magnet, - share: record.share, - ), - ], + ], + ), ), - ), - ), - Padding( - padding: edgeH16, - child: Text( - record.title, - style: theme.textTheme.bodySmall, - ), - ), - Container( - padding: edgeHB16T8, - child: Wrap( - spacing: 6.0, - runSpacing: 6.0, - children: [ - if (record.size.isNotBlank) - Container( - padding: edgeH6V4, - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, - borderRadius: borderRadius8, - ), - child: Text(record.size, style: sizeStyle), - ), - if (!tags.isNullOrEmpty) - for (final tag in tags) - Container( - padding: edgeH6V4, - decoration: BoxDecoration( - color: theme.colorScheme.tertiaryContainer, - borderRadius: borderRadius8, - ), - child: Text(tag, style: tagStyle), - ), - ], - ), + ], ), ], ), - ], - ), + ); + }, + next: RecordPage(record: record), ); } } diff --git a/lib/ui/components/simple_record_item.dart b/lib/ui/components/simple_record_item.dart index 5f030db..5582cba 100644 --- a/lib/ui/components/simple_record_item.dart +++ b/lib/ui/components/simple_record_item.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import '../../internal/extension.dart'; -import '../../mikan_routes.dart'; import '../../model/record_item.dart'; import '../../topvars.dart'; import '../../widget/icon_button.dart'; -import '../../widget/scalable_tap.dart'; +import '../../widget/ripple_tap.dart'; +import '../../widget/transition_container.dart'; +import '../pages/record.dart'; @immutable class SimpleRecordItem extends StatelessWidget { @@ -25,75 +26,80 @@ class SimpleRecordItem extends StatelessWidget { final sizeStyle = theme.textTheme.labelSmall!.copyWith( color: theme.colorScheme.onSecondaryContainer, ); - return ScalableCard( - onTap: () { - Navigator.pushNamed( - context, - Routes.record.name, - arguments: Routes.record.d(url: record.url), - ); - }, - child: Padding( - padding: const EdgeInsets.only( - left: 16.0, - right: 16.0, - top: 16.0, - bottom: 4.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - record.title, - style: theme.textTheme.bodyMedium, - ), - sizedBoxH8, - Wrap( - runSpacing: 6.0, - spacing: 6.0, - children: [ - if (record.size.isNotBlank) - Container( - padding: edgeH6V4, - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, - borderRadius: borderRadius8, - ), - child: Text(record.size, style: sizeStyle), - ), - if (!record.tags.isNullOrEmpty) - for (final tag in record.tags) - Container( - padding: edgeH6V4, - decoration: BoxDecoration( - color: theme.colorScheme.tertiaryContainer, - borderRadius: borderRadius8, - ), - child: Text(tag, style: tagStyle), - ), - ], + final closedColor = ElevationOverlay.applySurfaceTint( + theme.cardColor, + theme.colorScheme.surfaceTint, + 1.0, + ); + return TransitionContainer( + closedColor: closedColor, + builder: (context, open) { + return RippleTap( + onTap: open, + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 16.0, + bottom: 4.0, ), - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text( - record.publishAt, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, - ), + Text( + record.title, + style: theme.textTheme.bodyMedium, ), - sizedBoxW8, - TMSMenuButton( - torrent: record.torrent, - magnet: record.magnet, - share: record.share, + sizedBoxH8, + Wrap( + runSpacing: 6.0, + spacing: 6.0, + children: [ + if (record.size.isNotBlank) + Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: borderRadius8, + ), + child: Text(record.size, style: sizeStyle), + ), + if (!record.tags.isNullOrEmpty) + for (final tag in record.tags) + Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer, + borderRadius: borderRadius8, + ), + child: Text(tag, style: tagStyle), + ), + ], + ), + Row( + children: [ + Expanded( + child: Text( + record.publishAt, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ), + ), + sizedBoxW8, + TMSMenuButton( + torrent: record.torrent, + magnet: record.magnet, + share: record.share, + ), + ], ), ], ), - ], - ), - ), + ), + ); + }, + next: RecordPage(record: record), ); } } diff --git a/lib/ui/fragments/index.dart b/lib/ui/fragments/index.dart index 51023b4..cb4b255 100644 --- a/lib/ui/fragments/index.dart +++ b/lib/ui/fragments/index.dart @@ -11,7 +11,7 @@ import 'package:waterfall_flow/waterfall_flow.dart'; import '../../internal/extension.dart'; import '../../internal/image_provider.dart'; import '../../internal/kit.dart'; -import '../../mikan_routes.dart'; +import '../../internal/lifecycle.dart'; import '../../model/bangumi_row.dart'; import '../../model/carousel.dart'; import '../../model/record_item.dart'; @@ -22,9 +22,11 @@ import '../../providers/op_model.dart'; import '../../topvars.dart'; import '../../widget/bottom_sheet.dart'; import '../../widget/ripple_tap.dart'; -import '../../widget/scalable_tap.dart'; import '../../widget/sliver_pinned_header.dart'; +import '../../widget/transition_container.dart'; import '../components/simple_record_item.dart'; +import '../pages/bangumi.dart'; +import '../pages/search.dart'; import 'select_season.dart'; import 'select_tablet_mode.dart'; import 'settings.dart'; @@ -37,7 +39,7 @@ class IndexFragment extends StatefulWidget { State createState() => _IndexFragmentState(); } -class _IndexFragmentState extends State { +class _IndexFragmentState extends LifecycleState { final _infiniteScrollController = InfiniteScrollController(); Timer? _timer; @@ -45,6 +47,21 @@ class _IndexFragmentState extends State { @override void initState() { super.initState(); + _newTimer(); + } + + @override + void onPause() { + _timer?.cancel(); + } + + @override + void onResume() { + _newTimer(); + } + + void _newTimer() { + _timer?.cancel(); _timer = Timer.periodic(const Duration(milliseconds: 3600), (timer) { _infiniteScrollController.animateToItem( (_infiniteScrollController.offset / 300.0).round() + 1, @@ -184,8 +201,6 @@ class _IndexFragmentState extends State { child: InfiniteCarousel.builder( itemBuilder: (context, index, realIndex) { final carousel = carousels[index]; - final String currFlag = - 'carousel:$realIndex:${carousel.id}:${carousel.cover}'; final currentOffset = 300.0 * realIndex; return AnimatedBuilder( animation: _infiniteScrollController, @@ -193,30 +208,25 @@ class _IndexFragmentState extends State { final diff = _infiniteScrollController.offset - currentOffset; final ver = (diff / 36.0).abs(); - return Hero( - tag: currFlag, - child: Padding( - padding: EdgeInsetsDirectional.only( - start: 24.0, - top: (ver > 12.0 ? 12.0 : ver) + 8, - bottom: 8.0, - ), - child: ScalableCard( - onTap: () { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: carousel.id, - cover: carousel.cover, - ), - ); - }, - child: Image( - fit: BoxFit.cover, - image: CacheImage(carousel.cover), - ), + return Padding( + padding: EdgeInsetsDirectional.only( + start: 24.0, + top: (ver > 12.0 ? 12.0 : ver) + 8, + bottom: 8.0, + ), + child: TransitionContainer( + builder: (context, open) { + return RippleTap( + onTap: open, + child: Image( + fit: BoxFit.cover, + image: CacheImage(carousel.cover), + ), + ); + }, + next: BangumiPage( + bangumiId: carousel.id, + cover: carousel.cover, ), ), ); @@ -226,7 +236,6 @@ class _IndexFragmentState extends State { controller: _infiniteScrollController, itemExtent: 300.0, itemCount: carousels.length, - center: false, velocityFactor: 0.8, ), ), @@ -302,10 +311,6 @@ class _IndexFragmentState extends State { } } -void showSearchPanel(BuildContext context) { - Navigator.pushNamed(context, Routes.search.name); -} - void showSettingsPanel(BuildContext context) { MBottomSheet.show( context, @@ -381,15 +386,18 @@ class _PinedHeader extends StatelessWidget { ]; if (!isTablet) { children.add( - RippleTap( - onTap: () { - showSearchPanel(context); + TransitionContainer( + next: const SearchPage(), + builder: (context, open) { + return RippleTap( + onTap: open, + shape: const CircleBorder(), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.search_rounded), + ), + ); }, - shape: const CircleBorder(), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(Icons.search_rounded), - ), ), ); children.add(buildAvatarWithAction(context)); diff --git a/lib/ui/fragments/list.dart b/lib/ui/fragments/list.dart index a18c6ac..cb372ea 100644 --- a/lib/ui/fragments/list.dart +++ b/lib/ui/fragments/list.dart @@ -5,12 +5,12 @@ import 'package:waterfall_flow/waterfall_flow.dart'; import '../../internal/delegate.dart'; import '../../internal/kit.dart'; -import '../../mikan_routes.dart'; import '../../providers/list_model.dart'; import '../../topvars.dart'; import '../../widget/sliver_pinned_header.dart'; +import '../../widget/transition_container.dart'; import '../components/list_record_item.dart'; -import 'index.dart'; +import '../pages/search.dart'; import 'select_tablet_mode.dart'; @immutable @@ -62,13 +62,6 @@ class ListFragment extends StatelessWidget { return ListRecordItem( index: index, record: record, - onTap: () { - Navigator.pushNamed( - context, - Routes.record.name, - arguments: Routes.record.d(url: record.url), - ); - }, ); }, childCount: records.length, @@ -97,11 +90,14 @@ class _PinedHeader extends StatelessWidget { actions: isTablet ? null : [ - IconButton( - onPressed: () { - showSearchPanel(context); + TransitionContainer( + next: const SearchPage(), + builder: (context, open) { + return IconButton( + onPressed: open, + icon: const Icon(Icons.search_rounded), + ); }, - icon: const Icon(Icons.search_rounded), ), ], ); diff --git a/lib/ui/fragments/sliver_bangumi_list.dart b/lib/ui/fragments/sliver_bangumi_list.dart index 2683ca0..71459da 100644 --- a/lib/ui/fragments/sliver_bangumi_list.dart +++ b/lib/ui/fragments/sliver_bangumi_list.dart @@ -8,12 +8,13 @@ import '../../internal/extension.dart'; import '../../internal/hive.dart'; import '../../internal/image_provider.dart'; import '../../internal/kit.dart'; -import '../../mikan_routes.dart'; import '../../model/bangumi.dart'; import '../../providers/op_model.dart'; import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/scalable_tap.dart'; +import '../../widget/transition_container.dart'; +import '../pages/bangumi.dart'; typedef HandleSubscribe = void Function(Bangumi bangumi, String flag); @@ -96,54 +97,55 @@ class SliverBangumiList extends StatelessWidget { ) { final currFlag = '$flag:bangumi:${bangumi.id}:${bangumi.cover}'; final cover = _buildBangumiItemCover(imageWidth, bangumi); - return Hero( - tag: currFlag, - child: ScalableCard( - onTap: () { - if (bangumi.grey) { - '此番组下暂无作品'.toast(); - } else { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: bangumi.id, - cover: bangumi.cover, - title: bangumi.name, - ), - ); - } - }, - child: Stack( - children: [ - Positioned.fill(child: cover), - if (bangumi.num != null && bangumi.num! > 0) + return TransitionContainer( + next: BangumiPage( + bangumiId: bangumi.id, + cover: bangumi.cover, + name: bangumi.name, + ), + builder: (context, open) { + return ScalableCard( + onTap: () { + if (bangumi.grey) { + '此番组下暂无作品'.toast(); + } else { + open(); + } + }, + child: Stack( + children: [ + Positioned.fill(child: cover), PositionedDirectional( - top: 14.0, + top: 0.0, end: 12.0, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: theme.colorScheme.error, - shape: const StadiumBorder(), - ), - padding: edgeH6V2, - child: Text( - bangumi.num! > 99 ? '99+' : '+${bangumi.num}', - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.onError, - height: 1.25, - ), - ), + start: 0.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSubscribeButton(theme, bangumi, currFlag), + if (bangumi.num != null && bangumi.num! > 0) + Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: theme.colorScheme.error, + shape: const StadiumBorder(), + ), + padding: edgeH6V2, + child: Text( + bangumi.num! > 99 ? '99+' : '+${bangumi.num}', + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onError, + height: 1.25, + ), + ), + ), + ], ), ), - PositionedDirectional( - child: _buildSubscribeButton(theme, bangumi, currFlag), - ), - ], - ), - ), + ], + ), + ); + }, ); } @@ -161,85 +163,85 @@ class SliverBangumiList extends StatelessWidget { end: Alignment.bottomCenter, colors: [ Colors.transparent, - Colors.black87, + Colors.black45, ], - stops: [0.68, 1.0], + stops: [0.5, 1.0], ), ), child: _buildBangumiItemCover(imageWidth, bangumi), ); - return Hero( - tag: currFlag, - child: ScalableCard( - onTap: () { - if (bangumi.grey) { - '此番组下暂无作品'.toast(); - } else { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: bangumi.id, - cover: bangumi.cover, - title: bangumi.name, - ), - ); - } - }, - child: Stack( - children: [ - Positioned.fill(child: cover), - if (bangumi.num != null && bangumi.num! > 0) + return TransitionContainer( + next: BangumiPage( + bangumiId: bangumi.id, + cover: bangumi.cover, + name: bangumi.name, + ), + builder: (context, open) { + return ScalableCard( + onTap: () { + if (bangumi.grey) { + '此番组下暂无作品'.toast(); + } else { + open(); + } + }, + child: Stack( + children: [ + Positioned.fill(child: cover), PositionedDirectional( - top: 14.0, end: 12.0, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: theme.colorScheme.error, - shape: const StadiumBorder(), - ), - padding: edgeH6V2, - child: Text( - bangumi.num! > 99 ? '99+' : '+${bangumi.num}', - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.onError, - height: 1.25, - ), - ), + start: 0.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSubscribeButton(theme, bangumi, currFlag), + if (bangumi.num != null && bangumi.num! > 0) + Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: theme.colorScheme.error, + shape: const StadiumBorder(), + ), + padding: edgeH6V2, + child: Text( + bangumi.num! > 99 ? '99+' : '+${bangumi.num}', + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onError, + height: 1.25, + ), + ), + ), + ], ), ), - PositionedDirectional( - child: _buildSubscribeButton(theme, bangumi, currFlag), - ), - PositionedDirectional( - bottom: 12.0, - start: 12.0, - end: 12.0, - child: Column( - children: [ - Text( - bangumi.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleSmall! - .copyWith(color: Colors.white), - ), - if (bangumi.updateAt.isNotBlank) + PositionedDirectional( + bottom: 12.0, + start: 12.0, + end: 12.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( bangumi.updateAt, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall! - .copyWith(color: Colors.white70), + .copyWith(color: Colors.white), ), - ], + Text( + bangumi.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall! + .copyWith(color: Colors.white), + ), + ], + ), ), - ), - ], - ), - ), + ], + ), + ); + }, ); } @@ -255,59 +257,62 @@ class SliverBangumiList extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - child: Hero( - tag: currFlag, - child: ScalableCard( - onTap: () { - if (bangumi.grey) { - '此番组下暂无作品'.toast(); - } else { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: bangumi.id, - cover: bangumi.cover, - title: bangumi.name, - ), - ); - } - }, - child: bangumi.grey - ? cover - : Stack( - children: [ - Positioned.fill( - child: cover, - ), - if (bangumi.num != null && bangumi.num! > 0) + child: TransitionContainer( + next: BangumiPage( + bangumiId: bangumi.id, + cover: bangumi.cover, + name: bangumi.name, + ), + builder: (context, open) { + return ScalableCard( + onTap: () { + if (bangumi.grey) { + '此番组下暂无作品'.toast(); + } else { + open(); + } + }, + child: bangumi.grey + ? cover + : Stack( + children: [ + Positioned.fill( + child: cover, + ), PositionedDirectional( - top: 14.0, + top: 0.0, end: 12.0, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: theme.colorScheme.error, - shape: const StadiumBorder(), - ), - padding: edgeH6V2, - child: Text( - bangumi.num! > 99 ? '99+' : '+${bangumi.num}', - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.onError, - height: 1.25, - ), - ), + start: 0.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSubscribeButton(theme, bangumi, currFlag), + if (bangumi.num != null && bangumi.num! > 0) + Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: theme.colorScheme.error, + shape: const StadiumBorder(), + ), + padding: edgeH6V2, + child: Text( + bangumi.num! > 99 + ? '99+' + : '+${bangumi.num}', + style: + theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onError, + height: 1.25, + ), + ), + ), + ], ), ), - PositionedDirectional( - child: - _buildSubscribeButton(theme, bangumi, currFlag), - ), - ], - ), - ), + ], + ), + ); + }, ), ), sizedBoxH8, @@ -341,54 +346,49 @@ class SliverBangumiList extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - child: Hero( - tag: currFlag, - child: ScalableCard( - onTap: () { - if (bangumi.grey) { - '此番组下暂无作品'.toast(); - } else { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: bangumi.id, - cover: bangumi.cover, - title: bangumi.name, - ), - ); - } - }, - child: bangumi.grey || (bangumi.num == null || bangumi.num == 0) - ? cover - : Stack( - children: [ - Positioned.fill( - child: cover, - ), - PositionedDirectional( - top: 14.0, - end: 12.0, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: theme.colorScheme.error, - shape: const StadiumBorder(), - ), - padding: edgeH6V2, - child: Text( - bangumi.num! > 99 ? '99+' : '+${bangumi.num}', - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.onError, - height: 1.25, + child: TransitionContainer( + next: BangumiPage( + bangumiId: bangumi.id, + cover: bangumi.cover, + name: bangumi.name, + ), + builder: (context, open) { + return ScalableCard( + onTap: () { + if (bangumi.grey) { + '此番组下暂无作品'.toast(); + } else { + open(); + } + }, + child: bangumi.grey || (bangumi.num == null || bangumi.num == 0) + ? cover + : Stack( + children: [ + Positioned.fill(child: cover), + PositionedDirectional( + top: 14.0, + end: 12.0, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: theme.colorScheme.error, + shape: const StadiumBorder(), + ), + padding: edgeH6V2, + child: Text( + bangumi.num! > 99 ? '99+' : '+${bangumi.num}', + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onError, + height: 1.25, + ), ), ), ), - ), - ], - ), - ), + ], + ), + ); + }, ), ), sizedBoxH8, @@ -430,42 +430,37 @@ class SliverBangumiList extends StatelessWidget { Bangumi bangumi, String currFlag, ) { - return bangumi.grey - ? const IconButton( - icon: Icon(Icons.favorite_border_rounded), - onPressed: null, - ) - : Selector( - selector: (_, model) => model.flag, - shouldRebuild: (_, next) => next == currFlag, - builder: (_, __, ___) { - return bangumi.subscribed - ? Tooltip( - message: '取消订阅', - child: IconButton( - icon: Icon( - Icons.favorite_rounded, - color: theme.colorScheme.error, - ), - onPressed: () { - handleSubscribe.call(bangumi, currFlag); - }, - ), - ) - : Tooltip( - message: '订阅', - child: IconButton( - icon: Icon( - Icons.favorite_border_rounded, - color: theme.colorScheme.error, - ), - onPressed: () { - handleSubscribe.call(bangumi, currFlag); - }, - ), - ); - }, - ); + return Selector( + selector: (_, model) => model.flag, + shouldRebuild: (_, next) => next == currFlag, + builder: (_, __, ___) { + return bangumi.subscribed + ? Tooltip( + message: '取消订阅', + child: IconButton( + icon: Icon( + Icons.favorite_rounded, + color: theme.colorScheme.error, + ), + onPressed: () { + handleSubscribe.call(bangumi, currFlag); + }, + ), + ) + : Tooltip( + message: '订阅', + child: IconButton( + icon: Icon( + Icons.favorite_border_rounded, + color: theme.colorScheme.error, + ), + onPressed: () { + handleSubscribe.call(bangumi, currFlag); + }, + ), + ); + }, + ); } Widget _buildBangumiItemCover( diff --git a/lib/ui/fragments/subscribed.dart b/lib/ui/fragments/subscribed.dart index 461864b..68a3ff2 100644 --- a/lib/ui/fragments/subscribed.dart +++ b/lib/ui/fragments/subscribed.dart @@ -23,7 +23,9 @@ import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/scalable_tap.dart'; import '../../widget/sliver_pinned_header.dart'; +import '../../widget/transition_container.dart'; import '../components/rss_record_item.dart'; +import '../pages/bangumi.dart'; import 'index.dart'; import 'select_tablet_mode.dart'; import 'sliver_bangumi_list.dart'; @@ -36,7 +38,7 @@ class SubscribedFragment extends StatefulWidget { State createState() => _SubscribedFragmentState(); } -class _SubscribedFragmentState extends LifecycleAppState { +class _SubscribedFragmentState extends LifecycleState { final _infiniteScrollController = InfiniteScrollController(); Timer? _timer; @@ -44,7 +46,12 @@ class _SubscribedFragmentState extends LifecycleAppState { @override void initState() { super.initState(); - _timer = Timer.periodic(const Duration(milliseconds: 3600), (timer) { + _newTimer(); + } + + void _newTimer() { + _timer?.cancel(); + _timer = Timer.periodic(const Duration(milliseconds: 3600), (_) { if (_infiniteScrollController.hasClients) { _infiniteScrollController.animateToItem( (_infiniteScrollController.offset / 280.0).round() + 1, @@ -62,11 +69,15 @@ class _SubscribedFragmentState extends LifecycleAppState { super.dispose(); } + @override + void onPause() { + _timer?.cancel(); + } + @override void onResume() { - if (mounted) { - Provider.of(context, listen: false).refresh(); - } + _newTimer(); + Provider.of(context, listen: false).refresh(); } @override @@ -371,7 +382,6 @@ class _SubscribedFragmentState extends LifecycleAppState { final String bangumiCover = record.cover; final String bangumiId = entry.key; final String badge = recordsLength > 99 ? '99+' : '+$recordsLength'; - final String currFlag = 'rss:$bangumiId:$bangumiCover:$index'; return Padding( padding: const EdgeInsetsDirectional.only( start: 24.0, @@ -382,57 +392,52 @@ class _SubscribedFragmentState extends LifecycleAppState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Stack( - children: [ - ScalableCard( - onTap: () { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: bangumiId, - cover: bangumiCover, - title: record.name, - ), - ); - }, - child: Hero( - tag: currFlag, - child: Tooltip( - message: records.first.name, - child: SizedBox.expand( - child: FadeInImage( - placeholder: Assets.mikan.provider(), - image: ResizeImage( - CacheImage(bangumiCover), - width: (280.0 * context.devicePixelRatio).ceil(), + child: TransitionContainer( + next: BangumiPage( + bangumiId: bangumiId, + cover: bangumiCover, + name: record.name, + ), + builder: (context, open) { + return Stack( + children: [ + ScalableCard( + onTap: open, + child: Tooltip( + message: records.first.name, + child: SizedBox.expand( + child: FadeInImage( + placeholder: Assets.mikan.provider(), + image: ResizeImage( + CacheImage(bangumiCover), + width: (280.0 * context.devicePixelRatio).ceil(), + ), + fit: BoxFit.cover, ), - fit: BoxFit.cover, ), ), ), - ), - ), - PositionedDirectional( - end: 12.0, - top: 12.0, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: theme.colorScheme.error, - shape: const StadiumBorder(), - ), - padding: edgeH6V2, - child: Text( - badge, - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.onError, + PositionedDirectional( + end: 12.0, + top: 12.0, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: theme.colorScheme.error, + shape: const StadiumBorder(), + ), + padding: edgeH6V2, + child: Text( + badge, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onError, + ), + ), ), ), - ), - ), - ], + ], + ); + }, ), ), sizedBoxH10, @@ -512,18 +517,7 @@ class _SubscribedFragmentState extends LifecycleAppState { delegate: SliverChildBuilderDelegate( (context, index) { final record = records[index]; - return RssRecordItem( - index: index, - record: record, - enableHero: false, - onTap: () { - Navigator.pushNamed( - context, - Routes.record.name, - arguments: Routes.record.d(url: record.url), - ); - }, - ); + return RssRecordItem(index: index, record: record); }, childCount: records!.length, ), diff --git a/lib/ui/fragments/theme_color.dart b/lib/ui/fragments/theme_color.dart index cf2e295..817cedc 100644 --- a/lib/ui/fragments/theme_color.dart +++ b/lib/ui/fragments/theme_color.dart @@ -77,6 +77,13 @@ class _ThemeColorPanelState extends LifecycleAppState { return Switch( onChanged: (v) { MyHive.enableDynamicColor(v); + if (v) { + theme.brightness == Brightness.light + ? _controller + .play(_colorSchemePair!.light.primary) + : _controller + .play(_colorSchemePair!.dark.primary); + } setState(() {}); }, value: v, @@ -101,6 +108,10 @@ class _ThemeColorPanelState extends LifecycleAppState { return ColorPicker( color: color, padding: EdgeInsets.zero, + pickerTypeLabels: const { + ColorPickerType.both: '主色调', + ColorPickerType.wheel: '自定义', + }, pickersEnabled: const { ColorPickerType.both: true, ColorPickerType.primary: false, @@ -115,8 +126,8 @@ class _ThemeColorPanelState extends LifecycleAppState { if (v == color) { return; } - _controller.play(v); MyHive.setColorSeed(v); + _controller.play(v); }, ); }, diff --git a/lib/ui/pages/bangumi.dart b/lib/ui/pages/bangumi.dart index 12c9e2b..ae9a2ff 100644 --- a/lib/ui/pages/bangumi.dart +++ b/lib/ui/pages/bangumi.dart @@ -3,7 +3,6 @@ import 'dart:math' as math; import 'package:auto_size_text/auto_size_text.dart'; import 'package:collection/collection.dart'; import 'package:easy_refresh/easy_refresh.dart'; -import 'package:ff_annotation_route_core/ff_annotation_route_core.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -21,21 +20,18 @@ import '../components/simple_record_item.dart'; import '../fragments/subgroup_bangumis.dart'; import '../fragments/subgroup_subscribe.dart'; -@FFRoute(name: '/bangumi') @immutable class BangumiPage extends StatelessWidget { BangumiPage({ super.key, required this.bangumiId, required this.cover, - required this.heroTag, - this.title, + this.name, }); - final String heroTag; final String bangumiId; final String cover; - final String? title; + final String? name; final ValueNotifier _scrollRatio = ValueNotifier(0); @@ -101,7 +97,7 @@ class BangumiPage extends StatelessWidget { sizedBoxW16, if (ratio > 0.88) Expanded( - child: title == null + child: name == null ? Selector( selector: (_, model) => model.bangumiDetail?.name, @@ -120,7 +116,7 @@ class BangumiPage extends StatelessWidget { }, ) : Text( - title!, + name!, style: theme.textTheme.titleLarge, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -365,12 +361,12 @@ class BangumiPage extends StatelessWidget { ], ), ) - else if (title != null) + else if (name != null) Expanded( child: Tooltip( - message: title, + message: name, child: AutoSizeText( - '$title\n', + '$name\n', style: theme.textTheme.titleLarge ?.copyWith(color: theme.secondary), maxLines: 3, @@ -468,42 +464,39 @@ class BangumiPage extends StatelessWidget { } Widget _buildCover(String cover) { - return Hero( - tag: heroTag, - child: ScalableCard( - onTap: () {}, - child: Image( - image: CacheImage(cover), - width: 148.0, - loadingBuilder: (_, child, event) { - return event == null - ? child - : AspectRatio( - aspectRatio: 3 / 4, - child: Container( - padding: edge28, - child: Center( - child: Assets.mikan.image(), - ), + return ScalableCard( + onTap: () {}, + child: Image( + image: CacheImage(cover), + width: 148.0, + loadingBuilder: (_, child, event) { + return event == null + ? child + : AspectRatio( + aspectRatio: 3 / 4, + child: Container( + padding: edge28, + child: Center( + child: Assets.mikan.image(), ), - ); - }, - errorBuilder: (_, __, ___) { - return AspectRatio( - aspectRatio: 3 / 4, - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: Assets.mikan.provider(), - fit: BoxFit.cover, - colorFilter: - const ColorFilter.mode(Colors.grey, BlendMode.color), ), + ); + }, + errorBuilder: (_, __, ___) { + return AspectRatio( + aspectRatio: 3 / 4, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: Assets.mikan.provider(), + fit: BoxFit.cover, + colorFilter: + const ColorFilter.mode(Colors.grey, BlendMode.color), ), ), - ); - }, - ), + ), + ); + }, ), ); } diff --git a/lib/ui/pages/home.dart b/lib/ui/pages/home.dart index 6d6c367..af97ee2 100644 --- a/lib/ui/pages/home.dart +++ b/lib/ui/pages/home.dart @@ -7,11 +7,13 @@ import '../../internal/extension.dart'; import '../../internal/kit.dart'; import '../../internal/lifecycle.dart'; import '../../providers/home_model.dart'; +import '../../widget/transition_container.dart'; import '../fragments/index.dart'; import '../fragments/list.dart'; import '../fragments/select_tablet_mode.dart'; import '../fragments/settings.dart'; import '../fragments/subscribed.dart'; +import 'search.dart'; @FFRoute(name: '/index') class HomePage extends StatefulWidget { @@ -121,11 +123,14 @@ class _HomePageState extends State { children: [ SizedBox(height: 16.0 + context.statusBarHeight), buildAvatarWithAction(context), - IconButton( - onPressed: () { - showSearchPanel(context); + TransitionContainer( + next: const SearchPage(), + builder: (context, open) { + return IconButton( + onPressed: open, + icon: const Icon(Icons.search_rounded), + ); }, - icon: const Icon(Icons.search_rounded), ), ], ), diff --git a/lib/ui/pages/recent_subscribed.dart b/lib/ui/pages/recent_subscribed.dart index 412c4d5..1c73afd 100644 --- a/lib/ui/pages/recent_subscribed.dart +++ b/lib/ui/pages/recent_subscribed.dart @@ -8,7 +8,6 @@ import 'package:waterfall_flow/waterfall_flow.dart'; import '../../internal/delegate.dart'; import '../../internal/extension.dart'; import '../../internal/kit.dart'; -import '../../mikan_routes.dart'; @FFArgumentImport() import '../../model/record_item.dart'; import '../../providers/index_model.dart'; @@ -93,14 +92,6 @@ class RecentSubscribedPage extends StatelessWidget { return RssRecordItem( index: index, record: record, - enableHero: false, - onTap: () { - Navigator.pushNamed( - context, - Routes.record.name, - arguments: Routes.record.d(url: record.url), - ); - }, ); }, childCount: records.length, diff --git a/lib/ui/pages/record.dart b/lib/ui/pages/record.dart index 57f9aa5..dc10936 100644 --- a/lib/ui/pages/record.dart +++ b/lib/ui/pages/record.dart @@ -12,18 +12,18 @@ import '../../internal/extension.dart'; import '../../internal/image_provider.dart'; import '../../internal/kit.dart'; import '../../model/record_details.dart'; +import '../../model/record_item.dart'; import '../../providers/op_model.dart'; import '../../providers/record_detail_model.dart'; import '../../res/assets.gen.dart'; import '../../topvars.dart'; import '../../widget/icon_button.dart'; -@FFRoute(name: '/record') @immutable -class Record extends StatelessWidget { - Record({super.key, required this.url}); +class RecordPage extends StatelessWidget { + RecordPage({super.key, required this.record}); - final String url; + final RecordItem record; final ValueNotifier _scrollRatio = ValueNotifier(0); @override @@ -36,7 +36,7 @@ class Record extends StatelessWidget { return AnnotatedRegion( value: context.fitSystemUiOverlayStyle, child: ChangeNotifierProvider( - create: (_) => RecordDetailModel(url), + create: (_) => RecordDetailModel(record), child: Builder( builder: (context) { final model = @@ -88,7 +88,7 @@ class Record extends StatelessWidget { Expanded( child: Selector( selector: (_, model) => - model.recordDetail?.name, + model.recordDetail.name, shouldRebuild: (pre, next) => pre != next, builder: (_, value, __) { if (value == null) { @@ -108,14 +108,14 @@ class Record extends StatelessWidget { sizedBoxW16, IconButton( onPressed: () { - model.recordDetail?.share.share(); + model.recordDetail.share.share(); }, icon: const Icon(Icons.share_rounded), ), _buildSubscribeBtn(context, theme, model), IconButton( onPressed: () { - model.recordDetail?.magnet + model.recordDetail.magnet .launchAppAndCopy(); }, icon: const Icon(Icons.downloading_rounded), @@ -144,73 +144,67 @@ class Record extends StatelessWidget { final safeArea = MediaQuery.of(context).padding; final scale = (64.0 + context.screenWidth) / context.screenWidth; return Positioned.fill( - child: Selector( + child: Selector( selector: (context, model) => model.recordDetail, shouldRebuild: (pre, next) => pre != next, builder: (context, detail, __) { - final list = detail == null - ? [] - : [ - Stack( - children: [ - if (!detail.cover.endsWith('noimageavailble_icon.png')) - Positioned.fill( - child: Transform.scale( - scale: scale, - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.fitWidth, - image: CacheImage(detail.cover), - alignment: Alignment.topCenter, - isAntiAlias: true, - ), - ), - foregroundDecoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - theme.colorScheme.background - .withOpacity(0.64), - theme.colorScheme.background, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.0, 0.56], - ), - ), - ), + final list = [ + Stack( + children: [ + if (!detail.cover.endsWith('noimageavailble_icon.png')) + Positioned.fill( + child: Transform.scale( + scale: scale, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fitWidth, + image: CacheImage(detail.cover), + alignment: Alignment.topCenter, + isAntiAlias: true, ), ), - Padding( - padding: EdgeInsets.only( - top: 120.0 + context.statusBarHeight, + foregroundDecoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + theme.colorScheme.background.withOpacity(0.64), + theme.colorScheme.background, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, 0.56], + ), ), - child: Row( + ), + ), + ), + Padding( + padding: EdgeInsets.only( + top: 120.0 + context.statusBarHeight, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildBangumiCover(context, detail), + sizedBoxW16, + Expanded( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildBangumiCover(context, detail), - sizedBoxW16, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - sizedBoxH12, - AutoSizeText( - detail.name, - maxLines: 3, - style: theme.textTheme.titleLarge!.copyWith( - color: theme.colorScheme.secondary, - ), - ), - sizedBoxH8, - ...detail.more.entries.map( - (e) => Text( - '${e.key}: ${e.value}', - softWrap: true, - style: theme.textTheme.bodyLarge, - ), - ), - ], + sizedBoxH12, + AutoSizeText( + detail.name, + maxLines: 3, + style: theme.textTheme.titleLarge!.copyWith( + color: theme.colorScheme.secondary, + ), + ), + sizedBoxH8, + ...detail.more.entries.map( + (e) => Text( + '${e.key}: ${e.value}', + softWrap: true, + style: theme.textTheme.bodyMedium, ), ), ], @@ -218,63 +212,81 @@ class Record extends StatelessWidget { ), ], ), - Transform.scale( - scale: scale, - child: Container( - color: theme.colorScheme.background, - height: 36.0, - ), - ), - if (detail.title.isNotBlank) - Text( - detail.title, - style: theme.textTheme.bodyMedium, - ), - sizedBoxH8, - if (!detail.tags.isNullOrEmpty) - Wrap( - spacing: 6.0, - runSpacing: 6.0, - children: [ - ...List.generate( - detail.tags.length, - (index) { - return Container( - padding: edgeH6V4, - decoration: BoxDecoration( - color: theme.colorScheme.surfaceVariant, - borderRadius: borderRadius8, - ), - child: Text( - detail.tags[index], - style: theme.textTheme.labelMedium!.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ); - }, + ), + ], + ), + Transform.scale( + scale: scale, + child: Container( + color: theme.colorScheme.background, + height: 36.0, + ), + ), + if (detail.title.isNotBlank) + Text( + detail.title, + style: theme.textTheme.bodyMedium, + ), + sizedBoxH8, + if (!detail.tags.isNullOrEmpty) + Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: [ + if (record.size.isNotBlank) + Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: borderRadius8, + ), + child: Text( + record.size, + style: theme.textTheme.labelSmall!.copyWith( + color: theme.colorScheme.onSecondaryContainer, ), - ], + ), ), - sizedBoxH24, - Text( - '概况简介', - style: theme.textTheme.titleLarge, - ), - sizedBoxH12, - HtmlWidget( - detail.intro, - customWidgetBuilder: (element) { - if (element.localName == 'img') { - final String? src = element.attributes['src']; - if (src.isNotBlank) { - return _buildImageWidget(src!); - } - } - return null; + ...List.generate( + detail.tags.length, + (index) { + return Container( + padding: edgeH6V4, + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer, + borderRadius: borderRadius8, + ), + child: Text( + detail.tags[index], + style: theme.textTheme.labelMedium!.copyWith( + color: theme.colorScheme.onTertiaryContainer, + ), + ), + ); }, ), - ]; + ], + ), + sizedBoxH24, + if (detail.intro.isNotEmpty) + Text( + '概况简介', + style: theme.textTheme.titleLarge, + ), + sizedBoxH12, + HtmlWidget( + detail.intro, + customWidgetBuilder: (element) { + if (element.localName == 'img') { + final String? src = element.attributes['src']; + if (src.isNotBlank) { + return _buildImageWidget(src!); + } + } + return null; + }, + ), + ]; return EasyRefresh( onRefresh: model.refresh, refreshOnStart: true, @@ -348,13 +360,10 @@ class Record extends StatelessWidget { RecordDetailModel model, ) { return Selector( - selector: (_, model) => model.recordDetail?.subscribed ?? false, + selector: (_, model) => model.recordDetail.subscribed, shouldRebuild: (pre, next) => pre != next, builder: (_, subscribed, __) { final recordDetail = model.recordDetail; - if (recordDetail == null) { - return const SizedBox(); - } return subscribed ? IconButton( tooltip: '取消订阅', @@ -364,7 +373,7 @@ class Record extends StatelessWidget { ), onPressed: () { context.read().subscribeBangumi( - recordDetail.id, + recordDetail.id!, recordDetail.subscribed, onSuccess: () { recordDetail.subscribed = !recordDetail.subscribed; @@ -381,7 +390,7 @@ class Record extends StatelessWidget { icon: const Icon(Icons.favorite_border_rounded), onPressed: () { context.read().subscribeBangumi( - recordDetail.id, + recordDetail.id!, recordDetail.subscribed, onSuccess: () { recordDetail.subscribed = !recordDetail.subscribed; diff --git a/lib/ui/pages/search.dart b/lib/ui/pages/search.dart index cbc3cdb..39c4551 100644 --- a/lib/ui/pages/search.dart +++ b/lib/ui/pages/search.dart @@ -1,4 +1,3 @@ -import 'package:ff_annotation_route_core/ff_annotation_route_core.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:provider/provider.dart'; @@ -11,7 +10,6 @@ import '../../internal/hive.dart'; import '../../internal/image_provider.dart'; import '../../internal/kit.dart'; import '../../internal/method.dart'; -import '../../mikan_routes.dart'; import '../../model/bangumi.dart'; import '../../model/record_item.dart'; import '../../model/subgroup.dart'; @@ -21,11 +19,12 @@ import '../../topvars.dart'; import '../../widget/ripple_tap.dart'; import '../../widget/scalable_tap.dart'; import '../../widget/sliver_pinned_header.dart'; +import '../../widget/transition_container.dart'; import '../components/simple_record_item.dart'; +import 'bangumi.dart'; -@FFRoute(name: '/search') -class Search extends StatelessWidget { - const Search({super.key}); +class SearchPage extends StatelessWidget { + const SearchPage({super.key}); @override Widget build(BuildContext context) { @@ -392,64 +391,62 @@ class Search extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: ScalableCard( - onTap: () { - Navigator.pushNamed( - context, - Routes.bangumi.name, - arguments: Routes.bangumi.d( - heroTag: currFlag, - bangumiId: bangumi.id, - cover: bangumi.cover, - title: bangumi.name, - ), - ); - }, - child: Image( - image: provider, - loadingBuilder: (_, child, event) { - return event == null - ? child - : Hero( - tag: currFlag, - child: Container( - padding: edge28, - child: Center( - child: Assets.mikan.image(), + child: TransitionContainer( + builder: (context, open) { + return ScalableCard( + onTap: open, + child: Image( + image: provider, + loadingBuilder: (_, child, event) { + return event == null + ? child + : Hero( + tag: currFlag, + child: Container( + padding: edge28, + child: Center( + child: Assets.mikan.image(), + ), + ), + ); + }, + errorBuilder: (_, __, ___) { + return Hero( + tag: currFlag, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: Assets.mikan.provider(), + fit: BoxFit.cover, + colorFilter: const ColorFilter.mode( + Colors.grey, + BlendMode.color, + ), ), ), - ); - }, - errorBuilder: (_, __, ___) { - return Hero( - tag: currFlag, - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: Assets.mikan.provider(), - fit: BoxFit.cover, - colorFilter: const ColorFilter.mode( - Colors.grey, - BlendMode.color, - ), ), - ), - ), - ); - }, - frameBuilder: (_, __, ___, ____) { - return Hero( - tag: currFlag, - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: provider, - fit: BoxFit.cover, + ); + }, + frameBuilder: (_, __, ___, ____) { + return Hero( + tag: currFlag, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: provider, + fit: BoxFit.cover, + ), + ), ), - ), - ), - ); - }, + ); + }, + ), + ); + }, + next: BangumiPage( + bangumiId: bangumi.id, + cover: bangumi.cover, + name: bangumi.name, ), ), ), diff --git a/lib/widget/bottom_sheet.dart b/lib/widget/bottom_sheet.dart index 6226a0d..2cdca48 100644 --- a/lib/widget/bottom_sheet.dart +++ b/lib/widget/bottom_sheet.dart @@ -42,7 +42,7 @@ class MBottomSheet extends StatelessWidget { child: child, ); return Material( - color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.12), + color: Theme.of(context).colorScheme.primary.withOpacity(0.12), borderRadius: borderRadiusT28, child: Padding( padding: EdgeInsets.only( diff --git a/lib/widget/particle.dart b/lib/widget/particle.dart index ca46657..937bb44 100644 --- a/lib/widget/particle.dart +++ b/lib/widget/particle.dart @@ -3,61 +3,22 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class ParticleController { - ParticleWrapperState? _state; +final _random = math.Random(); - final _random = math.Random(); - - void play([Color color = Colors.blue]) { - final state = _state; - if (state == null) { - return; - } - final widget = state.widget; - state.particles = List.generate( - (_random.nextDouble() * - (widget.maxNumberOfParticle - widget.minNumberOfParticle)) - .toInt() + - widget.minNumberOfParticle, - (index) => Particle( - size: _random.nextDouble() * (widget.maxSize - widget.minSize) + - widget.minSize, - color: color, - percentOfDistance: _random.nextDouble() * - (widget.maxPercentOfDistance - widget.minPercentOfDistance) + - widget.minPercentOfDistance, - shape: ParticleShape.from(_random.nextDouble()), - speedOfRotate: _random.nextDouble() * - (widget.maxSpeedOfRotate - widget.minSpeedOfRotate) + - widget.minSpeedOfRotate, - ), - ); - state._controller.reset(); - state._controller.forward(); - } - - void _dispose() { - _state = null; - } -} - -class ParticleWrapper extends StatefulWidget { - const ParticleWrapper({ - super.key, - required this.controller, +class ParticleController extends ChangeNotifier + implements ValueListenable> { + ParticleController({ this.maxSize = 10.0, this.minSize = 6.0, - this.maxPercentOfDistance = 0.9, - this.minPercentOfDistance = 0.7, + this.maxPercentOfDistance = 0.96, + this.minPercentOfDistance = 0.64, this.maxNumberOfParticle = 64, this.minNumberOfParticle = 48, this.maxSpeedOfRotate = 16.0, this.minSpeedOfRotate = 1.0, this.duration = const Duration(milliseconds: 2400), - this.child, }); - final ParticleController controller; final double maxSize; final double minSize; @@ -73,7 +34,42 @@ class ParticleWrapper extends StatefulWidget { final double minSpeedOfRotate; final Duration duration; - final Widget? child; + List _particles = []; + + void play(Color color) { + _particles = List.generate( + (_random.nextDouble() * (maxNumberOfParticle - minNumberOfParticle)) + .toInt() + + minNumberOfParticle, + (index) => Particle( + size: _random.nextDouble() * (maxSize - minSize) + minSize, + color: color, + percentOfDistance: _random.nextDouble() * + (maxPercentOfDistance - minPercentOfDistance) + + minPercentOfDistance, + shape: ParticleShape.from(_random.nextDouble()), + speedOfRotate: + _random.nextDouble() * (maxSpeedOfRotate - minSpeedOfRotate) + + minSpeedOfRotate, + ), + ); + notifyListeners(); + } + + @override + List get value => _particles; +} + +class ParticleWrapper extends StatefulWidget { + const ParticleWrapper({ + super.key, + required this.controller, + required this.child, + }); + + final ParticleController controller; + + final Widget child; @override ParticleWrapperState createState() => ParticleWrapperState(); @@ -82,39 +78,41 @@ class ParticleWrapper extends StatefulWidget { class ParticleWrapperState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; - List particles = []; @override void initState() { super.initState(); - widget.controller._state = this; _controller = AnimationController( vsync: this, - duration: widget.duration, + duration: widget.controller.duration, ); - // _controller.addStatusListener((status) { - // if (status == AnimationStatus.completed) { - // _controller.reset(); - // } - // }); + widget.controller.addListener(() { + _controller.reset(); + _controller.forward(); + }); } @override void dispose() { + widget.controller.dispose(); _controller.dispose(); - widget.controller._dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return CustomPaint( - foregroundPainter: ParticlePainter( - particles: particles, - animation: _controller.view, - curve: Curves.fastLinearToSlowEaseIn, - ), - child: widget.child, + return ValueListenableBuilder( + valueListenable: widget.controller, + builder: (context, v, child) { + return CustomPaint( + foregroundPainter: ParticlePainter( + particles: v, + animation: _controller.view, + curve: Curves.fastLinearToSlowEaseIn, + ), + child: widget.child, + ); + }, ); } } @@ -127,11 +125,11 @@ enum ParticleShape { ; factory ParticleShape.from(double v) { - if (v < 0.1) { + if (v < 0.05) { return ParticleShape.circle; - } else if (v < 0.2) { + } else if (v < 0.1) { return ParticleShape.rectangle; - } else if (v < 0.3) { + } else if (v < 0.15) { return ParticleShape.triangle; } else { return ParticleShape.square; diff --git a/lib/widget/scalable_tap.dart b/lib/widget/scalable_tap.dart index 25b890f..44e102e 100644 --- a/lib/widget/scalable_tap.dart +++ b/lib/widget/scalable_tap.dart @@ -245,12 +245,17 @@ class ScalableCard extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final color = ElevationOverlay.applySurfaceTint( + theme.cardColor, + colorScheme.surfaceTint, + 1.0, + ); return ScalableTap( onTap: onTap, - elevation: 1.0, type: MaterialType.card, - color: colorScheme.surface, + color: color, shadowColor: colorScheme.shadow, surfaceTintColor: colorScheme.surfaceTint, shape: const RoundedRectangleBorder( diff --git a/lib/widget/transition_container.dart b/lib/widget/transition_container.dart new file mode 100644 index 0000000..d2277e7 --- /dev/null +++ b/lib/widget/transition_container.dart @@ -0,0 +1,41 @@ +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; + +class TransitionContainer extends StatelessWidget { + const TransitionContainer({ + super.key, + required this.next, + required this.builder, + this.shape = const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + this.transitionType = ContainerTransitionType.fade, + this.transitionDuration = const Duration(milliseconds: 360), + this.closedColor, + }); + + final Widget next; + final CloseContainerBuilder builder; + final ShapeBorder shape; + final ContainerTransitionType transitionType; + final Duration transitionDuration; + final Color? closedColor; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return OpenContainer( + closedColor: + closedColor ?? theme.scaffoldBackgroundColor.withOpacity(0.0), + openColor: theme.scaffoldBackgroundColor, + openElevation: 0.0, + closedElevation: 0.0, + closedShape: shape, + transitionType: transitionType, + transitionDuration: transitionDuration, + openBuilder: (_, __) => next, + tappable: false, + closedBuilder: builder, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 96a93f4..ab646c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mikan description: mikanani.me -version: 1.2.2+68 +version: 1.2.3+69 publish_to: none @@ -15,7 +15,7 @@ dependencies: firebase_core: ^2.13.0 firebase_analytics: ^10.4.1 firebase_crashlytics: ^3.3.1 - path_provider: ^2.0.15 + path_provider: ^2.1.1 dio: ^5.1.2 dio_cookie_manager: ^3.0.0 html: ^0.15.3 @@ -23,9 +23,9 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 clipboard: ^0.1.3 - url_launcher: ^6.1.11 + url_launcher: ^6.1.14 share_plus: ^7.0.1 - flutter_widget_from_html_core: ^0.10.0 + flutter_widget_from_html_core: ^0.10.5 extended_sliver: ^2.1.3 waterfall_flow: ^3.0.2 jiffy: ^6.1.0 @@ -44,12 +44,13 @@ dependencies: async: ^2.10.0 auto_size_text: ^3.0.0 easy_refresh: ^3.3.2+1 - window_manager: ^0.3.2 - dynamic_color: ^1.6.5 + window_manager: ^0.3.6 + dynamic_color: ^1.6.7 decimal: ^2.3.2 flutter_smart_dialog: ^4.9.4 infinite_carousel: ^1.0.3 package_info_plus: ^4.1.0 + animations: ^2.0.8 dev_dependencies: flutter_test: @@ -64,7 +65,7 @@ dev_dependencies: flutter_gen_runner: ^5.3.1 json_serializable: ^6.6.2 hive_generator: ^2.0.0 - flutter_lints: ^2.0.1 + flutter_lints: ^2.0.3 icons_launcher: ^2.1.0 quiver: ^3.2.1